From d915aac101a0fd7576994c19591f0c912a914ec3 Mon Sep 17 00:00:00 2001 From: Thorsten Hoeger Date: Tue, 13 Aug 2019 23:36:37 +0200 Subject: [PATCH] feat(bootstrap): add kms option to cdk bootstrap (#3634) * feat(bootstrap): add kms option to cdk bootstrap * feat(bootstrap): PR review * feat(bootstrap): more aliases * fix(bootstrap): swap aliases --- packages/aws-cdk/bin/cdk.ts | 21 +-- .../aws-cdk/lib/api/bootstrap-environment.ts | 29 +++- packages/aws-cdk/lib/settings.ts | 4 + packages/aws-cdk/test/api/test.bootstrap.ts | 156 ++++++++++++++++++ 4 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 packages/aws-cdk/test/api/test.bootstrap.ts diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 4f6c2311dab3a..dfa221d144bcc 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -5,7 +5,7 @@ import colors = require('colors/safe'); import path = require('path'); import yargs = require('yargs'); -import { bootstrapEnvironment, destroyStack, SDK } from '../lib'; +import { bootstrapEnvironment, BootstrapEnvironmentProps, destroyStack, SDK } from '../lib'; import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments'; import { execProgram } from '../lib/api/cxapp/exec'; import { AppStacks, DefaultSelection, ExtendedStackSelection } from '../lib/api/cxapp/stacks'; @@ -51,13 +51,14 @@ async function parseCommandLineArguments() { .command([ 'synthesize [STACKS..]', 'synth [STACKS..]' ], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs .option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' })) .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', yargs => yargs - .option('toolkit-bucket-name', { type: 'string', alias: 'b', desc: 'The name of the CDK toolkit bucket', default: undefined })) + .option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket', default: undefined }) + .option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined })) .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'do not rebuild asset with the given ID. Can be specified multiple times.', default: [] }) .option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' }) - .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' })) + .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' }) .option('ci', { type: 'boolean', desc: 'Force CI detection. Use --no-ci to disable CI autodetection.', default: process.env.CI !== undefined }) - .option('tags', { type: 'array', alias: 't', desc: 'tags to add to the stack (KEY=VALUE)', nargs: 1, requiresArg: true }) + .option('tags', { type: 'array', alias: 't', desc: 'tags to add to the stack (KEY=VALUE)', nargs: 1, requiresArg: true })) .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs .option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependees' }) .option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' })) @@ -186,7 +187,10 @@ async function initCommandLine() { }); case 'bootstrap': - return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn, args.toolkitBucketName); + return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn, { + bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']), + kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']), + }); case 'deploy': return await cli.deploy({ @@ -236,7 +240,7 @@ async function initCommandLine() { * all stacks are implicitly selected. * @param toolkitStackName the name to be used for the CDK Toolkit stack. */ - async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined, toolkitBucketName: string | undefined): Promise { + async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps): Promise { // Two modes of operation. // // If there is an '--app' argument, we select the environments from the app. Otherwise we just take the user @@ -246,13 +250,10 @@ async function initCommandLine() { const environments = app ? await globEnvironmentsFromStacks(appStacks, environmentGlobs, aws) : environmentsFromDescriptors(environmentGlobs); - // Bucket name can be passed using --toolkit-bucket-name or set in cdk.json - const bucketName = configuration.settings.get(['toolkitBucketName']) || toolkitBucketName; - await Promise.all(environments.map(async (environment) => { success(' ⏳ Bootstrapping environment %s...', colors.blue(environment.name)); try { - const result = await bootstrapEnvironment(environment, aws, toolkitStackName, roleArn, bucketName); + const result = await bootstrapEnvironment(environment, aws, toolkitStackName, roleArn, props); const message = result.noOp ? ' ✅ Environment %s bootstrapped (no changes).' : ' ✅ Environment %s bootstrapped.'; success(message, colors.blue(environment.name)); diff --git a/packages/aws-cdk/lib/api/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap-environment.ts index d586a1b4f4898..bac091385f926 100644 --- a/packages/aws-cdk/lib/api/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap-environment.ts @@ -12,8 +12,24 @@ export const BUCKET_NAME_OUTPUT = 'BucketName'; /** @experimental */ export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName'; +export interface BootstrapEnvironmentProps { + /** + * The name to be given to the CDK Bootstrap bucket. + * + * @default - a name is generated by CloudFormation. + */ + readonly bucketName?: string; + + /** + * The ID of an existing KMS key to be used for encrypting items in the bucket. + * + * @default - the default KMS key for S3 will be used. + */ + readonly kmsKeyId?: string; +} + /** @experimental */ -export async function bootstrapEnvironment(environment: cxapi.Environment, aws: ISDK, toolkitStackName: string, roleArn: string | undefined, toolkitBucketName: string | undefined): Promise { +export async function bootstrapEnvironment(environment: cxapi.Environment, aws: ISDK, toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps = {}): Promise { const template = { Description: "The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.", @@ -21,9 +37,16 @@ export async function bootstrapEnvironment(environment: cxapi.Environment, aws: StagingBucket: { Type: "AWS::S3::Bucket", Properties: { - BucketName: toolkitBucketName, + BucketName: props.bucketName, AccessControl: "Private", - BucketEncryption: { ServerSideEncryptionConfiguration: [{ ServerSideEncryptionByDefault: { SSEAlgorithm: "aws:kms" } }] } + BucketEncryption: { + ServerSideEncryptionConfiguration: [{ + ServerSideEncryptionByDefault: { + SSEAlgorithm: "aws:kms", + KMSMasterKeyID: props.kmsKeyId, + }, + }] + } } } }, diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 7054051ecc803..6112b2af725ce 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -207,6 +207,10 @@ export class Settings { plugin: argv.plugin, requireApproval: argv.requireApproval, toolkitStackName: argv.toolkitStackName, + toolkitBucket: { + bucketName: argv.bootstrapBucketName, + kmsKeyId: argv.bootstrapKmsKeyId, + }, versionReporting: argv.versionReporting, staging: argv.staging, output: argv.output, diff --git a/packages/aws-cdk/test/api/test.bootstrap.ts b/packages/aws-cdk/test/api/test.bootstrap.ts new file mode 100644 index 0000000000000..f39168b88da2c --- /dev/null +++ b/packages/aws-cdk/test/api/test.bootstrap.ts @@ -0,0 +1,156 @@ +import { CreateChangeSetInput } from 'aws-sdk/clients/cloudformation'; +import { Test } from 'nodeunit'; +import { bootstrapEnvironment } from '../../lib'; +import { fromYAML } from '../../lib/serialize'; +import { MockSDK } from '../util/mock-sdk'; + +export = { + async 'do bootstrap'(test: Test) { + // GIVEN + const sdk = new MockSDK(); + + let executed = false; + + sdk.stubCloudFormation({ + describeStacks() { + return { + Stacks: [] + }; + }, + + createChangeSet(info: CreateChangeSetInput) { + const template = fromYAML(info.TemplateBody as string); + const bucketProperties = template.Resources.StagingBucket.Properties; + test.equals(bucketProperties.BucketName, undefined, 'Expected BucketName to be undefined'); + test.equals(bucketProperties.BucketEncryption.ServerSideEncryptionConfiguration[0].ServerSideEncryptionByDefault.KMSMasterKeyID, + undefined, 'Expected KMSMasterKeyID to be undefined'); + return {}; + }, + + describeChangeSet() { + return { + Status: 'CREATE_COMPLETE', + Changes: [], + }; + }, + + executeChangeSet() { + executed = true; + return {}; + } + }); + + // WHEN + const ret = await bootstrapEnvironment({ + account: '123456789012', + region: 'us-east-1', + name: 'mock', + }, sdk, 'mockStack', undefined); + + // THEN + test.equals(ret.noOp, false); + test.equals(executed, true); + + test.done(); + }, + async 'do bootstrap using custom bucket name'(test: Test) { + // GIVEN + const sdk = new MockSDK(); + + let executed = false; + + sdk.stubCloudFormation({ + describeStacks() { + return { + Stacks: [] + }; + }, + + createChangeSet(info: CreateChangeSetInput) { + const template = fromYAML(info.TemplateBody as string); + const bucketProperties = template.Resources.StagingBucket.Properties; + test.equals(bucketProperties.BucketName, 'foobar', 'Expected BucketName to be foobar'); + test.equals(bucketProperties.BucketEncryption.ServerSideEncryptionConfiguration[0].ServerSideEncryptionByDefault.KMSMasterKeyID, + undefined, 'Expected KMSMasterKeyID to be undefined'); + return {}; + }, + + describeChangeSet() { + return { + Status: 'CREATE_COMPLETE', + Changes: [], + }; + }, + + executeChangeSet() { + executed = true; + return {}; + } + }); + + // WHEN + const ret = await bootstrapEnvironment({ + account: '123456789012', + region: 'us-east-1', + name: 'mock', + }, sdk, 'mockStack', undefined, { + bucketName: 'foobar', + }); + + // THEN + test.equals(ret.noOp, false); + test.equals(executed, true); + + test.done(); + }, + async 'do bootstrap using KMS CMK'(test: Test) { + // GIVEN + const sdk = new MockSDK(); + + let executed = false; + + sdk.stubCloudFormation({ + describeStacks() { + return { + Stacks: [] + }; + }, + + createChangeSet(info: CreateChangeSetInput) { + const template = fromYAML(info.TemplateBody as string); + const bucketProperties = template.Resources.StagingBucket.Properties; + test.equals(bucketProperties.BucketName, undefined, 'Expected BucketName to be undefined'); + test.equals(bucketProperties.BucketEncryption.ServerSideEncryptionConfiguration[0].ServerSideEncryptionByDefault.KMSMasterKeyID, + 'myKmsKey', 'Expected KMSMasterKeyID to be myKmsKey'); + return {}; + }, + + describeChangeSet() { + return { + Status: 'CREATE_COMPLETE', + Changes: [], + }; + }, + + executeChangeSet() { + executed = true; + return {}; + } + }); + + // WHEN + const ret = await bootstrapEnvironment({ + account: '123456789012', + region: 'us-east-1', + name: 'mock', + }, sdk, 'mockStack', undefined, { + kmsKeyId: 'myKmsKey', + }); + + // THEN + test.equals(ret.noOp, false); + test.equals(executed, true); + + test.done(); + }, +};