diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index cd79e56322fba..79d4f2771b7d8 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -7,7 +7,7 @@ import yargs = require('yargs'); import { bootstrapEnvironment, destroyStack, SDK } from '../lib'; import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments'; import { execProgram } from '../lib/api/cxapp/exec'; -import { AppStacks, ExtendedStackSelection } from '../lib/api/cxapp/stacks'; +import { AppStacks, DefaultSelection, ExtendedStackSelection } from '../lib/api/cxapp/stacks'; import { CloudFormationDeploymentTarget, DEFAULT_TOOLKIT_STACK_NAME } from '../lib/api/deployment-target'; import { CdkToolkit } from '../lib/cdk-toolkit'; import { RequireApproval } from '../lib/diff'; @@ -45,7 +45,7 @@ async function parseCommandLineArguments() { .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack', requiresArg: true }) .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 }) .option('output', { type: 'string', alias: 'o', desc: 'emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true }) - .command([ 'list', 'ls' ], 'Lists all stacks in the app', yargs => yargs + .command([ 'list [STACKS..]', 'ls [STACKS..]' ], 'Lists all stacks in the app', yargs => yargs .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'display environment information for each stack' })) .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' })) @@ -173,7 +173,7 @@ async function initCommandLine() { switch (command) { case 'ls': case 'list': - return await cliList({ long: args.long }); + return await cliList(args.STACKS, { long: args.long }); case 'diff': return await cli.diff({ @@ -276,7 +276,10 @@ async function initCommandLine() { // Only autoselect dependencies if it doesn't interfere with user request or output options const autoSelectDependencies = !exclusively; - const stacks = await appStacks.selectStacks(stackNames, autoSelectDependencies ? ExtendedStackSelection.Upstream : ExtendedStackSelection.None); + const stacks = await appStacks.selectStacks(stackNames, { + extend: autoSelectDependencies ? ExtendedStackSelection.Upstream : ExtendedStackSelection.None, + defaultBehavior: DefaultSelection.AllStacks + }); // if we have a single stack, print it to STDOUT if (stacks.length === 1) { @@ -295,11 +298,12 @@ async function initCommandLine() { return stacks.map(s => s.template); } - return appStacks.assembly!.directory; + // no output to stdout + return undefined; } - async function cliList(options: { long?: boolean } = { }) { - const stacks = await appStacks.listStacks(); + async function cliList(selectors: string[], options: { long?: boolean } = { }) { + const stacks = await appStacks.selectStacks(selectors, { defaultBehavior: DefaultSelection.AllStacks }); // if we are in "long" mode, emit the array as-is (JSON/YAML) if (options.long) { @@ -322,7 +326,10 @@ async function initCommandLine() { } async function cliDestroy(stackNames: string[], exclusively: boolean, force: boolean, roleArn: string | undefined) { - const stacks = await appStacks.selectStacks(stackNames, exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream); + const stacks = await appStacks.selectStacks(stackNames, { + extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, + defaultBehavior: DefaultSelection.OnlySingle + }); // The stacks will have been ordered for deployment, so reverse them for deletion. stacks.reverse(); @@ -351,7 +358,10 @@ async function initCommandLine() { * Match a single stack from the list of available stacks */ async function findStack(name: string): Promise { - const stacks = await appStacks.selectStacks([name], ExtendedStackSelection.None); + const stacks = await appStacks.selectStacks([name], { + extend: ExtendedStackSelection.None, + defaultBehavior: DefaultSelection.None + }); // Could have been a glob so check that we evaluated to exactly one if (stacks.length > 1) { diff --git a/packages/aws-cdk/lib/api/cxapp/environments.ts b/packages/aws-cdk/lib/api/cxapp/environments.ts index ff63349b85e15..17867d35e0a65 100644 --- a/packages/aws-cdk/lib/api/cxapp/environments.ts +++ b/packages/aws-cdk/lib/api/cxapp/environments.ts @@ -1,12 +1,13 @@ import cxapi = require('@aws-cdk/cx-api'); import minimatch = require('minimatch'); -import { AppStacks, ExtendedStackSelection } from './stacks'; +import { AppStacks } from './stacks'; export async function globEnvironmentsFromStacks(appStacks: AppStacks, environmentGlobs: string[]): Promise { if (environmentGlobs.length === 0) { environmentGlobs = [ '**' ]; // default to ALL } - const stacks = await appStacks.selectStacks([], ExtendedStackSelection.None); + + const stacks = await appStacks.listStacks(); const availableEnvironments = distinct(stacks.map(stack => stack.environment) .filter(env => env !== undefined) as cxapi.Environment[]); diff --git a/packages/aws-cdk/lib/api/cxapp/stacks.ts b/packages/aws-cdk/lib/api/cxapp/stacks.ts index e88d52a2a566e..74e7e7be74858 100644 --- a/packages/aws-cdk/lib/api/cxapp/stacks.ts +++ b/packages/aws-cdk/lib/api/cxapp/stacks.ts @@ -51,6 +51,37 @@ export interface AppStacksProps { synthesizer: Synthesizer; } +export interface SelectStacksOptions { + /** + * Extend the selection to upstread/downstream stacks + * @default ExtendedStackSelection.None only select the specified stacks. + */ + extend?: ExtendedStackSelection; + + /** + * The behavior if if no selectors are privided. + */ + defaultBehavior: DefaultSelection; +} + +export enum DefaultSelection { + /** + * Returns an empty selection in case there are no selectors. + */ + None = 'none', + + /** + * If the app includes a single stack, returns it. Otherwise throws an exception. + * This behavior is used by "deploy". + */ + OnlySingle = 'single', + + /** + * If no selectors are provided, returns all stacks in the app. + */ + AllStacks = 'all', +} + /** * Routines to get stacks from an app * @@ -72,7 +103,7 @@ export class AppStacks { * It's an error if there are no stacks to select, or if one of the requested parameters * refers to a nonexistant stack. */ - public async selectStacks(selectors: string[], extendedSelection: ExtendedStackSelection): Promise { + public async selectStacks(selectors: string[], options: SelectStacksOptions): Promise { selectors = selectors.filter(s => s != null); // filter null/undefined const stacks = await this.listStacks(); @@ -81,9 +112,21 @@ export class AppStacks { } if (selectors.length === 0) { - // remove non-auto deployed Stacks - debug('Stack name not specified, so defaulting to all available stacks: ' + listStackNames(stacks)); - return stacks; + switch (options.defaultBehavior) { + case DefaultSelection.AllStacks: + return stacks; + case DefaultSelection.None: + return []; + case DefaultSelection.OnlySingle: + if (stacks.length === 1) { + return stacks; + } else { + throw new Error(`Since this app includes more than a single stack, specify which stacks to use (wildcards are supported)\n` + + `Stacks: ${stacks.map(x => x.name).join(' ')}`); + } + default: + throw new Error(`invalid default behavior: ${options.defaultBehavior}`); + } } const allStacks = new Map(); @@ -108,7 +151,8 @@ export class AppStacks { } } - switch (extendedSelection) { + const extend = options.extend || ExtendedStackSelection.None; + switch (extend) { case ExtendedStackSelection.Downstream: includeDownstreamStacks(selectedStacks, allStacks); break; diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 5ca800ed07690..b62617da4333d 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -1,7 +1,7 @@ import colors = require('colors/safe'); import fs = require('fs-extra'); import { format } from 'util'; -import { AppStacks, ExtendedStackSelection, Tag } from "./api/cxapp/stacks"; +import { AppStacks, DefaultSelection, ExtendedStackSelection, Tag } from "./api/cxapp/stacks"; import { IDeploymentTarget } from './api/deployment-target'; import { printSecurityDiff, printStackDiff, RequireApproval } from './diff'; import { data, error, highlight, print, success } from './logging'; @@ -38,9 +38,10 @@ export class CdkToolkit { } public async diff(options: DiffOptions): Promise { - const stacks = await this.appStacks.selectStacks( - options.stackNames, - options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream); + const stacks = await this.appStacks.selectStacks(options.stackNames, { + extend: options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream, + defaultBehavior: DefaultSelection.AllStacks + }); const strict = !!options.strict; const contextLines = options.contextLines || 3; @@ -75,9 +76,10 @@ export class CdkToolkit { public async deploy(options: DeployOptions) { const requireApproval = options.requireApproval !== undefined ? options.requireApproval : RequireApproval.Broadening; - const stacks = await this.appStacks.selectStacks( - options.stackNames, - options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream); + const stacks = await this.appStacks.selectStacks(options.stackNames, { + extend: options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream, + defaultBehavior: DefaultSelection.OnlySingle + }); for (const stack of stacks) { if (stacks.length !== 1) { highlight(stack.name); } diff --git a/packages/aws-cdk/test/api/test.stacks.ts b/packages/aws-cdk/test/api/test.stacks.ts index 032914a5f5003..ceb172ea18c9a 100644 --- a/packages/aws-cdk/test/api/test.stacks.ts +++ b/packages/aws-cdk/test/api/test.stacks.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { SDK } from '../../lib'; -import { AppStacks, ExtendedStackSelection } from '../../lib/api/cxapp/stacks'; +import { AppStacks, DefaultSelection } from '../../lib/api/cxapp/stacks'; import { Configuration } from '../../lib/settings'; import { testAssembly } from '../util'; @@ -25,14 +25,12 @@ const FIXED_RESULT = testAssembly({ export = { async 'do not throw when selecting stack without errors'(test: Test) { // GIVEN - const stacks = new AppStacks({ - configuration: new Configuration(), - aws: new SDK(), - synthesizer: async () => FIXED_RESULT, - }); + const stacks = testStacks(); // WHEN - const selected = await stacks.selectStacks(['withouterrors'], ExtendedStackSelection.None); + const selected = await stacks.selectStacks(['withouterrors'], { + defaultBehavior: DefaultSelection.AllStacks + }); // THEN test.equal(selected[0].template.resource, 'noerrorresource'); @@ -42,15 +40,14 @@ export = { async 'do throw when selecting stack with errors'(test: Test) { // GIVEN - const stacks = new AppStacks({ - configuration: new Configuration(), - aws: new SDK(), - synthesizer: async () => FIXED_RESULT, - }); + const stacks = testStacks(); // WHEN try { - await stacks.selectStacks(['witherrors'], ExtendedStackSelection.None); + await stacks.selectStacks(['witherrors'], { + defaultBehavior: DefaultSelection.AllStacks + }); + test.ok(false, 'Did not get exception'); } catch (e) { test.ok(/Found errors/.test(e.toString()), 'Wrong error'); @@ -58,4 +55,54 @@ export = { test.done(); }, + + async 'select behavior: all'(test: Test) { + // GIVEN + const stacks = testStacks(); + + // WHEN + const x = await stacks.selectStacks([], { defaultBehavior: DefaultSelection.AllStacks }); + + // THEN + test.deepEqual(x.length, 2); + test.done(); + }, + + async 'select behavior: none'(test: Test) { + // GIVEN + const stacks = testStacks(); + + // WHEN + const x = await stacks.selectStacks([], { defaultBehavior: DefaultSelection.None }); + + // THEN + test.deepEqual(x.length, 0); + test.done(); + }, + + async 'select behavior: single'(test: Test) { + // GIVEN + const stacks = testStacks(); + + // WHEN + let thrown: string | undefined; + try { + await stacks.selectStacks([], { defaultBehavior: DefaultSelection.OnlySingle }); + } catch (e) { + thrown = e.message; + } + + // THEN + test.ok(thrown && thrown.includes('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported)')); + test.done(); + } + }; + +function testStacks() { + return new AppStacks({ + configuration: new Configuration(), + aws: new SDK(), + synthesizer: async () => FIXED_RESULT, + }); +} \ No newline at end of file