Skip to content

Commit

Permalink
feat(aws-codepipeline): Make the Stage insertion API in CodePipeline …
Browse files Browse the repository at this point in the history
…more flexible.

This commit allows clients of CodePipeline to create new Stages placed
at an arbitrary index in the Pipeline, or before/after a given Stage
(instead of only appending new Stages at the end).
  • Loading branch information
skinny85 committed Sep 17, 2018
1 parent 965b918 commit 92ecac4
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 5 deletions.
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ const sourceStage = new Stage(this, 'Source', {
});
```

You can insert the new Stage at an arbitrary point in the Pipeline:

```ts
const sourceStage = new Stage(this, 'Source', {
pipeline,
placement: {
// note: you can only specify one of the below properties
rightBefore: anotherStage,
justAfter: anotherStage,
atIndex: 3, // indexing starts at 0
// pipeline.stageCount returns the number of Stages currently in the Pipeline
}
})
```

Add an Action to a Stage:

```ts
Expand Down
65 changes: 61 additions & 4 deletions packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
import util = require('@aws-cdk/util');
import { cloudformation, PipelineName, PipelineVersion } from './codepipeline.generated';
import { Stage } from './stage';
import { Stage, StagePlacement } from './stage';

/**
* The ARN of a pipeline
Expand Down Expand Up @@ -200,6 +200,13 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget {
]);
}

/**
* Get the number of Stages in this Pipeline.
*/
public get stageCount(): number {
return this.stages.length;
}

/**
* Adds a Stage to this Pipeline.
* This is an internal operation -
Expand All @@ -208,8 +215,9 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget {
* so there is never a need to call this method explicitly.
*
* @param stage the newly created Stage to add to this Pipeline
* @param placement an optional specification of where to place the newly added Stage in the Pipeline
*/
public _addStage(stage: Stage): void {
public _addStage(stage: Stage, placement?: StagePlacement): void {
// _addStage should be idempotent, in case a customer ever calls it directly
if (this.stages.includes(stage)) {
return;
Expand All @@ -219,7 +227,56 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget {
throw new Error(`A stage with name '${stage.name}' already exists`);
}

this.stages.push(stage);
const index = placement
? this.calculateInsertIndexFromPlacement(placement)
: this.stageCount;

this.stages.splice(index, 0, stage);
}

private calculateInsertIndexFromPlacement(placement: StagePlacement): number {
// check if at most one placement property was provided
const providedPlacementProps = ['rightBefore', 'justAfter', 'atIndex']
.filter((prop) => (placement as any)[prop] !== undefined);
if (providedPlacementProps.length > 1) {
throw new Error("Error adding Stage to the Pipeline: " +
'you can only provide at most one placement property, but ' +
`'${providedPlacementProps.join(', ')}' were given`);
}

if (placement.rightBefore !== undefined) {
const targetIndex = this.findStageIndex(placement.rightBefore);
if (targetIndex === -1) {
throw new Error("Error adding Stage to the Pipeline: " +
`the requested Stage to add it before, '${placement.rightBefore.name}', was not found`);
}
return targetIndex;
}

if (placement.justAfter !== undefined) {
const targetIndex = this.findStageIndex(placement.justAfter);
if (targetIndex === -1) {
throw new Error("Error adding Stage to the Pipeline: " +
`the requested Stage to add it after, '${placement.justAfter.name}', was not found`);
}
return targetIndex + 1;
}

if (placement.atIndex !== undefined) {
const index = placement.atIndex;
if (index < 0 || index > this.stageCount) {
throw new Error("Error adding Stage to the Pipeline: " +
`{ placed: atIndex } should be between 0 and the number of stages in the Pipeline (${this.stageCount}), ` +
` got: ${index}`);
}
return index;
}

return this.stageCount;
}

private findStageIndex(targetStage: Stage) {
return this.stages.findIndex((stage: Stage) => stage === targetStage);
}

private validateSourceActionLocations(): string[] {
Expand All @@ -232,7 +289,7 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget {
}

private validateHasStages(): string[] {
if (this.stages.length < 2) {
if (this.stageCount < 2) {
return ['Pipeline must have at least two stages'];
}
return [];
Expand Down
43 changes: 42 additions & 1 deletion packages/@aws-cdk/aws-codepipeline/lib/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,39 @@ import cdk = require('@aws-cdk/cdk');
import { cloudformation } from './codepipeline.generated';
import { Pipeline } from './pipeline';

/**
* Allows you to control where to place a new Stage when it's added to the Pipeline.
* Note that you can provide only one of the below properties -
* specifying more than one will result in a validation error.
*
* @see #rightBefore
* @see #justAfter
* @see #atIndex
*/
export interface StagePlacement {
/**
* Inserts the new Stage as a parent of the given Stage
* (changing its current parent Stage, if it had one).
*/
readonly rightBefore?: Stage;

/**
* Inserts the new Stage as a child of the given Stage
* (changing its current child Stage, if it had one).
*/
readonly justAfter?: Stage;

/**
* Inserts the new Stage at the given index in the Pipeline,
* moving the Stage currently at that index,
* and any subsequent ones, one index down.
* Indexing starts at 0.
* The maximum allowed value is {@link Pipeline#stageCount},
* which will insert the new Stage at the end of the Pipeline.
*/
readonly atIndex?: number;
}

/**
* The construction properties for {@link Stage}.
*/
Expand All @@ -13,6 +46,14 @@ export interface StageProps {
* The Pipeline to add the newly created Stage to.
*/
pipeline: Pipeline;

/**
* Allows specifying where should the newly created {@link Stage}
* be placed in the Pipeline.
*
* @default the stage is added at the end of the Pipeline
*/
placement?: StagePlacement;
}

/**
Expand Down Expand Up @@ -44,7 +85,7 @@ export class Stage extends cdk.Construct implements actions.IStage {
this.pipeline = props.pipeline;
actions.validateName('Stage', name);

this.pipeline._addStage(this);
this.pipeline._addStage(this, props.placement);
}

/**
Expand Down
169 changes: 169 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/test/test.stages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { expect, haveResource } from '@aws-cdk/assert';
import cdk = require('@aws-cdk/cdk');
import { Test } from 'nodeunit';
import codepipeline = require('../lib');

// tslint:disable:object-literal-key-quotes

export = {
'Pipeline Stages': {
'can be inserted at index 0'(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');

new codepipeline.Stage(stack, 'SecondStage', { pipeline });
new codepipeline.Stage(stack, 'FirstStage', {
pipeline,
placement: {
atIndex: 0,
},
});

expect(stack, true).to(haveResource('AWS::CodePipeline::Pipeline', {
"Stages": [
{ "Name": "FirstStage" },
{ "Name": "SecondStage" },
],
}));

test.done();
},

'can be inserted before another Stage'(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');

const secondStage = new codepipeline.Stage(stack, 'SecondStage', { pipeline });
new codepipeline.Stage(stack, 'FirstStage', {
pipeline,
placement: {
rightBefore: secondStage,
},
});

expect(stack, true).to(haveResource('AWS::CodePipeline::Pipeline', {
"Stages": [
{ "Name": "FirstStage" },
{ "Name": "SecondStage" },
],
}));

test.done();
},

'can be inserted after another Stage'(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');

const firstStage = new codepipeline.Stage(stack, 'FirstStage', { pipeline });
new codepipeline.Stage(stack, 'ThirdStage', { pipeline });
new codepipeline.Stage(stack, 'SecondStage', {
pipeline,
placement: {
justAfter: firstStage,
},
});

expect(stack, true).to(haveResource('AWS::CodePipeline::Pipeline', {
"Stages": [
{ "Name": "FirstStage" },
{ "Name": "SecondStage" },
{ "Name": "ThirdStage" },
],
}));

test.done();
},

'attempting to insert a Stage at a negative index results in an error'(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');

test.throws(() => {
new codepipeline.Stage(stack, 'Stage', {
pipeline,
placement: {
atIndex: -1,
},
});
}, /atIndex/);

test.done();
},

'attempting to insert a Stage at an index larger than the current number of Stages results in an error'(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');

test.throws(() => {
new codepipeline.Stage(stack, 'Stage', {
pipeline,
placement: {
atIndex: 1,
},
});
}, /atIndex/);

test.done();
},

"attempting to insert a Stage before a Stage that doesn't exist results in an error"(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');
const stage = new codepipeline.Stage(stack, 'Stage', { pipeline });

const anotherPipeline = new codepipeline.Pipeline(stack, 'AnotherPipeline');
test.throws(() => {
new codepipeline.Stage(stack, 'AnotherStage', {
pipeline: anotherPipeline,
placement: {
rightBefore: stage,
},
});
}, /before/i);

test.done();
},

"attempting to insert a Stage after a Stage that doesn't exist results in an error"(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');
const stage = new codepipeline.Stage(stack, 'Stage', { pipeline });

const anotherPipeline = new codepipeline.Pipeline(stack, 'AnotherPipeline');
test.throws(() => {
new codepipeline.Stage(stack, 'AnotherStage', {
pipeline: anotherPipeline,
placement: {
justAfter: stage,
},
});
}, /after/i);

test.done();
},

"providing more than one placement value results in an error"(test: Test) {
const stack = new cdk.Stack();
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline');
const stage = new codepipeline.Stage(stack, 'FirstStage', { pipeline });

test.throws(() => {
new codepipeline.Stage(stack, 'SecondStage', {
pipeline,
placement: {
rightBefore: stage,
justAfter: stage,
},
});
// incredibly, an arrow function below causes nodeunit to crap out with:
// "TypeError: Function has non-object prototype 'undefined' in instanceof check"
// tslint:disable-next-line:only-arrow-functions
}, function(e: any) {
return /rightBefore/.test(e) && /justAfter/.test(e);
});

test.done();
},
},
};

0 comments on commit 92ecac4

Please sign in to comment.