Skip to content

Commit aef751e

Browse files
authored
feat: refactor execution (#674)
This change introduces the creation and execution of stack refactors. It builds on previous work that put all the pieces in place (mapping computation, mapping file reading, exclude lists, dry-run etc). The following flowcharts summarize the behavior in interactive and non-interactive modes, for each environment in the application: #### Non-interactive case ```mermaid flowchart LR mapping{Refactor file present?} empty{Empty mapping?} dryrun{--dry-run?} compute[Compute mapping] use[Use mapping] print[Print mapping] mapping ---|No| compute mapping ---|Yes| use compute --- empty use --- empty empty ---|Yes| Exit empty ---|No| print print --- dryrun dryrun ---|Yes| Exit dryrun ---|No| Refactor Refactor --- Exit ``` #### Interactive case ```mermaid flowchart LR mapping{Refactor file present?} empty{Empty mapping?} dryrun{--dry-run?} force{--force?} compute[Compute mapping] use[Use mapping] print[Print mapping] ask[Ask user] mapping ---|No| compute mapping ---|Yes| use compute --- empty use --- empty empty ---|Yes| Exit empty ---|No| print print --- dryrun dryrun ---|Yes| Exit dryrun ---|No| force force ---|Yes| Refactor force ---|No| ask ask ---|Yes| Refactor ask ---|No| Exit Refactor --- Exit ``` Closes #140. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent a890d3f commit aef751e

32 files changed

+1419
-2146
lines changed

.projenrc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,7 @@ const toolkitLib = configureProject(
834834
'cdk-from-cfn',
835835
'chalk@^4',
836836
'chokidar@^3',
837+
'fast-deep-equal',
837838
'fs-extra@^9',
838839
'glob',
839840
'minimatch',
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
const cdk = require('aws-cdk-lib');
22
const sqs = require('aws-cdk-lib/aws-sqs');
3-
4-
class BasicStack extends cdk.Stack {
5-
constructor(parent, id, props) {
6-
super(parent, id, props);
7-
new sqs.Queue(this, props.queueName);
8-
}
9-
}
3+
const lambda = require('aws-cdk-lib/aws-lambda');
4+
const s3 = require('aws-cdk-lib/aws-s3');
105

116
const stackPrefix = process.env.STACK_NAME_PREFIX;
127
const app = new cdk.App();
138

14-
new BasicStack(app, `${stackPrefix}-basic`, {
15-
queueName: process.env.BASIC_QUEUE_LOGICAL_ID ?? 'BasicQueue',
9+
const mainStack = new cdk.Stack(app, `${stackPrefix}-basic`);
10+
new sqs.Queue(mainStack, process.env.BASIC_QUEUE_LOGICAL_ID ?? 'BasicQueue');
11+
12+
const bucketStack = new cdk.Stack(app, `${stackPrefix}-bucket-stack`);
13+
const dummy = new s3.Bucket(bucketStack, 'DummyBucket', {
14+
bucketName: `dummy-bucket-for-${stackPrefix}`
1615
});
1716

17+
// This part is to test adding a resource to an existing stack
18+
19+
if (process.env.ADDITIONAL_QUEUE_LOGICAL_ID) {
20+
new sqs.Queue(mainStack, process.env.ADDITIONAL_QUEUE_LOGICAL_ID);
21+
}
22+
23+
// This part is to test moving a resource to a separate stack
24+
const bucket = new s3.Bucket(process.env.BUCKET_IN_SEPARATE_STACK ? bucketStack : mainStack, 'Bucket');
25+
26+
new lambda.Function(mainStack, 'Func', {
27+
runtime: lambda.Runtime.NODEJS_22_X,
28+
code: lambda.Code.fromInline(`exports.handler = handler.toString()`),
29+
handler: 'index.handler',
30+
environment: {
31+
BUCKET: bucket.bucketName
32+
}
33+
});
34+
35+
36+
1837
app.synth();

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { integTest, withSpecificFixture } from '../../../lib';
22

33
integTest(
4-
'detects refactoring changes and prints the result',
4+
'cdk refactor - detects refactoring changes and prints the result',
55
withSpecificFixture('refactoring', async (fixture) => {
6-
// First, deploy a stack
6+
// First, deploy the stacks
7+
await fixture.cdkDeploy('bucket-stack');
78
await fixture.cdkDeploy('basic', {
89
modEnv: {
910
BASIC_QUEUE_LOGICAL_ID: 'OldName',
@@ -14,8 +15,8 @@ integTest(
1415
const stdErr = await fixture.cdkRefactor({
1516
options: ['--dry-run', '--unstable=refactor'],
1617
allowErrExit: true,
17-
// Making sure the synthesized stack has the new name
18-
// so that a refactor is detected
18+
// Making sure the synthesized stack has a queue with
19+
// the new name so that a refactor is detected
1920
modEnv: {
2021
BASIC_QUEUE_LOGICAL_ID: 'NewName',
2122
},
@@ -27,13 +28,14 @@ integTest(
2728
);
2829

2930
integTest(
30-
'no refactoring changes detected',
31+
'cdk refactor - no refactoring changes detected',
3132
withSpecificFixture('refactoring', async (fixture) => {
3233
const modEnv = {
3334
BASIC_QUEUE_LOGICAL_ID: 'OldName',
3435
};
3536

36-
// First, deploy a stack
37+
// First, deploy the stacks
38+
await fixture.cdkDeploy('bucket-stack');
3739
await fixture.cdkDeploy('basic', { modEnv });
3840

3941
// Then see if the refactoring tool detects the change
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { DescribeStackResourcesCommand, ListStacksCommand, type StackResource } from '@aws-sdk/client-cloudformation';
2+
import { integTest, withSpecificFixture } from '../../../lib';
3+
4+
integTest(
5+
'cdk refactor - moves a referenced resource to a different stack',
6+
withSpecificFixture('refactoring', async (fixture) => {
7+
// First, deploy the stacks
8+
await fixture.cdkDeploy('bucket-stack');
9+
const originalStackArn = await fixture.cdkDeploy('basic');
10+
const originalStackInfo = getStackInfoFromArn(originalStackArn);
11+
const stackPrefix = originalStackInfo.name.replace(/-basic$/, '');
12+
13+
// Then see if the refactoring tool detects the change
14+
const stdErr = await fixture.cdkRefactor({
15+
options: ['--unstable=refactor', '--force'],
16+
allowErrExit: true,
17+
// Making sure the synthesized stack has a queue with
18+
// the new name so that a refactor is detected
19+
modEnv: {
20+
BUCKET_IN_SEPARATE_STACK: 'true',
21+
},
22+
});
23+
24+
expect(stdErr).toMatch('Stack refactor complete');
25+
26+
const stacks = await fixture.aws.cloudFormation.send(new ListStacksCommand());
27+
28+
const bucketStack = (stacks.StackSummaries ?? []).find((s) => s.StackName === `${stackPrefix}-bucket-stack`);
29+
30+
expect(bucketStack).toBeDefined();
31+
32+
const stackDescription = await fixture.aws.cloudFormation.send(
33+
new DescribeStackResourcesCommand({
34+
StackName: bucketStack!.StackName,
35+
}),
36+
);
37+
38+
const resources = Object.fromEntries(
39+
(stackDescription.StackResources ?? []).map(
40+
(resource) => [resource.LogicalResourceId!, resource] as [string, StackResource],
41+
),
42+
);
43+
44+
expect(resources.Bucket83908E77).toBeDefined();
45+
46+
// CloudFormation may complete the refactoring, while the stack is still in the "UPDATE_IN_PROGRESS" state.
47+
// Give it a couple of seconds to finish the update.
48+
await new Promise((resolve) => setTimeout(resolve, 2000));
49+
}),
50+
);
51+
52+
interface StackInfo {
53+
readonly account: string;
54+
readonly region: string;
55+
readonly name: string;
56+
}
57+
58+
export function getStackInfoFromArn(stackArn: string): StackInfo {
59+
// Example ARN: arn:aws:cloudformation:region:account-id:stack/stack-name/guid
60+
const arnParts = stackArn.split(':');
61+
const resource = arnParts[5]; // "stack/stack-name/guid"
62+
const resourceParts = resource.split('/');
63+
// The stack name is the second part: ["stack", "stack-name", "guid"]
64+
return {
65+
region: arnParts[3],
66+
account: arnParts[4],
67+
name: resourceParts[1],
68+
};
69+
}
70+
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { DescribeStackResourcesCommand, type StackResource } from '@aws-sdk/client-cloudformation';
2+
import { integTest, withSpecificFixture } from '../../../lib';
3+
4+
integTest(
5+
'cdk refactor - detects refactoring changes and executes the refactor',
6+
withSpecificFixture('refactoring', async (fixture) => {
7+
// First, deploy the stacks
8+
await fixture.cdkDeploy('bucket-stack');
9+
const stackArn = await fixture.cdkDeploy('basic', {
10+
modEnv: {
11+
BASIC_QUEUE_LOGICAL_ID: 'OldName',
12+
},
13+
});
14+
15+
// Then see if the refactoring tool detects the change
16+
const stdErr = await fixture.cdkRefactor({
17+
options: ['--unstable=refactor', '--force'],
18+
allowErrExit: true,
19+
// Making sure the synthesized stack has a queue with
20+
// the new name so that a refactor is detected
21+
modEnv: {
22+
BASIC_QUEUE_LOGICAL_ID: 'NewName',
23+
},
24+
});
25+
26+
expect(stdErr).toMatch('Stack refactor complete');
27+
28+
const stackDescription = await fixture.aws.cloudFormation.send(
29+
new DescribeStackResourcesCommand({
30+
StackName: getStackNameFromArn(stackArn),
31+
}),
32+
);
33+
34+
const resources = Object.fromEntries(
35+
(stackDescription.StackResources ?? []).map(
36+
(resource) => [resource.LogicalResourceId!, resource] as [string, StackResource],
37+
),
38+
);
39+
40+
expect(resources.NewName57B171FE).toBeDefined();
41+
42+
// CloudFormation may complete the refactoring, while the stack is still in the "UPDATE_IN_PROGRESS" state.
43+
// Give it a couple of seconds to finish the update.
44+
await new Promise((resolve) => setTimeout(resolve, 2000));
45+
}),
46+
);
47+
48+
export function getStackNameFromArn(stackArn: string): string {
49+
// Example ARN: arn:aws:cloudformation:region:account-id:stack/stack-name/guid
50+
const arnParts = stackArn.split(':');
51+
const resource = arnParts[5]; // "stack/stack-name/guid"
52+
const resourceParts = resource.split('/');
53+
// The stack name is the second part: ["stack", "stack-name", "guid"]
54+
return resourceParts[1];
55+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as fs from 'node:fs';
2+
import * as os from 'node:os';
3+
import * as path from 'node:path';
4+
import type { StackResource } from '@aws-sdk/client-cloudformation';
5+
import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation';
6+
import { integTest, withSpecificFixture } from '../../../lib';
7+
8+
integTest(
9+
'cdk refactor - detects refactoring changes and executes the refactor, overriding ambiguities',
10+
withSpecificFixture('refactoring', async (fixture) => {
11+
// First, deploy the stacks
12+
await fixture.cdkDeploy('bucket-stack');
13+
const stackArn = await fixture.cdkDeploy('basic', {
14+
modEnv: {
15+
BASIC_QUEUE_LOGICAL_ID: 'OldName',
16+
ADDITIONAL_QUEUE_LOGICAL_ID: 'AdditionalOldName',
17+
},
18+
});
19+
20+
const stackInfo = getStackInfoFromArn(stackArn);
21+
const stackName = stackInfo.name;
22+
23+
const overrides = {
24+
environments: [
25+
{
26+
account: stackInfo.account,
27+
region: stackInfo.region,
28+
resources: {
29+
[`${stackName}/OldName/Resource`]: `${stackName}/NewName/Resource`,
30+
[`${stackName}/AdditionalOldName/Resource`]: `${stackName}/AdditionalNewName/Resource`,
31+
},
32+
},
33+
],
34+
};
35+
36+
const overridesPath = path.join(os.tmpdir(), `overrides-${Date.now()}.json`);
37+
fs.writeFileSync(overridesPath, JSON.stringify(overrides));
38+
39+
// Then see if the refactoring tool detects the change
40+
const stdErr = await fixture.cdkRefactor({
41+
options: ['--unstable=refactor', '--force', `--override-file=${overridesPath}`],
42+
allowErrExit: true,
43+
// Making sure the synthesized stack has a queue with
44+
// the new name so that a refactor is detected
45+
modEnv: {
46+
BASIC_QUEUE_LOGICAL_ID: 'NewName',
47+
ADDITIONAL_QUEUE_LOGICAL_ID: 'AdditionalNewName',
48+
},
49+
});
50+
51+
expect(stdErr).toMatch('Stack refactor complete');
52+
53+
const stackDescription = await fixture.aws.cloudFormation.send(
54+
new DescribeStackResourcesCommand({
55+
StackName: stackName,
56+
}),
57+
);
58+
59+
const resources = Object.fromEntries(
60+
(stackDescription.StackResources ?? []).map(
61+
(resource) => [resource.LogicalResourceId!, resource] as [string, StackResource],
62+
),
63+
);
64+
65+
expect(resources.AdditionalNewNameE2FC5A4C).toBeDefined();
66+
expect(resources.NewName57B171FE).toBeDefined();
67+
68+
// CloudFormation may complete the refactoring, while the stack is still in the "UPDATE_IN_PROGRESS" state.
69+
// Give it a couple of seconds to finish the update.
70+
await new Promise((resolve) => setTimeout(resolve, 2000));
71+
}),
72+
);
73+
74+
interface StackInfo {
75+
readonly account: string;
76+
readonly region: string;
77+
readonly name: string;
78+
}
79+
80+
export function getStackInfoFromArn(stackArn: string): StackInfo {
81+
// Example ARN: arn:aws:cloudformation:region:account-id:stack/stack-name/guid
82+
const arnParts = stackArn.split(':');
83+
const resource = arnParts[5]; // "stack/stack-name/guid"
84+
const resourceParts = resource.split('/');
85+
// The stack name is the second part: ["stack", "stack-name", "guid"]
86+
return {
87+
region: arnParts[3],
88+
account: arnParts[4],
89+
name: resourceParts[1],
90+
};
91+
}

packages/@aws-cdk/cloudformation-diff/lib/mappings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export function formatAmbiguousMappings(
3535
formatter.print('Detected ambiguities:');
3636
formatter.print(tables.join('\n\n'));
3737
formatter.print(' ');
38+
formatter.print(chalk.yellow('Please provide an override file to resolve these ambiguous mappings.'));
39+
formatter.print(' ');
3840

3941
function renderTable([removed, added]: [string[], string[]]) {
4042
return formatTable([['', 'Resource'], renderRemoval(removed), renderAddition(added)], undefined);

packages/@aws-cdk/toolkit-lib/.projen/deps.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/toolkit-lib/.projen/tasks.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/toolkit-lib/docs/message-registry.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ Please let us know by [opening an issue](https://github.com/aws/aws-cdk-cli/issu
127127
| `CDK_TOOLKIT_E7900` | Stack deletion failed | `error` | {@link ErrorPayload} |
128128
| `CDK_TOOLKIT_E8900` | Stack refactor failed | `error` | {@link ErrorPayload} |
129129
| `CDK_TOOLKIT_I8900` | Refactor result | `result` | {@link RefactorResult} |
130+
| `CDK_TOOLKIT_I8910` | Confirm refactor | `info` | {@link ConfirmationRequest} |
130131
| `CDK_TOOLKIT_W8010` | Refactor execution not yet supported | `warn` | n/a |
131132
| `CDK_TOOLKIT_I9000` | Provides bootstrap times | `info` | {@link Duration} |
132133
| `CDK_TOOLKIT_I9100` | Bootstrap progress | `info` | {@link BootstrapEnvironmentProgress} |

0 commit comments

Comments
 (0)