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 Aug 15, 2018
1 parent 561e76b commit 1e41ed3
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 8 deletions.
76 changes: 69 additions & 7 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 } from './codepipeline.generated';
import { Stage } from './stage';
import { Stage, StagePlacement } from './stage';

/**
* The ARN of a pipeline
Expand Down Expand Up @@ -86,7 +86,7 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget {
*/
public readonly artifactBucket: s3.BucketRef;

private readonly stages = new Array<Stage>();
private readonly _stages = new Array<Stage>();
private eventsRole?: iam.Role;

constructor(parent: cdk.Construct, name: string, props?: PipelineProps) {
Expand Down Expand Up @@ -210,7 +210,21 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget {
]);
}

public _addStage(stage: Stage): void {
/**
* Get a duplicate of this Pipeline's list of Stages.
*/
public get stages(): Stage[] {
return this._stages.slice();
}

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

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 @@ -220,11 +234,59 @@ 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, ${providedPlacementProps} 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[] {
return util.flatMap(this.stages, (stage, i) => {
return util.flatMap(this._stages, (stage, i) => {
const onlySourceActionsPermitted = i === 0;
return util.flatMap(stage.actions, (action, _) =>
actions.validateSourceAction(onlySourceActionsPermitted, action.category, action.id, stage.id)
Expand All @@ -233,7 +295,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 Expand Up @@ -262,6 +324,6 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget {
}

private renderStages(): cloudformation.PipelineResource.StageDeclarationProperty[] {
return this.stages.map(stage => stage.render());
return this._stages.map(stage => stage.render());
}
}
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
*/
placed?: 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.placed);
}

/**
Expand Down
148 changes: 148 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,148 @@
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 pipeline = pipelineForTesting();

const secondStage = new codepipeline.Stage(pipeline, 'SecondStage', { pipeline });
const firstStage = new codepipeline.Stage(pipeline, 'FirstStage', {
pipeline,
placed: {
atIndex: 0,
}
});

test.equal(pipeline.stages[0].name, firstStage.name);
test.equal(pipeline.stages[1].name, secondStage.name);

test.done();
},

'can be inserted before another Stage'(test: Test) {
const pipeline = pipelineForTesting();

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

test.equal(pipeline.stages[0].name, firstStage.name);
test.equal(pipeline.stages[1].name, secondStage.name);

test.done();
},

'can be inserted after another Stage'(test: Test) {
const pipeline = pipelineForTesting();

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

test.equal(pipeline.stages[0].name, firstStage.name);
test.equal(pipeline.stages[1].name, secondStage.name);
test.equal(pipeline.stages[2].name, thirdStage.name);

test.done();
},

'attempting to insert a Stage at a negative index results in an error'(test: Test) {
const pipeline = pipelineForTesting();

test.throws(() => {
new codepipeline.Stage(pipeline, 'Stage', {
pipeline,
placed: {
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 pipeline = pipelineForTesting();

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

test.done();
},

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

const anotherPipeline = pipelineForTesting();
test.throws(() => {
new codepipeline.Stage(anotherPipeline, 'Stage', {
pipeline: anotherPipeline,
placed: {
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 pipeline = pipelineForTesting();
const stage = new codepipeline.Stage(pipeline, 'Stage', { pipeline });

const anotherPipeline = pipelineForTesting();
test.throws(() => {
new codepipeline.Stage(anotherPipeline, 'Stage', {
pipeline: anotherPipeline,
placed: {
justAfter: stage,
}
});
}, /after/i);

test.done();
},

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

test.throws(() => {
new codepipeline.Stage(pipeline, 'SecondStage', {
pipeline,
placed: {
rightBefore: stage,
justAfter: stage,
}
});
});

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

function pipelineForTesting(): codepipeline.Pipeline {
const stack = new cdk.Stack();
return new codepipeline.Pipeline(stack, 'Pipeline');
}

0 comments on commit 1e41ed3

Please sign in to comment.