diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 20c99168d08f8..ead9ef4ff990c 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1333,8 +1333,8 @@ export class Table extends TableBase { encryptionType = props.encryptionKey != null // If there is a configured encyptionKey, the encryption is implicitly CUSTOMER_MANAGED ? TableEncryption.CUSTOMER_MANAGED - // Otherwise, if severSideEncryption is enabled, it's AWS_MANAGED; else DEFAULT - : props.serverSideEncryption ? TableEncryption.AWS_MANAGED : TableEncryption.DEFAULT; + // Otherwise, if severSideEncryption is enabled, it's AWS_MANAGED; else undefined (do not set anything) + : props.serverSideEncryption ? TableEncryption.AWS_MANAGED : undefined; } if (encryptionType !== TableEncryption.CUSTOMER_MANAGED && props.encryptionKey) { @@ -1362,6 +1362,9 @@ export class Table extends TableBase { return { sseSpecification: { sseEnabled: true } }; case TableEncryption.DEFAULT: + return { sseSpecification: { sseEnabled: false } }; + + case undefined: // Not specifying "sseEnabled: false" here because it would cause phony changes to existing stacks. return { sseSpecification: undefined }; diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json index 3a3b5788fd907..c8e4ada3c14bd 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json @@ -507,6 +507,9 @@ "ProvisionedThroughput": { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5 + }, + "SSESpecification": { + "SSEEnabled": false } }, "UpdateReplacePolicy": "Delete", diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 6ab46cb987f1d..dc49750a46f2f 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -7,6 +7,7 @@ import { AssetHashType, AssetOptions } from './assets'; import { BundlingOptions } from './bundling'; import { Construct } from './construct-compat'; import { FileSystem, FingerprintOptions } from './fs'; +import { Stack } from './stack'; import { Stage } from './stage'; /** @@ -97,16 +98,26 @@ export class AssetStaging extends Construct { const hashType = determineHashType(props.assetHashType, props.assetHash); if (props.bundling) { - // Determine the source hash in advance of bundling if the asset hash type - // is SOURCE so that the bundler can opt to re-use its previous output. - const sourceHash = hashType === AssetHashType.SOURCE - ? this.calculateHash(hashType, props.assetHash, props.bundling) - : undefined; - - this.bundleDir = this.bundle(props.bundling, outdir, sourceHash); - this.assetHash = sourceHash ?? this.calculateHash(hashType, props.assetHash, props.bundling); - this.relativePath = renderAssetFilename(this.assetHash); - this.stagedPath = this.relativePath; + // Check if we actually have to bundle for this stack + const bundlingStacks: string[] = this.node.tryGetContext(cxapi.BUNDLING_STACKS) ?? ['*']; + const runBundling = bundlingStacks.includes(Stack.of(this).stackName) || bundlingStacks.includes('*'); + if (runBundling) { + // Determine the source hash in advance of bundling if the asset hash type + // is SOURCE so that the bundler can opt to re-use its previous output. + const sourceHash = hashType === AssetHashType.SOURCE + ? this.calculateHash(hashType, props.assetHash, props.bundling) + : undefined; + + this.bundleDir = this.bundle(props.bundling, outdir, sourceHash); + this.assetHash = sourceHash ?? this.calculateHash(hashType, props.assetHash, props.bundling); + this.relativePath = renderAssetFilename(this.assetHash); + this.stagedPath = this.relativePath; + } else { // Bundling is skipped + this.assetHash = props.assetHashType === AssetHashType.BUNDLE + ? this.calculateHash(AssetHashType.CUSTOM, this.node.path) // Use node path as dummy hash because we're not bundling + : this.calculateHash(hashType, props.assetHash); + this.stagedPath = this.sourcePath; + } } else { this.assetHash = this.calculateHash(hashType, props.assetHash); diff --git a/packages/@aws-cdk/core/lib/private/runtime-info.ts b/packages/@aws-cdk/core/lib/private/runtime-info.ts index 25fc3ccaf1818..0cc74d3c9f8f3 100644 --- a/packages/@aws-cdk/core/lib/private/runtime-info.ts +++ b/packages/@aws-cdk/core/lib/private/runtime-info.ts @@ -3,7 +3,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { major as nodeMajorVersion } from './node-version'; // list of NPM scopes included in version reporting e.g. @aws-cdk and @aws-solutions-konstruk -const WHITELIST_SCOPES = ['@aws-cdk', '@aws-solutions-konstruk', '@aws-solutions-constructs']; +const WHITELIST_SCOPES = ['@aws-cdk', '@aws-solutions-konstruk', '@aws-solutions-constructs', '@amzn']; // list of NPM packages included in version reporting const WHITELIST_PACKAGES = ['aws-rfdk']; diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index 8b6c831231d8a..ff7279172bb00 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -580,6 +580,31 @@ export = { test.done(); }, + + 'bundling looks at bundling stacks in context'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'MyStack'); + stack.node.setContext(cxapi.BUNDLING_STACKS, ['OtherStack']); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const asset = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.BUNDLE, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + test.throws(() => readDockerStubInput()); // Bundling did not run + test.equal(asset.assetHash, '3d96e735e26b857743a7c44523c9160c285c2d3ccf273d80fa38a1e674c32cb3'); // hash of MyStack/Asset + test.equal(asset.sourcePath, directory); + test.equal(stack.resolve(asset.stagedPath), directory); + + test.done(); + }, }; // Reads a docker stub and cleans the volume paths out of the stub. diff --git a/packages/@aws-cdk/cx-api/lib/app.ts b/packages/@aws-cdk/cx-api/lib/app.ts index d135e9cf40f07..41283679f0db2 100644 --- a/packages/@aws-cdk/cx-api/lib/app.ts +++ b/packages/@aws-cdk/cx-api/lib/app.ts @@ -27,3 +27,8 @@ export const DISABLE_ASSET_STAGING_CONTEXT = 'aws:cdk:disable-asset-staging'; * Omits stack traces from construct metadata entries. */ export const DISABLE_METADATA_STACK_TRACE = 'aws:cdk:disable-stack-trace'; + +/** + * Run bundling for stacks specified in this context key + */ +export const BUNDLING_STACKS = 'aws:cdk:bundling-stacks'; diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index af536d6f184dc..4bb1c4eb6d3d6 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -307,6 +307,11 @@ $ cdk doctor - AWS_SDK_LOAD_CONFIG = 1 ``` +#### Bundling +By default asset bundling is skipped for `cdk list` and `cdk destroy`. For `cdk deploy`, `cdk diff` +and `cdk synthesize` the default is to bundle assets for all stacks unless `exclusively` is specified. +In this case, only the listed stacks will have their assets bundled. + ### MFA support If `mfa_serial` is found in the active profile of the shared ini file AWS CDK diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 8f864e5c7f8af..5ae3896e2bec4 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -17,7 +17,7 @@ import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib import { data, debug, error, print, setLogLevel } from '../lib/logging'; import { PluginHost } from '../lib/plugin'; import { serializeStructure } from '../lib/serialize'; -import { Configuration, Settings } from '../lib/settings'; +import { Command, Configuration, Settings } from '../lib/settings'; import * as version from '../lib/version'; /* eslint-disable max-len */ @@ -137,7 +137,10 @@ async function initCommandLine() { debug('CDK toolkit version:', version.DISPLAY_VERSION); debug('Command line arguments:', argv); - const configuration = new Configuration(argv); + const configuration = new Configuration({ + ...argv, + _: argv._ as [Command, ...string[]], // TypeScript at its best + }); await configuration.load(); const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({ diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index 597a236ec1b3c..735e9a7622e19 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -39,6 +39,9 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom context[cxapi.DISABLE_ASSET_STAGING_CONTEXT] = true; } + const bundlingStacks = config.settings.get(['bundlingStacks']) ?? ['*']; + context[cxapi.BUNDLING_STACKS] = bundlingStacks; + debug('context:', context); env[cxapi.CONTEXT_ENV] = JSON.stringify(context); diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 1cd74d42e858c..af9193d9a6456 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -18,7 +18,33 @@ export const TRANSIENT_CONTEXT_KEY = '$dontSaveContext'; const CONTEXT_KEY = 'context'; -export type Arguments = { readonly [name: string]: unknown }; +export enum Command { + LS = 'ls', + LIST = 'list', + DIFF = 'diff', + BOOTSTRAP = 'bootstrap', + DEPLOY = 'deploy', + DESTROY = 'destroy', + SYNTHESIZE = 'synthesize', + SYNTH = 'synth', + METADATA = 'metadata', + INIT = 'init', + VERSION = 'version', +} + +const BUNDLING_COMMANDS = [ + Command.DEPLOY, + Command.DIFF, + Command.SYNTH, + Command.SYNTHESIZE, +]; + +export type Arguments = { + readonly _: [Command, ...string[]]; + readonly exclusively?: boolean; + readonly STACKS?: string[]; + readonly [name: string]: unknown; +}; /** * All sources of settings combined @@ -185,6 +211,18 @@ export class Settings { const context = this.parseStringContextListToObject(argv); const tags = this.parseStringTagsListToObject(expectStringList(argv.tags)); + // Determine bundling stacks + let bundlingStacks: string[]; + if (BUNDLING_COMMANDS.includes(argv._[0])) { + // If we deploy, diff or synth a list of stacks exclusively we skip + // bundling for all other stacks. + bundlingStacks = argv.exclusively + ? argv.STACKS ?? ['*'] + : ['*']; + } else { // Skip bundling for all stacks + bundlingStacks = []; + } + return new Settings({ app: argv.app, browser: argv.browser, @@ -205,6 +243,7 @@ export class Settings { staging: argv.staging, output: argv.output, progress: argv.progress, + bundlingStacks, }); } @@ -396,4 +435,4 @@ function expectStringList(x: unknown): string[] | undefined { throw new Error(`Expected list of strings, found ${nonStrings}`); } return x; -} \ No newline at end of file +} diff --git a/packages/aws-cdk/test/context.test.ts b/packages/aws-cdk/test/context.test.ts index 373c42090daec..04a5626c2d4b7 100644 --- a/packages/aws-cdk/test/context.test.ts +++ b/packages/aws-cdk/test/context.test.ts @@ -100,7 +100,7 @@ test('surive no context in old file', async () => { test('command line context is merged with stored context', async () => { // GIVEN await fs.writeJSON('cdk.context.json', { boo: 'far' }); - const config = await new Configuration({ context: ['foo=bar'] } as any).load(); + const config = await new Configuration({ context: ['foo=bar'], _: ['command'] } as any).load(); // WHEN expect(config.context.all).toEqual({ foo: 'bar', boo: 'far' }); diff --git a/packages/aws-cdk/test/settings.test.ts b/packages/aws-cdk/test/settings.test.ts index 28d07d686dcc8..b3bc9dded811c 100644 --- a/packages/aws-cdk/test/settings.test.ts +++ b/packages/aws-cdk/test/settings.test.ts @@ -1,4 +1,4 @@ -import { Context, Settings } from '../lib/settings'; +import { Command, Context, Settings } from '../lib/settings'; test('can delete values from Context object', () => { // GIVEN @@ -62,8 +62,8 @@ test('can clear all values in all objects', () => { test('can parse string context from command line arguments', () => { // GIVEN - const settings1 = Settings.fromCommandLineArguments({ context: ['foo=bar'] }); - const settings2 = Settings.fromCommandLineArguments({ context: ['foo='] }); + const settings1 = Settings.fromCommandLineArguments({ context: ['foo=bar'], _: [Command.DEPLOY] }); + const settings2 = Settings.fromCommandLineArguments({ context: ['foo='], _: [Command.DEPLOY] }); // THEN expect(settings1.get(['context']).foo).toEqual( 'bar'); @@ -72,10 +72,42 @@ test('can parse string context from command line arguments', () => { test('can parse string context from command line arguments with equals sign in value', () => { // GIVEN - const settings1 = Settings.fromCommandLineArguments({ context: ['foo==bar='] }); - const settings2 = Settings.fromCommandLineArguments({ context: ['foo=bar='] }); + const settings1 = Settings.fromCommandLineArguments({ context: ['foo==bar='], _: [Command.DEPLOY] }); + const settings2 = Settings.fromCommandLineArguments({ context: ['foo=bar='], _: [Command.DEPLOY] }); // THEN expect(settings1.get(['context']).foo).toEqual( '=bar='); expect(settings2.get(['context']).foo).toEqual( 'bar='); }); + +test('bundling stacks defaults to an empty list', () => { + // GIVEN + const settings = Settings.fromCommandLineArguments({ + _: [Command.LIST], + }); + + // THEN + expect(settings.get(['bundlingStacks'])).toEqual([]); +}); + +test('bundling stacks defaults to * for deploy', () => { + // GIVEN + const settings = Settings.fromCommandLineArguments({ + _: [Command.DEPLOY], + }); + + // THEN + expect(settings.get(['bundlingStacks'])).toEqual(['*']); +}); + +test('bundling stacks with deploy exclusively', () => { + // GIVEN + const settings = Settings.fromCommandLineArguments({ + _: [Command.DEPLOY], + exclusively: true, + STACKS: ['cool-stack'], + }); + + // THEN + expect(settings.get(['bundlingStacks'])).toEqual(['cool-stack']); +});