Skip to content

Commit 2679959

Browse files
otaviomacedomrgrain
authored andcommitted
refactor: filter stacks before getting their templates (#960)
The `refactor` command only operates on the stacks that are relevant for the target CDK application. However, it gets all the templates of the deployed stacks before filtering them, which is wasteful. Invert the order, so that the filtering happens before getting the templates. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent f979da7 commit 2679959

6 files changed

Lines changed: 372 additions & 10 deletions

File tree

packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,10 @@ switch (stackSet) {
11841184
case 'stage-with-no-stacks':
11851185
break;
11861186

1187+
case 'stage-only':
1188+
new SomeStage(app, `${stackPrefix}-stage`);
1189+
break;
1190+
11871191
default:
11881192
throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`);
11891193
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { integTest, withDefaultFixture } from '../../../lib';
2+
3+
integTest('cdk destroy does not fail even if the stacks do not exist', withDefaultFixture(async (fixture) => {
4+
const nonExistingStackName1 = 'non-existing-stack-1';
5+
const nonExistingStackName2 = 'non-existing-stack-2';
6+
7+
await expect(fixture.cdkDestroy([nonExistingStackName1, nonExistingStackName2])).resolves.not.toThrow();
8+
}));
9+
10+
integTest('cdk destroy with no force option exits without prompt if the stacks do not exist', withDefaultFixture(async (fixture) => {
11+
const nonExistingStackName1 = 'non-existing-stack-1';
12+
const nonExistingStackName2 = 'non-existing-stack-2';
13+
14+
await expect(fixture.cdkDestroy([nonExistingStackName1, nonExistingStackName2], {
15+
force: false,
16+
})).resolves.not.toThrow();
17+
}));
18+
19+
integTest('cdk destroy does not fail even if the stages do not exist', withDefaultFixture(async (fixture) => {
20+
await expect(fixture.cdkDestroy('NonExistent/*')).resolves.not.toThrow();
21+
}));
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
2+
import { integTest, withDefaultFixture } from '../../../lib';
3+
4+
integTest('cdk destroy can destroy stacks in stage-only configuration', withDefaultFixture(async (fixture) => {
5+
const integStackSet = 'stage-only';
6+
7+
const stageNameSuffix = 'stage';
8+
const specifiedStackName = `${stageNameSuffix}/*`;
9+
10+
await fixture.cdkDeploy(specifiedStackName, {
11+
modEnv: {
12+
INTEG_STACK_SET: integStackSet,
13+
},
14+
});
15+
16+
const stackName = `${fixture.fullStackName(stageNameSuffix)}-StackInStage`;
17+
const stack = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName }));
18+
expect(stack.Stacks?.length ?? 0).toEqual(1);
19+
20+
await fixture.cdkDestroy(specifiedStackName, {
21+
modEnv: {
22+
INTEG_STACK_SET: integStackSet,
23+
},
24+
});
25+
26+
await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName })))
27+
.rejects.toThrow(/does not exist/);
28+
}));

packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Toolkit } from '../../lib/toolkit';
1111
import { cdkOutFixture, disposableCloudAssemblySource, TestIoHost } from '../_helpers';
1212
import { mockCloudFormationClient, MockSdk, mockSSMClient, restoreSdkMocksToDefault, setDefaultSTSMocks } from '../_helpers/mock-sdk';
1313

14-
jest.setTimeout(20_000);
14+
jest.setTimeout(60_000);
1515

1616
let ioHost: TestIoHost;
1717
let toolkit: Toolkit;

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as chalk from 'chalk';
1010
import * as chokidar from 'chokidar';
1111
import { type EventName, EVENTS } from 'chokidar/handler.js';
1212
import * as fs from 'fs-extra';
13+
import * as picomatch from 'picomatch';
1314
import { CliIoHost } from './io-host';
1415
import type { Configuration } from './user-configuration';
1516
import { PROJECT_CONFIG } from './user-configuration';
@@ -917,6 +918,17 @@ export class CdkToolkit {
917918

918919
const stacks = await this.selectStacksForDestroy(options.selector, options.exclusively);
919920

921+
await this.suggestStacks({
922+
selector: options.selector,
923+
stacks,
924+
exclusively: options.exclusively,
925+
});
926+
927+
if (stacks.stackArtifacts.length === 0) {
928+
await this.ioHost.asIoHelper().defaults.warn(`No stacks match the name(s): ${chalk.red(options.selector.patterns.join(', '))}`);
929+
return;
930+
}
931+
920932
if (!options.force) {
921933
const motivation = 'Destroying stacks is an irreversible action';
922934
const question = `Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))}`;
@@ -1319,6 +1331,51 @@ export class CdkToolkit {
13191331
return stacks;
13201332
}
13211333

1334+
private async suggestStacks(props: {
1335+
selector: StackSelector;
1336+
stacks: StackCollection;
1337+
exclusively: boolean;
1338+
}) {
1339+
if (props.selector.patterns.length === 0) {
1340+
return;
1341+
}
1342+
1343+
const assembly = await this.assembly();
1344+
const selectorWithoutPatterns: StackSelector = {
1345+
patterns: [],
1346+
};
1347+
const stacksWithoutPatterns = await assembly.selectStacks(selectorWithoutPatterns, {
1348+
extend: props.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream,
1349+
defaultBehavior: DefaultSelection.AllStacks,
1350+
});
1351+
1352+
const patterns = props.selector.patterns.map(pattern => {
1353+
const notExist = !props.stacks.stackArtifacts.find(stack =>
1354+
picomatch.isMatch(stack.hierarchicalId, pattern),
1355+
);
1356+
1357+
const closelyMatched = notExist ? stacksWithoutPatterns.stackArtifacts.map(stack => {
1358+
if (picomatch.isMatch(stack.hierarchicalId.toLowerCase(), pattern.toLowerCase())) {
1359+
return stack.hierarchicalId;
1360+
}
1361+
return;
1362+
}).filter((stack): stack is string => stack !== undefined) : [];
1363+
1364+
return {
1365+
pattern,
1366+
notExist,
1367+
closelyMatched,
1368+
};
1369+
});
1370+
1371+
for (const pattern of patterns) {
1372+
if (pattern.notExist) {
1373+
const closelyMatched = pattern.closelyMatched.length > 0 ? ` Do you mean ${chalk.blue(pattern.closelyMatched.join(', '))}?` : '';
1374+
await this.ioHost.asIoHelper().defaults.warn(`${chalk.red(pattern.pattern)} does not exist.${closelyMatched}`);
1375+
}
1376+
}
1377+
}
1378+
13221379
/**
13231380
* Validate the stacks for errors and warnings according to the CLI's current settings
13241381
*/

0 commit comments

Comments
 (0)