Skip to content

Commit

Permalink
feat(aws-codecommit): use CloudWatch Events instead of polling by def…
Browse files Browse the repository at this point in the history
…ault in the CodePipeline Action. (#1026)

BREAKING CHANGE: this modifies the default behavior of the CodeCommit Action.
It also changes the internal API contract between the aws-codepipeline-api module and the CodePipeline Actions in the service packages.
  • Loading branch information
skinny85 committed Nov 2, 2018
1 parent fdabe95 commit d09d30c
Show file tree
Hide file tree
Showing 18 changed files with 261 additions and 104 deletions.
10 changes: 5 additions & 5 deletions packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts
Expand Up @@ -92,7 +92,7 @@ export class PipelineExecuteChangeSetAction extends PipelineCloudFormationAction
ChangeSetName: props.changeSetName,
});

SingletonPolicy.forRole(props.stage.pipelineRole)
SingletonPolicy.forRole(props.stage.pipeline.role)
.grantExecuteChangeSet(props);
}
}
Expand Down Expand Up @@ -210,7 +210,7 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo
}
}

SingletonPolicy.forRole(props.stage.pipelineRole).grantPassRole(this.role);
SingletonPolicy.forRole(props.stage.pipeline.role).grantPassRole(this.role);
}

/**
Expand Down Expand Up @@ -255,7 +255,7 @@ export class PipelineCreateReplaceChangeSetAction extends PipelineCloudFormation
this.addInputArtifact(props.templateConfiguration.artifact);
}

SingletonPolicy.forRole(props.stage.pipelineRole).grantCreateReplaceChangeSet(props);
SingletonPolicy.forRole(props.stage.pipeline.role).grantCreateReplaceChangeSet(props);
}
}

Expand Down Expand Up @@ -310,7 +310,7 @@ export class PipelineCreateUpdateStackAction extends PipelineCloudFormationDeplo
this.addInputArtifact(props.templateConfiguration.artifact);
}

SingletonPolicy.forRole(props.stage.pipelineRole).grantCreateUpdateStack(props);
SingletonPolicy.forRole(props.stage.pipeline.role).grantCreateUpdateStack(props);
}
}

Expand All @@ -332,7 +332,7 @@ export class PipelineDeleteStackAction extends PipelineCloudFormationDeployActio
super(parent, id, props, {
ActionMode: 'DELETE_ONLY',
});
SingletonPolicy.forRole(props.stage.pipelineRole).grantDeleteStack(props);
SingletonPolicy.forRole(props.stage.pipeline.role).grantDeleteStack(props);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudformation/package.json
Expand Up @@ -57,6 +57,7 @@
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/assert": "^0.14.1",
"@aws-cdk/aws-events": "^0.14.1",
"@types/lodash": "^4.14.116",
"cdk-build-tools": "^0.14.1",
"cdk-integ-tools": "^0.14.1",
Expand Down
55 changes: 36 additions & 19 deletions packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts
@@ -1,4 +1,5 @@
import cpapi = require('@aws-cdk/aws-codepipeline-api');
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import _ = require('lodash');
Expand All @@ -10,7 +11,7 @@ export = nodeunit.testCase({
'works'(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const stage = new StageDouble({ pipelineRole });
const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) });
const artifact = new cpapi.Artifact(stack as any, 'TestArtifact');
const action = new cloudformation.PipelineCreateReplaceChangeSetAction(stack, 'Action', {
stage,
Expand Down Expand Up @@ -43,7 +44,7 @@ export = nodeunit.testCase({
'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const stage = new StageDouble({ pipelineRole });
const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) });
const artifact = new cpapi.Artifact(stack as any, 'TestArtifact');
new cloudformation.PipelineCreateReplaceChangeSetAction(stack, 'ActionA', {
stage,
Expand Down Expand Up @@ -97,7 +98,7 @@ export = nodeunit.testCase({
'works'(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const stage = new StageDouble({ pipelineRole });
const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) });
new cloudformation.PipelineExecuteChangeSetAction(stack, 'Action', {
stage,
changeSetName: 'MyChangeSet',
Expand All @@ -120,7 +121,7 @@ export = nodeunit.testCase({
'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const stage = new StageDouble({ pipelineRole });
const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) });
new cloudformation.PipelineExecuteChangeSetAction(stack, 'ActionA', {
stage,
changeSetName: 'MyChangeSet',
Expand Down Expand Up @@ -158,7 +159,7 @@ export = nodeunit.testCase({
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const action = new cloudformation.PipelineCreateUpdateStackAction(stack, 'Action', {
stage: new StageDouble({ pipelineRole }),
stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }),
templatePath: new cpapi.Artifact(stack as any, 'TestArtifact').atPath('some/file'),
stackName: 'MyStack',
replaceOnFailure: true,
Expand All @@ -179,7 +180,7 @@ export = nodeunit.testCase({
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const action = new cloudformation.PipelineDeleteStackAction(stack, 'Action', {
stage: new StageDouble({ pipelineRole }),
stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }),
stackName: 'MyStack',
});
const stackArn = _stackArn('MyStack');
Expand Down Expand Up @@ -273,26 +274,42 @@ function _stackArn(stackName: string): string {
});
}

class StageDouble implements cpapi.IStage, cpapi.IInternalStage {
public readonly name: string;
class PipelineDouble implements cpapi.IPipeline {
public readonly pipelineArn: string;
public readonly pipelineRole: iam.Role;
public readonly _internal = this;
public readonly role: iam.Role;

public readonly actions = new Array<cpapi.Action>();

constructor({ name, pipelineName, pipelineRole }: { name?: string, pipelineName?: string, pipelineRole: iam.Role }) {
this.name = name || 'TestStage';
constructor({ pipelineName, role }: { pipelineName?: string, role: iam.Role }) {
this.pipelineArn = cdk.ArnUtils.fromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: pipelineName || 'TestPipeline' });
this.pipelineRole = pipelineRole;
this.role = role;
}

public grantPipelineBucketRead() {
throw new Error('Unsupported');
public get uniqueId(): string {
throw new Error("Unsupported");
}

public grantPipelineBucketReadWrite() {
throw new Error('Unsupported');
public grantBucketRead(): void {
throw new Error("Unsupported");
}

public grantBucketReadWrite(): void {
throw new Error("Unsupported");
}

public asEventRuleTarget(): events.EventRuleTargetProps {
throw new Error("Unsupported");
}
}

class StageDouble implements cpapi.IStage, cpapi.IInternalStage {
public readonly name: string;
public readonly pipeline: cpapi.IPipeline;
public readonly _internal = this;

public readonly actions = new Array<cpapi.Action>();

constructor({ name, pipeline }: { name?: string, pipeline: cpapi.IPipeline }) {
this.name = name || 'TestStage';
this.pipeline = pipeline;
}

public _attachAction(action: cpapi.Action) {
Expand Down
12 changes: 5 additions & 7 deletions packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts
Expand Up @@ -108,7 +108,7 @@ export class PipelineTestAction extends codepipeline.TestAction {
function setCodeBuildNeededPermissions(stage: codepipeline.IStage, project: ProjectRef,
needsPipelineBucketWrite: boolean) {
// grant the Pipeline role the required permissions to this Project
stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
.addResource(project.projectArn)
.addActions(
'codebuild:BatchGetBuilds',
Expand All @@ -117,11 +117,9 @@ function setCodeBuildNeededPermissions(stage: codepipeline.IStage, project: Proj
));

// allow the Project access to the Pipline's artifact Bucket
if (project.role) {
if (needsPipelineBucketWrite) {
stage.grantPipelineBucketReadWrite(project.role);
} else {
stage.grantPipelineBucketRead(project.role);
}
if (needsPipelineBucketWrite) {
stage.pipeline.grantBucketReadWrite(project.role);
} else {
stage.pipeline.grantBucketRead(project.role);
}
}
14 changes: 9 additions & 5 deletions packages/@aws-cdk/aws-codecommit/lib/pipeline-action.ts
Expand Up @@ -22,11 +22,11 @@ export interface CommonPipelineSourceActionProps extends codepipeline.CommonActi
*/
branch?: string;

// TODO: use CloudWatch events instead
/**
* Whether or not AWS CodePipeline should poll for source changes.
* Whether AWS CodePipeline should poll for source changes.
* If this is `false`, the Pipeline will use CloudWatch Events to detect source changes instead.
*
* @default true
* @default false
*/
pollForSourceChanges?: boolean;
}
Expand Down Expand Up @@ -54,11 +54,15 @@ export class PipelineSourceAction extends codepipeline.SourceAction {
configuration: {
RepositoryName: props.repository.repositoryName,
BranchName: props.branch || 'master',
PollForSourceChanges: props.pollForSourceChanges !== undefined ? props.pollForSourceChanges : true
PollForSourceChanges: props.pollForSourceChanges || false,
},
outputArtifactName: props.outputArtifactName
});

if (!props.pollForSourceChanges) {
props.repository.onCommit(props.stage.pipeline.uniqueId + 'EventRule', props.stage.pipeline, props.branch || 'master');
}

// https://docs.aws.amazon.com/codecommit/latest/userguide/auth-and-access-control-permissions-reference.html#aa-acp
const actions = [
'codecommit:GetBranch',
Expand All @@ -68,7 +72,7 @@ export class PipelineSourceAction extends codepipeline.SourceAction {
'codecommit:CancelUploadArchive',
];

props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
props.stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
.addResource(props.repository.repositoryArn)
.addActions(...actions));
}
Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts
Expand Up @@ -54,7 +54,7 @@ export class PipelineDeployAction extends codepipeline.DeployAction {
resourceName: props.applicationName,
sep: ':',
});
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
props.stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
.addResource(applicationArn)
.addActions(
'codedeploy:GetApplicationRevision',
Expand All @@ -67,7 +67,7 @@ export class PipelineDeployAction extends codepipeline.DeployAction {
resourceName: `${props.applicationName}/${props.deploymentGroupName}`,
sep: ':',
});
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
props.stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
.addResource(deploymentGroupArn)
.addActions(
'codedeploy:CreateDeployment',
Expand All @@ -80,7 +80,7 @@ export class PipelineDeployAction extends codepipeline.DeployAction {
resourceName: '*',
sep: ':',
});
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
props.stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
.addResource(deployConfigArn)
.addActions(
'codedeploy:GetDeploymentConfig',
Expand Down
47 changes: 32 additions & 15 deletions packages/@aws-cdk/aws-codepipeline-api/lib/action.ts
Expand Up @@ -68,42 +68,59 @@ export interface IInternalStage {
}

/**
* The abstract interface of a Pipeline Stage that is used by Actions.
* The abstract view of an AWS CodePipeline as required and used by Actions.
* It extends {@link events.IEventRuleTarget},
* so this interface can be used as a Target for CloudWatch Events.
*/
export interface IStage {
/**
* The physical, human-readable name of this Pipeline Stage.
*/
readonly name: string;

export interface IPipeline extends events.IEventRuleTarget {
/**
* The ARN of the Pipeline.
*/
readonly pipelineArn: string;

/**
* The service Role of the Pipeline.
* The unique ID of the Pipeline Construct.
*/
readonly pipelineRole: iam.Role;
readonly uniqueId: string;

/**
* The API of Stage used internally by the CodePipeline Construct.
* You should never need to call any of the methods inside of it yourself.
* The service Role of the Pipeline.
*/
readonly _internal: IInternalStage;
readonly role: iam.Role;

/* Grants read permissions to the Pipeline's S3 Bucket to the given Identity.
*
* @param identity the IAM Identity to grant the permissions to
*/
grantPipelineBucketRead(identity: iam.IPrincipal): void;
grantBucketRead(identity?: iam.IPrincipal): void;

/**
* Grants read & write permissions to the Pipeline's S3 Bucket to the given Identity.
*
* @param identity the IAM Identity to grant the permissions to
*/
grantPipelineBucketReadWrite(identity: iam.IPrincipal): void;
grantBucketReadWrite(identity?: iam.IPrincipal): void;
}

/**
* The abstract interface of a Pipeline Stage that is used by Actions.
*/
export interface IStage {
/**
* The physical, human-readable name of this Pipeline Stage.
*/
readonly name: string;

/**
* The Pipeline this Stage belongs to.
*/
readonly pipeline: IPipeline;

/**
* The API of Stage used internally by the CodePipeline Construct.
* You should never need to call any of the methods inside of it yourself.
*/
readonly _internal: IInternalStage;
}

/**
Expand Down Expand Up @@ -215,7 +232,7 @@ export abstract class Action extends cdk.Construct {
rule.addEventPattern({
detailType: [ 'CodePipeline Stage Execution State Change' ],
source: [ 'aws.codepipeline' ],
resources: [ this.stage.pipelineArn ],
resources: [ this.stage.pipeline.pipelineArn ],
detail: {
stage: [ this.stage.name ],
action: [ this.id ],
Expand Down

0 comments on commit d09d30c

Please sign in to comment.