From 4dd29a3a2cfc0b57901449da27f0e911e188962a Mon Sep 17 00:00:00 2001 From: Damian Silbergleith <78872820+dsilbergleithcu-godaddy@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:56:20 -0700 Subject: [PATCH] Update ssm-context to prevent raising an error on missing parameter Updates StringParameter.valueFromLookup with an optional "defaultValue" When specified this value will be used: - in place of the standard dummyValue - to signal that an Error should not be raised during synthesis if the parameter is not found in the account. Test are updated to prove that this works --- packages/aws-cdk-lib/aws-ssm/lib/parameter.ts | 9 +++++-- .../aws-ssm/test/parameter.test.ts | 27 +++++++++++++++++++ .../aws-cdk-lib/core/lib/context-provider.ts | 23 +++++++++++++--- .../lib/context-providers/ssm-parameters.ts | 11 ++++++-- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ssm/lib/parameter.ts b/packages/aws-cdk-lib/aws-ssm/lib/parameter.ts index 2e096376d67d9..f93a1cec83883 100644 --- a/packages/aws-cdk-lib/aws-ssm/lib/parameter.ts +++ b/packages/aws-cdk-lib/aws-ssm/lib/parameter.ts @@ -574,12 +574,17 @@ export class StringParameter extends ParameterBase implements IStringParameter { * * Requires that the stack this scope is defined in will have explicit * account/region information. Otherwise, it will fail during synthesis. + * + * If defaultValue is provided, it will be used as the dummyValue + * and the ContextProvider will be told NOT to raise an error on synthesis + * if the SSM Parameter is not found in the account at synth time. */ - public static valueFromLookup(scope: Construct, parameterName: string): string { + public static valueFromLookup(scope: Construct, parameterName: string, defaultValue?: string): string { const value = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.SSM_PARAMETER_PROVIDER, props: { parameterName }, - dummyValue: `dummy-value-for-${parameterName}`, + dummyValue: defaultValue || `dummy-value-for-${parameterName}`, + ignoreErrorOnMissingContext: !!defaultValue, }).value; return value; diff --git a/packages/aws-cdk-lib/aws-ssm/test/parameter.test.ts b/packages/aws-cdk-lib/aws-ssm/test/parameter.test.ts index cd855ceffca8a..7a72d8aa4869f 100644 --- a/packages/aws-cdk-lib/aws-ssm/test/parameter.test.ts +++ b/packages/aws-cdk-lib/aws-ssm/test/parameter.test.ts @@ -725,6 +725,33 @@ test('fromLookup will use the SSM context provider to read value during synthesi key: 'ssm:account=12344:parameterName=my-param-name:region=us-east-1', props: { account: '12344', + ignoreErrorOnMissingContext: false, + dummyValue: 'dummy-value-for-my-param-name', + region: 'us-east-1', + parameterName: 'my-param-name', + }, + provider: 'ssm', + }, + ]); +}); + +test('fromLookup will return defaultValue when it is provided', () => { + // GIVEN + const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + const stack = new cdk.Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' } }); + + // WHEN + const value = ssm.StringParameter.valueFromLookup(stack, 'my-param-name', 'some-default-value'); + + // THEN + expect(value).toEqual('some-default-value'); + expect(app.synth().manifest.missing).toEqual([ + { + key: 'ssm:account=12344:parameterName=my-param-name:region=us-east-1', + props: { + account: '12344', + ignoreErrorOnMissingContext: true, + dummyValue: 'some-default-value', region: 'us-east-1', parameterName: 'my-param-name', }, diff --git a/packages/aws-cdk-lib/core/lib/context-provider.ts b/packages/aws-cdk-lib/core/lib/context-provider.ts index 764c05fd947fd..fb4cdd00496c2 100644 --- a/packages/aws-cdk-lib/core/lib/context-provider.ts +++ b/packages/aws-cdk-lib/core/lib/context-provider.ts @@ -31,10 +31,17 @@ export interface GetContextKeyOptions { export interface GetContextValueOptions extends GetContextKeyOptions { /** * The value to return if the context value was not found and a missing - * context is reported. This should be a dummy value that should preferably - * fail during deployment since it represents an invalid state. + * context is reported. */ readonly dummyValue: any; + + /** + * When True, the context provider will not throw an error if missing context + * is reported + * + * @default false + */ + readonly ignoreErrorOnMissingContext?: boolean; } /** @@ -101,10 +108,20 @@ export class ContextProvider { // if context is missing or an error occurred during context retrieval, // report and return a dummy value. if (value === undefined || providerError !== undefined) { + // build a version of the props which includes the dummyValue and ignoreError flag + const extendedProps: { [p: string]: any } = { + dummyValue: options.dummyValue, + ignoreErrorOnMissingContext: options.ignoreErrorOnMissingContext, + ...props, + }; + + // We'll store the extendedProps in the missingContextKey report + // so that we can retrieve the dummyValue and ignoreError flag + // in the aws-cdk's ssm-context provider stack.reportMissingContextKey({ key, provider: options.provider as cxschema.ContextProvider, - props: props as cxschema.ContextQueryProperties, + props: extendedProps as cxschema.ContextQueryProperties, }); if (providerError !== undefined) { diff --git a/packages/aws-cdk/lib/context-providers/ssm-parameters.ts b/packages/aws-cdk/lib/context-providers/ssm-parameters.ts index 5adaa00116d30..d0f2bbfbbbf44 100644 --- a/packages/aws-cdk/lib/context-providers/ssm-parameters.ts +++ b/packages/aws-cdk/lib/context-providers/ssm-parameters.ts @@ -16,6 +16,7 @@ export class SSMContextProviderPlugin implements ContextProviderPlugin { public async getValue(args: cxschema.SSMParameterContextQuery) { const region = args.region; const account = args.account; + if (!('parameterName' in args)) { throw new Error('parameterName must be provided in props for SSMContextProviderPlugin'); } @@ -23,10 +24,16 @@ export class SSMContextProviderPlugin implements ContextProviderPlugin { debug(`Reading SSM parameter ${account}:${region}:${parameterName}`); const response = await this.getSsmParameterValue(account, region, parameterName, args.lookupRoleArn); - if (!response.Parameter || response.Parameter.Value === undefined) { + const parameterNotFound: boolean = !response.Parameter || response.Parameter.Value === undefined; + const suppressError = 'ignoreErrorOnMissingContext' in args && args.ignoreErrorOnMissingContext as boolean + if (parameterNotFound && suppressError && 'dummyValue' in args) { + return args.dummyValue; + } + if (parameterNotFound) { throw new Error(`SSM parameter not available in account ${account}, region ${region}: ${parameterName}`); } - return response.Parameter.Value; + // will not be undefined because we've handled undefined cases above + return response.Parameter!.Value; } /**