Skip to content

Commit 089fc93

Browse files
skinny85Elad Ben-Israel
authored andcommitted
refactor(codepipeline): introduce IAction and unify the Action.bind() signature (#3012)
This brings the Action `bind()` API in line with our conventions. It also introduces an `IAction` interface for those who want to work with the low-level Action interface. The Action class has been moved from the aws-codepipeline to the aws-codepipeline-actions module. This API is much more flexible, and I show the capabilities by changing the implementation of the `PipelineDeployStackAction` from `@app-delivery`. BREAKING CHANGE: `app-delivery.PipelineDeployStackAction` is now a `codepipeline.IAction` instead of a construct.
1 parent 758d496 commit 089fc93

35 files changed

+831
-791
lines changed

packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cfn = require('@aws-cdk/aws-cloudformation');
22
import codepipeline = require('@aws-cdk/aws-codepipeline');
33
import cpactions = require('@aws-cdk/aws-codepipeline-actions');
4+
import events = require('@aws-cdk/aws-events');
45
import iam = require('@aws-cdk/aws-iam');
56
import cdk = require('@aws-cdk/core');
67
import cxapi = require('@aws-cdk/cx-api');
@@ -11,11 +12,6 @@ export interface PipelineDeployStackActionProps {
1112
*/
1213
readonly stack: cdk.Stack;
1314

14-
/**
15-
* The CodePipeline stage in which to perform the deployment.
16-
*/
17-
readonly stage: codepipeline.IStage;
18-
1915
/**
2016
* The CodePipeline artifact that holds the synthesized app, which is the
2117
* contents of the ``<directory>`` when running ``cdk synth -o <directory>``.
@@ -86,42 +82,40 @@ export interface PipelineDeployStackActionProps {
8682
}
8783

8884
/**
89-
* A Construct to deploy a stack that is part of a CDK App, using CodePipeline.
85+
* A class to deploy a stack that is part of a CDK App, using CodePipeline.
9086
* This composite Action takes care of preparing and executing a CloudFormation ChangeSet.
9187
*
9288
* It currently does *not* support stacks that make use of ``Asset``s, and
9389
* requires the deployed stack is in the same account and region where the
9490
* CodePipeline is hosted.
9591
*/
96-
export class PipelineDeployStackAction extends cdk.Construct {
97-
92+
export class PipelineDeployStackAction implements codepipeline.IAction {
9893
/**
9994
* The role used by CloudFormation for the deploy action
10095
*/
101-
public readonly deploymentRole: iam.IRole;
96+
private _deploymentRole: iam.IRole;
10297

10398
private readonly stack: cdk.Stack;
99+
private readonly prepareChangeSetAction: cpactions.CloudFormationCreateReplaceChangeSetAction;
100+
private readonly executeChangeSetAction: cpactions.CloudFormationExecuteChangeSetAction;
104101

105-
constructor(scope: cdk.Construct, id: string, props: PipelineDeployStackActionProps) {
106-
super(scope, id);
107-
108-
if (props.stack.environment !== cdk.Stack.of(this).environment) {
109-
// FIXME: Add the necessary to extend to stacks in a different account
110-
throw new Error(`Cross-environment deployment is not supported`);
102+
constructor(props: PipelineDeployStackActionProps) {
103+
this.stack = props.stack;
104+
const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA);
105+
if (assets.length > 0) {
106+
// FIXME: Implement the necessary actions to publish assets
107+
throw new Error(`Cannot deploy the stack ${this.stack.stackName} because it references ${assets.length} asset(s)`);
111108
}
112109

113110
const createChangeSetRunOrder = props.createChangeSetRunOrder || 1;
114111
const executeChangeSetRunOrder = props.executeChangeSetRunOrder || (createChangeSetRunOrder + 1);
115-
116112
if (createChangeSetRunOrder >= executeChangeSetRunOrder) {
117113
throw new Error(`createChangeSetRunOrder (${createChangeSetRunOrder}) must be < executeChangeSetRunOrder (${executeChangeSetRunOrder})`);
118114
}
119115

120-
this.stack = props.stack;
121116
const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet';
122-
123117
const capabilities = cfnCapabilities(props.adminPermissions, props.capabilities);
124-
const changeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({
118+
this.prepareChangeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({
125119
actionName: 'ChangeSet',
126120
changeSetName,
127121
runOrder: createChangeSetRunOrder,
@@ -131,15 +125,29 @@ export class PipelineDeployStackAction extends cdk.Construct {
131125
deploymentRole: props.role,
132126
capabilities,
133127
});
134-
props.stage.addAction(changeSetAction);
135-
props.stage.addAction(new cpactions.CloudFormationExecuteChangeSetAction({
128+
this.executeChangeSetAction = new cpactions.CloudFormationExecuteChangeSetAction({
136129
actionName: 'Execute',
137130
changeSetName,
138131
runOrder: executeChangeSetRunOrder,
139-
stackName: props.stack.stackName,
140-
}));
132+
stackName: this.stack.stackName,
133+
});
134+
}
135+
136+
public bind(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
137+
codepipeline.ActionConfig {
138+
if (this.stack.environment !== cdk.Stack.of(scope).environment) {
139+
// FIXME: Add the necessary to extend to stacks in a different account
140+
throw new Error(`Cross-environment deployment is not supported`);
141+
}
141142

142-
this.deploymentRole = changeSetAction.deploymentRole;
143+
stage.addAction(this.prepareChangeSetAction);
144+
this._deploymentRole = this.prepareChangeSetAction.deploymentRole;
145+
146+
return this.executeChangeSetAction.bind(scope, stage, options);
147+
}
148+
149+
public get deploymentRole(): iam.IRole {
150+
return this._deploymentRole;
143151
}
144152

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

158-
protected validate(): string[] {
159-
const result = super.validate();
160-
const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA);
161-
if (assets.length > 0) {
162-
// FIXME: Implement the necessary actions to publish assets
163-
result.push(`Cannot deploy the stack ${this.stack.stackName} because it references ${assets.length} asset(s)`);
164-
}
165-
return result;
166+
public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule {
167+
return this.executeChangeSetAction.onStateChange(name, target, options);
168+
}
169+
170+
public get actionProperties(): codepipeline.ActionProperties {
171+
return this.executeChangeSetAction.actionProperties;
166172
}
167173
}
168174

packages/@aws-cdk/app-delivery/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@aws-cdk/aws-codebuild": "^0.35.0",
4343
"@aws-cdk/aws-codepipeline": "^0.35.0",
4444
"@aws-cdk/aws-codepipeline-actions": "^0.35.0",
45+
"@aws-cdk/aws-events": "^0.35.0",
4546
"@aws-cdk/aws-iam": "^0.35.0",
4647
"@aws-cdk/core": "^0.35.0",
4748
"@aws-cdk/cx-api": "^0.35.0"
@@ -75,6 +76,7 @@
7576
"@aws-cdk/aws-codebuild": "^0.35.0",
7677
"@aws-cdk/aws-codepipeline": "^0.35.0",
7778
"@aws-cdk/aws-codepipeline-actions": "^0.35.0",
79+
"@aws-cdk/aws-events": "^0.35.0",
7880
"@aws-cdk/aws-iam": "^0.35.0",
7981
"@aws-cdk/core": "^0.35.0",
8082
"@aws-cdk/cx-api": "^0.35.0"

packages/@aws-cdk/app-delivery/test/integ.cicd.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,14 @@ pipeline.addStage({
2727
actions: [source],
2828
});
2929
const stage = pipeline.addStage({ stageName: 'Deploy' });
30-
new cicd.PipelineDeployStackAction(stack, 'DeployStack', {
31-
stage,
30+
stage.addAction(new cicd.PipelineDeployStackAction({
3231
stack,
3332
changeSetName: 'CICD-ChangeSet',
3433
createChangeSetRunOrder: 10,
3534
executeChangeSetRunOrder: 999,
3635
input: sourceOutput,
3736
adminPermissions: false,
3837
capabilities: [cfn.CloudFormationCapabilities.NONE],
39-
});
38+
}));
4039

4140
app.synth();

packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import cfn = require('@aws-cdk/aws-cloudformation');
33
import codebuild = require('@aws-cdk/aws-codebuild');
44
import codepipeline = require('@aws-cdk/aws-codepipeline');
55
import cpactions = require('@aws-cdk/aws-codepipeline-actions');
6+
import events = require('@aws-cdk/aws-events');
67
import iam = require('@aws-cdk/aws-iam');
78
import s3 = require('@aws-cdk/aws-s3');
89
import cdk = require('@aws-cdk/core');
9-
import { ConstructNode } from '@aws-cdk/core';
1010
import cxapi = require('@aws-cdk/cx-api');
1111
import fc = require('fast-check');
1212
import nodeunit = require('nodeunit');
@@ -34,13 +34,14 @@ export = nodeunit.testCase({
3434
stageName: 'FakeStage',
3535
actions: [fakeAction],
3636
});
37-
new PipelineDeployStackAction(stack, 'Action', {
37+
38+
const deployStage = pipeline.addStage({ stageName: 'DeployStage' });
39+
deployStage.addAction(new PipelineDeployStackAction({
3840
changeSetName: 'ChangeSet',
3941
input: fakeAction.outputArtifact,
4042
stack: new cdk.Stack(app, 'DeployedStack', { env: { account: stackAccount } }),
41-
stage: pipeline.addStage({ stageName: 'DeployStage' }),
4243
adminPermissions: false,
43-
});
44+
}));
4445
}, 'Cross-environment deployment is not supported');
4546
}
4647
)
@@ -63,15 +64,15 @@ export = nodeunit.testCase({
6364
stageName: 'FakeStage',
6465
actions: [fakeAction],
6566
});
66-
new PipelineDeployStackAction(stack, 'Action', {
67+
const deployStage = pipeline.addStage({ stageName: 'DeployStage' });
68+
deployStage.addAction(new PipelineDeployStackAction({
6769
changeSetName: 'ChangeSet',
6870
createChangeSetRunOrder: createRunOrder,
6971
executeChangeSetRunOrder: executeRunOrder,
7072
input: fakeAction.outputArtifact,
7173
stack: new cdk.Stack(app, 'DeployedStack'),
72-
stage: pipeline.addStage({ stageName: 'DeployStage' }),
7374
adminPermissions: false,
74-
});
75+
}));
7576
}, 'createChangeSetRunOrder must be < executeChangeSetRunOrder');
7677
}
7778
)
@@ -102,41 +103,36 @@ export = nodeunit.testCase({
102103
const selfUpdateStage4 = pipeline.addStage({ stageName: 'SelfUpdate4' });
103104
const selfUpdateStage5 = pipeline.addStage({ stageName: 'SelfUpdate5' });
104105

105-
new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
106-
stage: selfUpdateStage1,
106+
selfUpdateStage1.addAction(new PipelineDeployStackAction({
107107
stack: pipelineStack,
108108
input: selfUpdatingStack.synthesizedApp,
109109
capabilities: [cfn.CloudFormationCapabilities.NAMED_IAM],
110110
adminPermissions: false,
111-
});
112-
new PipelineDeployStackAction(pipelineStack, 'DeployStack', {
113-
stage: selfUpdateStage2,
111+
}));
112+
selfUpdateStage2.addAction(new PipelineDeployStackAction({
114113
stack: stackWithNoCapability,
115114
input: selfUpdatingStack.synthesizedApp,
116115
capabilities: [cfn.CloudFormationCapabilities.NONE],
117116
adminPermissions: false,
118-
});
119-
new PipelineDeployStackAction(pipelineStack, 'DeployStack2', {
120-
stage: selfUpdateStage3,
117+
}));
118+
selfUpdateStage3.addAction(new PipelineDeployStackAction({
121119
stack: stackWithAnonymousCapability,
122120
input: selfUpdatingStack.synthesizedApp,
123121
capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM],
124122
adminPermissions: false,
125-
});
126-
new PipelineDeployStackAction(pipelineStack, 'DeployStack3', {
127-
stage: selfUpdateStage4,
123+
}));
124+
selfUpdateStage4.addAction(new PipelineDeployStackAction({
128125
stack: stackWithAutoExpandCapability,
129126
input: selfUpdatingStack.synthesizedApp,
130127
capabilities: [cfn.CloudFormationCapabilities.AUTO_EXPAND],
131128
adminPermissions: false,
132-
});
133-
new PipelineDeployStackAction(pipelineStack, 'DeployStack4', {
134-
stage: selfUpdateStage5,
129+
}));
130+
selfUpdateStage5.addAction(new PipelineDeployStackAction({
135131
stack: stackWithAnonymousAndAutoExpandCapability,
136132
input: selfUpdatingStack.synthesizedApp,
137133
capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM, cfn.CloudFormationCapabilities.AUTO_EXPAND],
138134
adminPermissions: false,
139-
});
135+
}));
140136
expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({
141137
Configuration: {
142138
StackName: "TestStack",
@@ -193,12 +189,11 @@ export = nodeunit.testCase({
193189

194190
const pipeline = selfUpdatingStack.pipeline;
195191
const selfUpdateStage = pipeline.addStage({ stageName: 'SelfUpdate' });
196-
new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
197-
stage: selfUpdateStage,
192+
selfUpdateStage.addAction(new PipelineDeployStackAction({
198193
stack: pipelineStack,
199194
input: selfUpdatingStack.synthesizedApp,
200195
adminPermissions: true,
201-
});
196+
}));
202197
expect(pipelineStack).to(haveResource('AWS::IAM::Policy', {
203198
PolicyDocument: {
204199
Version: '2012-10-17',
@@ -229,13 +224,13 @@ export = nodeunit.testCase({
229224
});
230225
const pipeline = selfUpdatingStack.pipeline;
231226
const selfUpdateStage = pipeline.addStage({ stageName: 'SelfUpdate' });
232-
const deployAction = new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
233-
stage: selfUpdateStage,
227+
const deployAction = new PipelineDeployStackAction({
234228
stack: pipelineStack,
235229
input: selfUpdatingStack.synthesizedApp,
236230
adminPermissions: false,
237231
role
238232
});
233+
selfUpdateStage.addAction(deployAction);
239234
test.same(deployAction.deploymentRole, role);
240235
test.done();
241236
},
@@ -252,12 +247,12 @@ export = nodeunit.testCase({
252247
// WHEN //
253248
// this our app/service/infra to deploy
254249
const deployStage = pipeline.addStage({ stageName: 'Deploy' });
255-
const deployAction = new PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', {
256-
stage: deployStage,
250+
const deployAction = new PipelineDeployStackAction({
257251
stack: emptyStack,
258252
input: selfUpdatingStack.synthesizedApp,
259253
adminPermissions: false,
260254
});
255+
deployStage.addAction(deployAction);
261256
// we might need to add permissions
262257
deployAction.addToDeploymentRolePolicy(new iam.PolicyStatement({
263258
actions: [
@@ -309,50 +304,48 @@ export = nodeunit.testCase({
309304
fc.integer(1, 5),
310305
(assetCount) => {
311306
const app = new cdk.App();
312-
const stack = new cdk.Stack(app, 'Test');
313-
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');
314-
const fakeAction = new FakeAction('Fake');
315-
pipeline.addStage({
316-
stageName: 'FakeStage',
317-
actions: [fakeAction],
318-
});
307+
319308
const deployedStack = new cdk.Stack(app, 'DeployedStack');
320-
const deployStage = pipeline.addStage({ stageName: 'DeployStage' });
321-
const action = new PipelineDeployStackAction(stack, 'Action', {
322-
changeSetName: 'ChangeSet',
323-
input: fakeAction.outputArtifact,
324-
stack: deployedStack,
325-
stage: deployStage,
326-
adminPermissions: false,
327-
});
328309
for (let i = 0 ; i < assetCount ; i++) {
329310
deployedStack.node.addMetadata(cxapi.ASSET_METADATA, {});
330311
}
331-
test.deepEqual(ConstructNode.validate(action.node).map(x => x.message),
332-
[`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]);
312+
313+
test.throws(() => {
314+
new PipelineDeployStackAction({
315+
changeSetName: 'ChangeSet',
316+
input: new codepipeline.Artifact(),
317+
stack: deployedStack,
318+
adminPermissions: false,
319+
});
320+
}, /Cannot deploy the stack DeployedStack because it references/);
333321
}
334322
)
335323
);
336324
test.done();
337325
}
338326
});
339327

340-
class FakeAction extends codepipeline.Action {
328+
class FakeAction implements codepipeline.IAction {
329+
public readonly actionProperties: codepipeline.ActionProperties;
341330
public readonly outputArtifact: codepipeline.Artifact;
342331

343332
constructor(actionName: string) {
344-
super({
333+
this.actionProperties = {
345334
actionName,
346335
artifactBounds: { minInputs: 0, maxInputs: 5, minOutputs: 0, maxOutputs: 5 },
347336
category: codepipeline.ActionCategory.TEST,
348337
provider: 'Test',
349-
});
350-
338+
};
351339
this.outputArtifact = new codepipeline.Artifact('OutputArtifact');
352340
}
353341

354-
protected bind(_info: codepipeline.ActionBind): void {
355-
// do nothing
342+
public bind(_scope: cdk.Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions):
343+
codepipeline.ActionConfig {
344+
return {};
345+
}
346+
347+
public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule {
348+
throw new Error('onStateChange() is not available on FakeAction');
356349
}
357350
}
358351

0 commit comments

Comments
 (0)