Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 39 additions & 17 deletions packages/aws-cdk/lib/context-providers/cc-api-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {

if (args.exactIdentifier) {
// use getResource to get the exact indentifier
return this.getResource(cc, args.typeName, args.exactIdentifier, args.propertiesToReturn);
return this.getResource(cc, args);
} else {
// use listResource
return this.listResources(cc, args.typeName, args.propertyMatch!, args.propertiesToReturn);
return this.listResources(cc, args);
}
}

Expand All @@ -49,26 +49,30 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
*/
private async getResource(
cc: ICloudControlClient,
typeName: string,
exactIdentifier: string,
propertiesToReturn: string[],
args: CcApiContextQuery,
): Promise<{[key: string]: any}[]> {
const resultObjs: {[key: string]: any}[] = [];
try {
const result = await cc.getResource({
TypeName: typeName,
Identifier: exactIdentifier,
TypeName: args.typeName,
Identifier: args.exactIdentifier,
});
const id = result.ResourceDescription?.Identifier ?? '';
if (id !== '') {
const propsObject = JSON.parse(result.ResourceDescription?.Properties ?? '');
const propsObj = getResultObj(propsObject, result.ResourceDescription?.Identifier!, propertiesToReturn);
const propsObj = getResultObj(propsObject, result.ResourceDescription?.Identifier!, args.propertiesToReturn);
resultObjs.push(propsObj);
} else {
throw new ContextProviderError(`Could not get resource ${exactIdentifier}.`);
throw new ContextProviderError(`Could not get resource ${args.exactIdentifier}.`);
}
} catch (err) {
throw new ContextProviderError(`Encountered CC API error while getting resource ${exactIdentifier}. Error: ${err}`);
const dummyValue = this.getDummyValueIfErrorIgnored(args);
if (dummyValue) {
const propsObj = getResultObj(dummyValue, 'dummy-id', args.propertiesToReturn);
resultObjs.push(propsObj);
return resultObjs;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this behavior be in a try/catch in getValue() ? It seems duplicated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: let's decide what a good error protocol would be here. I'm not convinced that return a single dummy object (which a caller has to supply a value for, and then has to test for that value... and it needs to be different from the default dummy object) is the best protocol.

Also; context provider return values are usually cached. Do we really want "not found" dummy values to be cached? (I.e., if they notice and create the resource after the first synth, we will still behave as if the resource doesn't exist).

Is that behavior going to match expected user behavior?

Copy link
Contributor Author

@go-to-k go-to-k Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this behavior be in a try/catch in getValue() ? It seems duplicated.

Certainly. Decided to use try/catch in findResources so that the following errors are not ignored

    if (args.exactIdentifier && args.propertyMatch) {
      throw new ContextProviderError(`Specify either exactIdentifier or propertyMatch, but not both. Failed to find resources using CC API for type ${args.typeName}.`);
    }
    if (!args.exactIdentifier && !args.propertyMatch) {
      throw new ContextProviderError(`Neither exactIdentifier nor propertyMatch is specified. Failed to find resources using CC API for type ${args.typeName}.`);
    }

ffc040e

Copy link
Contributor Author

@go-to-k go-to-k Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: let's decide what a good error protocol would be here. I'm not convinced that return a single dummy object (which a caller has to supply a value for, and then has to test for that value... and it needs to be different from the default dummy object) is the best protocol.

I see. However, throwing an error will cause the CLI to exit, so it must be returned with the normal return type ({[key: string]: any} []). The normal return value is also constructed and returned based on the following function.

Would it then be better to return an array of objects such as [{ Identifier: ‘dummy-id’ }] or [{ ErrorIgnored: true }] (or an empty object...)?

export function getResultObj(jsonObject: any, identifier: string, propertiesToReturn: string[]): {[key: string]: any} {
  const propsObj = {};
  propertiesToReturn.forEach((propName) => {
    Object.assign(propsObj, { [propName]: findJsonValue(jsonObject, propName) });
  });
  Object.assign(propsObj, { ['Identifier']: identifier });
  return propsObj;
}

Also; context provider return values are usually cached. Do we really want "not found" dummy values to be cached? (I.e., if they notice and create the resource after the first synth, we will still behave as if the resource doesn't exist).

I believe this is a matter on the cdk-lib side. The command for clearing the context can avoid that behaviour, but if we are concerned about the hassle, we could add a process on the cdk-lib side that also reports a missingContext and doesn't cache it if a dummy value is stored.

https://github.com/aws/aws-cdk/blob/v2.183.0/packages/aws-cdk-lib/core/lib/context-provider.ts#L108-L131

Either way, as cdk-cli, if we don't create a mechanism to ignore the error, the CDK CLI will terminate with an error when the resource is not found. This means that it will be difficult to fulfil the CDK use case of "use an existing resource if it is available, otherwise create it", so I think this PR should make sense. The existing SSM and KMS providers already support it, so it would be natural to do it in the CC API as well.

Copy link
Contributor Author

@go-to-k go-to-k Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: let's decide what a good error protocol would be here. I'm not convinced that return a single dummy object (which a caller has to supply a value for, and then has to test for that value... and it needs to be different from the default dummy object) is the best protocol.

Alternatively, I came up with the idea of throwing an error with a message indicating something "ignored" in the CLI and not doing Annotations.addError (here) if the message is detected on the cdk-lib side. However, this may result in less clean code as it handles the error message and may be a bit confusing as it goes against the meaning of the property name ignoreErrorOnMissingContext...

Copy link
Contributor Author

@go-to-k go-to-k Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is a matter on the cdk-lib side. The command for clearing the context can avoid that behaviour, but if we are concerned about the hassle, we could add a process on the cdk-lib side that also reports a missingContext if a dummy value is stored.

Alternatively, I came up with the idea of throwing an error with a message indicating something "ignored" in the CLI and not doing Annotations.addError (here) if the message is detected on the cdk-lib side. However, this may result in less clean code as it handles the error message and may be a bit confusing as it goes against the meaning of the property name ignoreErrorOnMissingContext...

I'm not sure, but if a custom class like class IgnoredContextProviderError extends ContextProviderError {} is created and it is thrown in getValue (findResources) if ignoreErrorOnMissingContext is true, and this code is changed with handling the error (if (e instanceof IgnoredContextProviderError) ...), perhaps it could be handled more naturally... (Even so, changes will still need to be made on the cdk-lib side, but...)

For example:

/**
 * Represents an error originating from a Context Provider that will be ignored.
 */
export class IgnoredContextProviderError extends ContextProviderError {}
    try {
      if (args.exactIdentifier) {
        // use getResource to get the exact indentifier
        return await this.getResource(cc, args.typeName, args.exactIdentifier, args.propertiesToReturn);
      } else {
        // use listResource
        return await this.listResources(cc, args.typeName, args.propertyMatch!, args.propertiesToReturn);
      }
    } catch (err) {
      if (args.ignoreErrorOnMissingContext) {
        throw new IgnoredContextProviderError(`Resource for type ${args.typeName} not found but ignore the error.`);
      }
      throw err;
    }
export async function provideContextValues(
    // ...
    // ...
    try {
      // ...
      // ...
      value = await provider.getValue({ ...missingContext.props, lookupRoleArn: arns.lookupRoleArn });
    } catch (e: any) {
      if (e instanceof IgnoredContextProviderError) {
        // Ignore the error if `ignoreErrorOnMissingContext` is set to true on `missingContext`.
        value = { [cxapi.PROVIDER_ERROR_KEY]: formatErrorMessage(e), [IGNORED_ERROR_ON_MISSING_CONTEXT_KEY]: true, [TRANSIENT_CONTEXT_KEY]: true };
      } else {
        // Set a specially formatted provider value which will be interpreted
        // as a lookup failure in the toolkit.
        value = { [cxapi.PROVIDER_ERROR_KEY]: formatErrorMessage(e), [TRANSIENT_CONTEXT_KEY]: true };
      }
    }
const isIgnoredError = isIgnoredErrorOnMissingContext(value);
if (ignoredError) {
  return { value: options.dummyValue };
}
if (value === undefined || providerError !== undefined) {
  // ...

This way, the error protocol does not depend on dummy values in cdk-cli, and it may be possible to avoid caching them in the context (although it may require a few changes on the lib side, and if a new key should be added, on the cxapi side too).

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the volume of comments (I've given it a lot of thought).

Either way, I am willing to act once I have your opinion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had sort of missed how this error ignoring protocol, where we record dummyValue as the "official response" had already been implemented in aws/aws-cdk#31415 before, and is being used in 2 other context providers already.

I still can't say I'm happy about it, but there's precedent so we might as well do the same thing as these other context providers until we decide that all of their behaviors should be changed!

I've documented my understanding of these flags here: aws/aws-cdk#33875. If you think this is accurate, then we should just be doing what the documented protocol says.

Copy link
Contributor Author

@go-to-k go-to-k Mar 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think this is accurate, then we should just be doing what the documented protocol says.

I read and thought it is accurate. So I have changed to use ignoreFailedLookup instead of ignoreErrorOnMissingContext.

0262d51

Copy link
Contributor Author

@go-to-k go-to-k Mar 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that the property called by cdk-lib is still ignoreErrorOnMissingContext.

https://github.com/aws/aws-cdk/pull/33875/files#diff-0d73bed6d4cd92ebf3292b51185a26f6e85c3e9cf6eb37c28bfc48a457600876R169-R172

So I changed the property name back to ignoreErrorOnMissingContext.

4ea2594

throw new ContextProviderError(`Encountered CC API error while getting resource ${args.exactIdentifier}. Error: ${err}`);
}
return resultObjs;
}
Expand All @@ -82,22 +86,20 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
*/
private async listResources(
cc: ICloudControlClient,
typeName: string,
propertyMatch: Record<string, unknown>,
propertiesToReturn: string[],
args: CcApiContextQuery,
): Promise<{[key: string]: any}[]> {
const resultObjs: {[key: string]: any}[] = [];

try {
const result = await cc.listResources({
TypeName: typeName,
TypeName: args.typeName,
});
result.ResourceDescriptions?.forEach((resource) => {
const id = resource.Identifier ?? '';
if (id !== '') {
const propsObject = JSON.parse(resource.Properties ?? '');

const filters = Object.entries(propertyMatch);
const filters = Object.entries(args.propertyMatch!);
let match = false;
if (filters) {
match = filters.every((record, _index, _arr) => {
Expand All @@ -114,14 +116,34 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
}

if (match) {
const propsObj = getResultObj(propsObject, resource.Identifier!, propertiesToReturn);
const propsObj = getResultObj(propsObject, resource.Identifier!, args.propertiesToReturn);
resultObjs.push(propsObj);
}
}
});
} catch (err) {
throw new ContextProviderError(`Could not get resources ${JSON.stringify(propertyMatch)}. Error: ${err}`);
const dummyValue = this.getDummyValueIfErrorIgnored(args);
if (dummyValue) {
const propsObj = getResultObj(dummyValue, 'dummy-id', args.propertiesToReturn);
resultObjs.push(propsObj);
return resultObjs;
}
throw new ContextProviderError(`Could not get resources ${JSON.stringify(args.propertyMatch)}. Error: ${err}`);
}
return resultObjs;
}

private getDummyValueIfErrorIgnored(args: CcApiContextQuery): Record<string, any> | undefined {
if (!('ignoreErrorOnMissingContext' in args) || !args.ignoreErrorOnMissingContext) {
return undefined;
}
if (!('dummyValue' in args) || !Array.isArray(args.dummyValue) || args.dummyValue.length === 0) {
return undefined;
}
const dummyValue = args.dummyValue[0];
if (typeof dummyValue !== 'object' || dummyValue === null) {
return undefined;
}
return dummyValue;
}
}
187 changes: 187 additions & 0 deletions packages/aws-cdk/test/context-providers/cc-api-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GetResourceCommand, ListResourcesCommand } from '@aws-sdk/client-cloudcontrol';
import { CcApiContextProviderPlugin } from '../../lib/context-providers/cc-api-provider';
import { mockCloudControlClient, MockSdkProvider, restoreSdkMocksToDefault } from '../util/mock-sdk';
import { CcApiContextQuery } from '@aws-cdk/cloud-assembly-schema';

let provider: CcApiContextProviderPlugin;

Expand Down Expand Up @@ -240,4 +241,190 @@ test('error by specifying neither exactIdentifier or propertyMatch', async () =>
}),
).rejects.toThrow('Neither exactIdentifier nor propertyMatch is specified. Failed to find resources using CC API for type AWS::RDS::DBInstance.'); // THEN
});

describe('dummy value', () => {
test('returns dummy value when CC API getResource fails', async () => {
// GIVEN
mockCloudControlClient.on(GetResourceCommand).rejects('No data found');

// WHEN
const results = await provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreErrorOnMissingContext: true,
dummyValue: [
{
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
],
} as TestQueryWithDummy);

// THEN
expect(results.length).toEqual(1);
expect(results[0]).toEqual({
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
Identifier: 'dummy-id',
});
});

test('returns dummy value when CC API listResources fails', async () => {
// GIVEN
mockCloudControlClient.on(ListResourcesCommand).rejects('No data found');

// WHEN
const results = await provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
propertyMatch: { 'StorageEncrypted': 'true' },
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreErrorOnMissingContext: true,
dummyValue: [
{
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
],
} as TestQueryWithDummy);

// THEN
expect(results.length).toEqual(1);
expect(results[0]).toEqual({
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
Identifier: 'dummy-id',
});
});

test('throws error when CC API fails and ignoreErrorOnMissingContext is not provided', async () => {
// GIVEN
mockCloudControlClient.on(GetResourceCommand).rejects('No data found');

// WHEN/THEN
await expect(
provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
dummyValue: [
{
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
],
} as TestQueryWithDummy),
).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.');
});

test('throws error when CC API fails and ignoreErrorOnMissingContext is false', async () => {
// GIVEN
mockCloudControlClient.on(GetResourceCommand).rejects('No data found');

// WHEN/THEN
await expect(
provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreErrorOnMissingContext: false,
dummyValue: [
{
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
],
} as TestQueryWithDummy),
).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.');
});

test('throws error when CC API fails and dummyValue is not provided', async () => {
// GIVEN
mockCloudControlClient.on(GetResourceCommand).rejects('No data found');

// WHEN/THEN
await expect(
provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreErrorOnMissingContext: true,
} as TestQueryWithDummy),
).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.');
});

test('throws error when CC API fails and dummyValue is not an array', async () => {
// GIVEN
mockCloudControlClient.on(GetResourceCommand).rejects('No data found');

// WHEN/THEN
await expect(
provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreErrorOnMissingContext: true,
dummyValue: {
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
} as TestQueryWithDummy),
).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.');
});

test('throws error when CC API fails and dummyValue is an empty array', async () => {
// GIVEN
mockCloudControlClient.on(GetResourceCommand).rejects('No data found');

// WHEN/THEN
await expect(
provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreErrorOnMissingContext: true,
dummyValue: [],
} as TestQueryWithDummy),
).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.');
});

test('throws error when CC API fails and dummyValue is not an object array', async () => {
// GIVEN
mockCloudControlClient.on(GetResourceCommand).rejects('No data found');

// WHEN/THEN
await expect(
provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreErrorOnMissingContext: true,
dummyValue: [
'not an object',
],
} as TestQueryWithDummy),
).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.');
});
});
/* eslint-enable */

interface TestQueryWithDummy extends CcApiContextQuery {
ignoreErrorOnMissingContext?: boolean;
dummyValue?: any;
}
Loading