Skip to content

Commit c199378

Browse files
authored
feat(cli): warn of non-existent stacks in cdk destroy (#32636)
### Issue # (if applicable) Closes #32545. Fixes #27179. ### Reason for this change Once this [PR](#27921) was reverted by other cli-integ test regression. - revert PR: #29577 - the test for regression: https://github.com/aws/aws-cdk/blob/07ce8ecc42782475d099b89944571375341c28d3/packages/%40aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts#L190 But the regression was apparently due to a cause unrelated to that PR. That has been corrected in [this PR](#31107) (see: https://github.com/aws/aws-cdk/blob/v2.173.1/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts#L286-L291). Therefore, I submit the same PR again. ### Description of changes This PR for cli is to warn if stacks with wrong cases (=not exist) specified in `cdk destroy`. \* It does not display the message `Are you sure you want to delete:` if there is no matching stack. \* Even if the stack does not exist, `cdk destroy` will not fail, it will just print a warning. Actual Outputs: ![cdk-destroy-warn](https://github.com/user-attachments/assets/c0d70037-c863-4c78-bc22-8b51264393ac) ### Describe any new or updated permissions being added <!— What new or updated IAM permissions are needed to support the changes being introduced ? --> Nothing. ### Description of how you validated changes Both of unit tests and cli-integ tests. The changes were already approved in the last PR. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 2abc23c commit c199378

File tree

3 files changed

+132
-0
lines changed

3 files changed

+132
-0
lines changed

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

+14
Original file line numberDiff line numberDiff line change
@@ -2634,6 +2634,20 @@ integTest('hotswap ECS deployment respects properties override', withDefaultFixt
26342634
expect(describeServicesResponse.services?.[0].deploymentConfiguration?.maximumPercent).toEqual(ecsMaximumHealthyPercent);
26352635
}));
26362636

2637+
integTest('cdk destroy does not fail even if the stacks do not exist', withDefaultFixture(async (fixture) => {
2638+
const nonExistingStackName1 = 'non-existing-stack-1';
2639+
const nonExistingStackName2 = 'non-existing-stack-2';
2640+
2641+
await expect(fixture.cdkDestroy([nonExistingStackName1, nonExistingStackName2])).resolves.not.toThrow();
2642+
}));
2643+
2644+
integTest('cdk destroy with no force option exits without prompt if the stacks do not exist', withDefaultFixture(async (fixture) => {
2645+
const nonExistingStackName1 = 'non-existing-stack-1';
2646+
const nonExistingStackName2 = 'non-existing-stack-2';
2647+
2648+
await expect(fixture.cdk(['destroy', ...fixture.fullStackName([nonExistingStackName1, nonExistingStackName2])])).resolves.not.toThrow();
2649+
}));
2650+
26372651
async function listChildren(parent: string, pred: (x: string) => Promise<boolean>) {
26382652
const ret = new Array<string>();
26392653
for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) {

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

+54
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api';
44
import * as chalk from 'chalk';
55
import * as chokidar from 'chokidar';
66
import * as fs from 'fs-extra';
7+
import { minimatch } from 'minimatch';
78
import * as promptly from 'promptly';
89
import * as uuid from 'uuid';
910
import { DeploymentMethod, SuccessfulDeployStackResult } from './api';
@@ -799,6 +800,16 @@ export class CdkToolkit {
799800
public async destroy(options: DestroyOptions) {
800801
let stacks = await this.selectStacksForDestroy(options.selector, options.exclusively);
801802

803+
await this.suggestStacks({
804+
selector: options.selector,
805+
stacks,
806+
exclusively: options.exclusively,
807+
});
808+
if (stacks.stackArtifacts.length === 0) {
809+
warning(`No stacks match the name(s): ${chalk.red(options.selector.patterns.join(', '))}`);
810+
return;
811+
}
812+
802813
// The stacks will have been ordered for deployment, so reverse them for deletion.
803814
stacks = stacks.reversed();
804815

@@ -1162,6 +1173,49 @@ export class CdkToolkit {
11621173
return stacks;
11631174
}
11641175

1176+
private async suggestStacks(props: {
1177+
selector: StackSelector;
1178+
stacks: StackCollection;
1179+
exclusively?: boolean;
1180+
}) {
1181+
const assembly = await this.assembly();
1182+
const selectorWithoutPatterns: StackSelector = {
1183+
...props.selector,
1184+
allTopLevel: true,
1185+
patterns: [],
1186+
};
1187+
const stacksWithoutPatterns = await assembly.selectStacks(selectorWithoutPatterns, {
1188+
extend: props.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream,
1189+
defaultBehavior: DefaultSelection.OnlySingle,
1190+
});
1191+
1192+
const patterns = props.selector.patterns.map(pattern => {
1193+
const notExist = !props.stacks.stackArtifacts.find(stack =>
1194+
minimatch(stack.hierarchicalId, pattern),
1195+
);
1196+
1197+
const closelyMatched = notExist ? stacksWithoutPatterns.stackArtifacts.map(stack => {
1198+
if (minimatch(stack.hierarchicalId.toLowerCase(), pattern.toLowerCase())) {
1199+
return stack.hierarchicalId;
1200+
}
1201+
return;
1202+
}).filter((stack): stack is string => stack !== undefined) : [];
1203+
1204+
return {
1205+
pattern,
1206+
notExist,
1207+
closelyMatched,
1208+
};
1209+
});
1210+
1211+
for (const pattern of patterns) {
1212+
if (pattern.notExist) {
1213+
const closelyMatched = pattern.closelyMatched.length > 0 ? ` Do you mean ${chalk.blue(pattern.closelyMatched.join(', '))}?` : '';
1214+
warning(`${chalk.red(pattern.pattern)} does not exist.${closelyMatched}`);
1215+
}
1216+
};
1217+
}
1218+
11651219
/**
11661220
* Validate the stacks for errors and warnings according to the CLI's current settings
11671221
*/

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

+64
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,70 @@ describe('destroy', () => {
953953
});
954954
}).resolves;
955955
});
956+
957+
test('does not throw and warns if there are only non-existent stacks', async () => {
958+
const toolkit = defaultToolkitSetup();
959+
960+
await toolkit.destroy({
961+
selector: { patterns: ['Test-Stack-X', 'Test-Stack-Y'] },
962+
exclusively: true,
963+
force: true,
964+
fromDeploy: true,
965+
});
966+
967+
expect(flatten(stderrMock.mock.calls)).toEqual(
968+
expect.arrayContaining([
969+
expect.stringMatching(/Test-Stack-X does not exist./),
970+
expect.stringMatching(/Test-Stack-Y does not exist./),
971+
expect.stringMatching(/No stacks match the name\(s\): Test-Stack-X, Test-Stack-Y/),
972+
]),
973+
);
974+
});
975+
976+
test('does not throw and warns if there is a non-existent stack and the other exists', async () => {
977+
const toolkit = defaultToolkitSetup();
978+
979+
await toolkit.destroy({
980+
selector: { patterns: ['Test-Stack-X', 'Test-Stack-B'] },
981+
exclusively: true,
982+
force: true,
983+
fromDeploy: true,
984+
});
985+
986+
expect(flatten(stderrMock.mock.calls)).toEqual(
987+
expect.arrayContaining([
988+
expect.stringMatching(/Test-Stack-X does not exist./),
989+
]),
990+
);
991+
expect(flatten(stderrMock.mock.calls)).not.toEqual(
992+
expect.arrayContaining([
993+
expect.stringMatching(/Test-Stack-B does not exist./),
994+
]),
995+
);
996+
expect(flatten(stderrMock.mock.calls)).not.toEqual(
997+
expect.arrayContaining([
998+
expect.stringMatching(/No stacks match the name\(s\)/),
999+
]),
1000+
);
1001+
});
1002+
1003+
test('does not throw and suggests valid names if there is a non-existent but closely matching stack', async () => {
1004+
const toolkit = defaultToolkitSetup();
1005+
1006+
await toolkit.destroy({
1007+
selector: { patterns: ['test-stack-b'] },
1008+
exclusively: true,
1009+
force: true,
1010+
fromDeploy: true,
1011+
});
1012+
1013+
expect(flatten(stderrMock.mock.calls)).toEqual(
1014+
expect.arrayContaining([
1015+
expect.stringMatching(/test-stack-b does not exist. Do you mean Test-Stack-B?/),
1016+
expect.stringMatching(/No stacks match the name\(s\): test-stack-b/),
1017+
]),
1018+
);
1019+
});
9561020
});
9571021

9581022
describe('watch', () => {

0 commit comments

Comments
 (0)