Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,6 @@ export interface CcApiContextQuery extends ContextLookupRoleOptions {

/**
* This indicates the property to search for.
* If both exactIdentifier and propertyMatch are specified, then exactIdentifier is used.
* Specifying propertyMatch will return 0 or more results.
* Either exactIdentifier or propertyMatch should be specified.
* @default - None
Expand All @@ -385,6 +384,18 @@ export interface CcApiContextQuery extends ContextLookupRoleOptions {
* This is a set of properties returned from CC API that we want to return from ContextQuery.
*/
readonly propertiesToReturn: string[];

/**
* The value to return if the context value was not found.
* @default - None
*/
readonly dummyValue?: any;

/**
* Ignore an error and return the `dummyValue` instead if the resource was not found.
* @default false
*/
readonly ignoreFailedLookup?: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,7 @@
"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": "This indicates the property to search for.\nSpecifying propertyMatch will return 0 or more results.\nEither exactIdentifier or propertyMatch should be specified. (Default - None)",
"$ref": "#/definitions/Record<string,unknown>"
},
"propertiesToReturn": {
Expand All @@ -1046,6 +1046,14 @@
"type": "string"
}
},
"dummyValue": {
"description": "The value to return if the context value was not found. (Default - None)"
},
"ignoreFailedLookup": {
"description": "Ignore an error and return the `dummyValue` instead if the resource was not found.",
"default": false,
"type": "boolean"
},
"account": {
"description": "Query account",
"type": "string"
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/cloud-assembly-schema/schema/version.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"schemaHash": "ba7d47a7a023c39293e99a374af293384eaf1ccd207e515dbdc59dfb5cae4ed6",
"revision": 41
"schemaHash": "5fe173a6d0d4d783245c472e0c03c2b6d7d216ace91b35635b8f7a3dacd2fd7e",
"revision": 42
}
34 changes: 30 additions & 4 deletions packages/aws-cdk/lib/context-providers/cc-api-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type SdkProvider, initContextProviderSdk } from '../api/aws-auth/sdk-pr
import type { ContextProviderPlugin } from '../api/plugin';
import { ContextProviderError } from '../toolkit/error';
import { findJsonValue, getResultObj } from '../util';
import { ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol';

export class CcApiContextProviderPlugin implements ContextProviderPlugin {
constructor(private readonly aws: SdkProvider) {
Expand All @@ -19,8 +20,18 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
public async getValue(args: CcApiContextQuery) {
const cloudControl = (await initContextProviderSdk(this.aws, args)).cloudControl();

const result = await this.findResources(cloudControl, args);
return result;
try {
const result = await this.findResources(cloudControl, args);
return result;
} catch (err) {
if (err instanceof ResourceNotFoundException) {
const dummyObject = this.getDummyObject(args);
if (dummyObject) {
return [dummyObject];
}
}
throw err;
}
}

private async findResources(cc: ICloudControlClient, args: CcApiContextQuery): Promise<{[key: string]: any} []> {
Expand All @@ -33,10 +44,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 await this.getResource(cc, args.typeName, args.exactIdentifier, args.propertiesToReturn, args.ignoreFailedLookup);
} else {
// use listResource
return this.listResources(cc, args.typeName, args.propertyMatch!, args.propertiesToReturn);
return await this.listResources(cc, args.typeName, args.propertyMatch!, args.propertiesToReturn, args.ignoreFailedLookup);
}
}

Expand All @@ -52,6 +63,7 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
typeName: string,
exactIdentifier: string,
propertiesToReturn: string[],
ignoreFailedLookup?: boolean,
): Promise<{[key: string]: any}[]> {
const resultObjs: {[key: string]: any}[] = [];
try {
Expand All @@ -68,6 +80,9 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
throw new ContextProviderError(`Could not get resource ${exactIdentifier}.`);
}
} catch (err) {
if (err instanceof ResourceNotFoundException && ignoreFailedLookup) {
throw err;
}
throw new ContextProviderError(`Encountered CC API error while getting resource ${exactIdentifier}. Error: ${err}`);
}
return resultObjs;
Expand All @@ -85,6 +100,7 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
typeName: string,
propertyMatch: Record<string, unknown>,
propertiesToReturn: string[],
ignoreFailedLookup?: boolean,
): Promise<{[key: string]: any}[]> {
const resultObjs: {[key: string]: any}[] = [];

Expand Down Expand Up @@ -120,8 +136,18 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
}
});
} catch (err) {
if (err instanceof ResourceNotFoundException && ignoreFailedLookup) {
throw err;
}
throw new ContextProviderError(`Could not get resources ${JSON.stringify(propertyMatch)}. Error: ${err}`);
}
return resultObjs;
}

private getDummyObject(args: CcApiContextQuery): Record<string, any> | undefined {
if (!Array.isArray(args.dummyValue) || args.dummyValue.length === 0 || typeof args.dummyValue[0] !== 'object' || args.dummyValue[0] === null) {
throw new ContextProviderError(`dummyValue must be an array with at least one object. Failed to get dummy object for type ${args.typeName}.`);
}
return getResultObj(args.dummyValue[0], 'dummy-id', args.propertiesToReturn);
}
}
245 changes: 244 additions & 1 deletion packages/aws-cdk/test/context-providers/cc-api-provider.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -240,4 +240,247 @@ 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(createResourceNotFoundException());

// WHEN
const results = await provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreFailedLookup: 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('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'],
ignoreFailedLookup: 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'],
ignoreFailedLookup: true,
dummyValue: [
{
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
],
}),
).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.');
});

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'],
ignoreFailedLookup: true,
dummyValue: [
{
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
],
}),
).rejects.toThrow('Could not get resources {"StorageEncrypted":"true"}.');
});

test('throws error when CC API fails and ignoreFailedLookup 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('Encountered CC API error while getting resource bad-identifier.');
});

test('throws error when CC API fails and ignoreFailedLookup 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'],
ignoreFailedLookup: false,
dummyValue: [
{
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
],
}),
).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'],
ignoreFailedLookup: true,
}),
).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(createResourceNotFoundException());

// WHEN/THEN
await expect(
provider.getValue({
account: '123456789012',
region: 'us-east-1',
typeName: 'AWS::RDS::DBInstance',
exactIdentifier: 'bad-identifier',
propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'],
ignoreFailedLookup: true,
dummyValue: {
DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:dummy-instance',
StorageEncrypted: 'true',
},
}),
).rejects.toThrow('dummyValue must be an array with at least one object. Failed to get dummy object for type AWS::RDS::DBInstance.');
});

test('throws error when CC API fails and dummyValue is an empty 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'],
ignoreFailedLookup: true,
dummyValue: [],
}),
).rejects.toThrow('dummyValue must be an array with at least one object. Failed to get dummy object for type AWS::RDS::DBInstance.');
});

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'],
ignoreFailedLookup: true,
dummyValue: [
'not an object',
],
}),
).rejects.toThrow('dummyValue must be an array with at least one object. Failed to get dummy object for type AWS::RDS::DBInstance.');
});
});
/* 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'
});
}
Loading