From 37c79b94e3093b0cecc21c77e8bc79579ea002e5 Mon Sep 17 00:00:00 2001 From: Michael Sambol Date: Wed, 10 Jan 2024 11:06:18 -0600 Subject: [PATCH] feat(cli): option to ignore no stacks (#28387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm new to development on this package—any feedback regarding testing is appreciated. Closes #28371. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cli-integ/resources/cdk-apps/app/app.js | 3 ++ .../tests/cli-integ-tests/cli.integtest.ts | 19 +++++++++ packages/aws-cdk/README.md | 14 +++++++ .../aws-cdk/lib/api/cxapp/cloud-assembly.ts | 12 +++++- packages/aws-cdk/lib/api/deployments.ts | 7 ++++ packages/aws-cdk/lib/cdk-toolkit.ts | 17 ++++++-- packages/aws-cdk/lib/cli.ts | 4 +- packages/aws-cdk/lib/settings.ts | 1 + .../aws-cdk/test/api/cloud-assembly.test.ts | 40 +++++++++++++++++++ 9 files changed, 112 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index 048e6fabd5165..eeb1ad83673e2 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -492,6 +492,9 @@ switch (stackSet) { stage.synth({ validateOnSynthesis: true }); break; + case 'stage-with-no-stacks': + break; + default: throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`); } diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index ccdf07b166f33..b64a3c085bd34 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -780,6 +780,25 @@ integTest('deploy stack without resource', withDefaultFixture(async (fixture) => .rejects.toThrow('conditional-resource does not exist'); })); +integTest('deploy no stacks with --ignore-no-stacks', withDefaultFixture(async (fixture) => { + // empty array for stack names + await fixture.cdkDeploy([], { + options: ['--ignore-no-stacks'], + modEnv: { + INTEG_STACK_SET: 'stage-with-no-stacks', + }, + }); +})); + +integTest('deploy no stacks error', withDefaultFixture(async (fixture) => { + // empty array for stack names + await expect(fixture.cdkDeploy([], { + modEnv: { + INTEG_STACK_SET: 'stage-with-no-stacks', + }, + })).rejects.toThrow('exited with error'); +})); + integTest('IAM diff', withDefaultFixture(async (fixture) => { const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 7642508136e9d..c16acb3f690ab 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -386,6 +386,20 @@ $ cdk deploy --method=prepare-change-set --change-set-name MyChangeSetName For more control over when stack changes are deployed, the CDK can generate a CloudFormation change set but not execute it. +#### Ignore No Stacks + +You may have an app with multiple environments, e.g., dev and prod. When starting +development, your prod app may not have any resources or the resources are commented +out. In this scenario, you will receive an error message stating that the app has no +stacks. + +To bypass this error messages, you can pass the `--ignore-no-stacks` flag to the +`deploy` command: + +```console +$ cdk deploy --ignore-no-stacks +``` + #### Hotswap deployments for faster development You can pass the `--hotswap` flag to the `deploy` command: diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 84cfa775e0cf7..c797e3a3492c4 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -38,9 +38,16 @@ export interface SelectStacksOptions { extend?: ExtendedStackSelection; /** - * The behavior if if no selectors are privided. + * The behavior if if no selectors are provided. */ defaultBehavior: DefaultSelection; + + /** + * Whether to deploy if the app contains no stacks. + * + * @default false + */ + ignoreNoStacks?: boolean; } /** @@ -100,6 +107,9 @@ export class CloudAssembly { const patterns = sanitizePatterns(selector.patterns); if (stacks.length === 0) { + if (options.ignoreNoStacks) { + return new StackCollection(this, []); + } throw new Error('This app contains no stacks'); } diff --git a/packages/aws-cdk/lib/api/deployments.ts b/packages/aws-cdk/lib/api/deployments.ts index 4da0d27837c92..09264f393fc3c 100644 --- a/packages/aws-cdk/lib/api/deployments.ts +++ b/packages/aws-cdk/lib/api/deployments.ts @@ -199,6 +199,13 @@ export interface DeployStackOptions { * @default true To remain backward compatible. */ readonly assetParallelism?: boolean; + + /** + * Whether to deploy if the app contains no stacks. + * + * @default false + */ + ignoreNoStacks?: boolean; } interface AssetOptions { diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 43a2638154a3a..4a8a40949ab4d 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -200,7 +200,8 @@ export class CdkToolkit { } const startSynthTime = new Date().getTime(); - const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively, options.cacheCloudAssembly); + const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively, + options.cacheCloudAssembly, options.ignoreNoStacks); const elapsedSynthTime = new Date().getTime() - startSynthTime; print('\n✨ Synthesis time: %ss\n', formatTime(elapsedSynthTime)); @@ -317,6 +318,7 @@ export class CdkToolkit { hotswap: options.hotswap, extraUserAgent: options.extraUserAgent, assetParallelism: options.assetParallelism, + ignoreNoStacks: options.ignoreNoStacks, }); const message = result.noOp @@ -491,7 +493,7 @@ export class CdkToolkit { } public async import(options: ImportOptions) { - const stacks = await this.selectStacksForDeploy(options.selector, true, true); + const stacks = await this.selectStacksForDeploy(options.selector, true, true, false); if (stacks.stackCount > 1) { throw new Error(`Stack selection is ambiguous, please choose a specific stack for import [${stacks.stackArtifacts.map(x => x.id).join(', ')}]`); @@ -741,11 +743,13 @@ export class CdkToolkit { return stacks; } - private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean, cacheCloudAssembly?: boolean): Promise { + private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean, + cacheCloudAssembly?: boolean, ignoreNoStacks?: boolean): Promise { const assembly = await this.assembly(cacheCloudAssembly); const stacks = await assembly.selectStacks(selector, { extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream, defaultBehavior: DefaultSelection.OnlySingle, + ignoreNoStacks, }); this.validateStacksSelected(stacks, selector.patterns); @@ -1159,6 +1163,13 @@ export interface DeployOptions extends CfnDeployOptions, WatchOptions { * @default AssetBuildTime.ALL_BEFORE_DEPLOY */ readonly assetBuildTime?: AssetBuildTime; + + /** + * Whether to deploy if the app contains no stacks. + * + * @default false + */ + readonly ignoreNoStacks?: boolean; } export interface ImportOptions extends CfnDeployOptions { diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index df2f3c24fa569..5574f487a496c 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -172,7 +172,8 @@ async function parseCommandLineArguments(args: string[]) { }) .option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }) .option('asset-parallelism', { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }) - .option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true }), + .option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true }) + .option('ignore-no-stacks', { type: 'boolean', desc: 'Whether to deploy if the app contains no stacks', default: false }), ) .command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) @@ -585,6 +586,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { expect(x.stackCount).toBe(2); }); +test('select behavior with no stacks and ignore stacks option', async() => { + // GIVEN + const cxasm = await testCloudAssemblyNoStacks(); + + // WHEN + const x = await cxasm.selectStacks({ patterns: [] }, { + defaultBehavior: DefaultSelection.AllStacks, + ignoreNoStacks: true, + }); + + // THEN + expect(x.stackCount).toBe(0); +}); + +test('select behavior with no stacks and no ignore stacks option', async() => { + // GIVEN + const cxasm = await testCloudAssemblyNoStacks(); + + // WHEN & THEN + await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks, ignoreNoStacks: false })) + .rejects.toThrow('This app contains no stacks'); +}); + +test('select behavior with no stacks and default ignore stacks options (false)', async() => { + // GIVEN + const cxasm = await testCloudAssemblyNoStacks(); + + // WHEN & THEN + await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks })) + .rejects.toThrow('This app contains no stacks'); +}); + async function testCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) { const cloudExec = new MockCloudExecutable({ stacks: [{ @@ -182,6 +214,14 @@ async function testCloudAssembly({ env }: { env?: string, versionReporting?: boo return cloudExec.synthesize(); } +async function testCloudAssemblyNoStacks() { + const cloudExec = new MockCloudExecutable({ + stacks: [], + }); + + return cloudExec.synthesize(); +} + async function testNestedCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) { const cloudExec = new MockCloudExecutable({ stacks: [{