Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: re-factor the CodePipeline Action bind method #2085

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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