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

refactor(codepipeline): introduce IAction and unify the Action.bind() signature #3012

Merged
merged 5 commits into from
Jun 24, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
70 changes: 38 additions & 32 deletions packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import cfn = require('@aws-cdk/aws-cloudformation');
import codepipeline = require('@aws-cdk/aws-codepipeline');
import cpactions = require('@aws-cdk/aws-codepipeline-actions');
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/core');
import cxapi = require('@aws-cdk/cx-api');
Expand All @@ -11,11 +12,6 @@ export interface PipelineDeployStackActionProps {
*/
readonly stack: cdk.Stack;

/**
* The CodePipeline stage in which to perform the deployment.
*/
readonly stage: codepipeline.IStage;

/**
* The CodePipeline artifact that holds the synthesized app, which is the
* contents of the ``<directory>`` when running ``cdk synth -o <directory>``.
Expand Down Expand Up @@ -86,42 +82,40 @@ export interface PipelineDeployStackActionProps {
}

/**
* A Construct to deploy a stack that is part of a CDK App, using CodePipeline.
* A class to deploy a stack that is part of a CDK App, using CodePipeline.
* This composite Action takes care of preparing and executing a CloudFormation ChangeSet.
*
* It currently does *not* support stacks that make use of ``Asset``s, and
* requires the deployed stack is in the same account and region where the
* CodePipeline is hosted.
*/
export class PipelineDeployStackAction extends cdk.Construct {

export class PipelineDeployStackAction implements codepipeline.IAction {
/**
* The role used by CloudFormation for the deploy action
*/
public readonly deploymentRole: iam.IRole;
private _deploymentRole: iam.IRole;

private readonly stack: cdk.Stack;
private readonly prepareChangeSetAction: cpactions.CloudFormationCreateReplaceChangeSetAction;
private readonly executeChangeSetAction: cpactions.CloudFormationExecuteChangeSetAction;

constructor(scope: cdk.Construct, id: string, props: PipelineDeployStackActionProps) {
super(scope, id);

if (props.stack.environment !== cdk.Stack.of(this).environment) {
// FIXME: Add the necessary to extend to stacks in a different account
throw new Error(`Cross-environment deployment is not supported`);
constructor(props: PipelineDeployStackActionProps) {
this.stack = props.stack;
const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA);
if (assets.length > 0) {
// FIXME: Implement the necessary actions to publish assets
throw new Error(`Cannot deploy the stack ${this.stack.stackName} because it references ${assets.length} asset(s)`);
}

const createChangeSetRunOrder = props.createChangeSetRunOrder || 1;
const executeChangeSetRunOrder = props.executeChangeSetRunOrder || (createChangeSetRunOrder + 1);

if (createChangeSetRunOrder >= executeChangeSetRunOrder) {
throw new Error(`createChangeSetRunOrder (${createChangeSetRunOrder}) must be < executeChangeSetRunOrder (${executeChangeSetRunOrder})`);
}

this.stack = props.stack;
const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet';

const capabilities = cfnCapabilities(props.adminPermissions, props.capabilities);
const changeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({
this.prepareChangeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({
actionName: 'ChangeSet',
changeSetName,
runOrder: createChangeSetRunOrder,
Expand All @@ -131,15 +125,29 @@ export class PipelineDeployStackAction extends cdk.Construct {
deploymentRole: props.role,
capabilities,
});
props.stage.addAction(changeSetAction);
props.stage.addAction(new cpactions.CloudFormationExecuteChangeSetAction({
this.executeChangeSetAction = new cpactions.CloudFormationExecuteChangeSetAction({
actionName: 'Execute',
changeSetName,
runOrder: executeChangeSetRunOrder,
stackName: props.stack.stackName,
}));
stackName: this.stack.stackName,
});
}

public bind(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
if (this.stack.environment !== cdk.Stack.of(scope).environment) {
// FIXME: Add the necessary to extend to stacks in a different account
throw new Error(`Cross-environment deployment is not supported`);
}

this.deploymentRole = changeSetAction.deploymentRole;
stage.addAction(this.prepareChangeSetAction);
this._deploymentRole = this.prepareChangeSetAction.deploymentRole;

return this.executeChangeSetAction.bind(scope, stage, options);
}

public get deploymentRole(): iam.IRole {
return this._deploymentRole;
}

/**
Expand All @@ -155,14 +163,12 @@ export class PipelineDeployStackAction extends cdk.Construct {
this.deploymentRole.addToPolicy(statement);
}

protected validate(): string[] {
const result = super.validate();
const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA);
if (assets.length > 0) {
// FIXME: Implement the necessary actions to publish assets
result.push(`Cannot deploy the stack ${this.stack.stackName} because it references ${assets.length} asset(s)`);
}
return result;
public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule {
return this.executeChangeSetAction.onStateChange(name, target, options);
}

public get actionProperties(): codepipeline.ActionProperties {
return this.executeChangeSetAction.actionProperties;
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/app-delivery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@aws-cdk/aws-codebuild": "^0.35.0",
"@aws-cdk/aws-codepipeline": "^0.35.0",
"@aws-cdk/aws-codepipeline-actions": "^0.35.0",
"@aws-cdk/aws-events": "^0.35.0",
"@aws-cdk/aws-iam": "^0.35.0",
"@aws-cdk/core": "^0.35.0",
"@aws-cdk/cx-api": "^0.35.0"
Expand Down Expand Up @@ -75,6 +76,7 @@
"@aws-cdk/aws-codebuild": "^0.35.0",
"@aws-cdk/aws-codepipeline": "^0.35.0",
"@aws-cdk/aws-codepipeline-actions": "^0.35.0",
"@aws-cdk/aws-events": "^0.35.0",
"@aws-cdk/aws-iam": "^0.35.0",
"@aws-cdk/core": "^0.35.0",
"@aws-cdk/cx-api": "^0.35.0"
Expand Down
5 changes: 2 additions & 3 deletions packages/@aws-cdk/app-delivery/test/integ.cicd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,14 @@ pipeline.addStage({
actions: [source],
});
const stage = pipeline.addStage({ stageName: 'Deploy' });
new cicd.PipelineDeployStackAction(stack, 'DeployStack', {
stage,
stage.addAction(new cicd.PipelineDeployStackAction({
stack,
changeSetName: 'CICD-ChangeSet',
createChangeSetRunOrder: 10,
executeChangeSetRunOrder: 999,
input: sourceOutput,
adminPermissions: false,
capabilities: [cfn.CloudFormationCapabilities.NONE],
});
}));

app.synth();
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import cfn = require('@aws-cdk/aws-cloudformation');
import codebuild = require('@aws-cdk/aws-codebuild');
import codepipeline = require('@aws-cdk/aws-codepipeline');
import cpactions = require('@aws-cdk/aws-codepipeline-actions');
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/core');
import { ConstructNode } from '@aws-cdk/core';
import cxapi = require('@aws-cdk/cx-api');
import fc = require('fast-check');
import nodeunit = require('nodeunit');
Expand Down Expand Up @@ -34,13 +34,14 @@ export = nodeunit.testCase({
stageName: 'FakeStage',
actions: [fakeAction],
});
new PipelineDeployStackAction(stack, 'Action', {

const deployStage = pipeline.addStage({ stageName: 'DeployStage' });
deployStage.addAction(new PipelineDeployStackAction({
changeSetName: 'ChangeSet',
input: fakeAction.outputArtifact,
stack: new cdk.Stack(app, 'DeployedStack', { env: { account: stackAccount } }),
stage: pipeline.addStage({ stageName: 'DeployStage' }),
adminPermissions: false,
});
}));
}, 'Cross-environment deployment is not supported');
}
)
Expand All @@ -63,15 +64,15 @@ export = nodeunit.testCase({
stageName: 'FakeStage',
actions: [fakeAction],
});
new PipelineDeployStackAction(stack, 'Action', {
const deployStage = pipeline.addStage({ stageName: 'DeployStage' });
deployStage.addAction(new PipelineDeployStackAction({
changeSetName: 'ChangeSet',
createChangeSetRunOrder: createRunOrder,
executeChangeSetRunOrder: executeRunOrder,
input: fakeAction.outputArtifact,
stack: new cdk.Stack(app, 'DeployedStack'),
stage: pipeline.addStage({ stageName: 'DeployStage' }),
adminPermissions: false,
});
}));
}, 'createChangeSetRunOrder must be < executeChangeSetRunOrder');
}
)
Expand Down Expand Up @@ -102,41 +103,36 @@ export = nodeunit.testCase({
const selfUpdateStage4 = pipeline.addStage({ stageName: 'SelfUpdate4' });
const selfUpdateStage5 = pipeline.addStage({ stageName: 'SelfUpdate5' });

new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
stage: selfUpdateStage1,
selfUpdateStage1.addAction(new PipelineDeployStackAction({
stack: pipelineStack,
input: selfUpdatingStack.synthesizedApp,
capabilities: [cfn.CloudFormationCapabilities.NAMED_IAM],
adminPermissions: false,
});
new PipelineDeployStackAction(pipelineStack, 'DeployStack', {
stage: selfUpdateStage2,
}));
selfUpdateStage2.addAction(new PipelineDeployStackAction({
stack: stackWithNoCapability,
input: selfUpdatingStack.synthesizedApp,
capabilities: [cfn.CloudFormationCapabilities.NONE],
adminPermissions: false,
});
new PipelineDeployStackAction(pipelineStack, 'DeployStack2', {
stage: selfUpdateStage3,
}));
selfUpdateStage3.addAction(new PipelineDeployStackAction({
stack: stackWithAnonymousCapability,
input: selfUpdatingStack.synthesizedApp,
capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM],
adminPermissions: false,
});
new PipelineDeployStackAction(pipelineStack, 'DeployStack3', {
stage: selfUpdateStage4,
}));
selfUpdateStage4.addAction(new PipelineDeployStackAction({
stack: stackWithAutoExpandCapability,
input: selfUpdatingStack.synthesizedApp,
capabilities: [cfn.CloudFormationCapabilities.AUTO_EXPAND],
adminPermissions: false,
});
new PipelineDeployStackAction(pipelineStack, 'DeployStack4', {
stage: selfUpdateStage5,
}));
selfUpdateStage5.addAction(new PipelineDeployStackAction({
stack: stackWithAnonymousAndAutoExpandCapability,
input: selfUpdatingStack.synthesizedApp,
capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM, cfn.CloudFormationCapabilities.AUTO_EXPAND],
adminPermissions: false,
});
}));
expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({
Configuration: {
StackName: "TestStack",
Expand Down Expand Up @@ -193,12 +189,11 @@ export = nodeunit.testCase({

const pipeline = selfUpdatingStack.pipeline;
const selfUpdateStage = pipeline.addStage({ stageName: 'SelfUpdate' });
new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
stage: selfUpdateStage,
selfUpdateStage.addAction(new PipelineDeployStackAction({
stack: pipelineStack,
input: selfUpdatingStack.synthesizedApp,
adminPermissions: true,
});
}));
expect(pipelineStack).to(haveResource('AWS::IAM::Policy', {
PolicyDocument: {
Version: '2012-10-17',
Expand Down Expand Up @@ -229,13 +224,13 @@ export = nodeunit.testCase({
});
const pipeline = selfUpdatingStack.pipeline;
const selfUpdateStage = pipeline.addStage({ stageName: 'SelfUpdate' });
const deployAction = new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
stage: selfUpdateStage,
const deployAction = new PipelineDeployStackAction({
stack: pipelineStack,
input: selfUpdatingStack.synthesizedApp,
adminPermissions: false,
role
});
selfUpdateStage.addAction(deployAction);
test.same(deployAction.deploymentRole, role);
test.done();
},
Expand All @@ -252,12 +247,12 @@ export = nodeunit.testCase({
// WHEN //
// this our app/service/infra to deploy
const deployStage = pipeline.addStage({ stageName: 'Deploy' });
const deployAction = new PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', {
stage: deployStage,
const deployAction = new PipelineDeployStackAction({
stack: emptyStack,
input: selfUpdatingStack.synthesizedApp,
adminPermissions: false,
});
deployStage.addAction(deployAction);
// we might need to add permissions
deployAction.addToDeploymentRolePolicy(new iam.PolicyStatement({
actions: [
Expand Down Expand Up @@ -309,50 +304,48 @@ export = nodeunit.testCase({
fc.integer(1, 5),
(assetCount) => {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Test');
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');
const fakeAction = new FakeAction('Fake');
pipeline.addStage({
stageName: 'FakeStage',
actions: [fakeAction],
});

const deployedStack = new cdk.Stack(app, 'DeployedStack');
const deployStage = pipeline.addStage({ stageName: 'DeployStage' });
const action = new PipelineDeployStackAction(stack, 'Action', {
changeSetName: 'ChangeSet',
input: fakeAction.outputArtifact,
stack: deployedStack,
stage: deployStage,
adminPermissions: false,
});
for (let i = 0 ; i < assetCount ; i++) {
deployedStack.node.addMetadata(cxapi.ASSET_METADATA, {});
}
test.deepEqual(ConstructNode.validate(action.node).map(x => x.message),
[`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]);

test.throws(() => {
new PipelineDeployStackAction({
changeSetName: 'ChangeSet',
input: new codepipeline.Artifact(),
stack: deployedStack,
adminPermissions: false,
});
}, /Cannot deploy the stack DeployedStack because it references/);
}
)
);
test.done();
}
});

class FakeAction extends codepipeline.Action {
class FakeAction implements codepipeline.IAction {
public readonly actionProperties: codepipeline.ActionProperties;
public readonly outputArtifact: codepipeline.Artifact;

constructor(actionName: string) {
super({
this.actionProperties = {
actionName,
artifactBounds: { minInputs: 0, maxInputs: 5, minOutputs: 0, maxOutputs: 5 },
category: codepipeline.ActionCategory.TEST,
provider: 'Test',
});

};
this.outputArtifact = new codepipeline.Artifact('OutputArtifact');
}

protected bind(_info: codepipeline.ActionBind): void {
// do nothing
public bind(_scope: cdk.Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
return {};
}

public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule {
throw new Error('onStateChange() is not available on FakeAction');
}
}

Expand Down
Loading