Skip to content

Commit

Permalink
feat(pipelines): add control over underlying CodePipeline (#10148)
Browse files Browse the repository at this point in the history
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.


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
rix0rrr committed Sep 3, 2020
1 parent 44fcb4e commit 41531b5
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 46 deletions.
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,18 @@ export class Pipeline extends PipelineBase {
return this._stages.slice();
}

/**
* Access one of the pipeline's stages by stage name
*/
public stage(stageName: string): IStage {
for (const stage of this._stages) {
if (stage.stageName === stageName) {
return stage;
}
}
throw new Error(`Pipeline does not contain a stage named '${stageName}'. Available stages: ${this._stages.map(s => s.stageName).join(', ')}`);
}

/**
* Returns all of the {@link CrossRegionSupportStack}s that were generated automatically
* when dealing with Actions that reside in a different region than the Pipeline itself.
Expand Down
64 changes: 52 additions & 12 deletions packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, haveResourceLike, ResourcePart } from '@aws-cdk/assert';
import { expect as ourExpect, ResourcePart, arrayWith, objectLike, haveResourceLike } from '@aws-cdk/assert';
import '@aws-cdk/assert/jest';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as s3 from '@aws-cdk/aws-s3';
Expand All @@ -22,7 +23,7 @@ nodeunitShim({
role,
});

expect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
ourExpect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
'RoleArn': {
'Fn::GetAtt': [
'Role1ABCC5F0',
Expand Down Expand Up @@ -109,7 +110,7 @@ nodeunitShim({
],
});

expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
'ArtifactStores': [
{
'Region': replicationRegion,
Expand All @@ -136,9 +137,9 @@ nodeunitShim({
'Region': pipelineRegion,
},
],
}));
});

expect(replicationStack).to(haveResourceLike('AWS::KMS::Key', {
expect(replicationStack).toHaveResourceLike('AWS::KMS::Key', {
'KeyPolicy': {
'Statement': [
{
Expand Down Expand Up @@ -170,7 +171,7 @@ nodeunitShim({
},
],
},
}));
});

test.done();
},
Expand Down Expand Up @@ -204,7 +205,7 @@ nodeunitShim({
],
});

expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
'ArtifactStores': [
{
'Region': replicationRegion,
Expand All @@ -231,12 +232,12 @@ nodeunitShim({
'Region': pipelineRegion,
},
],
}));
});

expect(pipeline.crossRegionSupport[replicationRegion].stack).to(haveResourceLike('AWS::KMS::Alias', {
expect(pipeline.crossRegionSupport[replicationRegion].stack).toHaveResourceLike('AWS::KMS::Alias', {
'DeletionPolicy': 'Delete',
'UpdateReplacePolicy': 'Delete',
}, ResourcePart.CompleteDefinition));
}, ResourcePart.CompleteDefinition);

test.done();
},
Expand Down Expand Up @@ -276,7 +277,7 @@ nodeunitShim({
],
});

expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
'ArtifactStores': [
{
'Region': replicationRegion,
Expand All @@ -293,7 +294,7 @@ nodeunitShim({
'Region': pipelineRegion,
},
],
}));
});

test.done();
},
Expand Down Expand Up @@ -392,3 +393,42 @@ nodeunitShim({
},
},
});

describe('test with shared setup', () => {
let stack: cdk.Stack;
let sourceArtifact: codepipeline.Artifact;
beforeEach(() => {
stack = new cdk.Stack();
sourceArtifact = new codepipeline.Artifact();
});

test('can add actions to stages after creation', () => {
// GIVEN
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
stages: [
{
stageName: 'Source',
actions: [new FakeSourceAction({ actionName: 'Fetch', output: sourceArtifact })],
},
{
stageName: 'Build',
actions: [new FakeBuildAction({ actionName: 'Gcc', input: sourceArtifact })],
},
],
});

// WHEN
pipeline.stage('Build').addAction(new FakeBuildAction({ actionName: 'debug.com', input: sourceArtifact }));

// THEN
expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
Stages: arrayWith({
Name: 'Build',
Actions: [
objectLike({ Name: 'Gcc' }),
objectLike({ Name: 'debug.com' }),
],
}),
});
});
});
23 changes: 23 additions & 0 deletions packages/@aws-cdk/pipelines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,29 @@ new MyPipelineStack(this, 'PipelineStack', {
});
```

If you prefer more control over the underlying CodePipeline object, you can
create one yourself, including custom Source and Build stages:

```ts
const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', {
stages: [
{
stageName: 'CustomSource',
actions: [...],
},
{
stageName: 'CustomBuild',
actions: [...],
},
],
});

const cdkPipeline = new CdkPipeline(this, 'CdkPipeline', {
codePipeline,
cloudAssemblyArtifact,
});
```

## Initial pipeline deployment

You provision this pipeline by making sure the target environment has been
Expand Down
111 changes: 86 additions & 25 deletions packages/@aws-cdk/pipelines/lib/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,44 @@ import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from '
export interface CdkPipelineProps {
/**
* The CodePipeline action used to retrieve the CDK app's source
*
* @default - Required unless `codePipeline` is given
*/
readonly sourceAction: codepipeline.IAction;
readonly sourceAction?: codepipeline.IAction;

/**
* The CodePipeline action build and synthesis step of the CDK app
*
* @default - Required unless `codePipeline` or `sourceAction` is given
*/
readonly synthAction: codepipeline.IAction;
readonly synthAction?: codepipeline.IAction;

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

/**
* Existing CodePipeline to add deployment stages to
*
* Use this if you want more control over the CodePipeline that gets created.
* You can choose to not pass this value, in which case a new CodePipeline is
* created with default settings.
*
* If you pass an existing CodePipeline, it should should have been created
* with `restartExecutionOnUpdate: true`.
*
* [disable-awslint:ref-via-interface]
*
* @default - A new CodePipeline is automatically generated
*/
readonly codePipeline?: codepipeline.Pipeline;

/**
* Name of the pipeline
*
* Can only be set if `codePipeline` is not set.
*
* @default - A name is automatically generated
*/
readonly pipelineName?: string;
Expand Down Expand Up @@ -72,28 +94,58 @@ export class CdkPipeline extends Construct {
this._cloudAssemblyArtifact = props.cloudAssemblyArtifact;
const pipelineStack = Stack.of(this);

this._pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
...props,
restartExecutionOnUpdate: true,
stages: [
{
stageName: 'Source',
actions: [props.sourceAction],
},
{
stageName: 'Build',
actions: [props.synthAction],
},
{
stageName: 'UpdatePipeline',
actions: [new UpdatePipelineAction(this, 'UpdatePipeline', {
cloudAssemblyInput: this._cloudAssemblyArtifact,
pipelineStackName: pipelineStack.stackName,
cdkCliVersion: props.cdkCliVersion,
projectName: maybeSuffix(props.pipelineName, '-selfupdate'),
})],
},
],
if (props.codePipeline) {
if (props.pipelineName) {
throw new Error('Cannot set \'pipelineName\' if an existing CodePipeline is given using \'codePipeline\'');
}

this._pipeline = props.codePipeline;
} else {
this._pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
pipelineName: props.pipelineName,
restartExecutionOnUpdate: true,
});
}

if (props.sourceAction && !props.synthAction) {
// Because of ordering limitations, you can: bring your own Source, bring your own
// Both, or bring your own Nothing. You cannot bring your own Build (which because of the
// current CodePipeline API must go BEFORE what we're adding) and then having us add a
// Source after it. That doesn't make any sense.
throw new Error('When passing a \'sourceAction\' you must also pass a \'synthAction\' (or a \'codePipeline\' that already has both)');
}
if (!props.sourceAction && (!props.codePipeline || props.codePipeline.stages.length < 1)) {
throw new Error('You must pass a \'sourceAction\' (or a \'codePipeline\' that already has a Source stage)');
}
if (!props.synthAction && (!props.codePipeline || props.codePipeline.stages.length < 2)) {
// This looks like a weirdly specific requirement, but actually the underlying CodePipeline
// requires that a Pipeline has at least 2 stages. We're just hitching onto upstream
// requirements to do this check.
throw new Error('You must pass a \'synthAction\' (or a \'codePipeline\' that already has a Build stage)');
}

if (props.sourceAction) {
this._pipeline.addStage({
stageName: 'Source',
actions: [props.sourceAction],
});
}

if (props.synthAction) {
this._pipeline.addStage({
stageName: 'Build',
actions: [props.synthAction],
});
}

this._pipeline.addStage({
stageName: 'UpdatePipeline',
actions: [new UpdatePipelineAction(this, 'UpdatePipeline', {
cloudAssemblyInput: this._cloudAssemblyArtifact,
pipelineStackName: pipelineStack.stackName,
cdkCliVersion: props.cdkCliVersion,
projectName: maybeSuffix(props.pipelineName, '-selfupdate'),
})],
});

this._assets = new AssetPublishing(this, 'Assets', {
Expand All @@ -112,10 +164,19 @@ export class CdkPipeline extends Construct {
* You can use this to add more Stages to the pipeline, or Actions
* to Stages.
*/
public get pipeline(): codepipeline.Pipeline {
public get codePipeline(): codepipeline.Pipeline {
return this._pipeline;
}

/**
* Access one of the pipeline's stages by stage name
*
* You can use this to add more Actions to a stage.
*/
public stage(stageName: string): codepipeline.IStage {
return this._pipeline.stage(stageName);
}

/**
* Add pipeline stage that will deploy the given application stage
*
Expand Down

0 comments on commit 41531b5

Please sign in to comment.