Skip to content

Commit

Permalink
feat(app-delivery) IAM policy for deploy stack
Browse files Browse the repository at this point in the history
 * The changeset and apply changeset may need specific IAM permissions
 and the user can now customize them via `deployStackAction.role`
 * Document updates for proper build stage configuration
 * Fixes aws#1151
  • Loading branch information
moofish32 committed Nov 13, 2018
1 parent 15b255c commit 478d1e8
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 5 deletions.
14 changes: 11 additions & 3 deletions packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import cfn = require('@aws-cdk/aws-cloudformation');
import codepipeline = require('@aws-cdk/aws-codepipeline-api');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import cxapi = require('@aws-cdk/cx-api');

Expand Down Expand Up @@ -52,6 +52,12 @@ export interface PipelineDeployStackActionProps {
* CodePipeline is hosted.
*/
export class PipelineDeployStackAction extends cdk.Construct {

/**
* The role used by CloudFormation for the deploy action
*/
public readonly role: iam.Role;

private readonly stack: cdk.Stack;

constructor(parent: cdk.Construct, id: string, props: PipelineDeployStackActionProps) {
Expand All @@ -69,16 +75,18 @@ export class PipelineDeployStackAction extends cdk.Construct {
throw new Error(`createChangeSetRunOrder (${createChangeSetRunOrder}) must be < executeChangeSetRunOrder (${executeChangeSetRunOrder})`);
}

this.stack = props.stack;
const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet';

new cfn.PipelineCreateReplaceChangeSetAction(this, 'ChangeSet', {
this.stack = props.stack;

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`),
});
this.role = changeSetAction.role;

new cfn.PipelineExecuteChangeSetAction(this, 'Execute', {
changeSetName,
Expand Down
5 changes: 4 additions & 1 deletion packages/@aws-cdk/app-delivery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@
"dependencies": {
"@aws-cdk/aws-cloudformation": "^0.16.0",
"@aws-cdk/aws-codebuild": "^0.16.0",
"@aws-cdk/aws-iam": "^0.16.0",
"@aws-cdk/aws-codepipeline-api": "^0.16.0",
"@aws-cdk/cdk": "^0.16.0",
"@aws-cdk/cx-api": "^0.16.0"
},
"devDependencies": {
"@aws-cdk/aws-codepipeline": "^0.16.0",
"@aws-cdk/assert": "^0.16.0",
"@aws-cdk/aws-s3": "^0.16.0",
"cdk-build-tools": "^0.16.0",
"cdk-integ-tools": "^0.16.0",
Expand All @@ -63,6 +65,7 @@
],
"peerDependencies": {
"@aws-cdk/aws-codepipeline-api": "^0.16.0",
"@aws-cdk/aws-iam": "^0.16.0",
"@aws-cdk/cdk": "^0.16.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import codebuild = require('@aws-cdk/aws-codebuild');
import code = require('@aws-cdk/aws-codepipeline');
import api = require('@aws-cdk/aws-codepipeline-api');
import iam = require('@aws-cdk/aws-iam');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
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 { PipelineDeployStackAction } from '../lib/pipeline-deploy-stack-action';

const accountId = fc.array(fc.integer(0, 9), 12, 12).map(arr => arr.join());
Expand Down Expand Up @@ -59,6 +64,85 @@ export = nodeunit.testCase({
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 pipeline = new code.Pipeline(pipelineStack, 'CodePipeline', {
restartExecutionOnUpdate: true,
});

// simple source
const bucket = s3.Bucket.import( pipeline, 'PatternBucket', { bucketArn: 'arn:aws:s3:::totally-fake-buckert' });
new s3.PipelineSourceAction(pipeline, 'S3Source', {
bucket,
bucketKey: 'patterns/intuit-ingress-corp',
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;
const selfUpdateStage = pipeline.addStage('SelfUpdate');
new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', {
stage: selfUpdateStage,
stack: pipelineStack,
inputArtifact: synthesizedApp,
});

// WHEN //
// this our app/service/infra to deploy
const deployStage = pipeline.addStage('Deploy');
const deployAction = new PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', {
stage: deployStage,
stack: emptyStack,
inputArtifact: synthesizedApp,
});
// we might need to add permissions
deployAction.role.addToPolicy( new iam.PolicyStatement().
addAction('ec2:AuthorizeSecurityGroupEgress').
addAction('ec2:AuthorizeSecurityGroupIngress').
addAction('ec2:DeleteSecurityGroup').
addAction('ec2:DescribeSecurityGroups').
addAction('ec2:CreateSecurityGroup').
addAction('ec2:RevokeSecurityGroupEgress').
addAction('ec2:RevokeSecurityGroupIngress').
addAllResources());

// THEN //
// there should be 3 policies 1. CodePipeline, 2. Codebuild, 3.
// ChangeSetDeploy Action
expect(pipelineStack).to(countResources('AWS::IAM::Policy', 3));
expect(pipelineStack).to(haveResource('AWS::IAM::Policy', {
PolicyDocument: {
Statement: [
{
Action: [
'ec2:AuthorizeSecurityGroupEgress',
'ec2:AuthorizeSecurityGroupIngress',
'ec2:DeleteSecurityGroup',
'ec2:DescribeSecurityGroups',
'ec2:CreateSecurityGroup',
'ec2:RevokeSecurityGroupEgress',
'ec2:RevokeSecurityGroupIngress'
],
Effect: 'Allow',
Resource: '*',
},
],
},
Roles: [
{
Ref: 'DeployServiceStackAChangeSetRoleA1245536',
},
],
}));
test.done();
},
'rejects stacks with assets'(test: nodeunit.Test) {
fc.assert(
fc.property(
Expand All @@ -79,7 +163,7 @@ export = nodeunit.testCase({
deployedStack.addMetadata(cxapi.ASSET_METADATA, {});
}
test.deepEqual(action.validate(),
[`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]);
[`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]);
}
)
);
Expand All @@ -101,3 +185,7 @@ class FakeAction extends api.Action {
this.outputArtifact = new api.Artifact(this, 'OutputArtifact');
}
}

function getTestStack(): cdk.Stack {
return new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } });
}

0 comments on commit 478d1e8

Please sign in to comment.