diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts index 82bb3e687..b9194ec36 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts @@ -365,26 +365,81 @@ export interface CcApiContextQuery extends ContextLookupRoleOptions { readonly typeName: string; /** - * exactIdentifier of the resource. - * Specifying exactIdentifier will return at most one result. - * Either exactIdentifier or propertyMatch should be specified. - * @default - None + * Identifier of the resource to look up using `GetResource`. + * + * Specifying exactIdentifier will return exactly one result, or throw an error. + * + * + * @default - Either exactIdentifier or propertyMatch should be specified. */ readonly exactIdentifier?: string; /** - * This indicates the property to search for. - * If both exactIdentifier and propertyMatch are specified, then exactIdentifier is used. + * Returns any resources matching these properties, using `ListResources`. + * * Specifying propertyMatch will return 0 or more results. - * Either exactIdentifier or propertyMatch should be specified. - * @default - None + * + * ## Notes on property completeness + * + * CloudControl API's `ListResources` may return fewer properties than + * `GetResource` would, depending on the resource implementation. + * + * The resources that `propertyMatch` matches against will *only ever* be the + * properties returned by the `ListResources` call. + * + * @default - Either exactIdentifier or propertyMatch should be specified. */ readonly propertyMatch?: Record; /** * This is a set of properties returned from CC API that we want to return from ContextQuery. + * + * If any properties listed here are absent from the target resource, an error will be thrown. + * + * The returned object will always include the key `Identifier` with the CC-API returned + * field `Identifier`. + * + * ## Notes on property completeness + * + * CloudControl API's `ListResources` may return fewer properties than + * `GetResource` would, depending on the resource implementation. + * + * The returned properties here are *currently* selected from the response + * object that CloudControl API returns to the CDK CLI. + * + * However, if we find there is need to do so, we may decide to change this + * behavior in the future: we might change it to perform an additional + * `GetResource` call for resources matched by `propertyMatch`. */ readonly propertiesToReturn: string[]; + + /** + * The value to return if the resource was not found and `ignoreErrorOnMissingContext` is true. + * + * If supplied, `dummyValue` should be an array of objects. + * + * `dummyValue` does not have to have elements, and it may have objects with + * different properties than the properties in `propertiesToReturn`, but it + * will be easiest for downstream code if the `dummyValue` conforms to + * the expected response shape. + * + * @default - No dummy value available + */ + readonly dummyValue?: any; + + /** + * Ignore an error and return the `dummyValue` instead if the resource was not found. + * + * - In case of an `exactIdentifier` lookup, return the `dummyValue` if the resource with + * that identifier was not found. + * - In case of a `propertyMatch` lookup, this setting currently does not have any effect, + * as `propertyMatch` queries can legally return 0 resources. + * + * if `ignoreErrorOnMissingContext` is set, `dummyValue` should be set and be an array. + * + * @default false + */ + readonly ignoreErrorOnMissingContext?: boolean; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 02559f26c..220587180 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -1032,20 +1032,28 @@ "type": "string" }, "exactIdentifier": { - "description": "exactIdentifier of the resource.\nSpecifying exactIdentifier will return at most one result.\nEither exactIdentifier or propertyMatch should be specified. (Default - None)", + "description": "Identifier of the resource to look up using `GetResource`.\n\nSpecifying exactIdentifier will return exactly one result, or throw an error. (Default - Either exactIdentifier or propertyMatch should be specified.)", "type": "string" }, "propertyMatch": { - "description": "This indicates the property to search for.\nIf both exactIdentifier and propertyMatch are specified, then exactIdentifier is used.\nSpecifying propertyMatch will return 0 or more results.\nEither exactIdentifier or propertyMatch should be specified. (Default - None)", + "description": "Returns any resources matching these properties, using `ListResources`.\n\nSpecifying propertyMatch will return 0 or more results.\n\n## Notes on property completeness\n\nCloudControl API's `ListResources` may return fewer properties than\n`GetResource` would, depending on the resource implementation.\n\nThe resources that `propertyMatch` matches against will *only ever* be the\nproperties returned by the `ListResources` call. (Default - Either exactIdentifier or propertyMatch should be specified.)", "$ref": "#/definitions/Record" }, "propertiesToReturn": { - "description": "This is a set of properties returned from CC API that we want to return from ContextQuery.", + "description": "This is a set of properties returned from CC API that we want to return from ContextQuery.\n\nIf any properties listed here are absent from the target resource, an error will be thrown.\n\nThe returned object will always include the key `Identifier` with the CC-API returned\nfield `Identifier`.\n\n## Notes on property completeness\n\nCloudControl API's `ListResources` may return fewer properties than\n`GetResource` would, depending on the resource implementation.\n\nThe returned properties here are *currently* selected from the response\nobject that CloudControl API returns to the CDK CLI.\n\nHowever, if we find there is need to do so, we may decide to change this\nbehavior in the future: we might change it to perform an additional\n`GetResource` call for resources matched by `propertyMatch`.", "type": "array", "items": { "type": "string" } }, + "dummyValue": { + "description": "The value to return if the resource was not found and `ignoreErrorOnMissingContext` is true.\n\nIf supplied, `dummyValue` should be an array of objects.\n\n`dummyValue` does not have to have elements, and it may have objects with\ndifferent properties than the properties in `propertiesToReturn`, but it\nwill be easiest for downstream code if the `dummyValue` conforms to\nthe expected response shape. (Default - No dummy value available)" + }, + "ignoreErrorOnMissingContext": { + "description": "Ignore an error and return the `dummyValue` instead if the resource was not found.\n\n- In case of an `exactIdentifier` lookup, return the `dummyValue` if the resource with\n that identifier was not found.\n- In case of a `propertyMatch` lookup, this setting currently does not have any effect,\n as `propertyMatch` queries can legally return 0 resources.\n\nif `ignoreErrorOnMissingContext` is set, `dummyValue` should be set and be an array.", + "default": false, + "type": "boolean" + }, "account": { "description": "Query account", "type": "string" diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index 29669cfcd..935cecd71 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,4 +1,4 @@ { - "schemaHash": "ba7d47a7a023c39293e99a374af293384eaf1ccd207e515dbdc59dfb5cae4ed6", - "revision": 41 + "schemaHash": "5683db246fac20b864d94d7bceef24ebda1a38c8c1f8ef0d5978534097dc9504", + "revision": 42 } \ No newline at end of file diff --git a/packages/aws-cdk/lib/context-providers/cc-api-provider.ts b/packages/aws-cdk/lib/context-providers/cc-api-provider.ts index bd26a6ee5..cfe040c64 100644 --- a/packages/aws-cdk/lib/context-providers/cc-api-provider.ts +++ b/packages/aws-cdk/lib/context-providers/cc-api-provider.ts @@ -1,4 +1,6 @@ import type { CcApiContextQuery } from '@aws-cdk/cloud-assembly-schema'; +import type { ResourceDescription } from '@aws-sdk/client-cloudcontrol'; +import { ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol'; import { ContextProviderError } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api'; import type { ICloudControlClient } from '../api'; import { type SdkProvider, initContextProviderSdk } from '../api/aws-auth/sdk-provider'; @@ -11,32 +13,44 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin { /** * This returns a data object with the value from CloudControl API result. - * args.typeName - see https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html - * args.exactIdentifier - use CC API getResource. - * args.propertyMatch - use CCP API listResources to get resources and propertyMatch to search through the list. - * args.propertiesToReturn - Properties from CC API to return. + * + * See the documentation in the Cloud Assembly Schema for the semantics of + * each query parameter. */ public async getValue(args: CcApiContextQuery) { - const cloudControl = (await initContextProviderSdk(this.aws, args)).cloudControl(); - - const result = await this.findResources(cloudControl, args); - return result; - } - - private async findResources(cc: ICloudControlClient, args: CcApiContextQuery): Promise<{[key: string]: any} []> { + // Validate input 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}.`); + throw new ContextProviderError(`Provider protocol error: specify either exactIdentifier or propertyMatch, but not both (got ${JSON.stringify(args)})`); } - 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}.`); + if (args.ignoreErrorOnMissingContext && args.dummyValue === undefined) { + throw new ContextProviderError(`Provider protocol error: if ignoreErrorOnMissingContext is set, a dummyValue must be supplied (got ${JSON.stringify(args)})`); } + if (args.dummyValue !== undefined && (!Array.isArray(args.dummyValue) || !args.dummyValue.every(isObject))) { + throw new ContextProviderError(`Provider protocol error: dummyValue must be an array of objects (got ${JSON.stringify(args.dummyValue)})`); + } + + // Do the lookup + const cloudControl = (await initContextProviderSdk(this.aws, args)).cloudControl(); - if (args.exactIdentifier) { - // use getResource to get the exact indentifier - return this.getResource(cc, args.typeName, args.exactIdentifier, args.propertiesToReturn); - } else { - // use listResource - return this.listResources(cc, args.typeName, args.propertyMatch!, args.propertiesToReturn); + try { + let resources: FoundResource[]; + if (args.exactIdentifier) { + // use getResource to get the exact indentifier + resources = await this.getResource(cloudControl, args.typeName, args.exactIdentifier); + } else if (args.propertyMatch) { + // use listResource + resources = await this.listResources(cloudControl, args.typeName, args.propertyMatch); + } else { + throw new ContextProviderError(`Provider protocol error: neither exactIdentifier nor propertyMatch is specified in ${JSON.stringify(args)}.`); + } + + return resources.map((r) => getResultObj(r.properties, r.identifier, args.propertiesToReturn)); + } catch (err) { + if (err instanceof ZeroResourcesFoundError && args.ignoreErrorOnMissingContext) { + // We've already type-checked dummyValue. + return args.dummyValue; + } + throw err; } } @@ -44,84 +58,104 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin { * Calls getResource from CC API to get the resource. * See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/get-resource.html * - * If the exactIdentifier is not found, then an empty map is returned. - * If the resource is found, then a map of the identifier to a map of property values is returned. + * Will always return exactly one resource, or fail. */ private async getResource( cc: ICloudControlClient, typeName: string, exactIdentifier: string, - propertiesToReturn: string[], - ): Promise<{[key: string]: any}[]> { - const resultObjs: {[key: string]: any}[] = []; + ): Promise { try { const result = await cc.getResource({ TypeName: typeName, Identifier: exactIdentifier, }); - const id = result.ResourceDescription?.Identifier ?? ''; - if (id !== '') { - const propsObject = JSON.parse(result.ResourceDescription?.Properties ?? ''); - const propsObj = getResultObj(propsObject, result.ResourceDescription?.Identifier!, propertiesToReturn); - resultObjs.push(propsObj); - } else { - throw new ContextProviderError(`Could not get resource ${exactIdentifier}.`); + if (!result.ResourceDescription) { + throw new ContextProviderError('Unexpected CloudControl API behavior: returned empty response'); } - } catch (err) { - throw new ContextProviderError(`Encountered CC API error while getting resource ${exactIdentifier}. Error: ${err}`); + + return [foundResourceFromCcApi(result.ResourceDescription)]; + } catch (err: any) { + if (err instanceof ResourceNotFoundException || (err as any).name === 'ResourceNotFoundException') { + throw new ZeroResourcesFoundError(`No resource of type ${typeName} with identifier: ${exactIdentifier}`); + } + if (!(err instanceof ContextProviderError)) { + throw new ContextProviderError(`Encountered CC API error while getting ${typeName} resource ${exactIdentifier}: ${err.message}`); + } + throw err; } - return resultObjs; } /** * Calls listResources from CC API to get the resources and apply args.propertyMatch to find the resources. * See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/list-resources.html * - * Since exactIdentifier is not specified, propertyMatch must be specified. - * This returns an object where the ids are object keys and values are objects with keys of args.propertiesToReturn. + * Will return 0 or more resources. + * + * Does not currently paginate through more than one result page. */ private async listResources( cc: ICloudControlClient, typeName: string, propertyMatch: Record, - propertiesToReturn: string[], - ): Promise<{[key: string]: any}[]> { - const resultObjs: {[key: string]: any}[] = []; - + ): Promise { try { const result = await cc.listResources({ TypeName: typeName, - }); - result.ResourceDescriptions?.forEach((resource) => { - const id = resource.Identifier ?? ''; - if (id !== '') { - const propsObject = JSON.parse(resource.Properties ?? ''); - - const filters = Object.entries(propertyMatch); - let match = false; - if (filters) { - match = filters.every((record, _index, _arr) => { - const key = record[0]; - const expected = record[1]; - const actual = findJsonValue(propsObject, key); - return propertyMatchesFilter(actual, expected); - }); - - function propertyMatchesFilter(actual: any, expected: unknown) { - // For now we just check for strict equality, but we can implement pattern matching and fuzzy matching here later - return expected === actual; - } - } - if (match) { - const propsObj = getResultObj(propsObject, resource.Identifier!, propertiesToReturn); - resultObjs.push(propsObj); - } - } }); - } catch (err) { - throw new ContextProviderError(`Could not get resources ${JSON.stringify(propertyMatch)}. Error: ${err}`); + const found = (result.ResourceDescriptions ?? []) + .map(foundResourceFromCcApi) + .filter((r) => { + return Object.entries(propertyMatch).every(([propPath, expected]) => { + const actual = findJsonValue(r.properties, propPath); + return propertyMatchesFilter(actual, expected); + }); + }); + + return found; + } catch (err: any) { + if (!(err instanceof ContextProviderError) && !(err instanceof ZeroResourcesFoundError)) { + throw new ContextProviderError(`Encountered CC API error while listing ${typeName} resources matching ${JSON.stringify(propertyMatch)}: ${err.message}`); + } + throw err; } - return resultObjs; } } + +/** + * Convert a CC API response object into a nicer object (parse the JSON) + */ +function foundResourceFromCcApi(desc: ResourceDescription): FoundResource { + return { + identifier: desc.Identifier ?? '*MISSING*', + properties: JSON.parse(desc.Properties ?? '{}'), + }; +} + +/** + * Whether the given property value matches the given filter + * + * For now we just check for strict equality, but we can implement pattern matching and fuzzy matching here later + */ +function propertyMatchesFilter(actual: unknown, expected: unknown) { + return expected === actual; +} + +function isObject(x: unknown): x is {[key: string]: unknown} { + return typeof x === 'object' && x !== null && !Array.isArray(x); +} + +/** + * A parsed version of the return value from CCAPI + */ +interface FoundResource { + readonly identifier: string; + readonly properties: Record; +} + +/** + * A specific lookup failure indicating 0 resources found that can be recovered + */ +class ZeroResourcesFoundError extends Error { +} diff --git a/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts b/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts index ab71efde5..f425bb199 100644 --- a/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts +++ b/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts @@ -1,4 +1,4 @@ -import { GetResourceCommand, ListResourcesCommand } from '@aws-sdk/client-cloudcontrol'; +import { GetResourceCommand, InvalidRequestException, ListResourcesCommand, ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol'; import { CcApiContextProviderPlugin } from '../../lib/context-providers/cc-api-provider'; import { mockCloudControlClient, MockSdkProvider, restoreSdkMocksToDefault } from '../util/mock-sdk'; @@ -38,30 +38,6 @@ test('looks up RDS instance using CC API getResource', async () => { })); }); -// In theory, this should never happen. We ask for my-db-instance-1 but CC API returns ''. -// Included this to test the code path. -test('looks up RDS instance using CC API getResource - wrong match', async () => { - // GIVEN - mockCloudControlClient.on(GetResourceCommand).resolves({ - TypeName: 'AWS::RDS::DBInstance', - ResourceDescription: { - Identifier: '', - Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true"}', - }, - }); - - await expect( - // WHEN - provider.getValue({ - account: '123456789012', - region: 'us-east-1', - typeName: 'AWS::RDS::DBInstance', - exactIdentifier: 'my-db-instance-1', - propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], - }), - ).rejects.toThrow('Encountered CC API error while getting resource my-db-instance-1.'); // THEN -}); - test('looks up RDS instance using CC API getResource - empty response', async () => { // GIVEN mockCloudControlClient.on(GetResourceCommand).resolves({ @@ -76,7 +52,7 @@ test('looks up RDS instance using CC API getResource - empty response', async () exactIdentifier: 'bad-identifier', propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], }), - ).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.'); // THEN + ).rejects.toThrow('Unexpected CloudControl API behavior: returned empty response'); // THEN }); test('looks up RDS instance using CC API getResource - error in CC API', async () => { @@ -92,7 +68,7 @@ test('looks up RDS instance using CC API getResource - error in CC API', async ( exactIdentifier: 'bad-identifier', propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], }), - ).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.'); // THEN + ).rejects.toThrow('Encountered CC API error while getting AWS::RDS::DBInstance resource bad-identifier'); // THEN }); test('looks up RDS instance using CC API listResources', async () => { @@ -201,7 +177,7 @@ test('looks up RDS instance using CC API listResources - error in CC API', async propertyMatch: { 'Endpoint.Port': '5432' }, propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], }), - ).rejects.toThrow('Could not get resources {"Endpoint.Port":"5432"}.'); // THEN + ).rejects.toThrow('error while listing AWS::RDS::DBInstance resources'); // THEN }); test('error by specifying both exactIdentifier and propertyMatch', async () => { @@ -222,7 +198,7 @@ test('error by specifying both exactIdentifier and propertyMatch', async () => { }, propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], }), - ).rejects.toThrow('Specify either exactIdentifier or propertyMatch, but not both. Failed to find resources using CC API for type AWS::RDS::DBInstance.'); // THEN + ).rejects.toThrow('specify either exactIdentifier or propertyMatch, but not both'); // THEN }); test('error by specifying neither exactIdentifier or propertyMatch', async () => { @@ -238,6 +214,232 @@ test('error by specifying neither exactIdentifier or propertyMatch', async () => typeName: 'AWS::RDS::DBInstance', propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], }), - ).rejects.toThrow('Neither exactIdentifier nor propertyMatch is specified. Failed to find resources using CC API for type AWS::RDS::DBInstance.'); // THEN + ).rejects.toThrow('neither exactIdentifier nor propertyMatch is specified'); +}); + +describe('dummy value', () => { + test('returns dummy value when CC API getResource fails', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects(createResourceNotFoundException()); + + // 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', + }, + ], + }); + + // THEN + expect(results.length).toEqual(1); + expect(results[0]).toEqual({ + DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance', + StorageEncrypted: 'true', + }); + }); + + // TODO: This test can be re-enabled when listResources can be made to fail, after + // https://github.com/aws/aws-cdk-cli/pull/251 is merged. + test.skip('returns dummy value when CC API listResources fails', async () => { + // GIVEN + mockCloudControlClient.on(ListResourcesCommand).rejects(createResourceNotFoundException()); + + // 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', + }, + ], + }); + + // 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 getResource fails but the error is not ResourceNotFoundException', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects(createOtherError()); + + // 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', + }, + ], + }), + ).rejects.toThrow('Encountered CC API error while getting AWS::RDS::DBInstance resource bad-identifier: Other error'); + }); + + test('throws error when CC API listResources fails but the error is not ResourceNotFoundException', async () => { + // GIVEN + mockCloudControlClient.on(ListResourcesCommand).rejects(createOtherError()); + + // WHEN/THEN + await expect( + 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', + }, + ], + }), + ).rejects.toThrow('Encountered CC API error while listing AWS::RDS::DBInstance resources matching {\"StorageEncrypted\":\"true\"}: Other error'); + }); + + test('throws error when CC API fails and ignoreErrorOnMissingContext is not provided', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects(createResourceNotFoundException()); + + // 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', + }, + ], + }), + ).rejects.toThrow('No resource of type AWS::RDS::DBInstance with identifier: bad-identifier'); + }); + + test('throws error when CC API fails and ignoreErrorOnMissingContext is false', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects(createResourceNotFoundException()); + + // 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', + }, + ], + }), + ).rejects.toThrow('No resource of type AWS::RDS::DBInstance with identifier: bad-identifier'); + }); + + test('throws error when CC API fails and dummyValue is not provided', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects(createResourceNotFoundException()); + + // WHEN/THEN + await expect( + provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'bad-identifier', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + ignoreErrorOnMissingContext: true, + }), + ).rejects.toThrow('if ignoreErrorOnMissingContext is set, a dummyValue must be supplied'); + }); + + test('throws error when CC API fails and dummyValue is not an array', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects(createResourceNotFoundException()); + + // 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', + }, + }), + ).rejects.toThrow('dummyValue must be an array of objects'); + }); + + test('throws error when CC API fails and dummyValue is not an object array', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects(createResourceNotFoundException()); + + // 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', + ], + }), + ).rejects.toThrow('dummyValue must be an array of objects'); + }); }); /* eslint-enable */ + +function createResourceNotFoundException() { + return new ResourceNotFoundException({ + $metadata: {}, + message: 'Resource not found', + Message: 'Resource not found' + }); +} + +function createOtherError() { + return new InvalidRequestException({ + $metadata: {}, + message: 'Other error', + Message: 'Other error' + }); +}