Skip to content

Commit c9340a6

Browse files
ScOut3Rrix0rrr
authored andcommitted
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.
1 parent ae789ed commit c9340a6

File tree

6 files changed

+206
-16
lines changed

6 files changed

+206
-16
lines changed

packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ export interface PipelineDeployStackActionProps {
6363
* information
6464
*
6565
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities
66-
* @default AnonymousIAM, unless `adminPermissions` is true
66+
* @default [AnonymousIAM, AutoExpand], unless `adminPermissions` is true
6767
*/
68-
readonly capabilities?: cfn.CloudFormationCapabilities;
68+
readonly capabilities?: cfn.CloudFormationCapabilities[];
6969

7070
/**
7171
* Whether to grant admin permissions to CloudFormation while deploying this template.
@@ -166,13 +166,13 @@ export class PipelineDeployStackAction extends cdk.Construct {
166166
}
167167
}
168168

169-
function cfnCapabilities(adminPermissions: boolean, capabilities?: cfn.CloudFormationCapabilities): cfn.CloudFormationCapabilities {
169+
function cfnCapabilities(adminPermissions: boolean, capabilities?: cfn.CloudFormationCapabilities[]): cfn.CloudFormationCapabilities[] {
170170
if (adminPermissions && capabilities === undefined) {
171-
// admin true default capability to NamedIAM
172-
return cfn.CloudFormationCapabilities.NamedIAM;
171+
// admin true default capability to NamedIAM and AutoExpand
172+
return [cfn.CloudFormationCapabilities.NamedIAM, cfn.CloudFormationCapabilities.AutoExpand];
173173
} else if (capabilities === undefined) {
174-
// else capabilities are undefined set AnonymousIAM
175-
return cfn.CloudFormationCapabilities.AnonymousIAM;
174+
// else capabilities are undefined set AnonymousIAM and AutoExpand
175+
return [cfn.CloudFormationCapabilities.AnonymousIAM, cfn.CloudFormationCapabilities.AutoExpand];
176176
} else {
177177
// else capabilities are defined use them
178178
return capabilities;

packages/@aws-cdk/app-delivery/test/integ.cicd.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ new cicd.PipelineDeployStackAction(stack, 'DeployStack', {
3535
executeChangeSetRunOrder: 999,
3636
input: sourceOutput,
3737
adminPermissions: false,
38-
capabilities: cfn.CloudFormationCapabilities.None,
38+
capabilities: [cfn.CloudFormationCapabilities.None],
3939
});
4040

4141
app.synth();

packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,32 +86,55 @@ export = nodeunit.testCase({
8686
const stackWithAnonymousCapability = new cdk.Stack(undefined, 'AnonymousIAM',
8787
{ env: { account: '123456789012', region: 'us-east-1' } });
8888

89+
const stackWithAutoExpandCapability = new cdk.Stack(undefined, 'AutoExpand',
90+
{ env: { account: '123456789012', region: 'us-east-1' } });
91+
92+
const stackWithAnonymousAndAutoExpandCapability = new cdk.Stack(undefined, 'AnonymousIAMAndAutoExpand',
93+
{ env: { account: '123456789012', region: 'us-east-1' } });
94+
8995
const selfUpdatingStack = createSelfUpdatingStack(pipelineStack);
9096

9197
const pipeline = selfUpdatingStack.pipeline;
98+
9299
const selfUpdateStage1 = pipeline.addStage({ stageName: 'SelfUpdate1' });
93100
const selfUpdateStage2 = pipeline.addStage({ stageName: 'SelfUpdate2' });
94101
const selfUpdateStage3 = pipeline.addStage({ stageName: 'SelfUpdate3' });
102+
const selfUpdateStage4 = pipeline.addStage({ stageName: 'SelfUpdate4' });
103+
const selfUpdateStage5 = pipeline.addStage({ stageName: 'SelfUpdate5' });
95104

96105
new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
97106
stage: selfUpdateStage1,
98107
stack: pipelineStack,
99108
input: selfUpdatingStack.synthesizedApp,
100-
capabilities: cfn.CloudFormationCapabilities.NamedIAM,
109+
capabilities: [cfn.CloudFormationCapabilities.NamedIAM],
101110
adminPermissions: false,
102111
});
103112
new PipelineDeployStackAction(pipelineStack, 'DeployStack', {
104113
stage: selfUpdateStage2,
105114
stack: stackWithNoCapability,
106115
input: selfUpdatingStack.synthesizedApp,
107-
capabilities: cfn.CloudFormationCapabilities.None,
116+
capabilities: [cfn.CloudFormationCapabilities.None],
108117
adminPermissions: false,
109118
});
110119
new PipelineDeployStackAction(pipelineStack, 'DeployStack2', {
111120
stage: selfUpdateStage3,
112121
stack: stackWithAnonymousCapability,
113122
input: selfUpdatingStack.synthesizedApp,
114-
capabilities: cfn.CloudFormationCapabilities.AnonymousIAM,
123+
capabilities: [cfn.CloudFormationCapabilities.AnonymousIAM],
124+
adminPermissions: false,
125+
});
126+
new PipelineDeployStackAction(pipelineStack, 'DeployStack3', {
127+
stage: selfUpdateStage4,
128+
stack: stackWithAutoExpandCapability,
129+
input: selfUpdatingStack.synthesizedApp,
130+
capabilities: [cfn.CloudFormationCapabilities.AutoExpand],
131+
adminPermissions: false,
132+
});
133+
new PipelineDeployStackAction(pipelineStack, 'DeployStack4', {
134+
stage: selfUpdateStage5,
135+
stack: stackWithAnonymousAndAutoExpandCapability,
136+
input: selfUpdatingStack.synthesizedApp,
137+
capabilities: [cfn.CloudFormationCapabilities.AnonymousIAM, cfn.CloudFormationCapabilities.AutoExpand],
115138
adminPermissions: false,
116139
});
117140
expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({
@@ -148,6 +171,20 @@ export = nodeunit.testCase({
148171
ActionMode: "CHANGE_SET_REPLACE",
149172
}
150173
})));
174+
expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({
175+
Configuration: {
176+
StackName: "AutoExpand",
177+
ActionMode: "CHANGE_SET_REPLACE",
178+
Capabilities: "CAPABILITY_AUTO_EXPAND",
179+
}
180+
})));
181+
expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({
182+
Configuration: {
183+
StackName: "AnonymousIAMAndAutoExpand",
184+
ActionMode: "CHANGE_SET_REPLACE",
185+
Capabilities: "CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND",
186+
}
187+
})));
151188
test.done();
152189
},
153190
'users can use admin permissions'(test: nodeunit.Test) {
@@ -178,7 +215,7 @@ export = nodeunit.testCase({
178215
Configuration: {
179216
StackName: "TestStack",
180217
ActionMode: "CHANGE_SET_REPLACE",
181-
Capabilities: "CAPABILITY_NAMED_IAM",
218+
Capabilities: "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND",
182219
}
183220
})));
184221
test.done();

packages/@aws-cdk/aws-cloudformation/lib/cloud-formation-capabilities.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,13 @@ export enum CloudFormationCapabilities {
2828
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities
2929
*/
3030
NamedIAM = 'CAPABILITY_NAMED_IAM',
31+
32+
/**
33+
* Capability to run CloudFormation macros
34+
*
35+
* Pass this capability if your template includes macros, for example AWS::Include or AWS::Serverless.
36+
*
37+
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html
38+
*/
39+
AutoExpand = 'CAPABILITY_AUTO_EXPAND'
3140
}

packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import cloudformation = require('@aws-cdk/aws-cloudformation');
2+
import { CloudFormationCapabilities } from '@aws-cdk/aws-cloudformation';
23
import codepipeline = require('@aws-cdk/aws-codepipeline');
34
import iam = require('@aws-cdk/aws-iam');
45
import cdk = require('@aws-cdk/cdk');
@@ -141,7 +142,7 @@ export interface CloudFormationDeployActionProps extends CloudFormationActionPro
141142
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities
142143
* @default None, unless `adminPermissions` is true
143144
*/
144-
readonly capabilities?: cloudformation.CloudFormationCapabilities;
145+
readonly capabilities?: cloudformation.CloudFormationCapabilities[];
145146

146147
/**
147148
* Whether to grant full permissions to CloudFormation while deploying this template.
@@ -221,12 +222,12 @@ export abstract class CloudFormationDeployAction extends CloudFormationAction {
221222

222223
constructor(props: CloudFormationDeployActionProps, configuration: any) {
223224
const capabilities = props.adminPermissions && props.capabilities === undefined
224-
? cloudformation.CloudFormationCapabilities.NamedIAM
225+
? [cloudformation.CloudFormationCapabilities.NamedIAM]
225226
: props.capabilities;
226227
super(props, {
227228
...configuration,
228229
// None evaluates to empty string which is falsey and results in undefined
229-
Capabilities: (capabilities && capabilities.toString()) || undefined,
230+
Capabilities: parseCapabilities(capabilities),
230231
RoleArn: cdk.Lazy.stringValue({ produce: () => this.deploymentRole.roleArn }),
231232
ParameterOverrides: cdk.Lazy.stringValue({ produce: () => Stack.of(this.scope).toJsonString(props.parameterOverrides) }),
232233
TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined,
@@ -543,3 +544,16 @@ interface StatementTemplate {
543544
}
544545

545546
type StatementCondition = { [op: string]: { [attribute: string]: string } };
547+
548+
function parseCapabilities(capabilities: CloudFormationCapabilities[] | undefined): string | undefined {
549+
if (capabilities === undefined) {
550+
return undefined;
551+
} else if (capabilities.length === 1) {
552+
const capability = capabilities.toString();
553+
return (capability === '') ? undefined : capability;
554+
} else if (capabilities.length > 1) {
555+
return capabilities.join(',');
556+
}
557+
558+
return undefined;
559+
}

packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert';
2+
import { CloudFormationCapabilities } from '@aws-cdk/aws-cloudformation';
23
import codebuild = require('@aws-cdk/aws-codebuild');
34
import { Repository } from '@aws-cdk/aws-codecommit';
45
import codepipeline = require('@aws-cdk/aws-codepipeline');
@@ -413,7 +414,136 @@ export = {
413414
}));
414415

415416
test.done();
416-
}
417+
},
418+
419+
'Single capability is passed to template'(test: Test) {
420+
// GIVEN
421+
const stack = new TestFixture();
422+
423+
// WHEN
424+
stack.deployStage.addAction(new cpactions.CloudFormationCreateUpdateStackAction({
425+
actionName: 'CreateUpdate',
426+
stackName: 'MyStack',
427+
templatePath: stack.sourceOutput.atPath('template.yaml'),
428+
adminPermissions: false,
429+
capabilities: [
430+
CloudFormationCapabilities.NamedIAM
431+
]
432+
}));
433+
434+
const roleId = "PipelineDeployCreateUpdateRole515CB7D4";
435+
436+
// THEN: Action in Pipeline has named IAM capabilities
437+
expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
438+
"Stages": [
439+
{ "Name": "Source" /* don't care about the rest */ },
440+
{
441+
"Name": "Deploy",
442+
"Actions": [
443+
{
444+
"Configuration": {
445+
"Capabilities": "CAPABILITY_NAMED_IAM",
446+
"RoleArn": { "Fn::GetAtt": [ roleId, "Arn" ] },
447+
"ActionMode": "CREATE_UPDATE",
448+
"StackName": "MyStack",
449+
"TemplatePath": "SourceArtifact::template.yaml"
450+
},
451+
"InputArtifacts": [{"Name": "SourceArtifact"}],
452+
"Name": "CreateUpdate",
453+
},
454+
],
455+
}
456+
]
457+
}));
458+
459+
test.done();
460+
},
461+
462+
'Multiple capabilities are passed to template'(test: Test) {
463+
// GIVEN
464+
const stack = new TestFixture();
465+
466+
// WHEN
467+
stack.deployStage.addAction(new cpactions.CloudFormationCreateUpdateStackAction({
468+
actionName: 'CreateUpdate',
469+
stackName: 'MyStack',
470+
templatePath: stack.sourceOutput.atPath('template.yaml'),
471+
adminPermissions: false,
472+
capabilities: [
473+
CloudFormationCapabilities.NamedIAM,
474+
CloudFormationCapabilities.AutoExpand
475+
]
476+
}));
477+
478+
const roleId = "PipelineDeployCreateUpdateRole515CB7D4";
479+
480+
// THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities
481+
expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
482+
"Stages": [
483+
{ "Name": "Source" /* don't care about the rest */ },
484+
{
485+
"Name": "Deploy",
486+
"Actions": [
487+
{
488+
"Configuration": {
489+
"Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND",
490+
"RoleArn": { "Fn::GetAtt": [ roleId, "Arn" ] },
491+
"ActionMode": "CREATE_UPDATE",
492+
"StackName": "MyStack",
493+
"TemplatePath": "SourceArtifact::template.yaml"
494+
},
495+
"InputArtifacts": [{"Name": "SourceArtifact"}],
496+
"Name": "CreateUpdate",
497+
},
498+
],
499+
}
500+
]
501+
}));
502+
503+
test.done();
504+
},
505+
506+
'Empty capabilities is not passed to template'(test: Test) {
507+
// GIVEN
508+
const stack = new TestFixture();
509+
510+
// WHEN
511+
stack.deployStage.addAction(new cpactions.CloudFormationCreateUpdateStackAction({
512+
actionName: 'CreateUpdate',
513+
stackName: 'MyStack',
514+
templatePath: stack.sourceOutput.atPath('template.yaml'),
515+
adminPermissions: false,
516+
capabilities: [
517+
CloudFormationCapabilities.None
518+
]
519+
}));
520+
521+
const roleId = "PipelineDeployCreateUpdateRole515CB7D4";
522+
523+
// THEN: Action in Pipeline has no capabilities
524+
expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', {
525+
"Stages": [
526+
{ "Name": "Source" /* don't care about the rest */ },
527+
{
528+
"Name": "Deploy",
529+
"Actions": [
530+
{
531+
"Configuration": {
532+
"RoleArn": { "Fn::GetAtt": [ roleId, "Arn" ] },
533+
"ActionMode": "CREATE_UPDATE",
534+
"StackName": "MyStack",
535+
"TemplatePath": "SourceArtifact::template.yaml"
536+
},
537+
"InputArtifacts": [{"Name": "SourceArtifact"}],
538+
"Name": "CreateUpdate",
539+
},
540+
],
541+
}
542+
]
543+
}));
544+
545+
test.done();
546+
},
417547
};
418548

419549
/**

0 commit comments

Comments
 (0)