Skip to content

Commit

Permalink
fix(codepipeline): allow adding an S3 source action with the same buc…
Browse files Browse the repository at this point in the history
…ket multiple times

If you attempted to add two source S3 actions using the same bucket into the same pipeline,
and both using CloudWatch events as triggers,
you would get an error about 'duplicate construct with id PipelineSourceEventRule'.
This is a valid use-case, though,
as each action might point to a different path in the bucket.

Fixes aws#4237
  • Loading branch information
skinny85 committed Oct 11, 2019
1 parent f98f42f commit 94fc3db
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,34 @@ export class S3SourceAction extends Action {
outputs: [props.output],
});

if (props.bucketKey.length === 0) {
throw new Error('Property bucketKey cannot be an empty string');
}

this.props = props;
}

protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
if (this.props.trigger === S3Trigger.EVENTS) {
this.props.bucket.onCloudTrailPutObject(stage.pipeline.node.uniqueId + 'SourceEventRule', {
const baseId = stage.pipeline.node.uniqueId + 'SourceEventRule';
// the same bucket can be added multiple times to the same pipeline,
// with different paths. So check whether that's the case
const event = this.props.bucket.node.tryFindChild(baseId);
let id: string;
if (event) {
// there is already an event with this ID,
// so add the bucketKey to the id
id = baseId + this.props.bucketKey;
if (this.props.bucket.node.tryFindChild(id)) {
// this means a duplicate path for the same bucket - error out
throw new Error(`S3 source action with path '${this.props.bucketKey}' is already present in the pipeline for this source bucket`);
}
} else {
// for backwards compatibility, keep the original ID for the first event
id = baseId;
}
this.props.bucket.onCloudTrailPutObject(id, {
target: new targets.CodePipeline(stage.pipeline),
paths: [this.props.bucketKey]
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export = {
'does not poll for source changes and uses Events for S3Trigger.EVENTS'(test: Test) {
const stack = new Stack();

minimalPipeline(stack, cpactions.S3Trigger.EVENTS);
minimalPipeline(stack, { trigger: cpactions.S3Trigger.EVENTS });

expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
"Stages": [
Expand All @@ -62,7 +62,7 @@ export = {
'polls for source changes and does not use Events for S3Trigger.POLL'(test: Test) {
const stack = new Stack();

minimalPipeline(stack, cpactions.S3Trigger.POLL);
minimalPipeline(stack, { trigger: cpactions.S3Trigger.POLL });

expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
"Stages": [
Expand All @@ -87,7 +87,7 @@ export = {
'does not poll for source changes and does not use Events for S3Trigger.NONE'(test: Test) {
const stack = new Stack();

minimalPipeline(stack, cpactions.S3Trigger.NONE);
minimalPipeline(stack, { trigger: cpactions.S3Trigger.NONE });

expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
"Stages": [
Expand All @@ -108,35 +108,108 @@ export = {

test.done();
},

'does not allow passing an empty string for the bucketKey property'(test: Test) {
const stack = new Stack();

test.throws(() => {
new cpactions.S3SourceAction({
actionName: 'Source',
bucket: new s3.Bucket(stack, 'MyBucket'),
bucketKey: '',
output: new codepipeline.Artifact(),
});
}, /Property bucketKey cannot be an empty string/);

test.done();
},

'allows using the same bucket with events trigger mutliple times with different bucket paths'(test: Test) {
const stack = new Stack();

const bucket = new s3.Bucket(stack, 'MyBucket');
const sourceStage = minimalPipeline(stack, {
bucket,
bucketKey: 'my/path',
trigger: cpactions.S3Trigger.EVENTS,
});
sourceStage.addAction(new cpactions.S3SourceAction({
actionName: 'Source2',
bucket,
bucketKey: 'my/other/path',
trigger: cpactions.S3Trigger.EVENTS,
output: new codepipeline.Artifact(),
}));

test.done();
},

'throws an error if the same bucket and path with trigger = Events are added to the same pipeline twice'(test: Test) {
const stack = new Stack();

const bucket = new s3.Bucket(stack, 'MyBucket');
const sourceStage = minimalPipeline(stack, {
bucket,
bucketKey: 'my/path',
trigger: cpactions.S3Trigger.EVENTS,
});
sourceStage.addAction(new cpactions.S3SourceAction({
actionName: 'Source2',
bucket,
bucketKey: 'my/other/path',
trigger: cpactions.S3Trigger.EVENTS,
output: new codepipeline.Artifact(),
}));

const duplicateBucketAndPath = new cpactions.S3SourceAction({
actionName: 'Source3',
bucket,
bucketKey: 'my/other/path',
trigger: cpactions.S3Trigger.EVENTS,
output: new codepipeline.Artifact(),
});

test.throws(() => {
sourceStage.addAction(duplicateBucketAndPath);
}, /S3 source action with path 'my\/other\/path' is already present in the pipeline for this source bucket/);

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

function minimalPipeline(stack: Stack, trigger: cpactions.S3Trigger | undefined): codepipeline.Pipeline {
interface MinimalPipelineOptions {
readonly trigger?: cpactions.S3Trigger;

readonly bucket?: s3.IBucket;

readonly bucketKey?: string;
}

function minimalPipeline(stack: Stack, options: MinimalPipelineOptions = {}): codepipeline.IStage {
const sourceOutput = new codepipeline.Artifact();
return new codepipeline.Pipeline(stack, 'MyPipeline', {
stages: [
{
stageName: 'Source',
actions: [
new cpactions.S3SourceAction({
actionName: 'Source',
bucket: new s3.Bucket(stack, 'MyBucket'),
bucketKey: 'some/path/to',
output: sourceOutput,
trigger,
}),
],
},
{
stageName: 'Build',
actions: [
new cpactions.CodeBuildAction({
actionName: 'Build',
project: new codebuild.PipelineProject(stack, 'MyProject'),
input: sourceOutput,
}),
],
},
const pipeline = new codepipeline.Pipeline(stack, 'MyPipeline');
const sourceStage = pipeline.addStage({
stageName: 'Source',
actions: [
new cpactions.S3SourceAction({
actionName: 'Source',
bucket: options.bucket || new s3.Bucket(stack, 'MyBucket'),
bucketKey: options.bucketKey || 'some/path/to',
output: sourceOutput,
trigger: options.trigger,
}),
],
});
pipeline.addStage({
stageName: 'Build',
actions: [
new cpactions.CodeBuildAction({
actionName: 'Build',
project: new codebuild.PipelineProject(stack, 'MyProject'),
input: sourceOutput,
}),
],
});
return sourceStage;
}

0 comments on commit 94fc3db

Please sign in to comment.