Skip to content

Commit

Permalink
feat(codepipeline): re-factor the CodePipeline Action bind method t…
Browse files Browse the repository at this point in the history
…o take a Role separately from the Pipeline. (#2085)
  • Loading branch information
skinny85 authored Mar 29, 2019
1 parent 7638a58 commit ffe0046
Show file tree
Hide file tree
Showing 18 changed files with 159 additions and 131 deletions.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/alexa-ask/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class AlexaSkillDeployAction extends codepipeline.DeployAction {
}
}

protected bind(_stage: codepipeline.IStage, _scope: cdk.Construct): void {
protected bind(_info: codepipeline.ActionBind): void {
// nothing to do
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ class FakeAction extends api.Action {
this.outputArtifact = new api.Artifact('OutputArtifact');
}

protected bind(_stage: api.IStage, _scope: cdk.Construct): void {
protected bind(_info: api.ActionBind): void {
// do nothing
}
}
Expand Down
35 changes: 17 additions & 18 deletions packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,8 @@ export class PipelineExecuteChangeSetAction extends PipelineCloudFormationAction
this.props = props;
}

protected bind(stage: codepipeline.IStage, _scope: cdk.Construct): void {
SingletonPolicy.forRole(stage.pipeline.role)
.grantExecuteChangeSet(this.props);
protected bind(info: codepipeline.ActionBind): void {
SingletonPolicy.forRole(info.role).grantExecuteChangeSet(this.props);
}
}

Expand Down Expand Up @@ -257,11 +256,11 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo
return this.getDeploymentRole('property role()');
}

protected bind(stage: codepipeline.IStage, scope: cdk.Construct): void {
protected bind(info: codepipeline.ActionBind): void {
if (this.props.deploymentRole) {
this._deploymentRole = this.props.deploymentRole;
} else {
this._deploymentRole = new iam.Role(scope, 'Role', {
this._deploymentRole = new iam.Role(info.scope, 'Role', {
assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com')
});

Expand All @@ -270,7 +269,7 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo
}
}

SingletonPolicy.forRole(stage.pipeline.role).grantPassRole(this._deploymentRole);
SingletonPolicy.forRole(info.role).grantPassRole(this._deploymentRole);
}

private getDeploymentRole(member: string): iam.IRole {
Expand Down Expand Up @@ -321,10 +320,10 @@ export class PipelineCreateReplaceChangeSetAction extends PipelineCloudFormation
this.props2 = props;
}

protected bind(stage: codepipeline.IStage, scope: cdk.Construct): void {
super.bind(stage, scope);
protected bind(info: codepipeline.ActionBind): void {
super.bind(info);

SingletonPolicy.forRole(stage.pipeline.role).grantCreateReplaceChangeSet(this.props2);
SingletonPolicy.forRole(info.role).grantCreateReplaceChangeSet(this.props2);
}
}

Expand Down Expand Up @@ -384,10 +383,10 @@ export class PipelineCreateUpdateStackAction extends PipelineCloudFormationDeplo
this.props2 = props;
}

protected bind(stage: codepipeline.IStage, scope: cdk.Construct): void {
super.bind(stage, scope);
protected bind(info: codepipeline.ActionBind): void {
super.bind(info);

SingletonPolicy.forRole(stage.pipeline.role).grantCreateUpdateStack(this.props2);
SingletonPolicy.forRole(info.role).grantCreateUpdateStack(this.props2);
}
}

Expand Down Expand Up @@ -415,10 +414,10 @@ export class PipelineDeleteStackAction extends PipelineCloudFormationDeployActio
this.props2 = props;
}

protected bind(stage: codepipeline.IStage, scope: cdk.Construct): void {
super.bind(stage, scope);
protected bind(info: codepipeline.ActionBind): void {
super.bind(info);

SingletonPolicy.forRole(stage.pipeline.role).grantDeleteStack(this.props2);
SingletonPolicy.forRole(info.role).grantDeleteStack(this.props2);
}
}

Expand Down Expand Up @@ -469,7 +468,7 @@ class SingletonPolicy extends cdk.Construct {
* @param role the Role this policy is bound to.
* @returns the SingletonPolicy for this role.
*/
public static forRole(role: iam.Role): SingletonPolicy {
public static forRole(role: iam.IRole): SingletonPolicy {
const found = role.node.tryFindChild(SingletonPolicy.UUID);
return (found as SingletonPolicy) || new SingletonPolicy(role);
}
Expand All @@ -478,8 +477,8 @@ class SingletonPolicy extends cdk.Construct {

private statements: { [key: string]: iam.PolicyStatement } = {};

private constructor(private readonly role: iam.Role) {
super(role, SingletonPolicy.UUID);
private constructor(private readonly role: iam.IRole) {
super(role as unknown as cdk.Construct, SingletonPolicy.UUID);
}

public grantExecuteChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,12 @@ class StageDouble implements cpapi.IStage {
const stageParent = new cdk.Construct(pipeline, this.stageName);
for (const action of actions) {
const actionParent = new cdk.Construct(stageParent, action.actionName);
(action as any)._attachActionToPipeline(this, actionParent);
(action as any)._actionAttachedToPipeline({
pipeline,
stage: this,
scope: actionParent,
role: pipeline.role,
});
}
this.actions = actions;
}
Expand Down
19 changes: 9 additions & 10 deletions packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import codepipeline = require('@aws-cdk/aws-codepipeline-api');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import { IProject } from './project';

/**
Expand Down Expand Up @@ -103,8 +102,8 @@ export class PipelineBuildAction extends codepipeline.BuildAction {
return findOutputArtifact(this.additionalOutputArtifacts(), name);
}

protected bind(stage: codepipeline.IStage, _scope: cdk.Construct): void {
setCodeBuildNeededPermissions(stage, this.props.project, true);
protected bind(info: codepipeline.ActionBind): void {
setCodeBuildNeededPermissions(info, this.props.project, true);
}
}

Expand Down Expand Up @@ -192,16 +191,16 @@ export class PipelineTestAction extends codepipeline.TestAction {
return findOutputArtifact(this.additionalOutputArtifacts(), name);
}

protected bind(stage: codepipeline.IStage, _scope: cdk.Construct): void {
protected bind(info: codepipeline.ActionBind): void {
// TODO: revert "as any" when we centralize pipeline actions
setCodeBuildNeededPermissions(stage, this.props.project, (this as any)._outputArtifacts.length > 0);
setCodeBuildNeededPermissions(info, this.props.project, (this as any)._outputArtifacts.length > 0);
}
}

function setCodeBuildNeededPermissions(stage: codepipeline.IStage, project: IProject,
needsPipelineBucketWrite: boolean) {
function setCodeBuildNeededPermissions(info: codepipeline.ActionBind, project: IProject,
needsPipelineBucketWrite: boolean): void {
// grant the Pipeline role the required permissions to this Project
stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
info.role.addToPolicy(new iam.PolicyStatement()
.addResource(project.projectArn)
.addActions(
'codebuild:BatchGetBuilds',
Expand All @@ -211,9 +210,9 @@ function setCodeBuildNeededPermissions(stage: codepipeline.IStage, project: IPro

// allow the Project access to the Pipline's artifact Bucket
if (needsPipelineBucketWrite) {
stage.pipeline.grantBucketReadWrite(project.role);
info.pipeline.grantBucketReadWrite(project.role);
} else {
stage.pipeline.grantBucketRead(project.role);
info.pipeline.grantBucketRead(project.role);
}
}

Expand Down
9 changes: 4 additions & 5 deletions packages/@aws-cdk/aws-codecommit/lib/pipeline-action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import codepipeline = require('@aws-cdk/aws-codepipeline-api');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import { IRepository } from './repository';

/**
Expand Down Expand Up @@ -62,10 +61,10 @@ export class PipelineSourceAction extends codepipeline.SourceAction {
this.props = props;
}

protected bind(stage: codepipeline.IStage, _scope: cdk.Construct): void {
protected bind(info: codepipeline.ActionBind): void {
if (!this.props.pollForSourceChanges) {
this.props.repository.onCommit(stage.pipeline.node.uniqueId + 'EventRule',
stage.pipeline, this.props.branch || 'master');
this.props.repository.onCommit(info.pipeline.node.uniqueId + 'EventRule',
info.pipeline, this.props.branch || 'master');
}

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

stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
info.role.addToPolicy(new iam.PolicyStatement()
.addResource(this.props.repository.repositoryArn)
.addActions(...actions));
}
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-codecommit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@
"peerDependencies": {
"@aws-cdk/aws-codepipeline-api": "^0.27.0",
"@aws-cdk/aws-events": "^0.27.0",
"@aws-cdk/aws-iam": "^0.27.0",
"@aws-cdk/cdk": "^0.27.0"
},
"engines": {
"node": ">= 8.10.0"
}
}
}
13 changes: 6 additions & 7 deletions packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import codepipeline = require('@aws-cdk/aws-codepipeline-api');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import { IServerDeploymentGroup } from './server';

/**
Expand Down Expand Up @@ -43,33 +42,33 @@ export class PipelineDeployAction extends codepipeline.DeployAction {
this.deploymentGroup = props.deploymentGroup;
}

protected bind(stage: codepipeline.IStage, scope: cdk.Construct): void {
protected bind(info: codepipeline.ActionBind): void {
// permissions, based on:
// https://docs.aws.amazon.com/codedeploy/latest/userguide/auth-and-access-control-permissions-reference.html

stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
info.role.addToPolicy(new iam.PolicyStatement()
.addResource(this.deploymentGroup.application.applicationArn)
.addActions(
'codedeploy:GetApplicationRevision',
'codedeploy:RegisterApplicationRevision',
));

stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
info.role.addToPolicy(new iam.PolicyStatement()
.addResource(this.deploymentGroup.deploymentGroupArn)
.addActions(
'codedeploy:CreateDeployment',
'codedeploy:GetDeployment',
));

stage.pipeline.role.addToPolicy(new iam.PolicyStatement()
.addResource(this.deploymentGroup.deploymentConfig.deploymentConfigArn(scope))
info.role.addToPolicy(new iam.PolicyStatement()
.addResource(this.deploymentGroup.deploymentConfig.deploymentConfigArn(info.scope))
.addActions(
'codedeploy:GetDeploymentConfig',
));

// grant the ASG Role permissions to read from the Pipeline Bucket
for (const asg of this.deploymentGroup.autoScalingGroups || []) {
stage.pipeline.grantBucketRead(asg.role);
info.pipeline.grantBucketRead(asg.role);
}
}
}
67 changes: 46 additions & 21 deletions packages/@aws-cdk/aws-codepipeline-api/lib/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,32 @@ export function defaultBounds(): ActionArtifactBounds {
};
}

/**
* The interface used in the {@link Action#bind()} callback.
*/
export interface ActionBind {
/**
* The pipeline this action has been added to.
*/
readonly pipeline: IPipeline;

/**
* The stage this action has been added to.
*/
readonly stage: IStage;

/**
* The scope construct for this action.
* Can be used by the action implementation to create any resources it needs to work correctly.
*/
readonly scope: cdk.Construct;

/**
* The IAM Role to add the necessary permissions to.
*/
readonly role: iam.IRole;
}

/**
* The abstract view of an AWS CodePipeline as required and used by Actions.
* It extends {@link events.IEventRuleTarget},
Expand All @@ -52,11 +78,6 @@ export interface IPipeline extends cdk.IConstruct, events.IEventRuleTarget {
*/
readonly pipelineArn: string;

/**
* The service Role of the Pipeline.
*/
readonly role: iam.Role;

/**
* Grants read permissions to the Pipeline's S3 Bucket to the given Identity.
*
Expand All @@ -81,11 +102,6 @@ export interface IStage {
*/
readonly stageName: string;

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

addAction(action: Action): void;

onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule;
Expand Down Expand Up @@ -202,6 +218,7 @@ export abstract class Action {
private readonly _actionOutputArtifacts = new Array<Artifact>();
private readonly artifactBounds: ActionArtifactBounds;

private _pipeline?: IPipeline;
private _stage?: IStage;
private _scope?: cdk.Construct;

Expand All @@ -226,7 +243,7 @@ export abstract class Action {
rule.addEventPattern({
detailType: [ 'CodePipeline Stage Execution State Change' ],
source: [ 'aws.codepipeline' ],
resources: [ this.stage.pipeline.pipelineArn ],
resources: [ this.pipeline.pipelineArn ],
detail: {
stage: [ this.stage.stageName ],
action: [ this.actionName ],
Expand Down Expand Up @@ -303,24 +320,32 @@ export abstract class Action {
* The method called when an Action is attached to a Pipeline.
* This method is guaranteed to be called only once for each Action instance.
*
* @param stage the stage this action has been added to
* (includes a reference to the pipeline as well)
* @param scope the scope construct for this action,
* can be used by the action implementation to create any resources it needs to work correctly
* @info an instance of the {@link ActionBind} class,
* that contains the necessary information for the Action
* to configure itself, like a reference to the Pipeline, Stage, Role, etc.
*/
protected abstract bind(stage: IStage, scope: cdk.Construct): void;
protected abstract bind(info: ActionBind): void;

// ignore unused private method (it's actually used in Stage)
// ignore unused private method (it's actually used in Pipeline)
// @ts-ignore
private _attachActionToPipeline(stage: IStage, scope: cdk.Construct): void {
private _actionAttachedToPipeline(info: ActionBind): void {
if (this._stage) {
throw new Error(`Action '${this.actionName}' has been added to a pipeline twice`);
}

this._stage = stage;
this._scope = scope;
this._pipeline = info.pipeline;
this._stage = info.stage;
this._scope = info.scope;

this.bind(info);
}

this.bind(stage, scope);
private get pipeline(): IPipeline {
if (this._pipeline) {
return this._pipeline;
} else {
throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange');
}
}

private get stage(): IStage {
Expand Down
Loading

0 comments on commit ffe0046

Please sign in to comment.