From 9dcfa2f9a770b16e334465f850a1e2a585040f03 Mon Sep 17 00:00:00 2001 From: Mike Cowgill Date: Wed, 14 Nov 2018 16:17:11 -0800 Subject: [PATCH] adding the ability to pass in CloudFormation Capabilities, enable full permissions, or pass in a role --- .../lib/pipeline-deploy-stack-action.ts | 28 ++++ packages/@aws-cdk/app-delivery/package.json | 1 + .../test/test.pipeline-deploy-stack-action.ts | 125 +++++++++++++++--- 3 files changed, 132 insertions(+), 22 deletions(-) diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index cee3336672445..6a73184b5e157 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -41,6 +41,29 @@ export interface PipelineDeployStackActionProps { * @default ``createChangeSetRunOrder + 1`` */ executeChangeSetRunOrder?: number; + + /** + * The role to use when creating and executing the ChangeSet. + * + * @default a new role is created + */ + role?: iam.Role; + + /** + * The CloudFormation Capabilities enabled for the ChangeSet. + * + * @default None + */ + capabilities?: cfn.CloudFormationCapabilities[]; + + /** + * Should CloudFormation receive full permissions for this ChangeSet. + * + * This results in a role with Administrator Permissions or *:*. + * + * @default false + */ + fullPermissions?: boolean; } /** @@ -79,12 +102,17 @@ export class PipelineDeployStackAction extends cdk.Construct { this.stack = props.stack; + const fullPermissions = props.fullPermissions === true; + const changeSetAction = new cfn.PipelineCreateReplaceChangeSetAction(this, 'ChangeSet', { changeSetName, runOrder: createChangeSetRunOrder, stackName: props.stack.name, stage: props.stage, templatePath: props.inputArtifact.atPath(`${props.stack.name}.template.yaml`), + fullPermissions, + role: props.role, + capabilities: props.capabilities, }); this.role = changeSetAction.role; diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index f0ed5b4e64ccf..04e9e350a1cb8 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -65,6 +65,7 @@ ], "peerDependencies": { "@aws-cdk/aws-codepipeline-api": "^0.16.0", + "@aws-cdk/aws-cloudformation": "^0.16.0", "@aws-cdk/aws-iam": "^0.16.0", "@aws-cdk/cdk": "^0.16.0" } diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index 5eeba45053dcb..6cae56e2d4853 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -1,3 +1,4 @@ +import cfn = require('@aws-cdk/aws-cloudformation'); import codebuild = require('@aws-cdk/aws-codebuild'); import code = require('@aws-cdk/aws-codepipeline'); import api = require('@aws-cdk/aws-codepipeline-api'); @@ -8,9 +9,13 @@ import cxapi = require('@aws-cdk/cx-api'); import fc = require('fast-check'); import nodeunit = require('nodeunit'); -import { countResources, expect, haveResource } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, isSuperObject } from '@aws-cdk/assert'; import { PipelineDeployStackAction } from '../lib/pipeline-deploy-stack-action'; +interface SelfUpdatingPipeline { + synthesizedApp: api.Artifact; + pipeline: code.Pipeline; +} const accountId = fc.array(fc.integer(0, 9), 12, 12).map(arr => arr.join()); export = nodeunit.testCase({ @@ -63,36 +68,79 @@ export = nodeunit.testCase({ ); test.done(); }, - - 'users can specify IAM permissions for the deploy action'(test: nodeunit.Test) { - // GIVEN // + 'users can supply CloudFormation capabilities'(test: nodeunit.Test) { const pipelineStack = getTestStack(); + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); - // the fake stack to deploy - const emptyStack = getTestStack(); - - const pipeline = new code.Pipeline(pipelineStack, 'CodePipeline', { - restartExecutionOnUpdate: true, + const pipeline = selfUpdatingStack.pipeline; + const selfUpdateStage = pipeline.addStage('SelfUpdate'); + new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { + stage: selfUpdateStage, + stack: pipelineStack, + inputArtifact: selfUpdatingStack.synthesizedApp, + capabilities: [cfn.CloudFormationCapabilities.IAM], }); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "TestStack", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_IAM", + } + }))); + test.done(); + }, + 'users can supply enable full permissions'(test: nodeunit.Test) { + const pipelineStack = getTestStack(); + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); - // simple source - const bucket = s3.Bucket.import( pipeline, 'PatternBucket', { bucketArn: 'arn:aws:s3:::totally-fake-buckert' }); - new s3.PipelineSourceAction(pipeline, 'S3Source', { - bucket, - bucketKey: 'the-great-key', - stage: pipeline.addStage('source'), + const pipeline = selfUpdatingStack.pipeline; + const selfUpdateStage = pipeline.addStage('SelfUpdate'); + new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { + stage: selfUpdateStage, + stack: pipelineStack, + inputArtifact: selfUpdatingStack.synthesizedApp, + fullPermissions: true, }); + expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: '*', + Effect: 'Allow', + Resource: '*', + } + ], + } + })); + test.done(); + }, + 'users can supply a role for deploy action'(test: nodeunit.Test) { + const pipelineStack = getTestStack(); + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); - const project = new codebuild.PipelineProject(pipelineStack, 'CodeBuild'); - const buildStage = pipeline.addStage('build'); - const buildAction = project.addBuildToPipeline(buildStage, 'CodeBuild'); - const synthesizedApp = buildAction.outputArtifact; + const pipeline = selfUpdatingStack.pipeline; + const role = new iam.Role(pipelineStack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com'), + }); const selfUpdateStage = pipeline.addStage('SelfUpdate'); - new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { + const deployAction = new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { stage: selfUpdateStage, stack: pipelineStack, - inputArtifact: synthesizedApp, + inputArtifact: selfUpdatingStack.synthesizedApp, + role }); + test.deepEqual(role.id, deployAction.role.id); + test.done(); + }, + 'users can specify IAM permissions for the deploy action'(test: nodeunit.Test) { + // GIVEN // + const pipelineStack = getTestStack(); + + // the fake stack to deploy + const emptyStack = getTestStack(); + + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); + const pipeline = selfUpdatingStack.pipeline; // WHEN // // this our app/service/infra to deploy @@ -100,7 +148,7 @@ export = nodeunit.testCase({ const deployAction = new PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', { stage: deployStage, stack: emptyStack, - inputArtifact: synthesizedApp, + inputArtifact: selfUpdatingStack.synthesizedApp, }); // we might need to add permissions deployAction.role.addToPolicy( new iam.PolicyStatement(). @@ -189,3 +237,36 @@ class FakeAction extends api.Action { function getTestStack(): cdk.Stack { return new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); } + +function createSelfUpdatingStack(pipelineStack: cdk.Stack): SelfUpdatingPipeline { + const pipeline = new code.Pipeline(pipelineStack, 'CodePipeline', { + restartExecutionOnUpdate: true, + }); + + // simple source + const bucket = s3.Bucket.import( pipeline, 'PatternBucket', { bucketArn: 'arn:aws:s3:::totally-fake-bucket' }); + new s3.PipelineSourceAction(pipeline, 'S3Source', { + bucket, + bucketKey: 'the-great-key', + stage: pipeline.addStage('source'), + }); + + const project = new codebuild.PipelineProject(pipelineStack, 'CodeBuild'); + const buildStage = pipeline.addStage('build'); + const buildAction = project.addBuildToPipeline(buildStage, 'CodeBuild'); + const synthesizedApp = buildAction.outputArtifact; + return {synthesizedApp, pipeline}; +} + +function hasPipelineAction(expectedAction: any): (props: any) => boolean { + return (props: any) => { + for (const stage of props.Stages) { + for (const action of stage.Actions) { + if (isSuperObject(action, expectedAction)) { + return true; + } + } + } + return false; + }; +}