From a3fde3b722bc1df9e61b0670ca79ef5991da7a71 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 04:13:23 +0900 Subject: [PATCH 01/16] feat(cli): warn of non-existent stacks in cdk destroy --- packages/aws-cdk/lib/cdk-toolkit.ts | 43 ++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 9eb2b24c473ef..2e55eb4778f92 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -4,7 +4,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; +import { minimatch } from 'minimatch'; import * as promptly from 'promptly'; +import * as semver from 'semver'; import * as uuid from 'uuid'; import { DeploymentMethod, SuccessfulDeployStackResult } from './api'; import { SdkProvider } from './api/aws-auth'; @@ -53,6 +55,7 @@ import { validateSnsTopicArn } from './util/validate-notification-arn'; import { Concurrency, WorkGraph } from './util/work-graph'; import { WorkGraphBuilder } from './util/work-graph-builder'; import { AssetBuildNode, AssetPublishNode, StackNode } from './util/work-graph-types'; +import { versionNumber } from './version'; import { environmentsFromDescriptors, globEnvironmentsFromStacks, looksLikeGlob } from '../lib/api/cxapp/environments'; // Must use a require() otherwise esbuild complains about calling a namespace @@ -802,6 +805,9 @@ export class CdkToolkit { stacks = stacks.reversed(); if (!options.force) { + if (stacks.stackArtifacts.length === 0) { + return; + } // eslint-disable-next-line max-len const confirmed = await promptly.confirm( `Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))} (y/n)?`, @@ -1157,8 +1163,43 @@ export class CdkToolkit { defaultBehavior: DefaultSelection.OnlySingle, }); - // No validation + const selectorWithoutPatterns: StackSelector = { + ...selector, + allTopLevel: true, + patterns: [], + }; + const stacksWithoutPatterns = await assembly.selectStacks(selectorWithoutPatterns, { + extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, + defaultBehavior: DefaultSelection.OnlySingle, + }); + + const patterns = selector.patterns.map(pattern => { + const notExist = !stacks.stackArtifacts.find(stack => + minimatch(stack.hierarchicalId, pattern) || (stack.id === pattern && semver.major(versionNumber()) < 2), + ); + + const closelyMatched = notExist ? stacksWithoutPatterns.stackArtifacts.map(stack => { + if (minimatch(stack.hierarchicalId.toLowerCase(), pattern.toLowerCase())) { + return stack.hierarchicalId; + } + if (stack.id.toLowerCase() === pattern.toLowerCase() && semver.major(versionNumber()) < 2) { + return stack.id; + } + return; + }).filter((stack): stack is string => stack !== undefined) : []; + return { + pattern, + notExist, + closelyMatched, + }; + }); + patterns.forEach(pattern => { + if (pattern.notExist) { + const closelyMatched = pattern.closelyMatched.length > 0 ? ` Do you mean ${pattern.closelyMatched.join(', ')}?` : ''; + warning(`${pattern.pattern} does not exist.${closelyMatched}`); + } + }); return stacks; } From 78e191ded0ecfa289eb92510a32557446e10f648 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:03:02 +0900 Subject: [PATCH 02/16] unit tests --- packages/aws-cdk/test/cdk-toolkit.test.ts | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index d69da1e007562..12a98e118f1da 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -945,6 +945,32 @@ describe('destroy', () => { }); }).resolves; }); + + test('does not throw even if there is a non-existent stack and the other exists', async () => { + const toolkit = defaultToolkitSetup(); + + await expect(() => { + return toolkit.destroy({ + selector: { patterns: ['Test-Stack-X', 'Test-Stack-A/Test-Stack-C'] }, + exclusively: true, + force: true, + fromDeploy: true, + }); + }).resolves; + }); + + test('does not throw even if there are only non-existent stacks', async () => { + const toolkit = defaultToolkitSetup(); + + await expect(() => { + return toolkit.destroy({ + selector: { patterns: ['Test-Stack-X', 'Test-Stack-Y'] }, + exclusively: true, + force: true, + fromDeploy: true, + }); + }).resolves; + }); }); describe('watch', () => { From 93064efba0198996aaf6b8daae4055d1a9920250 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:09:15 +0900 Subject: [PATCH 03/16] cli-integ test --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 c96fab3a476ba..dee5356e6b847 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 @@ -2575,6 +2575,13 @@ integTest('hotswap ECS deployment respects properties override', withDefaultFixt expect(describeServicesResponse.services?.[0].deploymentConfiguration?.maximumPercent).toEqual(ecsMaximumHealthyPercent); })); +integTest('cdk destroy does not fail even if the stacks do not exist', withDefaultFixture(async (fixture) => { + const nonExistingStackName1 = 'non-existing-stack-1'; + const nonExistingStackName2 = 'non-existing-stack-2'; + + await expect(fixture.cdkDestroy([nonExistingStackName1, nonExistingStackName2])).resolves.not.toThrow(); +})); + async function listChildren(parent: string, pred: (x: string) => Promise) { const ret = new Array(); for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) { From 4390c0aa313c87967854c3d3b6312ddfb22d7d06 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:32:14 +0900 Subject: [PATCH 04/16] style for lint(indents) --- packages/aws-cdk/test/cdk-toolkit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 39828ba75ea7a..9b25078791b60 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -18,8 +18,8 @@ const fakeChokidarWatcherOn = { }, get fileEventCallback(): ( - event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', - path: string, + event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', + path: string, ) => Promise { expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(2); const secondCall = mockChokidarWatcherOn.mock.calls[1]; From 346169b7001e5d4e431683be57b83646219bd809 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:36:16 +0900 Subject: [PATCH 05/16] unit test with force option false --- packages/aws-cdk/test/cdk-toolkit.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 9b25078791b60..3b7ae858a6ecb 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -979,6 +979,19 @@ describe('destroy', () => { }); }).resolves; }); + + test('does not throw even if there are only non-existent stacks and force option is false', async () => { + const toolkit = defaultToolkitSetup(); + + await expect(() => { + return toolkit.destroy({ + selector: { patterns: ['Test-Stack-X', 'Test-Stack-Y'] }, + exclusively: true, + force: false, + fromDeploy: true, + }); + }).resolves; + }); }); describe('watch', () => { From ebd967159a2993aab5135fc646453fe04ba7d7f3 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:52:33 +0900 Subject: [PATCH 06/16] add a cli-integ test --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 8 ++++++++ 1 file changed, 8 insertions(+) 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 60ab5a99a552d..6008bff79ea57 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 @@ -41,6 +41,7 @@ import { withSamIntegrationFixture, withSpecificFixture, } from '../../lib'; +import { options } from 'yargs'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime @@ -2582,6 +2583,13 @@ integTest('cdk destroy does not fail even if the stacks do not exist', withDefau await expect(fixture.cdkDestroy([nonExistingStackName1, nonExistingStackName2])).resolves.not.toThrow(); })); +integTest('cdk destroy with no force option exits without prompt if the stacks do not exist', withDefaultFixture(async (fixture) => { + const nonExistingStackName1 = 'non-existing-stack-1'; + const nonExistingStackName2 = 'non-existing-stack-2'; + + await expect(fixture.cdk(['destroy', ...fixture.fullStackName([nonExistingStackName1, nonExistingStackName2])])).resolves.not.toThrow(); +})); + async function listChildren(parent: string, pred: (x: string) => Promise) { const ret = new Array(); for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) { From b4c7fcaa9e1da8b85c4e99ae8322b9b4e45b7c77 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:53:58 +0900 Subject: [PATCH 07/16] rm unused import --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 1 - 1 file changed, 1 deletion(-) 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 6008bff79ea57..0f5ec880eefa9 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 @@ -41,7 +41,6 @@ import { withSamIntegrationFixture, withSpecificFixture, } from '../../lib'; -import { options } from 'yargs'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime From aee42f4ccbd2a165167b16cd46df416ba77fb2ea Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:54:48 +0900 Subject: [PATCH 08/16] undo style for indents --- packages/aws-cdk/test/cdk-toolkit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 3b7ae858a6ecb..3ff5f013f0bd5 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -18,8 +18,8 @@ const fakeChokidarWatcherOn = { }, get fileEventCallback(): ( - event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', - path: string, + event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', + path: string, ) => Promise { expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(2); const secondCall = mockChokidarWatcherOn.mock.calls[1]; From 56ea2b73bf5b0ee14f75f5e6d9dfc9f71b2158b2 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:58:43 +0900 Subject: [PATCH 09/16] change test name --- packages/aws-cdk/test/cdk-toolkit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 3ff5f013f0bd5..38c8e7287febf 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -980,7 +980,7 @@ describe('destroy', () => { }).resolves; }); - test('does not throw even if there are only non-existent stacks and force option is false', async () => { + test('exits without prompt if there are only non-existent stacks and force option is false', async () => { const toolkit = defaultToolkitSetup(); await expect(() => { From d52932428128eef7ebda60fa52dd250f7652d904 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:02:29 +0900 Subject: [PATCH 10/16] rm handling of versionNumber --- packages/aws-cdk/lib/cdk-toolkit.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index a0cb6ce25765a..0ab1b663d00ec 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -6,7 +6,6 @@ import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { minimatch } from 'minimatch'; import * as promptly from 'promptly'; -import * as semver from 'semver'; import * as uuid from 'uuid'; import { DeploymentMethod, SuccessfulDeployStackResult } from './api'; import { SdkProvider } from './api/aws-auth'; @@ -55,7 +54,6 @@ import { validateSnsTopicArn } from './util/validate-notification-arn'; import { Concurrency, WorkGraph } from './util/work-graph'; import { WorkGraphBuilder } from './util/work-graph-builder'; import { AssetBuildNode, AssetPublishNode, StackNode } from './util/work-graph-types'; -import { versionNumber } from './version'; import { environmentsFromDescriptors, globEnvironmentsFromStacks, looksLikeGlob } from '../lib/api/cxapp/environments'; // Must use a require() otherwise esbuild complains about calling a namespace @@ -1174,16 +1172,13 @@ export class CdkToolkit { const patterns = selector.patterns.map(pattern => { const notExist = !stacks.stackArtifacts.find(stack => - minimatch(stack.hierarchicalId, pattern) || (stack.id === pattern && semver.major(versionNumber()) < 2), + minimatch(stack.hierarchicalId, pattern), ); const closelyMatched = notExist ? stacksWithoutPatterns.stackArtifacts.map(stack => { if (minimatch(stack.hierarchicalId.toLowerCase(), pattern.toLowerCase())) { return stack.hierarchicalId; } - if (stack.id.toLowerCase() === pattern.toLowerCase() && semver.major(versionNumber()) < 2) { - return stack.id; - } return; }).filter((stack): stack is string => stack !== undefined) : []; return { From 17a925414ea9fcaca5798ff4bfc79df1cb345b6a Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:03:44 +0900 Subject: [PATCH 11/16] add message and colorize --- packages/aws-cdk/lib/cdk-toolkit.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 0ab1b663d00ec..875ecf757ace1 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -804,6 +804,7 @@ export class CdkToolkit { if (!options.force) { if (stacks.stackArtifacts.length === 0) { + warning(`No stacks match the name(s): ${chalk.red(options.selector.patterns.join(', '))}`); return; } // eslint-disable-next-line max-len @@ -1190,8 +1191,8 @@ export class CdkToolkit { patterns.forEach(pattern => { if (pattern.notExist) { - const closelyMatched = pattern.closelyMatched.length > 0 ? ` Do you mean ${pattern.closelyMatched.join(', ')}?` : ''; - warning(`${pattern.pattern} does not exist.${closelyMatched}`); + const closelyMatched = pattern.closelyMatched.length > 0 ? ` Do you mean ${chalk.blue(pattern.closelyMatched.join(', '))}?` : ''; + warning(`${chalk.red(pattern.pattern)} does not exist.${closelyMatched}`); } }); return stacks; From c55590a6dffbca9462fb611ec084d159d1badf1e Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:18:16 +0900 Subject: [PATCH 12/16] suggestStacks change order for calling methods foreach to for suggestStacks args to object --- packages/aws-cdk/lib/cdk-toolkit.ts | 46 +++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 875ecf757ace1..20add0295e3f2 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -797,16 +797,24 @@ export class CdkToolkit { } public async destroy(options: DestroyOptions) { - let stacks = await this.selectStacksForDestroy(options.selector, options.exclusively); + const assembly = await this.assembly(); + let stacks = await this.selectStacksForDestroy(options.selector, assembly, options.exclusively); + + await this.suggestStacks({ + selector: options.selector, + assembly, + stacks, + exclusively: options.exclusively, + }); + if (stacks.stackArtifacts.length === 0) { + warning(`No stacks match the name(s): ${chalk.red(options.selector.patterns.join(', '))}`); + return; + } // The stacks will have been ordered for deployment, so reverse them for deletion. stacks = stacks.reversed(); if (!options.force) { - if (stacks.stackArtifacts.length === 0) { - warning(`No stacks match the name(s): ${chalk.red(options.selector.patterns.join(', '))}`); - return; - } // eslint-disable-next-line max-len const confirmed = await promptly.confirm( `Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))} (y/n)?`, @@ -1154,25 +1162,33 @@ export class CdkToolkit { return selectedForDiff; } - private async selectStacksForDestroy(selector: StackSelector, exclusively?: boolean) { - const assembly = await this.assembly(); + private async selectStacksForDestroy(selector: StackSelector, assembly: CloudAssembly, exclusively?: boolean) { const stacks = await assembly.selectStacks(selector, { extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, defaultBehavior: DefaultSelection.OnlySingle, }); + return stacks; + } + + private async suggestStacks(props: { + selector: StackSelector; + assembly: CloudAssembly; + stacks: StackCollection; + exclusively?: boolean; + }) { const selectorWithoutPatterns: StackSelector = { - ...selector, + ...props.selector, allTopLevel: true, patterns: [], }; - const stacksWithoutPatterns = await assembly.selectStacks(selectorWithoutPatterns, { - extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, + const stacksWithoutPatterns = await props.assembly.selectStacks(selectorWithoutPatterns, { + extend: props.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, defaultBehavior: DefaultSelection.OnlySingle, }); - const patterns = selector.patterns.map(pattern => { - const notExist = !stacks.stackArtifacts.find(stack => + const patterns = props.selector.patterns.map(pattern => { + const notExist = !props.stacks.stackArtifacts.find(stack => minimatch(stack.hierarchicalId, pattern), ); @@ -1182,6 +1198,7 @@ export class CdkToolkit { } return; }).filter((stack): stack is string => stack !== undefined) : []; + return { pattern, notExist, @@ -1189,13 +1206,12 @@ export class CdkToolkit { }; }); - patterns.forEach(pattern => { + for (const pattern of patterns) { if (pattern.notExist) { const closelyMatched = pattern.closelyMatched.length > 0 ? ` Do you mean ${chalk.blue(pattern.closelyMatched.join(', '))}?` : ''; warning(`${chalk.red(pattern.pattern)} does not exist.${closelyMatched}`); } - }); - return stacks; + }; } /** From eef0c93d7f3f810de049fba9c609556047d166eb Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 24 Dec 2024 22:25:32 +0900 Subject: [PATCH 13/16] unit tests --- packages/aws-cdk/test/cdk-toolkit.test.ts | 79 +++++++++++++++-------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 38c8e7287febf..d807ed9c5c069 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -954,43 +954,68 @@ describe('destroy', () => { }).resolves; }); - test('does not throw even if there is a non-existent stack and the other exists', async () => { + test('does not throw and warns if there are only non-existent stacks', async () => { const toolkit = defaultToolkitSetup(); - await expect(() => { - return toolkit.destroy({ - selector: { patterns: ['Test-Stack-X', 'Test-Stack-A/Test-Stack-C'] }, - exclusively: true, - force: true, - fromDeploy: true, - }); - }).resolves; + await toolkit.destroy({ + selector: { patterns: ['Test-Stack-X', 'Test-Stack-Y'] }, + exclusively: true, + force: true, + fromDeploy: true, + }); + + expect(flatten(stderrMock.mock.calls)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/Test-Stack-X does not exist./), + expect.stringMatching(/Test-Stack-Y does not exist./), + expect.stringMatching(/No stacks match the name\(s\): Test-Stack-X, Test-Stack-Y/), + ]), + ); }); - test('does not throw even if there are only non-existent stacks', async () => { + test('does not throw and warns if there is a non-existent stack and the other exists', async () => { const toolkit = defaultToolkitSetup(); - await expect(() => { - return toolkit.destroy({ - selector: { patterns: ['Test-Stack-X', 'Test-Stack-Y'] }, - exclusively: true, - force: true, - fromDeploy: true, - }); - }).resolves; + await toolkit.destroy({ + selector: { patterns: ['Test-Stack-X', 'Test-Stack-B'] }, + exclusively: true, + force: true, + fromDeploy: true, + }); + + expect(flatten(stderrMock.mock.calls)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/Test-Stack-X does not exist./), + ]), + ); + expect(flatten(stderrMock.mock.calls)).not.toEqual( + expect.arrayContaining([ + expect.stringMatching(/Test-Stack-B does not exist./), + ]), + ); + expect(flatten(stderrMock.mock.calls)).not.toEqual( + expect.arrayContaining([ + expect.stringMatching(/No stacks match the name\(s\)/), + ]), + ); }); - test('exits without prompt if there are only non-existent stacks and force option is false', async () => { + test('does not throw and suggests valid names if there is a non-existent but closely matching stack', async () => { const toolkit = defaultToolkitSetup(); - await expect(() => { - return toolkit.destroy({ - selector: { patterns: ['Test-Stack-X', 'Test-Stack-Y'] }, - exclusively: true, - force: false, - fromDeploy: true, - }); - }).resolves; + await toolkit.destroy({ + selector: { patterns: ['test-stack-b'] }, + exclusively: true, + force: true, + fromDeploy: true, + }); + + expect(flatten(stderrMock.mock.calls)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/test-stack-b does not exist. Do you mean Test-Stack-B?/), + expect.stringMatching(/No stacks match the name\(s\): test-stack-b/), + ]), + ); }); }); From 122bfc0e12ac66c88956f8d690fe7c593318dc0d Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 24 Dec 2024 22:52:24 +0900 Subject: [PATCH 14/16] test name --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0f5ec880eefa9..acdfd736650fd 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 @@ -2582,7 +2582,7 @@ integTest('cdk destroy does not fail even if the stacks do not exist', withDefau await expect(fixture.cdkDestroy([nonExistingStackName1, nonExistingStackName2])).resolves.not.toThrow(); })); -integTest('cdk destroy with no force option exits without prompt if the stacks do not exist', withDefaultFixture(async (fixture) => { +integTest('cdk destroy without force option exits without prompt if the stacks do not exist', withDefaultFixture(async (fixture) => { const nonExistingStackName1 = 'non-existing-stack-1'; const nonExistingStackName2 = 'non-existing-stack-2'; From ad25a9600f0ec5f4a1b7040e39dde004324f121f Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 24 Dec 2024 22:59:07 +0900 Subject: [PATCH 15/16] test name --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 acdfd736650fd..0f5ec880eefa9 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 @@ -2582,7 +2582,7 @@ integTest('cdk destroy does not fail even if the stacks do not exist', withDefau await expect(fixture.cdkDestroy([nonExistingStackName1, nonExistingStackName2])).resolves.not.toThrow(); })); -integTest('cdk destroy without force option exits without prompt if the stacks do not exist', withDefaultFixture(async (fixture) => { +integTest('cdk destroy with no force option exits without prompt if the stacks do not exist', withDefaultFixture(async (fixture) => { const nonExistingStackName1 = 'non-existing-stack-1'; const nonExistingStackName2 = 'non-existing-stack-2'; From bc15bba8719f8939de29bdd1dd117a7f15aa7a26 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Fri, 3 Jan 2025 22:33:29 +0900 Subject: [PATCH 16/16] review --- packages/aws-cdk/lib/cdk-toolkit.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 20add0295e3f2..6a4a9ae8ba30d 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -797,12 +797,10 @@ export class CdkToolkit { } public async destroy(options: DestroyOptions) { - const assembly = await this.assembly(); - let stacks = await this.selectStacksForDestroy(options.selector, assembly, options.exclusively); + let stacks = await this.selectStacksForDestroy(options.selector, options.exclusively); await this.suggestStacks({ selector: options.selector, - assembly, stacks, exclusively: options.exclusively, }); @@ -1162,27 +1160,30 @@ export class CdkToolkit { return selectedForDiff; } - private async selectStacksForDestroy(selector: StackSelector, assembly: CloudAssembly, exclusively?: boolean) { + private async selectStacksForDestroy(selector: StackSelector, exclusively?: boolean) { + const assembly = await this.assembly(); const stacks = await assembly.selectStacks(selector, { extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, defaultBehavior: DefaultSelection.OnlySingle, }); + // No validation + return stacks; } private async suggestStacks(props: { selector: StackSelector; - assembly: CloudAssembly; stacks: StackCollection; exclusively?: boolean; }) { + const assembly = await this.assembly(); const selectorWithoutPatterns: StackSelector = { ...props.selector, allTopLevel: true, patterns: [], }; - const stacksWithoutPatterns = await props.assembly.selectStacks(selectorWithoutPatterns, { + const stacksWithoutPatterns = await assembly.selectStacks(selectorWithoutPatterns, { extend: props.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, defaultBehavior: DefaultSelection.OnlySingle, });