Skip to content

Commit 118a716

Browse files
author
Elad Ben-Israel
authored
feat(cli): deploy/destory require explicit stack selection if app contains more than a single stack (#2772)
To reduce risk to production systems, if an app includes more than a single stack, "cdk deploy" and "cdk destroy" will fail and require that explicit stack selector(s) will be specified. Since wildcards are supported "cdk deploy '*'" will select all stacks. Added support for stack selectors in "cdk ls" Closes #2731 BREAKING CHANGE: * **cli:** if an app includes more than one stack "cdk deploy" and "cdk destroy" now require that an explicit selector will be passed. Use "cdk deploy '*'" if you want to select all stacks.
1 parent b735d1c commit 118a716

File tree

5 files changed

+140
-36
lines changed

5 files changed

+140
-36
lines changed

packages/aws-cdk/bin/cdk.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import yargs = require('yargs');
77
import { bootstrapEnvironment, destroyStack, SDK } from '../lib';
88
import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments';
99
import { execProgram } from '../lib/api/cxapp/exec';
10-
import { AppStacks, ExtendedStackSelection } from '../lib/api/cxapp/stacks';
10+
import { AppStacks, DefaultSelection, ExtendedStackSelection } from '../lib/api/cxapp/stacks';
1111
import { CloudFormationDeploymentTarget, DEFAULT_TOOLKIT_STACK_NAME } from '../lib/api/deployment-target';
1212
import { CdkToolkit } from '../lib/cdk-toolkit';
1313
import { RequireApproval } from '../lib/diff';
@@ -45,7 +45,7 @@ async function parseCommandLineArguments() {
4545
.option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack', requiresArg: true })
4646
.option('staging', { type: 'boolean', desc: 'copy assets to the output directory (use --no-staging to disable, needed for local debugging the source files with SAM CLI)', default: true })
4747
.option('output', { type: 'string', alias: 'o', desc: 'emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true })
48-
.command([ 'list', 'ls' ], 'Lists all stacks in the app', yargs => yargs
48+
.command([ 'list [STACKS..]', 'ls [STACKS..]' ], 'Lists all stacks in the app', yargs => yargs
4949
.option('long', { type: 'boolean', default: false, alias: 'l', desc: 'display environment information for each stack' }))
5050
.command([ 'synthesize [STACKS..]', 'synth [STACKS..]' ], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs
5151
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' }))
@@ -173,7 +173,7 @@ async function initCommandLine() {
173173
switch (command) {
174174
case 'ls':
175175
case 'list':
176-
return await cliList({ long: args.long });
176+
return await cliList(args.STACKS, { long: args.long });
177177

178178
case 'diff':
179179
return await cli.diff({
@@ -276,7 +276,10 @@ async function initCommandLine() {
276276
// Only autoselect dependencies if it doesn't interfere with user request or output options
277277
const autoSelectDependencies = !exclusively;
278278

279-
const stacks = await appStacks.selectStacks(stackNames, autoSelectDependencies ? ExtendedStackSelection.Upstream : ExtendedStackSelection.None);
279+
const stacks = await appStacks.selectStacks(stackNames, {
280+
extend: autoSelectDependencies ? ExtendedStackSelection.Upstream : ExtendedStackSelection.None,
281+
defaultBehavior: DefaultSelection.AllStacks
282+
});
280283

281284
// if we have a single stack, print it to STDOUT
282285
if (stacks.length === 1) {
@@ -295,11 +298,12 @@ async function initCommandLine() {
295298
return stacks.map(s => s.template);
296299
}
297300

298-
return appStacks.assembly!.directory;
301+
// no output to stdout
302+
return undefined;
299303
}
300304

301-
async function cliList(options: { long?: boolean } = { }) {
302-
const stacks = await appStacks.listStacks();
305+
async function cliList(selectors: string[], options: { long?: boolean } = { }) {
306+
const stacks = await appStacks.selectStacks(selectors, { defaultBehavior: DefaultSelection.AllStacks });
303307

304308
// if we are in "long" mode, emit the array as-is (JSON/YAML)
305309
if (options.long) {
@@ -322,7 +326,10 @@ async function initCommandLine() {
322326
}
323327

324328
async function cliDestroy(stackNames: string[], exclusively: boolean, force: boolean, roleArn: string | undefined) {
325-
const stacks = await appStacks.selectStacks(stackNames, exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream);
329+
const stacks = await appStacks.selectStacks(stackNames, {
330+
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream,
331+
defaultBehavior: DefaultSelection.OnlySingle
332+
});
326333

327334
// The stacks will have been ordered for deployment, so reverse them for deletion.
328335
stacks.reverse();
@@ -351,7 +358,10 @@ async function initCommandLine() {
351358
* Match a single stack from the list of available stacks
352359
*/
353360
async function findStack(name: string): Promise<string> {
354-
const stacks = await appStacks.selectStacks([name], ExtendedStackSelection.None);
361+
const stacks = await appStacks.selectStacks([name], {
362+
extend: ExtendedStackSelection.None,
363+
defaultBehavior: DefaultSelection.None
364+
});
355365

356366
// Could have been a glob so check that we evaluated to exactly one
357367
if (stacks.length > 1) {

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import cxapi = require('@aws-cdk/cx-api');
22
import minimatch = require('minimatch');
3-
import { AppStacks, ExtendedStackSelection } from './stacks';
3+
import { AppStacks } from './stacks';
44

55
export async function globEnvironmentsFromStacks(appStacks: AppStacks, environmentGlobs: string[]): Promise<cxapi.Environment[]> {
66
if (environmentGlobs.length === 0) {
77
environmentGlobs = [ '**' ]; // default to ALL
88
}
9-
const stacks = await appStacks.selectStacks([], ExtendedStackSelection.None);
9+
10+
const stacks = await appStacks.listStacks();
1011

1112
const availableEnvironments = distinct(stacks.map(stack => stack.environment)
1213
.filter(env => env !== undefined) as cxapi.Environment[]);

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

+49-5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@ export interface AppStacksProps {
5151
synthesizer: Synthesizer;
5252
}
5353

54+
export interface SelectStacksOptions {
55+
/**
56+
* Extend the selection to upstread/downstream stacks
57+
* @default ExtendedStackSelection.None only select the specified stacks.
58+
*/
59+
extend?: ExtendedStackSelection;
60+
61+
/**
62+
* The behavior if if no selectors are privided.
63+
*/
64+
defaultBehavior: DefaultSelection;
65+
}
66+
67+
export enum DefaultSelection {
68+
/**
69+
* Returns an empty selection in case there are no selectors.
70+
*/
71+
None = 'none',
72+
73+
/**
74+
* If the app includes a single stack, returns it. Otherwise throws an exception.
75+
* This behavior is used by "deploy".
76+
*/
77+
OnlySingle = 'single',
78+
79+
/**
80+
* If no selectors are provided, returns all stacks in the app.
81+
*/
82+
AllStacks = 'all',
83+
}
84+
5485
/**
5586
* Routines to get stacks from an app
5687
*
@@ -72,7 +103,7 @@ export class AppStacks {
72103
* It's an error if there are no stacks to select, or if one of the requested parameters
73104
* refers to a nonexistant stack.
74105
*/
75-
public async selectStacks(selectors: string[], extendedSelection: ExtendedStackSelection): Promise<cxapi.CloudFormationStackArtifact[]> {
106+
public async selectStacks(selectors: string[], options: SelectStacksOptions): Promise<cxapi.CloudFormationStackArtifact[]> {
76107
selectors = selectors.filter(s => s != null); // filter null/undefined
77108

78109
const stacks = await this.listStacks();
@@ -81,9 +112,21 @@ export class AppStacks {
81112
}
82113

83114
if (selectors.length === 0) {
84-
// remove non-auto deployed Stacks
85-
debug('Stack name not specified, so defaulting to all available stacks: ' + listStackNames(stacks));
86-
return stacks;
115+
switch (options.defaultBehavior) {
116+
case DefaultSelection.AllStacks:
117+
return stacks;
118+
case DefaultSelection.None:
119+
return [];
120+
case DefaultSelection.OnlySingle:
121+
if (stacks.length === 1) {
122+
return stacks;
123+
} else {
124+
throw new Error(`Since this app includes more than a single stack, specify which stacks to use (wildcards are supported)\n` +
125+
`Stacks: ${stacks.map(x => x.name).join(' ')}`);
126+
}
127+
default:
128+
throw new Error(`invalid default behavior: ${options.defaultBehavior}`);
129+
}
87130
}
88131

89132
const allStacks = new Map<string, cxapi.CloudFormationStackArtifact>();
@@ -108,7 +151,8 @@ export class AppStacks {
108151
}
109152
}
110153

111-
switch (extendedSelection) {
154+
const extend = options.extend || ExtendedStackSelection.None;
155+
switch (extend) {
112156
case ExtendedStackSelection.Downstream:
113157
includeDownstreamStacks(selectedStacks, allStacks);
114158
break;

packages/aws-cdk/lib/cdk-toolkit.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import colors = require('colors/safe');
22
import fs = require('fs-extra');
33
import { format } from 'util';
4-
import { AppStacks, ExtendedStackSelection, Tag } from "./api/cxapp/stacks";
4+
import { AppStacks, DefaultSelection, ExtendedStackSelection, Tag } from "./api/cxapp/stacks";
55
import { IDeploymentTarget } from './api/deployment-target';
66
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
77
import { data, error, highlight, print, success } from './logging';
@@ -38,9 +38,10 @@ export class CdkToolkit {
3838
}
3939

4040
public async diff(options: DiffOptions): Promise<number> {
41-
const stacks = await this.appStacks.selectStacks(
42-
options.stackNames,
43-
options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream);
41+
const stacks = await this.appStacks.selectStacks(options.stackNames, {
42+
extend: options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream,
43+
defaultBehavior: DefaultSelection.AllStacks
44+
});
4445

4546
const strict = !!options.strict;
4647
const contextLines = options.contextLines || 3;
@@ -75,9 +76,10 @@ export class CdkToolkit {
7576
public async deploy(options: DeployOptions) {
7677
const requireApproval = options.requireApproval !== undefined ? options.requireApproval : RequireApproval.Broadening;
7778

78-
const stacks = await this.appStacks.selectStacks(
79-
options.stackNames,
80-
options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream);
79+
const stacks = await this.appStacks.selectStacks(options.stackNames, {
80+
extend: options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream,
81+
defaultBehavior: DefaultSelection.OnlySingle
82+
});
8183

8284
for (const stack of stacks) {
8385
if (stacks.length !== 1) { highlight(stack.name); }

packages/aws-cdk/test/api/test.stacks.ts

+60-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cxapi = require('@aws-cdk/cx-api');
22
import { Test } from 'nodeunit';
33
import { SDK } from '../../lib';
4-
import { AppStacks, ExtendedStackSelection } from '../../lib/api/cxapp/stacks';
4+
import { AppStacks, DefaultSelection } from '../../lib/api/cxapp/stacks';
55
import { Configuration } from '../../lib/settings';
66
import { testAssembly } from '../util';
77

@@ -25,14 +25,12 @@ const FIXED_RESULT = testAssembly({
2525
export = {
2626
async 'do not throw when selecting stack without errors'(test: Test) {
2727
// GIVEN
28-
const stacks = new AppStacks({
29-
configuration: new Configuration(),
30-
aws: new SDK(),
31-
synthesizer: async () => FIXED_RESULT,
32-
});
28+
const stacks = testStacks();
3329

3430
// WHEN
35-
const selected = await stacks.selectStacks(['withouterrors'], ExtendedStackSelection.None);
31+
const selected = await stacks.selectStacks(['withouterrors'], {
32+
defaultBehavior: DefaultSelection.AllStacks
33+
});
3634

3735
// THEN
3836
test.equal(selected[0].template.resource, 'noerrorresource');
@@ -42,20 +40,69 @@ export = {
4240

4341
async 'do throw when selecting stack with errors'(test: Test) {
4442
// GIVEN
45-
const stacks = new AppStacks({
46-
configuration: new Configuration(),
47-
aws: new SDK(),
48-
synthesizer: async () => FIXED_RESULT,
49-
});
43+
const stacks = testStacks();
5044

5145
// WHEN
5246
try {
53-
await stacks.selectStacks(['witherrors'], ExtendedStackSelection.None);
47+
await stacks.selectStacks(['witherrors'], {
48+
defaultBehavior: DefaultSelection.AllStacks
49+
});
50+
5451
test.ok(false, 'Did not get exception');
5552
} catch (e) {
5653
test.ok(/Found errors/.test(e.toString()), 'Wrong error');
5754
}
5855

5956
test.done();
6057
},
58+
59+
async 'select behavior: all'(test: Test) {
60+
// GIVEN
61+
const stacks = testStacks();
62+
63+
// WHEN
64+
const x = await stacks.selectStacks([], { defaultBehavior: DefaultSelection.AllStacks });
65+
66+
// THEN
67+
test.deepEqual(x.length, 2);
68+
test.done();
69+
},
70+
71+
async 'select behavior: none'(test: Test) {
72+
// GIVEN
73+
const stacks = testStacks();
74+
75+
// WHEN
76+
const x = await stacks.selectStacks([], { defaultBehavior: DefaultSelection.None });
77+
78+
// THEN
79+
test.deepEqual(x.length, 0);
80+
test.done();
81+
},
82+
83+
async 'select behavior: single'(test: Test) {
84+
// GIVEN
85+
const stacks = testStacks();
86+
87+
// WHEN
88+
let thrown: string | undefined;
89+
try {
90+
await stacks.selectStacks([], { defaultBehavior: DefaultSelection.OnlySingle });
91+
} catch (e) {
92+
thrown = e.message;
93+
}
94+
95+
// THEN
96+
test.ok(thrown && thrown.includes('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported)'));
97+
test.done();
98+
}
99+
61100
};
101+
102+
function testStacks() {
103+
return new AppStacks({
104+
configuration: new Configuration(),
105+
aws: new SDK(),
106+
synthesizer: async () => FIXED_RESULT,
107+
});
108+
}

0 commit comments

Comments
 (0)