From c9340a61b9027352124f010feaade6dafe0cf178 Mon Sep 17 00:00:00 2001 From: Mate Gabri Date: Fri, 21 Jun 2019 19:07:34 +1000 Subject: [PATCH] feat(codepipeline-actions): Add CAPABILITY_AUTO_EXPAND (#2851) (#2852) Adds CAPABILITY_AUTO_EXPAND and support to define a list of capabilities for the CodePipeline action. BREAKING CHANGE: * **codepipeline**: the `capabilities` property is now an array to support multiple capabilities. --- .../lib/pipeline-deploy-stack-action.ts | 14 +- .../@aws-cdk/app-delivery/test/integ.cicd.ts | 2 +- .../test/test.pipeline-deploy-stack-action.ts | 45 +++++- .../lib/cloud-formation-capabilities.ts | 9 ++ .../lib/cloudformation/pipeline-actions.ts | 20 ++- .../test.cloudformation-pipeline-actions.ts | 132 +++++++++++++++++- 6 files changed, 206 insertions(+), 16 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 d8222699ecd8b..bc8cbf9dbafa7 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 @@ -63,9 +63,9 @@ export interface PipelineDeployStackActionProps { * information * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities - * @default AnonymousIAM, unless `adminPermissions` is true + * @default [AnonymousIAM, AutoExpand], unless `adminPermissions` is true */ - readonly capabilities?: cfn.CloudFormationCapabilities; + readonly capabilities?: cfn.CloudFormationCapabilities[]; /** * Whether to grant admin permissions to CloudFormation while deploying this template. @@ -166,13 +166,13 @@ export class PipelineDeployStackAction extends cdk.Construct { } } -function cfnCapabilities(adminPermissions: boolean, capabilities?: cfn.CloudFormationCapabilities): cfn.CloudFormationCapabilities { +function cfnCapabilities(adminPermissions: boolean, capabilities?: cfn.CloudFormationCapabilities[]): cfn.CloudFormationCapabilities[] { if (adminPermissions && capabilities === undefined) { - // admin true default capability to NamedIAM - return cfn.CloudFormationCapabilities.NamedIAM; + // admin true default capability to NamedIAM and AutoExpand + return [cfn.CloudFormationCapabilities.NamedIAM, cfn.CloudFormationCapabilities.AutoExpand]; } else if (capabilities === undefined) { - // else capabilities are undefined set AnonymousIAM - return cfn.CloudFormationCapabilities.AnonymousIAM; + // else capabilities are undefined set AnonymousIAM and AutoExpand + return [cfn.CloudFormationCapabilities.AnonymousIAM, cfn.CloudFormationCapabilities.AutoExpand]; } else { // else capabilities are defined use them return capabilities; diff --git a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts index 7f58a8ca49164..ed56805675c1f 100644 --- a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts +++ b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts @@ -35,7 +35,7 @@ new cicd.PipelineDeployStackAction(stack, 'DeployStack', { executeChangeSetRunOrder: 999, input: sourceOutput, adminPermissions: false, - capabilities: cfn.CloudFormationCapabilities.None, + capabilities: [cfn.CloudFormationCapabilities.None], }); app.synth(); 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 6c4014f2a765e..e97deed370e38 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 @@ -86,32 +86,55 @@ export = nodeunit.testCase({ const stackWithAnonymousCapability = new cdk.Stack(undefined, 'AnonymousIAM', { env: { account: '123456789012', region: 'us-east-1' } }); + const stackWithAutoExpandCapability = new cdk.Stack(undefined, 'AutoExpand', + { env: { account: '123456789012', region: 'us-east-1' } }); + + const stackWithAnonymousAndAutoExpandCapability = new cdk.Stack(undefined, 'AnonymousIAMAndAutoExpand', + { env: { account: '123456789012', region: 'us-east-1' } }); + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); const pipeline = selfUpdatingStack.pipeline; + const selfUpdateStage1 = pipeline.addStage({ stageName: 'SelfUpdate1' }); const selfUpdateStage2 = pipeline.addStage({ stageName: 'SelfUpdate2' }); const selfUpdateStage3 = pipeline.addStage({ stageName: 'SelfUpdate3' }); + const selfUpdateStage4 = pipeline.addStage({ stageName: 'SelfUpdate4' }); + const selfUpdateStage5 = pipeline.addStage({ stageName: 'SelfUpdate5' }); new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { stage: selfUpdateStage1, stack: pipelineStack, input: selfUpdatingStack.synthesizedApp, - capabilities: cfn.CloudFormationCapabilities.NamedIAM, + capabilities: [cfn.CloudFormationCapabilities.NamedIAM], adminPermissions: false, }); new PipelineDeployStackAction(pipelineStack, 'DeployStack', { stage: selfUpdateStage2, stack: stackWithNoCapability, input: selfUpdatingStack.synthesizedApp, - capabilities: cfn.CloudFormationCapabilities.None, + capabilities: [cfn.CloudFormationCapabilities.None], adminPermissions: false, }); new PipelineDeployStackAction(pipelineStack, 'DeployStack2', { stage: selfUpdateStage3, stack: stackWithAnonymousCapability, input: selfUpdatingStack.synthesizedApp, - capabilities: cfn.CloudFormationCapabilities.AnonymousIAM, + capabilities: [cfn.CloudFormationCapabilities.AnonymousIAM], + adminPermissions: false, + }); + new PipelineDeployStackAction(pipelineStack, 'DeployStack3', { + stage: selfUpdateStage4, + stack: stackWithAutoExpandCapability, + input: selfUpdatingStack.synthesizedApp, + capabilities: [cfn.CloudFormationCapabilities.AutoExpand], + adminPermissions: false, + }); + new PipelineDeployStackAction(pipelineStack, 'DeployStack4', { + stage: selfUpdateStage5, + stack: stackWithAnonymousAndAutoExpandCapability, + input: selfUpdatingStack.synthesizedApp, + capabilities: [cfn.CloudFormationCapabilities.AnonymousIAM, cfn.CloudFormationCapabilities.AutoExpand], adminPermissions: false, }); expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ @@ -148,6 +171,20 @@ export = nodeunit.testCase({ ActionMode: "CHANGE_SET_REPLACE", } }))); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "AutoExpand", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_AUTO_EXPAND", + } + }))); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "AnonymousIAMAndAutoExpand", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND", + } + }))); test.done(); }, 'users can use admin permissions'(test: nodeunit.Test) { @@ -178,7 +215,7 @@ export = nodeunit.testCase({ Configuration: { StackName: "TestStack", ActionMode: "CHANGE_SET_REPLACE", - Capabilities: "CAPABILITY_NAMED_IAM", + Capabilities: "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", } }))); test.done(); diff --git a/packages/@aws-cdk/aws-cloudformation/lib/cloud-formation-capabilities.ts b/packages/@aws-cdk/aws-cloudformation/lib/cloud-formation-capabilities.ts index 55a4cb1b29f75..e1437d1c13704 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/cloud-formation-capabilities.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/cloud-formation-capabilities.ts @@ -28,4 +28,13 @@ export enum CloudFormationCapabilities { * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities */ NamedIAM = 'CAPABILITY_NAMED_IAM', + + /** + * Capability to run CloudFormation macros + * + * Pass this capability if your template includes macros, for example AWS::Include or AWS::Serverless. + * + * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html + */ + AutoExpand = 'CAPABILITY_AUTO_EXPAND' } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts index ec4b2648584e1..5721604717374 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts @@ -1,4 +1,5 @@ import cloudformation = require('@aws-cdk/aws-cloudformation'); +import { CloudFormationCapabilities } from '@aws-cdk/aws-cloudformation'; import codepipeline = require('@aws-cdk/aws-codepipeline'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); @@ -141,7 +142,7 @@ export interface CloudFormationDeployActionProps extends CloudFormationActionPro * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities * @default None, unless `adminPermissions` is true */ - readonly capabilities?: cloudformation.CloudFormationCapabilities; + readonly capabilities?: cloudformation.CloudFormationCapabilities[]; /** * Whether to grant full permissions to CloudFormation while deploying this template. @@ -221,12 +222,12 @@ export abstract class CloudFormationDeployAction extends CloudFormationAction { constructor(props: CloudFormationDeployActionProps, configuration: any) { const capabilities = props.adminPermissions && props.capabilities === undefined - ? cloudformation.CloudFormationCapabilities.NamedIAM + ? [cloudformation.CloudFormationCapabilities.NamedIAM] : props.capabilities; super(props, { ...configuration, // None evaluates to empty string which is falsey and results in undefined - Capabilities: (capabilities && capabilities.toString()) || undefined, + Capabilities: parseCapabilities(capabilities), RoleArn: cdk.Lazy.stringValue({ produce: () => this.deploymentRole.roleArn }), ParameterOverrides: cdk.Lazy.stringValue({ produce: () => Stack.of(this.scope).toJsonString(props.parameterOverrides) }), TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined, @@ -543,3 +544,16 @@ interface StatementTemplate { } type StatementCondition = { [op: string]: { [attribute: string]: string } }; + +function parseCapabilities(capabilities: CloudFormationCapabilities[] | undefined): string | undefined { + if (capabilities === undefined) { + return undefined; + } else if (capabilities.length === 1) { + const capability = capabilities.toString(); + return (capability === '') ? undefined : capability; + } else if (capabilities.length > 1) { + return capabilities.join(','); + } + + return undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts index aa51df74012af..fa9f684faab68 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts @@ -1,4 +1,5 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { CloudFormationCapabilities } from '@aws-cdk/aws-cloudformation'; import codebuild = require('@aws-cdk/aws-codebuild'); import { Repository } from '@aws-cdk/aws-codecommit'; import codepipeline = require('@aws-cdk/aws-codepipeline'); @@ -413,7 +414,136 @@ export = { })); test.done(); - } + }, + + 'Single capability is passed to template'(test: Test) { + // GIVEN + const stack = new TestFixture(); + + // WHEN + stack.deployStage.addAction(new cpactions.CloudFormationCreateUpdateStackAction({ + actionName: 'CreateUpdate', + stackName: 'MyStack', + templatePath: stack.sourceOutput.atPath('template.yaml'), + adminPermissions: false, + capabilities: [ + CloudFormationCapabilities.NamedIAM + ] + })); + + const roleId = "PipelineDeployCreateUpdateRole515CB7D4"; + + // THEN: Action in Pipeline has named IAM capabilities + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { "Name": "Source" /* don't care about the rest */ }, + { + "Name": "Deploy", + "Actions": [ + { + "Configuration": { + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { "Fn::GetAtt": [ roleId, "Arn" ] }, + "ActionMode": "CREATE_UPDATE", + "StackName": "MyStack", + "TemplatePath": "SourceArtifact::template.yaml" + }, + "InputArtifacts": [{"Name": "SourceArtifact"}], + "Name": "CreateUpdate", + }, + ], + } + ] + })); + + test.done(); + }, + + 'Multiple capabilities are passed to template'(test: Test) { + // GIVEN + const stack = new TestFixture(); + + // WHEN + stack.deployStage.addAction(new cpactions.CloudFormationCreateUpdateStackAction({ + actionName: 'CreateUpdate', + stackName: 'MyStack', + templatePath: stack.sourceOutput.atPath('template.yaml'), + adminPermissions: false, + capabilities: [ + CloudFormationCapabilities.NamedIAM, + CloudFormationCapabilities.AutoExpand + ] + })); + + const roleId = "PipelineDeployCreateUpdateRole515CB7D4"; + + // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { "Name": "Source" /* don't care about the rest */ }, + { + "Name": "Deploy", + "Actions": [ + { + "Configuration": { + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { "Fn::GetAtt": [ roleId, "Arn" ] }, + "ActionMode": "CREATE_UPDATE", + "StackName": "MyStack", + "TemplatePath": "SourceArtifact::template.yaml" + }, + "InputArtifacts": [{"Name": "SourceArtifact"}], + "Name": "CreateUpdate", + }, + ], + } + ] + })); + + test.done(); + }, + + 'Empty capabilities is not passed to template'(test: Test) { + // GIVEN + const stack = new TestFixture(); + + // WHEN + stack.deployStage.addAction(new cpactions.CloudFormationCreateUpdateStackAction({ + actionName: 'CreateUpdate', + stackName: 'MyStack', + templatePath: stack.sourceOutput.atPath('template.yaml'), + adminPermissions: false, + capabilities: [ + CloudFormationCapabilities.None + ] + })); + + const roleId = "PipelineDeployCreateUpdateRole515CB7D4"; + + // THEN: Action in Pipeline has no capabilities + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { "Name": "Source" /* don't care about the rest */ }, + { + "Name": "Deploy", + "Actions": [ + { + "Configuration": { + "RoleArn": { "Fn::GetAtt": [ roleId, "Arn" ] }, + "ActionMode": "CREATE_UPDATE", + "StackName": "MyStack", + "TemplatePath": "SourceArtifact::template.yaml" + }, + "InputArtifacts": [{"Name": "SourceArtifact"}], + "Name": "CreateUpdate", + }, + ], + } + ] + })); + + test.done(); + }, }; /**