From 5f1c77960322f63d6f1fc5f401ea53879cf74624 Mon Sep 17 00:00:00 2001 From: Mate GABRI Date: Thu, 13 Jun 2019 13:14:24 +1000 Subject: [PATCH 1/5] feat(aws-codepipeline-actions): Add CAPABILITY_AUTO_EXPAND (#2851) BREAKING CHANGE: the capabilities property is now an array to support multiple capabilities. Adds CAPABILITY_AUTO_EXPAND and support to define a list of capabilities for the CodePipeline action. --- .../lib/cloud-formation-capabilities.ts | 9 ++ .../lib/cloudformation/pipeline-actions.ts | 16 +++- .../test.cloudformation-pipeline-actions.ts | 90 ++++++++++++++++++- 3 files changed, 111 insertions(+), 4 deletions(-) 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 96db314388eae..a6dd58ef7d1a2 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 @@ -141,7 +141,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 +221,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, @@ -540,3 +540,13 @@ interface StatementTemplate { } type StatementCondition = { [op: string]: { [attribute: string]: string } }; + +function parseCapabilities(capabilities: any): any { + if (capabilities === undefined) { + return undefined; + } else if (capabilities.length === 1) { + return capabilities.toString(); + } else if (capabilities.length > 1) { + return capabilities.join(','); + } +} \ 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 f9fcedf104f00..7c0e00e77aa68 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 { CodePipelineBuildArtifacts, CodePipelineSource, Project } from '@aws-cdk/aws-codebuild'; import { Repository } from '@aws-cdk/aws-codecommit'; import codepipeline = require('@aws-cdk/aws-codepipeline'); @@ -415,7 +416,94 @@ 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(); + }, }; /** From 501ece970679783dafbdd3277d43a2aa06fec946 Mon Sep 17 00:00:00 2001 From: Mate GABRI Date: Thu, 13 Jun 2019 17:27:26 +1000 Subject: [PATCH 2/5] feat(aws-codepipeline-actions): Add CAPABILITY_AUTO_EXPAND (#2851) Properly handle CloudFormationCapabilities.None --- .../lib/cloudformation/pipeline-actions.ts | 3 +- .../test.cloudformation-pipeline-actions.ts | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) 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 a6dd58ef7d1a2..ec6080a93ad89 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 @@ -545,7 +545,8 @@ function parseCapabilities(capabilities: any): any { if (capabilities === undefined) { return undefined; } else if (capabilities.length === 1) { - return capabilities.toString(); + const capability = capabilities.toString(); + return (capability === '') ? undefined : capability; } else if (capabilities.length > 1) { return capabilities.join(','); } 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 7c0e00e77aa68..da392b2761c6a 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 @@ -504,6 +504,48 @@ export = { 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(); + }, }; /** From 61efbadd5cf04e2e9eea586f169674c19832b0a7 Mon Sep 17 00:00:00 2001 From: Mate GABRI Date: Thu, 13 Jun 2019 17:27:54 +1000 Subject: [PATCH 3/5] feat(aws-codepipeline-actions): Add CAPABILITY_AUTO_EXPAND (#2851) Update app-handler to support capabilities[] --- .../app-delivery/lib/pipeline-deploy-stack-action.ts | 10 +++++----- packages/@aws-cdk/app-delivery/test/integ.cicd.ts | 2 +- .../test/test.pipeline-deploy-stack-action.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 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..8570fe3b7926c 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], 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; + return [cfn.CloudFormationCapabilities.NamedIAM]; } else if (capabilities === undefined) { // else capabilities are undefined set AnonymousIAM - return cfn.CloudFormationCapabilities.AnonymousIAM; + return [cfn.CloudFormationCapabilities.AnonymousIAM]; } 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 594e494eb03ef..d0d89b85fb1f1 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 3dfaaa12e6391..c8d53b5cc8505 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 @@ -97,21 +97,21 @@ export = nodeunit.testCase({ 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, }); expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ From 50a7fa7fa83a82b2f33502197b6084be2477d95e Mon Sep 17 00:00:00 2001 From: Mate GABRI Date: Sat, 15 Jun 2019 16:34:07 +1000 Subject: [PATCH 4/5] feat(codepipeline-actions): Add CAPABILITY_AUTO_EXPAND (#2851) Update capabilities() function with strong types. --- .../lib/cloudformation/pipeline-actions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 ec6080a93ad89..f44a695bbeae9 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'); @@ -541,7 +542,7 @@ interface StatementTemplate { type StatementCondition = { [op: string]: { [attribute: string]: string } }; -function parseCapabilities(capabilities: any): any { +function parseCapabilities(capabilities: CloudFormationCapabilities[] | undefined): string | undefined { if (capabilities === undefined) { return undefined; } else if (capabilities.length === 1) { @@ -550,4 +551,6 @@ function parseCapabilities(capabilities: any): any { } else if (capabilities.length > 1) { return capabilities.join(','); } + + return undefined; } \ No newline at end of file From e7526dde0a18059f4fd3b7ee0c4c9aa0017bd136 Mon Sep 17 00:00:00 2001 From: Mate GABRI Date: Sat, 15 Jun 2019 16:35:20 +1000 Subject: [PATCH 5/5] feat(app-delivery): Add CAPABILITY_AUTO_EXPAND (#2851) Add CAPABILITY_AUTO_EXPAND to the default capabilities of the CloudFormation stack deploy role. --- .../lib/pipeline-deploy-stack-action.ts | 10 ++--- .../test/test.pipeline-deploy-stack-action.ts | 38 ++++++++++++++++++- 2 files changed, 42 insertions(+), 6 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 8570fe3b7926c..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,7 +63,7 @@ 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[]; @@ -168,11 +168,11 @@ export class PipelineDeployStackAction extends cdk.Construct { 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/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index c8d53b5cc8505..5fa390ab01a0e 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,12 +86,20 @@ 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({ name: 'SelfUpdate1' }); const selfUpdateStage2 = pipeline.addStage({ name: 'SelfUpdate2' }); const selfUpdateStage3 = pipeline.addStage({ name: 'SelfUpdate3' }); + const selfUpdateStage4 = pipeline.addStage({ name: 'SelfUpdate4' }); + const selfUpdateStage5 = pipeline.addStage({ name: 'SelfUpdate5' }); new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { stage: selfUpdateStage1, @@ -114,6 +122,20 @@ export = nodeunit.testCase({ 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({ Configuration: { StackName: "TestStack", @@ -148,6 +170,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 +214,7 @@ export = nodeunit.testCase({ Configuration: { StackName: "TestStack", ActionMode: "CHANGE_SET_REPLACE", - Capabilities: "CAPABILITY_NAMED_IAM", + Capabilities: "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", } }))); test.done();