Skip to content

Commit 85ff945

Browse files
committed
feat(pipelines): add control over underlying CodePipeline
For people with specific requirements: * Allow supplying an existing (preconfigured) CodePipeline object, via the `codePipeline` argument. This pipeline may already have Source and Build stages, in which case `sourceAction` and `synthAction` are no longer required. * Allow access to the underlying CodePipeline object via the `.codePipeline` property, and allow modifying it via `pipeline.stage("Source").addAction(...)`. Fixes #9021.
1 parent b757d88 commit 85ff945

File tree

7 files changed

+333
-46
lines changed

7 files changed

+333
-46
lines changed

packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts

+12
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,18 @@ export class Pipeline extends PipelineBase {
336336
return this._stages.slice();
337337
}
338338

339+
/**
340+
* Access one of the pipeline's stages by stage name
341+
*/
342+
public stage(stageName: string): IStage {
343+
for (const stage of this._stages) {
344+
if (stage.stageName === stageName) {
345+
return stage;
346+
}
347+
}
348+
throw new Error(`Pipeline does not contain a stage named '${stageName}'. Available stages: ${this._stages.map(s => s.stageName).join(', ')}`);
349+
}
350+
339351
/**
340352
* Returns all of the {@link CrossRegionSupportStack}s that were generated automatically
341353
* when dealing with Actions that reside in a different region than the Pipeline itself.

packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts

+52-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { expect, haveResourceLike, ResourcePart } from '@aws-cdk/assert';
1+
import { expect as ourExpect, ResourcePart, arrayWith, objectLike, haveResourceLike } from '@aws-cdk/assert';
2+
import '@aws-cdk/assert/jest';
23
import * as iam from '@aws-cdk/aws-iam';
34
import * as kms from '@aws-cdk/aws-kms';
45
import * as s3 from '@aws-cdk/aws-s3';
@@ -22,7 +23,7 @@ nodeunitShim({
2223
role,
2324
});
2425

25-
expect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
26+
ourExpect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
2627
'RoleArn': {
2728
'Fn::GetAtt': [
2829
'Role1ABCC5F0',
@@ -109,7 +110,7 @@ nodeunitShim({
109110
],
110111
});
111112

112-
expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
113+
expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
113114
'ArtifactStores': [
114115
{
115116
'Region': replicationRegion,
@@ -136,9 +137,9 @@ nodeunitShim({
136137
'Region': pipelineRegion,
137138
},
138139
],
139-
}));
140+
});
140141

141-
expect(replicationStack).to(haveResourceLike('AWS::KMS::Key', {
142+
expect(replicationStack).toHaveResourceLike('AWS::KMS::Key', {
142143
'KeyPolicy': {
143144
'Statement': [
144145
{
@@ -170,7 +171,7 @@ nodeunitShim({
170171
},
171172
],
172173
},
173-
}));
174+
});
174175

175176
test.done();
176177
},
@@ -204,7 +205,7 @@ nodeunitShim({
204205
],
205206
});
206207

207-
expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
208+
expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
208209
'ArtifactStores': [
209210
{
210211
'Region': replicationRegion,
@@ -231,12 +232,12 @@ nodeunitShim({
231232
'Region': pipelineRegion,
232233
},
233234
],
234-
}));
235+
});
235236

236-
expect(pipeline.crossRegionSupport[replicationRegion].stack).to(haveResourceLike('AWS::KMS::Alias', {
237+
expect(pipeline.crossRegionSupport[replicationRegion].stack).toHaveResourceLike('AWS::KMS::Alias', {
237238
'DeletionPolicy': 'Delete',
238239
'UpdateReplacePolicy': 'Delete',
239-
}, ResourcePart.CompleteDefinition));
240+
}, ResourcePart.CompleteDefinition);
240241

241242
test.done();
242243
},
@@ -276,7 +277,7 @@ nodeunitShim({
276277
],
277278
});
278279

279-
expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
280+
expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
280281
'ArtifactStores': [
281282
{
282283
'Region': replicationRegion,
@@ -293,7 +294,7 @@ nodeunitShim({
293294
'Region': pipelineRegion,
294295
},
295296
],
296-
}));
297+
});
297298

298299
test.done();
299300
},
@@ -392,3 +393,42 @@ nodeunitShim({
392393
},
393394
},
394395
});
396+
397+
describe('test with shared setup', () => {
398+
let stack: cdk.Stack;
399+
let sourceArtifact: codepipeline.Artifact;
400+
beforeEach(() => {
401+
stack = new cdk.Stack();
402+
sourceArtifact = new codepipeline.Artifact();
403+
});
404+
405+
test('can add actions to stages after creation', () => {
406+
// GIVEN
407+
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
408+
stages: [
409+
{
410+
stageName: 'Source',
411+
actions: [new FakeSourceAction({ actionName: 'Fetch', output: sourceArtifact })],
412+
},
413+
{
414+
stageName: 'Build',
415+
actions: [new FakeBuildAction({ actionName: 'Gcc', input: sourceArtifact })],
416+
},
417+
],
418+
});
419+
420+
// WHEN
421+
pipeline.stage('Build').addAction(new FakeBuildAction({ actionName: 'debug.com', input: sourceArtifact }));
422+
423+
// THEN
424+
expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
425+
Stages: arrayWith({
426+
Name: 'Build',
427+
Actions: [
428+
objectLike({ Name: 'Gcc' }),
429+
objectLike({ Name: 'debug.com' }),
430+
],
431+
}),
432+
});
433+
});
434+
});

packages/@aws-cdk/pipelines/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,29 @@ new MyPipelineStack(this, 'PipelineStack', {
152152
});
153153
```
154154

155+
If you prefer more control over the underlying CodePipeline object, you can
156+
create one yourself, including custom Source and Build stages:
157+
158+
```ts
159+
const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', {
160+
stages: [
161+
{
162+
stageName: 'CustomSource',
163+
actions: [...],
164+
},
165+
{
166+
stageName: 'CustomBuild',
167+
actions: [...],
168+
},
169+
],
170+
});
171+
172+
const cdkPipeline = new CdkPipeline(this, 'CdkPipeline', {
173+
codePipeline,
174+
cloudAssemblyArtifact,
175+
});
176+
```
177+
155178
## Initial pipeline deployment
156179

157180
You provision this pipeline by making sure the target environment has been

packages/@aws-cdk/pipelines/lib/pipeline.ts

+79-25
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,37 @@ import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from '
1212
export interface CdkPipelineProps {
1313
/**
1414
* The CodePipeline action used to retrieve the CDK app's source
15+
*
16+
* @default - Required unless `codePipeline` is given
1517
*/
16-
readonly sourceAction: codepipeline.IAction;
18+
readonly sourceAction?: codepipeline.IAction;
1719

1820
/**
1921
* The CodePipeline action build and synthesis step of the CDK app
22+
*
23+
* @default - Required unless `codePipeline` or `sourceAction` is given
2024
*/
21-
readonly synthAction: codepipeline.IAction;
25+
readonly synthAction?: codepipeline.IAction;
2226

2327
/**
2428
* The artifact you have defined to be the artifact to hold the cloudAssemblyArtifact for the synth action
2529
*/
2630
readonly cloudAssemblyArtifact: codepipeline.Artifact;
2731

32+
/**
33+
* Existing CodePipeline to modify
34+
*
35+
* The Pipeline should have been created with `restartExecutionOnUpdate: true`.
36+
*
37+
* @default - A new CodePipeline is automatically generated
38+
*/
39+
readonly codePipeline?: codepipeline.Pipeline;
40+
2841
/**
2942
* Name of the pipeline
3043
*
44+
* Can only be set if `codePipeline` is not set.
45+
*
3146
* @default - A name is automatically generated
3247
*/
3348
readonly pipelineName?: string;
@@ -72,28 +87,58 @@ export class CdkPipeline extends Construct {
7287
this._cloudAssemblyArtifact = props.cloudAssemblyArtifact;
7388
const pipelineStack = Stack.of(this);
7489

75-
this._pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
76-
...props,
77-
restartExecutionOnUpdate: true,
78-
stages: [
79-
{
80-
stageName: 'Source',
81-
actions: [props.sourceAction],
82-
},
83-
{
84-
stageName: 'Build',
85-
actions: [props.synthAction],
86-
},
87-
{
88-
stageName: 'UpdatePipeline',
89-
actions: [new UpdatePipelineAction(this, 'UpdatePipeline', {
90-
cloudAssemblyInput: this._cloudAssemblyArtifact,
91-
pipelineStackName: pipelineStack.stackName,
92-
cdkCliVersion: props.cdkCliVersion,
93-
projectName: maybeSuffix(props.pipelineName, '-selfupdate'),
94-
})],
95-
},
96-
],
90+
if (props.codePipeline) {
91+
if (props.pipelineName) {
92+
throw new Error('Cannot set \'pipelineName\' if an existing CodePipeline is given using \'codePipeline\'');
93+
}
94+
95+
this._pipeline = props.codePipeline;
96+
} else {
97+
this._pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
98+
pipelineName: props.pipelineName,
99+
restartExecutionOnUpdate: true,
100+
});
101+
}
102+
103+
if (props.sourceAction && !props.synthAction) {
104+
// Because of ordering limitations, you can: bring your own Source, bring your own
105+
// Both, or bring your own Nothing. You cannot bring your own Build (which because of the
106+
// current CodePipeline API must go BEFORE what we're adding) and then having us add a
107+
// Source after it. That doesn't make any sense.
108+
throw new Error('When passing a \'sourceAction\' you must also pass a \'synthAction\' (or a \'codePipeline\' that already has both)');
109+
}
110+
if (!props.sourceAction && (!props.codePipeline || props.codePipeline.stages.length < 1)) {
111+
throw new Error('You must pass a \'sourceAction\' (or a \'codePipeline\' that already has a Source stage)');
112+
}
113+
if (!props.synthAction && (!props.codePipeline || props.codePipeline.stages.length < 2)) {
114+
// This looks like a weirdly specific requirement, but actually the underlying CodePipeline
115+
// requires that a Pipeline has at least 2 stages. We're just hitching onto upstream
116+
// requirements to do this check.
117+
throw new Error('You must pass a \'synthAction\' (or a \'codePipeline\' that already has a Build stage)');
118+
}
119+
120+
if (props.sourceAction) {
121+
this._pipeline.addStage({
122+
stageName: 'Source',
123+
actions: [props.sourceAction],
124+
});
125+
}
126+
127+
if (props.synthAction) {
128+
this._pipeline.addStage({
129+
stageName: 'Build',
130+
actions: [props.synthAction],
131+
});
132+
}
133+
134+
this._pipeline.addStage({
135+
stageName: 'UpdatePipeline',
136+
actions: [new UpdatePipelineAction(this, 'UpdatePipeline', {
137+
cloudAssemblyInput: this._cloudAssemblyArtifact,
138+
pipelineStackName: pipelineStack.stackName,
139+
cdkCliVersion: props.cdkCliVersion,
140+
projectName: maybeSuffix(props.pipelineName, '-selfupdate'),
141+
})],
97142
});
98143

99144
this._assets = new AssetPublishing(this, 'Assets', {
@@ -112,10 +157,19 @@ export class CdkPipeline extends Construct {
112157
* You can use this to add more Stages to the pipeline, or Actions
113158
* to Stages.
114159
*/
115-
public get pipeline(): codepipeline.Pipeline {
160+
public get codePipeline(): codepipeline.Pipeline {
116161
return this._pipeline;
117162
}
118163

164+
/**
165+
* Access one of the pipeline's stages by stage name
166+
*
167+
* You can use this to add more Actions to a stage.
168+
*/
169+
public stage(stageName: string): codepipeline.IStage {
170+
return this._pipeline.stage(stageName);
171+
}
172+
119173
/**
120174
* Add pipeline stage that will deploy the given application stage
121175
*

0 commit comments

Comments
 (0)