Skip to content

Commit

Permalink
feat(codepipeline): use a special bootstrapless synthesizer for cross…
Browse files Browse the repository at this point in the history
…-region support Stacks

Fixes aws#8082
  • Loading branch information
skinny85 committed May 21, 2020
1 parent 60814b7 commit 0c3c530
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export interface CrossRegionSupportStackProps {
* @example '012345678901'
*/
readonly account: string;

readonly synthesizer: cdk.IStackSynthesizer | undefined;
}

/**
Expand All @@ -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');
Expand Down
23 changes: 22 additions & 1 deletion packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -483,6 +486,7 @@ export class Pipeline extends PipelineBase {
pipelineStackName: pipelineStack.stackName,
region: actionRegion,
account: pipelineAccount,
synthesizer: this.getCrossRegionSupportSynthesizer(),
});
}

Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 42 additions & 1 deletion packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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': {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}, []);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<asset_schema.ManifestFile['files']> = {};
Expand All @@ -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;

Expand All @@ -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
}
Expand Down Expand Up @@ -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
*
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/core/lib/stack-synthesizers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './types';
export * from './default-synthesizer';
export * from './legacy';
export * from './nested';
export * from './bootstrapless-synthesizer';
export * from './nested';

0 comments on commit 0c3c530

Please sign in to comment.