From a37108cef1132d21443561cc36771a30a7a53598 Mon Sep 17 00:00:00 2001 From: Chad Rabbitt Date: Mon, 7 Jun 2021 03:11:35 -0600 Subject: [PATCH] feat: Parameterize bootstrap stack version (#14626) The hard coding of the cdk version ssm parameter limits custom bootstrap configurations. In a managed account environment, the naming may not conform to environment requirements, preventing cdk bootstrap deployment. By also allowing the parameter name to be customized, multiple bootstrap stacks could exist in the same environment, which can enable more granular management of the bootstrap resources. This implementation resolves the issue by allowing the ssm parameter name to be customized through the same mechanism as other default/modern synthesizer parameters. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../stack-synthesizers/default-synthesizer.ts | 30 +++++++++++++++---- .../new-style-synthesis.test.ts | 28 ++++++++++++++++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index dba94194d6338..1dc8ff754c99a 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -165,6 +165,15 @@ export interface DefaultStackSynthesizerProps { * @default - DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PREFIX */ readonly bucketPrefix?: string; + + /** + * Bootstrap stack version SSM parameter. + * + * The placeholder `${Qualifier}` will be replaced with the value of qualifier. + * + * @default DefaultStackSynthesizer.DEFAULT_BOOTSTRAP_STACK_VERSION_SSM_PARAMETER + */ + readonly bootstrapStackVersionSsmParameter?: string; } /** @@ -227,6 +236,11 @@ export class DefaultStackSynthesizer extends StackSynthesizer { */ public static readonly DEFAULT_FILE_ASSET_PREFIX = ''; + /** + * Default bootstrap stack version SSM parameter. + */ + public static readonly DEFAULT_BOOTSTRAP_STACK_VERSION_SSM_PARAMETER = '/cdk-bootstrap/${Qualifier}/version'; + private _stack?: Stack; private bucketName?: string; private repositoryName?: string; @@ -237,6 +251,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer { private lookupRoleArn?: string; private qualifier?: string; private bucketPrefix?: string; + private bootstrapStackVersionSsmParameter?: string; private readonly files: NonNullable = {}; private readonly dockerImages: NonNullable = {}; @@ -297,6 +312,11 @@ export class DefaultStackSynthesizer extends StackSynthesizer { this.imageAssetPublishingRoleArn = specialize(this.props.imageAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN); this.lookupRoleArn = specialize(this.props.lookupRoleArn ?? DefaultStackSynthesizer.DEFAULT_LOOKUP_ROLE_ARN); this.bucketPrefix = specialize(this.props.bucketPrefix ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PREFIX); + this.bootstrapStackVersionSsmParameter = replaceAll( + this.props.bootstrapStackVersionSsmParameter ?? DefaultStackSynthesizer.DEFAULT_BOOTSTRAP_STACK_VERSION_SSM_PARAMETER, + '${Qualifier}', + qualifier, + ); /* eslint-enable max-len */ } @@ -393,7 +413,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer { // If it's done AFTER _synthesizeTemplate(), then the template won't contain the // right constructs. if (this.props.generateBootstrapVersionRule ?? true) { - addBootstrapVersionRule(this.stack, MIN_BOOTSTRAP_STACK_VERSION, this.qualifier); + addBootstrapVersionRule(this.stack, MIN_BOOTSTRAP_STACK_VERSION, this.bootstrapStackVersionSsmParameter); } this.synthesizeStackTemplate(this.stack, session); @@ -408,7 +428,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer { cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, - bootstrapStackVersionSsmParameter: `/cdk-bootstrap/${this.qualifier}/version`, + bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, additionalDependencies: [artifactId], }); } @@ -497,7 +517,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer { properties: { file: manifestFile, requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, - bootstrapStackVersionSsmParameter: `/cdk-bootstrap/${this.qualifier}/version`, + bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, }, }); @@ -564,7 +584,7 @@ function stackLocationOrInstrinsics(stack: Stack) { * The CLI normally checks this, but in a pipeline the CLI is not involved * so we encode this rule into the template in a way that CloudFormation will check it. */ -function addBootstrapVersionRule(stack: Stack, requiredVersion: number, qualifier: string) { +function addBootstrapVersionRule(stack: Stack, requiredVersion: number, bootstrapStackVersionSsmParameter: string) { // Because of https://github.com/aws/aws-cdk/blob/master/packages/assert-internal/lib/synth-utils.ts#L74 // synthesize() may be called more than once on a stack in unit tests, and the below would break // if we execute it a second time. Guard against the constructs already existing. @@ -573,7 +593,7 @@ function addBootstrapVersionRule(stack: Stack, requiredVersion: number, qualifie const param = new CfnParameter(stack, 'BootstrapVersion', { type: 'AWS::SSM::Parameter::Value', description: 'Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store.', - default: `/cdk-bootstrap/${qualifier}/version`, + default: bootstrapStackVersionSsmParameter, }); // There is no >= check in CloudFormation, so we have to check the number diff --git a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts index 5505f6ca9e8fd..2bd29743f6941 100644 --- a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts +++ b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts @@ -101,6 +101,33 @@ nodeunitShim({ test.done(); }, + 'customize version parameter'(test: Test) { + // GIVEN + const myapp = new App(); + + // WHEN + const mystack = new Stack(myapp, 'mystack', { + synthesizer: new DefaultStackSynthesizer({ + bootstrapStackVersionSsmParameter: 'stack-version-parameter', + }), + }); + + mystack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'file-asset-hash', + }); + + // THEN + const asm = myapp.synth(); + const manifestArtifact = getAssetManifest(asm); + + // THEN - the asset manifest has an SSM parameter entry + expect(manifestArtifact.bootstrapStackVersionSsmParameter).toEqual('stack-version-parameter'); + + test.done(); + }, + 'generates missing context with the lookup role ARN as one of the missing context properties'(test: Test) { // GIVEN stack = new Stack(app, 'Stack2', { @@ -246,7 +273,6 @@ nodeunitShim({ test.done(); }, - 'synthesis with bucketPrefix'(test: Test) { // GIVEN const myapp = new App();