From 0c3c53025d3adbbea3312eb727fcb3758caec2ce Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Tue, 19 May 2020 17:19:56 -0700 Subject: [PATCH] feat(codepipeline): use a special bootstrapless synthesizer for cross-region support Stacks Fixes #8082 --- .../lib/cross-region-support-stack.ts | 3 ++ .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 23 ++++++++- .../@aws-cdk/aws-codepipeline/package.json | 1 + .../aws-codepipeline/test/test.pipeline.ts | 43 +++++++++++++++- .../bootstrapless-synthesizer.ts | 51 +++++++++++++++++++ .../stack-synthesizers/default-synthesizer.ts | 40 ++++++++++++--- .../core/lib/stack-synthesizers/index.ts | 3 +- 7 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts diff --git a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts index 47227f4fb689d..00d0c5ca29493 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts @@ -71,6 +71,8 @@ export interface CrossRegionSupportStackProps { * @example '012345678901' */ readonly account: string; + + readonly synthesizer: cdk.IStackSynthesizer | undefined; } /** @@ -90,6 +92,7 @@ export class CrossRegionSupportStack extends cdk.Stack { region: props.region, account: props.account, }, + synthesizer: props.synthesizer, }); const crossRegionSupportConstruct = new CrossRegionSupportConstruct(this, 'Default'); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 05b4c174f6aa6..26c86887e98bc 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -2,7 +2,10 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; -import { App, Construct, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { + App, BootstraplessSynthesizer, Construct, DefaultStackSynthesizer, + IStackSynthesizer, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token, +} from '@aws-cdk/core'; import { ActionCategory, IAction, IPipeline, IStage } from './action'; import { CfnPipeline } from './codepipeline.generated'; import { CrossRegionSupportConstruct, CrossRegionSupportStack } from './cross-region-support-stack'; @@ -483,6 +486,7 @@ export class Pipeline extends PipelineBase { pipelineStackName: pipelineStack.stackName, region: actionRegion, account: pipelineAccount, + synthesizer: this.getCrossRegionSupportSynthesizer(), }); } @@ -492,6 +496,23 @@ export class Pipeline extends PipelineBase { }; } + private getCrossRegionSupportSynthesizer(): IStackSynthesizer | undefined { + if (this.stack.synthesizer instanceof DefaultStackSynthesizer) { + // if we have the new synthesizer, + // we need a bootstrapless copy of it, + // because we don't want to require bootstrapping the environment + // of the pipeline account in this replication region + return new BootstraplessSynthesizer({ + deployRoleArn: this.stack.synthesizer.deployRoleArn, + cloudFormationExecutionRoleArn: this.stack.synthesizer.cloudFormationExecutionRoleArn, + }); + } else { + // any other synthesizer: just return undefined + // (ie., use the default based on the context settings) + return undefined; + } + } + private generateNameForDefaultBucketKeyAlias(): string { const prefix = 'alias/codepipeline-'; const maxAliasLength = 256; diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 36154be191da0..0a94e85b6a724 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -68,6 +68,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 153e24d882f8a..5d1c91edd51af 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -3,6 +3,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; @@ -46,7 +47,7 @@ export = { }, 'that is cross-region': { - 'validates that source actions are in the same account as the pipeline'(test: Test) { + 'validates that source actions are in the same region as the pipeline'(test: Test) { const app = new cdk.App(); const stack = new cdk.Stack(app, 'PipelineStack', { env: { region: 'us-west-1', account: '123456789012' }}); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -296,6 +297,46 @@ export = { test.done(); }, + + 'generates the support stack containing the replication Bucket without the need to bootstrap in that environment'(test: Test) { + const app = new cdk.App({ + treeMetadata: false, // we can't set the context otherwise, because App will have a child + }); + app.node.setContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT, true); + + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { + env: { region: 'us-west-2', account: '123456789012' }, + }); + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(pipelineStack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'Source', + output: sourceOutput, + })], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: sourceOutput, + region: 'eu-south-1', + })], + }, + ], + }); + + const assembly = app.synth(); + const supportStackArtifact = assembly.getStackByName('PipelineStack-support-eu-south-1'); + test.equal(supportStackArtifact.assumeRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-us-west-2'); + test.equal(supportStackArtifact.cloudFormationExecutionRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-cfn-exec-role-123456789012-us-west-2'); + + test.done(); + }, }, 'that is cross-account': { diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts new file mode 100644 index 0000000000000..9a331907e8c4d --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -0,0 +1,51 @@ +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { ISynthesisSession } from '../construct-compat'; +import { addStackArtifactToAssembly, assertBound } from './_shared'; +import { DefaultStackSynthesizer } from './default-synthesizer'; + +/** + * Construction properties of {@link BootstraplessSynthesizer}. + */ +export interface BootstraplessSynthesizerProps { + /** The deploy Role ARN to use. */ + readonly deployRoleArn: string; + + /** The CFN execution Role ARN to use. */ + readonly cloudFormationExecutionRoleArn: string; +} + +/** + * A special synthesizer that behaves similarly to DefaultStackSynthesizer, + * but doesn't require bootstrapping the environment it operates in. + * Because of that, stacks using it cannot have assets inside of them. + * Used by the CodePipeline construct for the support stacks needed for + * cross-region replication S3 buckets. + */ +export class BootstraplessSynthesizer extends DefaultStackSynthesizer { + constructor(props: BootstraplessSynthesizerProps) { + super({ + deployRoleArn: props.deployRoleArn, + cloudFormationExecutionRole: props.cloudFormationExecutionRoleArn, + }); + } + + public addFileAsset(_asset: FileAssetSource): FileAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public addDockerImageAsset(_asset: DockerImageAssetSource): DockerImageAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public synthesizeStackArtifacts(session: ISynthesisSession): void { + assertBound(this.stack); + + // do _not_ treat the template as an asset, + // because this synthesizer doesn't have a bootstrap bucket to put it in + addStackArtifactToAssembly(session, this.stack, { + assumeRoleArn: this.deployRoleArn, + cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, + requiresBootstrapStackVersion: 1, + }, []); + } +} 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 13b65a5d8613c..ace086a9c4bd3 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -140,11 +140,11 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { */ public static readonly DEFAULT_FILE_ASSETS_BUCKET_NAME = 'cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region}'; - private stack?: Stack; + private _stack?: Stack; private bucketName?: string; private repositoryName?: string; - private deployRoleArn?: string; - private cloudFormationExecutionRoleArn?: string; + private _deployRoleArn?: string; + private _cloudFormationExecutionRoleArn?: string; private assetPublishingRoleArn?: string; private readonly files: NonNullable = {}; @@ -154,7 +154,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { } public bind(stack: Stack): void { - this.stack = stack; + this._stack = stack; const qualifier = this.props.qualifier ?? stack.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT) ?? DefaultStackSynthesizer.DEFAULT_QUALIFIER; @@ -176,8 +176,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { // tslint:disable:max-line-length this.bucketName = specialize(this.props.fileAssetsBucketName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME); this.repositoryName = specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); - this.deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); - this.cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); + this._deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); + this._cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); this.assetPublishingRoleArn = specialize(this.props.assetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN); // tslint:enable:max-line-length } @@ -259,13 +259,37 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { const artifactId = this.writeAssetManifest(session); addStackArtifactToAssembly(session, this.stack, { - assumeRoleArn: this.deployRoleArn, - cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, + assumeRoleArn: this._deployRoleArn, + cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, requiresBootstrapStackVersion: 1, }, [artifactId]); } + /** + * Returns the ARN of the deploy Role. + */ + public get deployRoleArn(): string { + if (!this._deployRoleArn) { + throw new Error('deployRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._deployRoleArn; + } + + /** + * Returns the ARN of the CFN execution Role. + */ + public get cloudFormationExecutionRoleArn(): string { + if (!this._cloudFormationExecutionRoleArn) { + throw new Error('cloudFormationExecutionRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._cloudFormationExecutionRoleArn; + } + + protected get stack(): Stack | undefined { + return this._stack; + } + /** * Add the stack's template as one of the manifest assets * diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts index 5920f19bae2c9..b4ad67384729d 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts @@ -1,4 +1,5 @@ export * from './types'; export * from './default-synthesizer'; export * from './legacy'; -export * from './nested'; \ No newline at end of file +export * from './bootstrapless-synthesizer'; +export * from './nested';