Skip to content

Commit

Permalink
feat(aws-codepipeline): support setting a Role for a CFN Action (#1449)
Browse files Browse the repository at this point in the history
This adds the option to set a Role for a CodePipeline Action.
As of now, this is limited to only the CloudFormation Pipeline Actions.
Setting the Role is needed for some more advanced setups,
for example cross-account deployments.

BREAKING CHANGE:  the `role` property in the CloudFormation Actions has been renamed to `deploymentRole`.
BREAKING CHANGE: the `role` property in the `app-delivery` package has been renamed to `deploymentRole`.
  • Loading branch information
rsmogura authored and skinny85 committed Jan 24, 2019
1 parent 036cfdf commit 77fe077
Show file tree
Hide file tree
Showing 15 changed files with 477 additions and 32 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.vscode
# VSCode extension
/.favorites.json
.DS_Store
node_modules
lerna-debug.log
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class PipelineDeployStackAction extends cdk.Construct {
/**
* The role used by CloudFormation for the deploy action
*/
public readonly role: iam.IRole;
public readonly deploymentRole: iam.IRole;

private readonly stack: cdk.Stack;

Expand Down Expand Up @@ -127,10 +127,10 @@ export class PipelineDeployStackAction extends cdk.Construct {
stage: props.stage,
templatePath: props.inputArtifact.atPath(`${props.stack.name}.template.yaml`),
adminPermissions: props.adminPermissions,
role: props.role,
deploymentRole: props.role,
capabilities,
});
this.role = changeSetAction.role;
this.deploymentRole = changeSetAction.deploymentRole;

new cfn.PipelineExecuteChangeSetAction(this, 'Execute', {
changeSetName,
Expand All @@ -149,8 +149,8 @@ export class PipelineDeployStackAction extends cdk.Construct {
* `adminPermissions` you need to identify the proper statements to add to
* this role based on the CloudFormation Resources in your stack.
*/
public addToRolePolicy(statement: iam.PolicyStatement) {
this.role.addToPolicy(statement);
public addToDeploymentRolePolicy(statement: iam.PolicyStatement) {
this.deploymentRole.addToPolicy(statement);
}

protected validate(): string[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export = nodeunit.testCase({
adminPermissions: false,
role
});
test.same(deployAction.role, role);
test.same(deployAction.deploymentRole, role);
test.done();
},
'users can specify IAM permissions for the deploy action'(test: nodeunit.Test) {
Expand All @@ -211,7 +211,7 @@ export = nodeunit.testCase({
adminPermissions: false,
});
// we might need to add permissions
deployAction.addToRolePolicy( new iam.PolicyStatement().
deployAction.addToDeploymentRolePolicy( new iam.PolicyStatement().
addActions(
'ec2:AuthorizeSecurityGroupEgress',
'ec2:AuthorizeSecurityGroupIngress',
Expand Down
32 changes: 20 additions & 12 deletions packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ export interface PipelineCloudFormationActionProps extends codepipeline.CommonAc
* @default the Action resides in the same region as the Pipeline
*/
region?: string;

/**
* The service role that is assumed during execution of action.
* This role is not mandatory, however more advanced configuration
* may require specifying it.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html
*/
role?: iam.IRole;
}

/**
Expand All @@ -59,8 +68,7 @@ export abstract class PipelineCloudFormationAction extends codepipeline.Action {

constructor(scope: cdk.Construct, id: string, props: PipelineCloudFormationActionProps, configuration?: any) {
super(scope, id, {
stage: props.stage,
runOrder: props.runOrder,
...props,
region: props.region,
artifactBounds: {
minInputs: 0,
Expand Down Expand Up @@ -123,7 +131,7 @@ export interface PipelineCloudFormationDeployActionProps extends PipelineCloudFo
*
* @default A fresh role with full or no permissions (depending on the value of `adminPermissions`).
*/
role?: iam.IRole;
deploymentRole?: iam.IRole;

/**
* Acknowledge certain changes made as part of deployment
Expand Down Expand Up @@ -194,40 +202,40 @@ export interface PipelineCloudFormationDeployActionProps extends PipelineCloudFo
* Base class for all CloudFormation actions that execute or stage deployments.
*/
export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFormationAction {
public readonly role: iam.IRole;
public readonly deploymentRole: iam.IRole;

constructor(scope: cdk.Construct, id: string, props: PipelineCloudFormationDeployActionProps, configuration: any) {
const capabilities = props.adminPermissions && props.capabilities === undefined ? CloudFormationCapabilities.NamedIAM : props.capabilities;
super(scope, id, props, {
...configuration,
// None evaluates to empty string which is falsey and results in undefined
Capabilities: (capabilities && capabilities.toString()) || undefined,
RoleArn: new cdk.Token(() => this.role.roleArn),
RoleArn: new cdk.Token(() => this.deploymentRole.roleArn),
ParameterOverrides: new cdk.Token(() => this.node.stringifyJson(props.parameterOverrides)),
TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined,
StackName: props.stackName,
});

if (props.role) {
this.role = props.role;
if (props.deploymentRole) {
this.deploymentRole = props.deploymentRole;
} else {
this.role = new iam.Role(this, 'Role', {
this.deploymentRole = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com')
});

if (props.adminPermissions) {
this.role.addToPolicy(new iam.PolicyStatement().addAction('*').addAllResources());
this.deploymentRole.addToPolicy(new iam.PolicyStatement().addAction('*').addAllResources());
}
}

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

/**
* Add statement to the service role assumed by CloudFormation while executing this action.
*/
public addToRolePolicy(statement: iam.PolicyStatement) {
return this.role.addToPolicy(statement);
public addToDeploymentRolePolicy(statement: iam.PolicyStatement) {
return this.deploymentRole.addToPolicy(statement);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export = nodeunit.testCase({
adminPermissions: false,
});

_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);
_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn);

const stackArn = _stackArn('MyStack', stack);
const changeSetCondition = { StringEqualsIfExists: { 'cloudformation:ChangeSetName': 'MyChangeSet' } };
Expand Down Expand Up @@ -175,7 +175,7 @@ export = nodeunit.testCase({
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:UpdateStack', stackArn);
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn);

_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);
_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn);

test.done();
},
Expand All @@ -193,7 +193,7 @@ export = nodeunit.testCase({
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn);
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn);

_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);
_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn);

test.done();
},
Expand Down
3 changes: 1 addition & 2 deletions packages/@aws-cdk/aws-codecommit/lib/pipeline-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ export interface PipelineSourceActionProps extends CommonPipelineSourceActionPro
export class PipelineSourceAction extends codepipeline.SourceAction {
constructor(scope: cdk.Construct, id: string, props: PipelineSourceActionProps) {
super(scope, id, {
stage: props.stage,
runOrder: props.runOrder,
...props,
provider: 'CodeCommit',
configuration: {
RepositoryName: props.repository.repositoryName,
Expand Down
3 changes: 1 addition & 2 deletions packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ export interface PipelineDeployActionProps extends CommonPipelineDeployActionPro
export class PipelineDeployAction extends codepipeline.DeployAction {
constructor(scope: cdk.Construct, id: string, props: PipelineDeployActionProps) {
super(scope, id, {
stage: props.stage,
runOrder: props.runOrder,
...props,
artifactBounds: { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 },
provider: 'CodeDeploy',
inputArtifact: props.inputArtifact,
Expand Down
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-api/lib/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ export interface ActionProps extends CommonActionProps, CommonActionConstructPro
*/
region?: string;

/**
* The service role that is assumed during execution of action.
* This role is not mandatory, however more advanced configuration
* may require specifying it.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html
*/
role?: iam.IRole;

artifactBounds: ActionArtifactBounds;
configuration?: any;
version?: string;
Expand Down Expand Up @@ -205,6 +214,15 @@ export abstract class Action extends cdk.Construct {
*/
public readonly configuration?: any;

/**
* The service role that is assumed during execution of action.
* This role is not mandatory, however more advanced configuration
* may require specifying it.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html
*/
public readonly role?: iam.IRole;

/**
* The order in which AWS CodePipeline runs this action.
* For more information, see the AWS CodePipeline User Guide.
Expand All @@ -218,6 +236,7 @@ export abstract class Action extends cdk.Construct {

private readonly _actionInputArtifacts = new Array<Artifact>();
private readonly _actionOutputArtifacts = new Array<Artifact>();

private readonly artifactBounds: ActionArtifactBounds;
private readonly stage: IStage;

Expand All @@ -235,6 +254,7 @@ export abstract class Action extends cdk.Construct {
this.artifactBounds = props.artifactBounds;
this.runOrder = props.runOrder === undefined ? 1 : props.runOrder;
this.stage = props.stage;
this.role = props.role;

this.stage._internal._attachAction(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ export interface GitHubSourceActionProps extends actions.CommonActionProps,
export class GitHubSourceAction extends actions.SourceAction {
constructor(scope: cdk.Construct, id: string, props: GitHubSourceActionProps) {
super(scope, id, {
stage: props.stage,
runOrder: props.runOrder,
...props,
owner: 'ThirdParty',
provider: 'GitHub',
configuration: {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export class Stage extends cdk.Construct implements cpapi.IStage, cpapi.IInterna
configuration: action.configuration,
outputArtifacts: action._outputArtifacts.map(a => ({ name: a.name })),
runOrder: action.runOrder,
roleArn: action.role ? action.role.roleArn : undefined
};
}

Expand Down
Loading

0 comments on commit 77fe077

Please sign in to comment.