Skip to content

Commit 28cee39

Browse files
authored
feat(cli): skip bundling for operations where stack is not needed (#9889)
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. Closes #9540 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b22cd08 commit 28cee39

File tree

9 files changed

+143
-20
lines changed

9 files changed

+143
-20
lines changed

packages/@aws-cdk/core/lib/asset-staging.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AssetHashType, AssetOptions } from './assets';
77
import { BundlingOptions } from './bundling';
88
import { Construct } from './construct-compat';
99
import { FileSystem, FingerprintOptions } from './fs';
10+
import { Stack } from './stack';
1011
import { Stage } from './stage';
1112

1213
/**
@@ -97,16 +98,26 @@ export class AssetStaging extends Construct {
9798
const hashType = determineHashType(props.assetHashType, props.assetHash);
9899

99100
if (props.bundling) {
100-
// Determine the source hash in advance of bundling if the asset hash type
101-
// is SOURCE so that the bundler can opt to re-use its previous output.
102-
const sourceHash = hashType === AssetHashType.SOURCE
103-
? this.calculateHash(hashType, props.assetHash, props.bundling)
104-
: undefined;
105-
106-
this.bundleDir = this.bundle(props.bundling, outdir, sourceHash);
107-
this.assetHash = sourceHash ?? this.calculateHash(hashType, props.assetHash, props.bundling);
108-
this.relativePath = renderAssetFilename(this.assetHash);
109-
this.stagedPath = this.relativePath;
101+
// Check if we actually have to bundle for this stack
102+
const bundlingStacks: string[] = this.node.tryGetContext(cxapi.BUNDLING_STACKS) ?? ['*'];
103+
const runBundling = bundlingStacks.includes(Stack.of(this).stackName) || bundlingStacks.includes('*');
104+
if (runBundling) {
105+
// Determine the source hash in advance of bundling if the asset hash type
106+
// is SOURCE so that the bundler can opt to re-use its previous output.
107+
const sourceHash = hashType === AssetHashType.SOURCE
108+
? this.calculateHash(hashType, props.assetHash, props.bundling)
109+
: undefined;
110+
111+
this.bundleDir = this.bundle(props.bundling, outdir, sourceHash);
112+
this.assetHash = sourceHash ?? this.calculateHash(hashType, props.assetHash, props.bundling);
113+
this.relativePath = renderAssetFilename(this.assetHash);
114+
this.stagedPath = this.relativePath;
115+
} else { // Bundling is skipped
116+
this.assetHash = props.assetHashType === AssetHashType.BUNDLE
117+
? this.calculateHash(AssetHashType.CUSTOM, this.node.path) // Use node path as dummy hash because we're not bundling
118+
: this.calculateHash(hashType, props.assetHash);
119+
this.stagedPath = this.sourcePath;
120+
}
110121
} else {
111122
this.assetHash = this.calculateHash(hashType, props.assetHash);
112123

packages/@aws-cdk/core/test/test.staging.ts

+25
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,31 @@ export = {
580580

581581
test.done();
582582
},
583+
584+
'bundling looks at bundling stacks in context'(test: Test) {
585+
// GIVEN
586+
const app = new App();
587+
const stack = new Stack(app, 'MyStack');
588+
stack.node.setContext(cxapi.BUNDLING_STACKS, ['OtherStack']);
589+
const directory = path.join(__dirname, 'fs', 'fixtures', 'test1');
590+
591+
// WHEN
592+
const asset = new AssetStaging(stack, 'Asset', {
593+
sourcePath: directory,
594+
assetHashType: AssetHashType.BUNDLE,
595+
bundling: {
596+
image: BundlingDockerImage.fromRegistry('alpine'),
597+
command: [DockerStubCommand.SUCCESS],
598+
},
599+
});
600+
601+
test.throws(() => readDockerStubInput()); // Bundling did not run
602+
test.equal(asset.assetHash, '3d96e735e26b857743a7c44523c9160c285c2d3ccf273d80fa38a1e674c32cb3'); // hash of MyStack/Asset
603+
test.equal(asset.sourcePath, directory);
604+
test.equal(stack.resolve(asset.stagedPath), directory);
605+
606+
test.done();
607+
},
583608
};
584609

585610
// Reads a docker stub and cleans the volume paths out of the stub.

packages/@aws-cdk/cx-api/lib/app.ts

+5
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ export const DISABLE_ASSET_STAGING_CONTEXT = 'aws:cdk:disable-asset-staging';
2727
* Omits stack traces from construct metadata entries.
2828
*/
2929
export const DISABLE_METADATA_STACK_TRACE = 'aws:cdk:disable-stack-trace';
30+
31+
/**
32+
* Run bundling for stacks specified in this context key
33+
*/
34+
export const BUNDLING_STACKS = 'aws:cdk:bundling-stacks';

packages/aws-cdk/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ $ cdk doctor
307307
- AWS_SDK_LOAD_CONFIG = 1
308308
```
309309

310+
#### Bundling
311+
By default asset bundling is skipped for `cdk list` and `cdk destroy`. For `cdk deploy`, `cdk diff`
312+
and `cdk synthesize` the default is to bundle assets for all stacks unless `exclusively` is specified.
313+
In this case, only the listed stacks will have their assets bundled.
314+
310315
### MFA support
311316

312317
If `mfa_serial` is found in the active profile of the shared ini file AWS CDK

packages/aws-cdk/bin/cdk.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib
1717
import { data, debug, error, print, setLogLevel } from '../lib/logging';
1818
import { PluginHost } from '../lib/plugin';
1919
import { serializeStructure } from '../lib/serialize';
20-
import { Configuration, Settings } from '../lib/settings';
20+
import { Command, Configuration, Settings } from '../lib/settings';
2121
import * as version from '../lib/version';
2222

2323
/* eslint-disable max-len */
@@ -137,7 +137,10 @@ async function initCommandLine() {
137137
debug('CDK toolkit version:', version.DISPLAY_VERSION);
138138
debug('Command line arguments:', argv);
139139

140-
const configuration = new Configuration(argv);
140+
const configuration = new Configuration({
141+
...argv,
142+
_: argv._ as [Command, ...string[]], // TypeScript at its best
143+
});
141144
await configuration.load();
142145

143146
const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({

packages/aws-cdk/lib/api/cxapp/exec.ts

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
3939
context[cxapi.DISABLE_ASSET_STAGING_CONTEXT] = true;
4040
}
4141

42+
const bundlingStacks = config.settings.get(['bundlingStacks']) ?? ['*'];
43+
context[cxapi.BUNDLING_STACKS] = bundlingStacks;
44+
4245
debug('context:', context);
4346
env[cxapi.CONTEXT_ENV] = JSON.stringify(context);
4447

packages/aws-cdk/lib/settings.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,33 @@ export const TRANSIENT_CONTEXT_KEY = '$dontSaveContext';
1818

1919
const CONTEXT_KEY = 'context';
2020

21-
export type Arguments = { readonly [name: string]: unknown };
21+
export enum Command {
22+
LS = 'ls',
23+
LIST = 'list',
24+
DIFF = 'diff',
25+
BOOTSTRAP = 'bootstrap',
26+
DEPLOY = 'deploy',
27+
DESTROY = 'destroy',
28+
SYNTHESIZE = 'synthesize',
29+
SYNTH = 'synth',
30+
METADATA = 'metadata',
31+
INIT = 'init',
32+
VERSION = 'version',
33+
}
34+
35+
const BUNDLING_COMMANDS = [
36+
Command.DEPLOY,
37+
Command.DIFF,
38+
Command.SYNTH,
39+
Command.SYNTHESIZE,
40+
];
41+
42+
export type Arguments = {
43+
readonly _: [Command, ...string[]];
44+
readonly exclusively?: boolean;
45+
readonly STACKS?: string[];
46+
readonly [name: string]: unknown;
47+
};
2248

2349
/**
2450
* All sources of settings combined
@@ -185,6 +211,18 @@ export class Settings {
185211
const context = this.parseStringContextListToObject(argv);
186212
const tags = this.parseStringTagsListToObject(expectStringList(argv.tags));
187213

214+
// Determine bundling stacks
215+
let bundlingStacks: string[];
216+
if (BUNDLING_COMMANDS.includes(argv._[0])) {
217+
// If we deploy, diff or synth a list of stacks exclusively we skip
218+
// bundling for all other stacks.
219+
bundlingStacks = argv.exclusively
220+
? argv.STACKS ?? ['*']
221+
: ['*'];
222+
} else { // Skip bundling for all stacks
223+
bundlingStacks = [];
224+
}
225+
188226
return new Settings({
189227
app: argv.app,
190228
browser: argv.browser,
@@ -205,6 +243,7 @@ export class Settings {
205243
staging: argv.staging,
206244
output: argv.output,
207245
progress: argv.progress,
246+
bundlingStacks,
208247
});
209248
}
210249

@@ -396,4 +435,4 @@ function expectStringList(x: unknown): string[] | undefined {
396435
throw new Error(`Expected list of strings, found ${nonStrings}`);
397436
}
398437
return x;
399-
}
438+
}

packages/aws-cdk/test/context.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ test('surive no context in old file', async () => {
100100
test('command line context is merged with stored context', async () => {
101101
// GIVEN
102102
await fs.writeJSON('cdk.context.json', { boo: 'far' });
103-
const config = await new Configuration({ context: ['foo=bar'] } as any).load();
103+
const config = await new Configuration({ context: ['foo=bar'], _: ['command'] } as any).load();
104104

105105
// WHEN
106106
expect(config.context.all).toEqual({ foo: 'bar', boo: 'far' });

packages/aws-cdk/test/settings.test.ts

+37-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Context, Settings } from '../lib/settings';
1+
import { Command, Context, Settings } from '../lib/settings';
22

33
test('can delete values from Context object', () => {
44
// GIVEN
@@ -62,8 +62,8 @@ test('can clear all values in all objects', () => {
6262

6363
test('can parse string context from command line arguments', () => {
6464
// GIVEN
65-
const settings1 = Settings.fromCommandLineArguments({ context: ['foo=bar'] });
66-
const settings2 = Settings.fromCommandLineArguments({ context: ['foo='] });
65+
const settings1 = Settings.fromCommandLineArguments({ context: ['foo=bar'], _: [Command.DEPLOY] });
66+
const settings2 = Settings.fromCommandLineArguments({ context: ['foo='], _: [Command.DEPLOY] });
6767

6868
// THEN
6969
expect(settings1.get(['context']).foo).toEqual( 'bar');
@@ -72,10 +72,42 @@ test('can parse string context from command line arguments', () => {
7272

7373
test('can parse string context from command line arguments with equals sign in value', () => {
7474
// GIVEN
75-
const settings1 = Settings.fromCommandLineArguments({ context: ['foo==bar='] });
76-
const settings2 = Settings.fromCommandLineArguments({ context: ['foo=bar='] });
75+
const settings1 = Settings.fromCommandLineArguments({ context: ['foo==bar='], _: [Command.DEPLOY] });
76+
const settings2 = Settings.fromCommandLineArguments({ context: ['foo=bar='], _: [Command.DEPLOY] });
7777

7878
// THEN
7979
expect(settings1.get(['context']).foo).toEqual( '=bar=');
8080
expect(settings2.get(['context']).foo).toEqual( 'bar=');
8181
});
82+
83+
test('bundling stacks defaults to an empty list', () => {
84+
// GIVEN
85+
const settings = Settings.fromCommandLineArguments({
86+
_: [Command.LIST],
87+
});
88+
89+
// THEN
90+
expect(settings.get(['bundlingStacks'])).toEqual([]);
91+
});
92+
93+
test('bundling stacks defaults to * for deploy', () => {
94+
// GIVEN
95+
const settings = Settings.fromCommandLineArguments({
96+
_: [Command.DEPLOY],
97+
});
98+
99+
// THEN
100+
expect(settings.get(['bundlingStacks'])).toEqual(['*']);
101+
});
102+
103+
test('bundling stacks with deploy exclusively', () => {
104+
// GIVEN
105+
const settings = Settings.fromCommandLineArguments({
106+
_: [Command.DEPLOY],
107+
exclusively: true,
108+
STACKS: ['cool-stack'],
109+
});
110+
111+
// THEN
112+
expect(settings.get(['bundlingStacks'])).toEqual(['cool-stack']);
113+
});

0 commit comments

Comments
 (0)