diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index e8e1ee3fcbfe7..8eb4d05e90073 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -61,6 +61,7 @@ export abstract class PipelineCloudFormationAction extends codepipeline.Action { super(parent, id, { stage: props.stage, runOrder: props.runOrder, + actionRole: props.actionRole, region: props.region, artifactBounds: { minInputs: 0, diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts index 0f6ef1754787d..fedbd998471c7 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -141,6 +141,15 @@ export interface CommonActionProps { * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html */ runOrder?: number; + + /** + * 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 + */ + actionRole?: iam.IRole; } /** @@ -210,6 +219,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 actionRole?: iam.IRole; + /** * The order in which AWS CodePipeline runs this action. * For more information, see the AWS CodePipeline User Guide. @@ -223,6 +241,7 @@ export abstract class Action extends cdk.Construct { private readonly _actionInputArtifacts = new Array(); private readonly _actionOutputArtifacts = new Array(); + private readonly artifactBounds: ActionArtifactBounds; private readonly stage: IStage; @@ -240,6 +259,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.actionRole = props.actionRole; this.stage._internal._attachAction(this); } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index bd1faa6398c80..8eb3455cda4d5 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -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.actionRole ? action.actionRole.roleArn : undefined }; } diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.expected.json new file mode 100644 index 0000000000000..3a18cd973a077 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.expected.json @@ -0,0 +1,264 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + } + }, + "MyPipelineRoleC0D47CA4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyPipelineRoleDefaultPolicy34F09EFA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CFNDeployRole68D5E8D3", + "Arn" + ] + } + }, + { + "Action": [ + "cloudformation:CreateStack", + "cloudformation:DescribeStack*", + "cloudformation:GetStackPolicy", + "cloudformation:GetTemplate*", + "cloudformation:SetStackPolicy", + "cloudformation:UpdateStack", + "cloudformation:ValidateTemplate" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":stack/aws-cdk-codepipeline-cross-region-deploy-stack/*" + ] + ] + } + }, + { + "Action": [ + "sts:AssumeRole", + "iam:PassRole" + ], + "Effect": "Allow", + "Resource":"*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyPipelineRoleDefaultPolicy34F09EFA", + "Roles": [ + { + "Ref": "MyPipelineRoleC0D47CA4" + } + ] + } + }, + "MyPipelineAED38ECF": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "MyPipelineRoleC0D47CA4", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "MyBucketF68F3FF0" + }, + "S3ObjectKey": "some/path", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "S3", + "OutputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecloudformationcrossregionwithactionroleMyBucketS30423514B" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "aws-cdk-codepipeline-cross-region-deploy-stack", + "ActionMode": "CREATE_UPDATE", + "TemplatePath": "Artifact_awscdkcodepipelinecloudformationcrossregionwithactionroleMyBucketS30423514B::template.yml", + "RoleArn": { + "Fn::GetAtt": [ + "CFNDeployRole68D5E8D3", + "Arn" + ] + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecloudformationcrossregionwithactionroleMyBucketS30423514B" + } + ], + "Name": "CFN_Deploy", + "OutputArtifacts": [], + "RoleArn": { + "Fn::GetAtt": [ + "MyPipelineActionRole0ADEF270", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "CFN" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "MyBucketF68F3FF0" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "MyPipelineRoleC0D47CA4", + "MyPipelineRoleDefaultPolicy34F09EFA" + ] + }, + "CFNDeployRole68D5E8D3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.ts new file mode 100644 index 0000000000000..99cf5f3c18fc6 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.ts @@ -0,0 +1,44 @@ +import cloudformation = require('@aws-cdk/aws-cloudformation'); +import iam = require('@aws-cdk/aws-iam'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import codepipeline = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-cloudformation-cross-region-with-action-role', {}); + +const bucket = new s3.Bucket(stack, 'MyBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.Destroy, +}); + +const pipeline = new codepipeline.Pipeline(stack, 'MyPipeline', { + artifactBucket: bucket, +}); + +const sourceStage = pipeline.addStage('Source'); +const sourceAction = bucket.addToPipeline(sourceStage, 'S3', { + bucketKey: 'some/path', +}); + +const cfnStage = pipeline.addStage('CFN'); + +const role = new iam.Role(pipeline, 'ActionRole', { + assumedBy: new iam.AccountPrincipal(new cdk.AwsAccountId().toString()) +}); + +new cloudformation.PipelineCreateUpdateStackAction(stack, 'CFN_Deploy', { + stage: cfnStage, + stackName: 'aws-cdk-codepipeline-cross-region-deploy-stack', + templatePath: sourceAction.outputArtifact.atPath('template.yml'), + adminPermissions: false, + actionRole: role +}); + +pipeline.addToRolePolicy(new iam.PolicyStatement() + .addActions("sts:AssumeRole", "iam:PassRole") + .addAllResources() +); + +app.run(); diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts index a33581084b12a..989d4b4f80632 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts @@ -356,6 +356,53 @@ export = { })); test.done(); + }, + + 'Action service role is passed to template'(test: Test) { + const stack = new TestFixture(); + + const importedRole = Role.import(stack, 'ImportedRole', { + roleArn: 'arn:aws:iam::000000000000:role/action-role' + }); + const freshRole = new Role(stack, 'FreshRole', { + assumedBy: new ServicePrincipal('magicservice') + }); + + new PipelineExecuteChangeSetAction(stack.pipeline, 'ImportedRoleAction', { + actionRole: importedRole, + changeSetName: 'magicSet', + stackName: 'magicStack', + stage: stack.deployStage + }); + + new PipelineExecuteChangeSetAction(stack.pipeline, 'FreshRoleAction', { + actionRole: freshRole, + changeSetName: 'magicSet', + stackName: 'magicStack', + stage: stack.deployStage + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [{ + "Name": "Source" /* don't care about the rest */ + }, { + "Name": "Deploy", + "Actions": [{ + "Name": "ImportedRoleAction", + "RoleArn": "arn:aws:iam::000000000000:role/action-role" + }, { + "Name": "FreshRoleAction", + "RoleArn": { + "Fn::GetAtt": [ + "FreshRole472F6E18", + "Arn" + ] + } + }] + }] + })); + + test.done(); } };