Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,10 @@ switch (stackSet) {
case 'stage-with-no-stacks':
break;

case 'stage-only':
new SomeStage(app, `${stackPrefix}-stage`);
break;

default:
throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { integTest, withDefaultFixture } from '../../../lib';

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();
}));

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.cdkDestroy([nonExistingStackName1, nonExistingStackName2], {
force: false,
})).resolves.not.toThrow();
}));

integTest('cdk destroy does not fail even if the stages do not exist', withDefaultFixture(async (fixture) => {
await expect(fixture.cdkDestroy('NonExistent/*')).resolves.not.toThrow();
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
import { integTest, withDefaultFixture } from '../../../lib';

integTest('cdk destroy can destroy stacks in stage-only configuration', withDefaultFixture(async (fixture) => {
const integStackSet = 'stage-only';

const stageNameSuffix = 'stage';
const specifiedStackName = `${stageNameSuffix}/*`;

await fixture.cdkDeploy(specifiedStackName, {
modEnv: {
INTEG_STACK_SET: integStackSet,
},
});

const stackName = `${fixture.fullStackName(stageNameSuffix)}-StackInStage`;
const stack = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName }));
expect(stack.Stacks?.length ?? 0).toEqual(1);

await fixture.cdkDestroy(specifiedStackName, {
modEnv: {
INTEG_STACK_SET: integStackSet,
},
});

await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName })))
.rejects.toThrow(/does not exist/);
}));
57 changes: 57 additions & 0 deletions packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PermissionChangeType, Toolkit, ToolkitError } from '@aws-cdk/toolkit-li
import * as chalk from 'chalk';
import * as chokidar from 'chokidar';
import * as fs from 'fs-extra';
import { minimatch } from 'minimatch';
import * as uuid from 'uuid';
import { CliIoHost } from './io-host';
import type { Configuration } from './user-configuration';
Expand Down Expand Up @@ -972,6 +973,17 @@ export class CdkToolkit {
// The stacks will have been ordered for deployment, so reverse them for deletion.
const stacks = (await this.selectStacksForDestroy(options.selector, options.exclusively)).reversed();

await this.suggestStacks({
selector: options.selector,
stacks,
exclusively: options.exclusively,
});

if (stacks.stackArtifacts.length === 0) {
await this.ioHost.asIoHelper().defaults.warn(`No stacks match the name(s): ${chalk.red(options.selector.patterns.join(', '))}`);
return;
}

if (!options.force) {
const motivation = 'Destroying stacks is an irreversible action';
const question = `Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))}`;
Expand Down Expand Up @@ -1373,6 +1385,51 @@ export class CdkToolkit {
return stacks;
}

private async suggestStacks(props: {
selector: StackSelector;
stacks: StackCollection;
exclusively: boolean;
}) {
if (props.selector.patterns.length === 0) {
return;
}

const assembly = await this.assembly();
const selectorWithoutPatterns: StackSelector = {
patterns: [],
};
const stacksWithoutPatterns = await assembly.selectStacks(selectorWithoutPatterns, {
extend: props.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream,
defaultBehavior: DefaultSelection.AllStacks,
});

const patterns = props.selector.patterns.map(pattern => {
const notExist = !props.stacks.stackArtifacts.find(stack =>
minimatch(stack.hierarchicalId, pattern),
);

const closelyMatched = notExist ? stacksWithoutPatterns.stackArtifacts.map(stack => {
if (minimatch(stack.hierarchicalId.toLowerCase(), pattern.toLowerCase())) {
return stack.hierarchicalId;
}
return;
}).filter((stack): stack is string => stack !== undefined) : [];

return {
pattern,
notExist,
closelyMatched,
};
});

for (const pattern of patterns) {
if (pattern.notExist) {
const closelyMatched = pattern.closelyMatched.length > 0 ? ` Do you mean ${chalk.blue(pattern.closelyMatched.join(', '))}?` : '';
await this.ioHost.asIoHelper().defaults.warn(`${chalk.red(pattern.pattern)} does not exist.${closelyMatched}`);
}
}
}

/**
* Validate the stacks for errors and warnings according to the CLI's current settings
*/
Expand Down
Loading
Loading