Skip to content

Commit 8c70b53

Browse files
go-to-krix0rrr
andauthored
feat(cli): can ignore errors and return dummy value in CloudControl API context provider (#211)
CDK app with CC API Context Provider fails if the resource we want to get doesn't exist. ``` [Error at /LookupStack] Encountered CC API error while getting resource MyLookupTestRole. Error: ResourceNotFoundException: AWS::IAM::Role Handler returned status FAILED: The role with name MyLookupTestRole cannot be found. (Service: Iam, Status Code: 404, Request ID: xxxx-xxx-xxxx-xxxx) (HandlerErrorCode: NotFound, RequestToken: xxxx-xxx-xxxx-xxxx) ``` Also CC API Context Provider (`CcApiContextProviderPlugin`) cannot ignore errors even if `ignoreErrorOnMissingContext` is passed in aws-cdk-lib because the current code in aws-cdk-**cli** doesn't handle the property. Sample cdk-lib code (based on my [PR](aws/aws-cdk#33603) for IAM Role fromLookup): ```ts const response: {[key: string]: any}[] = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.CC_API_PROVIDER, props: { typeName: 'AWS::IAM::Role', exactIdentifier: options.roleName, propertiesToReturn: [ 'Arn', ], } as cxschema.CcApiContextQuery, dummyValue: [ { // eslint-disable-next-line @cdklabs/no-literal-partition Arn: 'arn:aws:iam::123456789012:role/DUMMY_ARN', }, ], ignoreErrorOnMissingContext: options.returnDummyRoleOnMissing, // added }).value; ``` However it would be good if the provider can ignore errors and return any dummy value to cdk library. This allows all resources to be **used if available, or created if not.** Actually, SSM and KMS provider can ignore errors: KMS: https://github.com/aws/aws-cdk-cli/blob/main/packages/aws-cdk/lib/context-providers/keys.ts#L43-L48 SSM: https://github.com/aws/aws-cdk-cli/blob/main/packages/aws-cdk/lib/context-providers/ssm-parameters.ts#L27-L30 For example, in cdk-lib, we can specify [ignoreErrorOnMissingContext](https://github.com/aws/aws-cdk/blob/v2.182.0/packages/aws-cdk-lib/aws-kms/lib/key.ts#L741-L744) in the `fromLookup`. The `dummyValue` and `ignoreErrorOnMissingContext` properties can also be specified in [GetContextValueOptions](https://github.com/go-to-k/aws-cdk/blob/45a276fd0fc9b7b08f69f7faf3d0091796ab1663/packages/aws-cdk-lib/core/lib/context-provider.ts#L31-L45). ## cli-integ aws/aws-cdk-cli-testing#50 --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Rico Hermans <[email protected]>
1 parent 1f880a5 commit 8c70b53

File tree

5 files changed

+412
-113
lines changed

5 files changed

+412
-113
lines changed

packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts

+63-8
Original file line numberDiff line numberDiff line change
@@ -365,26 +365,81 @@ export interface CcApiContextQuery extends ContextLookupRoleOptions {
365365
readonly typeName: string;
366366

367367
/**
368-
* exactIdentifier of the resource.
369-
* Specifying exactIdentifier will return at most one result.
370-
* Either exactIdentifier or propertyMatch should be specified.
371-
* @default - None
368+
* Identifier of the resource to look up using `GetResource`.
369+
*
370+
* Specifying exactIdentifier will return exactly one result, or throw an error.
371+
*
372+
*
373+
* @default - Either exactIdentifier or propertyMatch should be specified.
372374
*/
373375
readonly exactIdentifier?: string;
374376

375377
/**
376-
* This indicates the property to search for.
377-
* If both exactIdentifier and propertyMatch are specified, then exactIdentifier is used.
378+
* Returns any resources matching these properties, using `ListResources`.
379+
*
378380
* Specifying propertyMatch will return 0 or more results.
379-
* Either exactIdentifier or propertyMatch should be specified.
380-
* @default - None
381+
*
382+
* ## Notes on property completeness
383+
*
384+
* CloudControl API's `ListResources` may return fewer properties than
385+
* `GetResource` would, depending on the resource implementation.
386+
*
387+
* The resources that `propertyMatch` matches against will *only ever* be the
388+
* properties returned by the `ListResources` call.
389+
*
390+
* @default - Either exactIdentifier or propertyMatch should be specified.
381391
*/
382392
readonly propertyMatch?: Record<string, unknown>;
383393

384394
/**
385395
* This is a set of properties returned from CC API that we want to return from ContextQuery.
396+
*
397+
* If any properties listed here are absent from the target resource, an error will be thrown.
398+
*
399+
* The returned object will always include the key `Identifier` with the CC-API returned
400+
* field `Identifier`.
401+
*
402+
* ## Notes on property completeness
403+
*
404+
* CloudControl API's `ListResources` may return fewer properties than
405+
* `GetResource` would, depending on the resource implementation.
406+
*
407+
* The returned properties here are *currently* selected from the response
408+
* object that CloudControl API returns to the CDK CLI.
409+
*
410+
* However, if we find there is need to do so, we may decide to change this
411+
* behavior in the future: we might change it to perform an additional
412+
* `GetResource` call for resources matched by `propertyMatch`.
386413
*/
387414
readonly propertiesToReturn: string[];
415+
416+
/**
417+
* The value to return if the resource was not found and `ignoreErrorOnMissingContext` is true.
418+
*
419+
* If supplied, `dummyValue` should be an array of objects.
420+
*
421+
* `dummyValue` does not have to have elements, and it may have objects with
422+
* different properties than the properties in `propertiesToReturn`, but it
423+
* will be easiest for downstream code if the `dummyValue` conforms to
424+
* the expected response shape.
425+
*
426+
* @default - No dummy value available
427+
*/
428+
readonly dummyValue?: any;
429+
430+
/**
431+
* Ignore an error and return the `dummyValue` instead if the resource was not found.
432+
*
433+
* - In case of an `exactIdentifier` lookup, return the `dummyValue` if the resource with
434+
* that identifier was not found.
435+
* - In case of a `propertyMatch` lookup, this setting currently does not have any effect,
436+
* as `propertyMatch` queries can legally return 0 resources.
437+
*
438+
* if `ignoreErrorOnMissingContext` is set, `dummyValue` should be set and be an array.
439+
*
440+
* @default false
441+
*/
442+
readonly ignoreErrorOnMissingContext?: boolean;
388443
}
389444

390445
/**

packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json

+11-3
Original file line numberDiff line numberDiff line change
@@ -1032,20 +1032,28 @@
10321032
"type": "string"
10331033
},
10341034
"exactIdentifier": {
1035-
"description": "exactIdentifier of the resource.\nSpecifying exactIdentifier will return at most one result.\nEither exactIdentifier or propertyMatch should be specified. (Default - None)",
1035+
"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.)",
10361036
"type": "string"
10371037
},
10381038
"propertyMatch": {
1039-
"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)",
1039+
"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.)",
10401040
"$ref": "#/definitions/Record<string,unknown>"
10411041
},
10421042
"propertiesToReturn": {
1043-
"description": "This is a set of properties returned from CC API that we want to return from ContextQuery.",
1043+
"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`.",
10441044
"type": "array",
10451045
"items": {
10461046
"type": "string"
10471047
}
10481048
},
1049+
"dummyValue": {
1050+
"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)"
1051+
},
1052+
"ignoreErrorOnMissingContext": {
1053+
"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.",
1054+
"default": false,
1055+
"type": "boolean"
1056+
},
10491057
"account": {
10501058
"description": "Query account",
10511059
"type": "string"
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"schemaHash": "ba7d47a7a023c39293e99a374af293384eaf1ccd207e515dbdc59dfb5cae4ed6",
3-
"revision": 41
2+
"schemaHash": "5683db246fac20b864d94d7bceef24ebda1a38c8c1f8ef0d5978534097dc9504",
3+
"revision": 42
44
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { CcApiContextQuery } from '@aws-cdk/cloud-assembly-schema';
2+
import type { ResourceDescription } from '@aws-sdk/client-cloudcontrol';
3+
import { ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol';
24
import { ContextProviderError } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api';
35
import type { ICloudControlClient } from '../api';
46
import { type SdkProvider, initContextProviderSdk } from '../api/aws-auth/sdk-provider';
@@ -11,117 +13,149 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
1113

1214
/**
1315
* This returns a data object with the value from CloudControl API result.
14-
* args.typeName - see https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html
15-
* args.exactIdentifier - use CC API getResource.
16-
* args.propertyMatch - use CCP API listResources to get resources and propertyMatch to search through the list.
17-
* args.propertiesToReturn - Properties from CC API to return.
16+
*
17+
* See the documentation in the Cloud Assembly Schema for the semantics of
18+
* each query parameter.
1819
*/
1920
public async getValue(args: CcApiContextQuery) {
20-
const cloudControl = (await initContextProviderSdk(this.aws, args)).cloudControl();
21-
22-
const result = await this.findResources(cloudControl, args);
23-
return result;
24-
}
25-
26-
private async findResources(cc: ICloudControlClient, args: CcApiContextQuery): Promise<{[key: string]: any} []> {
21+
// Validate input
2722
if (args.exactIdentifier && args.propertyMatch) {
28-
throw new ContextProviderError(`Specify either exactIdentifier or propertyMatch, but not both. Failed to find resources using CC API for type ${args.typeName}.`);
23+
throw new ContextProviderError(`Provider protocol error: specify either exactIdentifier or propertyMatch, but not both (got ${JSON.stringify(args)})`);
2924
}
30-
if (!args.exactIdentifier && !args.propertyMatch) {
31-
throw new ContextProviderError(`Neither exactIdentifier nor propertyMatch is specified. Failed to find resources using CC API for type ${args.typeName}.`);
25+
if (args.ignoreErrorOnMissingContext && args.dummyValue === undefined) {
26+
throw new ContextProviderError(`Provider protocol error: if ignoreErrorOnMissingContext is set, a dummyValue must be supplied (got ${JSON.stringify(args)})`);
3227
}
28+
if (args.dummyValue !== undefined && (!Array.isArray(args.dummyValue) || !args.dummyValue.every(isObject))) {
29+
throw new ContextProviderError(`Provider protocol error: dummyValue must be an array of objects (got ${JSON.stringify(args.dummyValue)})`);
30+
}
31+
32+
// Do the lookup
33+
const cloudControl = (await initContextProviderSdk(this.aws, args)).cloudControl();
3334

34-
if (args.exactIdentifier) {
35-
// use getResource to get the exact indentifier
36-
return this.getResource(cc, args.typeName, args.exactIdentifier, args.propertiesToReturn);
37-
} else {
38-
// use listResource
39-
return this.listResources(cc, args.typeName, args.propertyMatch!, args.propertiesToReturn);
35+
try {
36+
let resources: FoundResource[];
37+
if (args.exactIdentifier) {
38+
// use getResource to get the exact indentifier
39+
resources = await this.getResource(cloudControl, args.typeName, args.exactIdentifier);
40+
} else if (args.propertyMatch) {
41+
// use listResource
42+
resources = await this.listResources(cloudControl, args.typeName, args.propertyMatch);
43+
} else {
44+
throw new ContextProviderError(`Provider protocol error: neither exactIdentifier nor propertyMatch is specified in ${JSON.stringify(args)}.`);
45+
}
46+
47+
return resources.map((r) => getResultObj(r.properties, r.identifier, args.propertiesToReturn));
48+
} catch (err) {
49+
if (err instanceof ZeroResourcesFoundError && args.ignoreErrorOnMissingContext) {
50+
// We've already type-checked dummyValue.
51+
return args.dummyValue;
52+
}
53+
throw err;
4054
}
4155
}
4256

4357
/**
4458
* Calls getResource from CC API to get the resource.
4559
* See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/get-resource.html
4660
*
47-
* If the exactIdentifier is not found, then an empty map is returned.
48-
* If the resource is found, then a map of the identifier to a map of property values is returned.
61+
* Will always return exactly one resource, or fail.
4962
*/
5063
private async getResource(
5164
cc: ICloudControlClient,
5265
typeName: string,
5366
exactIdentifier: string,
54-
propertiesToReturn: string[],
55-
): Promise<{[key: string]: any}[]> {
56-
const resultObjs: {[key: string]: any}[] = [];
67+
): Promise<FoundResource[]> {
5768
try {
5869
const result = await cc.getResource({
5970
TypeName: typeName,
6071
Identifier: exactIdentifier,
6172
});
62-
const id = result.ResourceDescription?.Identifier ?? '';
63-
if (id !== '') {
64-
const propsObject = JSON.parse(result.ResourceDescription?.Properties ?? '');
65-
const propsObj = getResultObj(propsObject, result.ResourceDescription?.Identifier!, propertiesToReturn);
66-
resultObjs.push(propsObj);
67-
} else {
68-
throw new ContextProviderError(`Could not get resource ${exactIdentifier}.`);
73+
if (!result.ResourceDescription) {
74+
throw new ContextProviderError('Unexpected CloudControl API behavior: returned empty response');
6975
}
70-
} catch (err) {
71-
throw new ContextProviderError(`Encountered CC API error while getting resource ${exactIdentifier}. Error: ${err}`);
76+
77+
return [foundResourceFromCcApi(result.ResourceDescription)];
78+
} catch (err: any) {
79+
if (err instanceof ResourceNotFoundException || (err as any).name === 'ResourceNotFoundException') {
80+
throw new ZeroResourcesFoundError(`No resource of type ${typeName} with identifier: ${exactIdentifier}`);
81+
}
82+
if (!(err instanceof ContextProviderError)) {
83+
throw new ContextProviderError(`Encountered CC API error while getting ${typeName} resource ${exactIdentifier}: ${err.message}`);
84+
}
85+
throw err;
7286
}
73-
return resultObjs;
7487
}
7588

7689
/**
7790
* Calls listResources from CC API to get the resources and apply args.propertyMatch to find the resources.
7891
* See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/list-resources.html
7992
*
80-
* Since exactIdentifier is not specified, propertyMatch must be specified.
81-
* This returns an object where the ids are object keys and values are objects with keys of args.propertiesToReturn.
93+
* Will return 0 or more resources.
94+
*
95+
* Does not currently paginate through more than one result page.
8296
*/
8397
private async listResources(
8498
cc: ICloudControlClient,
8599
typeName: string,
86100
propertyMatch: Record<string, unknown>,
87-
propertiesToReturn: string[],
88-
): Promise<{[key: string]: any}[]> {
89-
const resultObjs: {[key: string]: any}[] = [];
90-
101+
): Promise<FoundResource[]> {
91102
try {
92103
const result = await cc.listResources({
93104
TypeName: typeName,
94-
});
95-
result.ResourceDescriptions?.forEach((resource) => {
96-
const id = resource.Identifier ?? '';
97-
if (id !== '') {
98-
const propsObject = JSON.parse(resource.Properties ?? '');
99-
100-
const filters = Object.entries(propertyMatch);
101-
let match = false;
102-
if (filters) {
103-
match = filters.every((record, _index, _arr) => {
104-
const key = record[0];
105-
const expected = record[1];
106-
const actual = findJsonValue(propsObject, key);
107-
return propertyMatchesFilter(actual, expected);
108-
});
109-
110-
function propertyMatchesFilter(actual: any, expected: unknown) {
111-
// For now we just check for strict equality, but we can implement pattern matching and fuzzy matching here later
112-
return expected === actual;
113-
}
114-
}
115105

116-
if (match) {
117-
const propsObj = getResultObj(propsObject, resource.Identifier!, propertiesToReturn);
118-
resultObjs.push(propsObj);
119-
}
120-
}
121106
});
122-
} catch (err) {
123-
throw new ContextProviderError(`Could not get resources ${JSON.stringify(propertyMatch)}. Error: ${err}`);
107+
const found = (result.ResourceDescriptions ?? [])
108+
.map(foundResourceFromCcApi)
109+
.filter((r) => {
110+
return Object.entries(propertyMatch).every(([propPath, expected]) => {
111+
const actual = findJsonValue(r.properties, propPath);
112+
return propertyMatchesFilter(actual, expected);
113+
});
114+
});
115+
116+
return found;
117+
} catch (err: any) {
118+
if (!(err instanceof ContextProviderError) && !(err instanceof ZeroResourcesFoundError)) {
119+
throw new ContextProviderError(`Encountered CC API error while listing ${typeName} resources matching ${JSON.stringify(propertyMatch)}: ${err.message}`);
120+
}
121+
throw err;
124122
}
125-
return resultObjs;
126123
}
127124
}
125+
126+
/**
127+
* Convert a CC API response object into a nicer object (parse the JSON)
128+
*/
129+
function foundResourceFromCcApi(desc: ResourceDescription): FoundResource {
130+
return {
131+
identifier: desc.Identifier ?? '*MISSING*',
132+
properties: JSON.parse(desc.Properties ?? '{}'),
133+
};
134+
}
135+
136+
/**
137+
* Whether the given property value matches the given filter
138+
*
139+
* For now we just check for strict equality, but we can implement pattern matching and fuzzy matching here later
140+
*/
141+
function propertyMatchesFilter(actual: unknown, expected: unknown) {
142+
return expected === actual;
143+
}
144+
145+
function isObject(x: unknown): x is {[key: string]: unknown} {
146+
return typeof x === 'object' && x !== null && !Array.isArray(x);
147+
}
148+
149+
/**
150+
* A parsed version of the return value from CCAPI
151+
*/
152+
interface FoundResource {
153+
readonly identifier: string;
154+
readonly properties: Record<string, unknown>;
155+
}
156+
157+
/**
158+
* A specific lookup failure indicating 0 resources found that can be recovered
159+
*/
160+
class ZeroResourcesFoundError extends Error {
161+
}

0 commit comments

Comments
 (0)