Skip to content

Commit d2505cd

Browse files
otaviomacedokaizenccmrgrain
authored
feat(refactor): support for stack filtering (#410)
Users can now pass the stacks they want to be considered for refactoring: cdk refactor StackA StackB ... The result will include any mapping that contains at least one of the selected stacks. If no pattern is passed, all stacks are considered. In this PR I also included the `refactor` command in the command line parsing list, which was missing. It was already working without it, but the command wouldn't show up in the help page, and it wouldn't recognize positional arguments. But the command itself (and its options) could be already used. Closes #363. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Co-authored-by: Momo Kornher <kornherm@amazon.co.uk>
1 parent db9bf88 commit d2505cd

File tree

11 files changed

+276
-37
lines changed

11 files changed

+276
-37
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/refactoring/index.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class AmbiguityError extends Error {
3939
* merely the stack name.
4040
*/
4141
export class ResourceLocation {
42-
constructor(readonly stack: CloudFormationStack, readonly logicalResourceId: string) {
42+
constructor(public readonly stack: CloudFormationStack, public readonly logicalResourceId: string) {
4343
}
4444

4545
public toPath(): string {
@@ -118,10 +118,20 @@ export function ambiguousMovements(movements: ResourceMovement[]) {
118118
* Converts a list of unambiguous resource movements into a list of resource mappings.
119119
*
120120
*/
121-
export function resourceMappings(movements: ResourceMovement[]): ResourceMapping[] {
121+
export function resourceMappings(movements: ResourceMovement[], stacks?: CloudFormationStack[]): ResourceMapping[] {
122+
const predicate = stacks == null
123+
? () => true
124+
: (m: ResourceMapping) => {
125+
// Any movement that involves one of the selected stacks (either moving from or to)
126+
// is considered a candidate for refactoring.
127+
const stackNames = [m.source.stack.stackName, m.destination.stack.stackName];
128+
return stacks.some((stack) => stackNames.includes(stack.stackName));
129+
};
130+
122131
return movements
123132
.filter(([pre, post]) => pre.length === 1 && post.length === 1 && !pre[0].equalTo(post[0]))
124-
.map(([pre, post]) => new ResourceMapping(pre[0], post[0]));
133+
.map(([pre, post]) => new ResourceMapping(pre[0], post[0]))
134+
.filter(predicate);
125135
}
126136

127137
function removeUnmovedResources(

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -979,19 +979,13 @@ export class Toolkit extends CloudAssemblySourceBuilder {
979979
throw new ToolkitError('Refactor is not available yet. Too see the proposed changes, use the --dry-run flag.');
980980
}
981981

982-
const strategy = options.stacks?.strategy ?? StackSelectionStrategy.ALL_STACKS;
983-
if (strategy !== StackSelectionStrategy.ALL_STACKS) {
984-
await ioHelper.notify(IO.CDK_TOOLKIT_W8010.msg(
985-
'Refactor does not yet support stack selection. Proceeding with the default behavior (considering all stacks).',
986-
));
987-
}
988982
const stacks = await assembly.selectStacksV2(ALL_STACKS);
989-
990983
const sdkProvider = await this.sdkProvider('refactor');
991984
const movements = await findResourceMovements(stacks.stackArtifacts, sdkProvider);
992985
const ambiguous = ambiguousMovements(movements);
993986
if (ambiguous.length === 0) {
994-
const typedMappings = resourceMappings(movements).map(m => m.toTypedMapping());
987+
const filteredStacks = await assembly.selectStacksV2(options.stacks ?? ALL_STACKS);
988+
const typedMappings = resourceMappings(movements, filteredStacks.stackArtifacts).map(m => m.toTypedMapping());
995989
await ioHelper.notify(IO.CDK_TOOLKIT_I8900.msg(formatTypedMappings(typedMappings), {
996990
typedMappings,
997991
}));

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

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ test('fails when dry-run is false', async () => {
156156
).rejects.toThrow('Refactor is not available yet. Too see the proposed changes, use the --dry-run flag.');
157157
});
158158

159-
test('warns when stack selector is passed', async () => {
159+
test('filters stacks when stack selector is passed', async () => {
160160
// GIVEN
161161
mockCloudFormationClient.on(ListStacksCommand).resolves({
162162
StackSummaries: [
@@ -166,6 +166,12 @@ test('warns when stack selector is passed', async () => {
166166
StackStatus: 'CREATE_COMPLETE',
167167
CreationTime: new Date(),
168168
},
169+
{
170+
StackName: 'Stack2',
171+
StackId: 'arn:aws:cloudformation:us-east-1:999999999999:stack/Stack2',
172+
StackStatus: 'CREATE_COMPLETE',
173+
CreationTime: new Date(),
174+
},
169175
],
170176
});
171177

@@ -176,20 +182,37 @@ test('warns when stack selector is passed', async () => {
176182
.resolves({
177183
TemplateBody: JSON.stringify({
178184
Resources: {
179-
OldLogicalID: {
185+
OldBucketName: {
180186
Type: 'AWS::S3::Bucket',
181187
UpdateReplacePolicy: 'Retain',
182188
DeletionPolicy: 'Retain',
183189
Metadata: {
184-
'aws:cdk:path': 'Stack1/OldLogicalID/Resource',
190+
'aws:cdk:path': 'Stack1/OldBucketName/Resource',
191+
},
192+
},
193+
},
194+
}),
195+
})
196+
.on(GetTemplateCommand, {
197+
StackName: 'Stack2',
198+
})
199+
.resolves({
200+
TemplateBody: JSON.stringify({
201+
Resources: {
202+
OldQueueName: {
203+
Type: 'AWS::SQS::Queue',
204+
UpdateReplacePolicy: 'Delete',
205+
DeletionPolicy: 'Delete',
206+
Metadata: {
207+
'aws:cdk:path': 'Stack2/OldQueueName/Resource',
185208
},
186209
},
187210
},
188211
}),
189212
});
190213

191214
// WHEN
192-
const cx = await builderFixture(toolkit, 'stack-with-bucket');
215+
const cx = await builderFixture(toolkit, 'two-different-stacks');
193216
await toolkit.refactor(cx, {
194217
dryRun: true,
195218
stacks: {
@@ -198,13 +221,29 @@ test('warns when stack selector is passed', async () => {
198221
},
199222
});
200223

224+
// Resources were renamed in both stacks, but we are only including Stack1.
225+
// So expect to see only changes for Stack1.
201226
expect(ioHost.notifySpy).toHaveBeenCalledWith(
202227
expect.objectContaining({
203228
action: 'refactor',
204-
level: 'warn',
205-
code: 'CDK_TOOLKIT_W8010',
206-
message:
207-
'Refactor does not yet support stack selection. Proceeding with the default behavior (considering all stacks).',
229+
level: 'result',
230+
code: 'CDK_TOOLKIT_I8900',
231+
message: expect.stringMatching(/AWS::S3::Bucket.*Stack1\/OldBucketName\/Resource.*Stack1\/MyBucket\/Resource/),
232+
data: expect.objectContaining({
233+
typedMappings: [
234+
{
235+
sourcePath: 'Stack1/OldBucketName/Resource',
236+
destinationPath: 'Stack1/MyBucket/Resource',
237+
type: 'AWS::S3::Bucket',
238+
},
239+
],
240+
}),
241+
}),
242+
);
243+
244+
expect(ioHost.notifySpy).toHaveBeenCalledWith(
245+
expect.objectContaining({
246+
message: expect.not.stringMatching(/OldQueueName/),
208247
}),
209248
);
210249
});

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,15 +1223,20 @@ export class CdkToolkit {
12231223
return 1;
12241224
}
12251225

1226-
if (options.selector.patterns.length > 0) {
1227-
warning('Refactor does not yet support stack selection. Proceeding with the default behavior (considering all stacks).');
1228-
}
1229-
1226+
// Initially, we select all stacks to find all resource movements.
1227+
// Otherwise, we might miss some resources that are not in the selected stacks.
1228+
// Example: resource X was moved from Stack A to Stack B. If we only select Stack A,
1229+
// we will only see a deletion of resource X, but not the creation of resource X in Stack B.
12301230
const stacks = await this.selectStacksForList([]);
12311231
const movements = await findResourceMovements(stacks.stackArtifacts, this.props.sdkProvider);
12321232
const ambiguous = ambiguousMovements(movements);
1233+
12331234
if (ambiguous.length === 0) {
1234-
const typedMappings = resourceMappings(movements).map(m => m.toTypedMapping());
1235+
// Now we can filter the stacks to only include the ones that are relevant for the user.
1236+
const patterns = options.selector.allTopLevel ? [] : options.selector.patterns;
1237+
const filteredStacks = await this.selectStacksForList(patterns);
1238+
const selectedMappings = resourceMappings(movements, filteredStacks.stackArtifacts);
1239+
const typedMappings = selectedMappings.map(m => m.toTypedMapping());
12351240
formatTypedMappings(process.stdout, typedMappings);
12361241
} else {
12371242
const e = new AmbiguityError(ambiguous);

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,20 @@ export async function makeConfig(): Promise<CliConfig> {
399399
doctor: {
400400
description: 'Check your set-up for potential problems',
401401
},
402+
refactor: {
403+
description: 'Moves resources between stacks or within the same stack',
404+
arg: {
405+
name: 'STACKS',
406+
variadic: true,
407+
},
408+
options: {
409+
'dry-run': {
410+
type: 'boolean',
411+
desc: 'Do not perform any changes, just show what would be done',
412+
default: false,
413+
},
414+
},
415+
},
402416
},
403417
};
404418
}

packages/aws-cdk/lib/cli/convert-to-user-input.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ export function convertYargsToUserInput(args: any): UserInput {
251251
case 'doctor':
252252
commandOptions = {};
253253
break;
254+
255+
case 'refactor':
256+
commandOptions = {
257+
dryRun: args.dryRun,
258+
STACKS: args.STACKS,
259+
};
260+
break;
254261
}
255262
const userInput: UserInput = {
256263
command: args._[0],
@@ -432,6 +439,9 @@ export function convertConfigToUserInput(config: any): UserInput {
432439
browser: config.docs?.browser,
433440
};
434441
const doctorOptions = {};
442+
const refactorOptions = {
443+
dryRun: config.refactor?.dryRun,
444+
};
435445
const userInput: UserInput = {
436446
globalOptions,
437447
list: listOptions,
@@ -452,6 +462,7 @@ export function convertConfigToUserInput(config: any): UserInput {
452462
context: contextOptions,
453463
docs: docsOptions,
454464
doctor: doctorOptions,
465+
refactor: refactorOptions,
455466
};
456467

457468
return userInput;

packages/aws-cdk/lib/cli/parse-command-line-arguments.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,13 @@ export function parseCommandLineArguments(args: Array<string>): any {
840840
}),
841841
)
842842
.command('doctor', 'Check your set-up for potential problems')
843+
.command('refactor [STACKS..]', 'Moves resources between stacks or within the same stack', (yargs: Argv) =>
844+
yargs.option('dry-run', {
845+
default: false,
846+
type: 'boolean',
847+
desc: 'Do not perform any changes, just show what would be done',
848+
}),
849+
)
843850
.version(helpers.cliVersion())
844851
.demandCommand(1, '')
845852
.recommendCommands()

packages/aws-cdk/lib/cli/user-configuration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export enum Command {
3636
DOCS = 'docs',
3737
DOC = 'doc',
3838
DOCTOR = 'doctor',
39+
REFACTOR = 'refactor',
3940
}
4041

4142
const BUNDLING_COMMANDS = [

packages/aws-cdk/lib/cli/user-input.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ export interface UserInput {
118118
* Check your set-up for potential problems
119119
*/
120120
readonly doctor?: {};
121+
122+
/**
123+
* Moves resources between stacks or within the same stack
124+
*/
125+
readonly refactor?: RefactorOptions;
121126
}
122127

123128
/**
@@ -1334,3 +1339,22 @@ export interface DocsOptions {
13341339
*/
13351340
readonly browser?: string;
13361341
}
1342+
1343+
/**
1344+
* Moves resources between stacks or within the same stack
1345+
*
1346+
* @struct
1347+
*/
1348+
export interface RefactorOptions {
1349+
/**
1350+
* Do not perform any changes, just show what would be done
1351+
*
1352+
* @default - false
1353+
*/
1354+
readonly dryRun?: boolean;
1355+
1356+
/**
1357+
* Positional argument for refactor
1358+
*/
1359+
readonly STACKS?: Array<string>;
1360+
}

0 commit comments

Comments
 (0)