Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): can ignore errors and return dummy value in CloudControl API context provider #211

Merged
merged 22 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from 17 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 @@ -365,28 +365,84 @@ 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<string, unknown>;

/**
* 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;
}


/**
* Query input for plugins
*
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 resource was not found and `ignoreErrorOnMissingContext` is true. (Default - None)"
},
"ignoreErrorOnMissingContext": {
"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": "7c650e458026e83b3de23aef106137c69f10031147873e2638b6d8e078419777",
"revision": 42
}
176 changes: 105 additions & 71 deletions packages/aws-cdk/lib/context-providers/cc-api-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,124 +4,158 @@ import type { ICloudControlClient } from '../api';
import { type SdkProvider, initContextProviderSdk } from '../api/aws-auth/sdk-provider';
import type { ContextProviderPlugin } from '../api/plugin';
import { findJsonValue, getResultObj } from '../util';
import { ResourceDescription, ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol';

export class CcApiContextProviderPlugin implements ContextProviderPlugin {
constructor(private readonly aws: SdkProvider) {
}

/**
* 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();

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)}.`);
}

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);
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;
}
}

/**
* 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<FoundResource[]> {
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<string, unknown>,
propertiesToReturn: string[],
): Promise<{[key: string]: any}[]> {
const resultObjs: {[key: string]: any}[] = [];

): Promise<FoundResource[]> {
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<string, unknown>;
}

/**
* A specific lookup failure indicating 0 resources found that can be recovered
*/
class ZeroResourcesFoundError extends Error {
}
Loading
Loading