Skip to content

Commit

Permalink
feat(codepipeline-actions): Add CAPABILITY_AUTO_EXPAND (#2851) (#2852)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ScOut3R authored and rix0rrr committed Jun 21, 2019
1 parent ae789ed commit c9340a6
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 16 deletions.
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/app-delivery/test/integ.cicd.ts
Expand Up @@ -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();
Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
Expand Up @@ -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'
}
@@ -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');
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
@@ -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');
Expand Down Expand Up @@ -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();
},
};

/**
Expand Down

0 comments on commit c9340a6

Please sign in to comment.