Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
225591b
feat!: modification is forbidden during a refactor
otaviomacedo Jun 27, 2025
88762a0
Merge branch 'main' into otaviom/forbid-modifications
otaviomacedo Jun 30, 2025
b2ad63d
feat: refactor execution
otaviomacedo Jul 1, 2025
9355e19
Better error message
otaviomacedo Jul 4, 2025
06c001f
Remove Rules and Parameters for new stacks
otaviomacedo Jul 4, 2025
97aae1d
Fix test
otaviomacedo Jul 4, 2025
71e2cf4
Don't send CDKMetadata if deployed doesn't have it
otaviomacedo Jul 4, 2025
4a76d0f
Merge branch 'main' into otaviom/isomorphic-refactor-execution
otaviomacedo Jul 16, 2025
e9f45d9
fixes after merge
otaviomacedo Jul 16, 2025
95d8daa
Improve ambiguity message
otaviomacedo Jul 16, 2025
66cc659
Overrides can be construct paths
otaviomacedo Jul 17, 2025
44a9b66
update test: new stack creation without CDKMetadata
otaviomacedo Jul 17, 2025
dfff897
Better message in case of modification
otaviomacedo Jul 18, 2025
d2bda95
More integ tests
otaviomacedo Jul 21, 2025
3341571
Using the deployment role or custom role
otaviomacedo Jul 25, 2025
834b5d1
Merge branch 'main' into otaviom/isomorphic-refactor-execution
otaviomacedo Aug 19, 2025
a709dca
Handling the case where the mappings contain stacks not present locally
otaviomacedo Aug 19, 2025
e8681ba
assumeRole -> assumeRoleArn
otaviomacedo Aug 21, 2025
bc6f519
Fix tests
otaviomacedo Aug 21, 2025
ed9f20f
Add stack refactor ID to unknown error
otaviomacedo Aug 29, 2025
a6a0e60
Deploy to finalize refactor
otaviomacedo Sep 1, 2025
fdf41ea
Admin access limitation
otaviomacedo Sep 2, 2025
ec8da7e
Admin access limitation
otaviomacedo Sep 2, 2025
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
1 change: 1 addition & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,7 @@ const toolkitLib = configureProject(
'cdk-from-cfn',
'chalk@^4',
'chokidar@^3',
'fast-deep-equal',
'fs-extra@^9',
'glob',
'minimatch',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
const cdk = require('aws-cdk-lib');
const sqs = require('aws-cdk-lib/aws-sqs');

class BasicStack extends cdk.Stack {
constructor(parent, id, props) {
super(parent, id, props);
new sqs.Queue(this, props.queueName);
}
}
const lambda = require('aws-cdk-lib/aws-lambda');
const s3 = require('aws-cdk-lib/aws-s3');

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

new BasicStack(app, `${stackPrefix}-basic`, {
queueName: process.env.BASIC_QUEUE_LOGICAL_ID ?? 'BasicQueue',
const stack = new cdk.Stack(app, `${stackPrefix}-basic`);
new sqs.Queue(stack, process.env.BASIC_QUEUE_LOGICAL_ID ?? 'BasicQueue');

if (process.env.ADDITIONAL_QUEUE_LOGICAL_ID) {
new sqs.Queue(stack, process.env.ADDITIONAL_QUEUE_LOGICAL_ID);
}

// This part is to test moving a resource to a separate stack
const bucketStack = process.env.BUCKET_IN_SEPARATE_STACK ? new cdk.Stack(app, `${stackPrefix}-bucket-stack`) : stack;
const bucket = new s3.Bucket(bucketStack, 'Bucket');

new lambda.Function(stack, 'Func', {
runtime: lambda.Runtime.NODEJS_22_X,
code: lambda.Code.fromInline(`exports.handler = handler.toString()`),
handler: 'index.handler',
environment: {
BUCKET: bucket.bucketName
}
});



app.synth();
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'detects refactoring changes and prints the result',
'cdk refactor - detects refactoring changes and prints the result',
withSpecificFixture('refactoring', async (fixture) => {
// First, deploy a stack
await fixture.cdkDeploy('basic', {
Expand All @@ -14,8 +14,8 @@ integTest(
const stdErr = await fixture.cdkRefactor({
options: ['--dry-run', '--unstable=refactor'],
allowErrExit: true,
// Making sure the synthesized stack has the new name
// so that a refactor is detected
// Making sure the synthesized stack has a queue with
// the new name so that a refactor is detected
modEnv: {
BASIC_QUEUE_LOGICAL_ID: 'NewName',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { DescribeStackResourcesCommand, ListStacksCommand, type StackResource } from '@aws-sdk/client-cloudformation';
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk refactor - moves a referenced resource to a different stack',
withSpecificFixture('refactoring', async (fixture) => {
// First, deploy a stack
const originalStackArn = await fixture.cdkDeploy('basic');
const originalStackInfo = getStackInfoFromArn(originalStackArn);
const stackPrefix = originalStackInfo.name.replace(/-basic$/, '');

// Then see if the refactoring tool detects the change
const stdErr = await fixture.cdkRefactor({
options: ['--unstable=refactor', '--force'],
allowErrExit: true,
// Making sure the synthesized stack has a queue with
// the new name so that a refactor is detected
modEnv: {
BUCKET_IN_SEPARATE_STACK: 'true',
},
});

expect(stdErr).toMatch('Stack refactor complete');

const stacks = await fixture.aws.cloudFormation.send(new ListStacksCommand());

const bucketStack = (stacks.StackSummaries ?? []).find((s) => s.StackName === `${stackPrefix}-bucket-stack`);

expect(bucketStack).toBeDefined();

const stackDescription = await fixture.aws.cloudFormation.send(
new DescribeStackResourcesCommand({
StackName: bucketStack!.StackName,
}),
);

const resources = Object.fromEntries(
(stackDescription.StackResources ?? []).map(
(resource) => [resource.LogicalResourceId!, resource] as [string, StackResource],
),
);

expect(resources.Bucket83908E77).toBeDefined();

// CloudFormation may complete the refactoring, while the stack is still in the "UPDATE_IN_PROGRESS" state.
// Give it a couple of seconds to finish the update.
await new Promise((resolve) => setTimeout(resolve, 2000));
}),
);

interface StackInfo {
readonly account: string;
readonly region: string;
readonly name: string;
}

export function getStackInfoFromArn(stackArn: string): StackInfo {
// Example ARN: arn:aws:cloudformation:region:account-id:stack/stack-name/guid
const arnParts = stackArn.split(':');
const resource = arnParts[5]; // "stack/stack-name/guid"
const resourceParts = resource.split('/');
// The stack name is the second part: ["stack", "stack-name", "guid"]
return {
region: arnParts[3],
account: arnParts[4],
name: resourceParts[1],
};
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { DescribeStackResourcesCommand, type StackResource } from '@aws-sdk/client-cloudformation';
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk refactor - detects refactoring changes and executes the refactor',
withSpecificFixture('refactoring', async (fixture) => {
// First, deploy a stack
const stackArn = await fixture.cdkDeploy('basic', {
modEnv: {
BASIC_QUEUE_LOGICAL_ID: 'OldName',
},
});

// Then see if the refactoring tool detects the change
const stdErr = await fixture.cdkRefactor({
options: ['--unstable=refactor', '--force'],
allowErrExit: true,
// Making sure the synthesized stack has a queue with
// the new name so that a refactor is detected
modEnv: {
BASIC_QUEUE_LOGICAL_ID: 'NewName',
},
});

expect(stdErr).toMatch('Stack refactor complete');

const stackDescription = await fixture.aws.cloudFormation.send(
new DescribeStackResourcesCommand({
StackName: getStackNameFromArn(stackArn),
}),
);

const resources = Object.fromEntries(
(stackDescription.StackResources ?? []).map(
(resource) => [resource.LogicalResourceId!, resource] as [string, StackResource],
),
);

expect(resources.NewName57B171FE).toBeDefined();

// CloudFormation may complete the refactoring, while the stack is still in the "UPDATE_IN_PROGRESS" state.
// Give it a couple of seconds to finish the update.
await new Promise((resolve) => setTimeout(resolve, 2000));
}),
);

export function getStackNameFromArn(stackArn: string): string {
// Example ARN: arn:aws:cloudformation:region:account-id:stack/stack-name/guid
const arnParts = stackArn.split(':');
const resource = arnParts[5]; // "stack/stack-name/guid"
const resourceParts = resource.split('/');
// The stack name is the second part: ["stack", "stack-name", "guid"]
return resourceParts[1];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import type { StackResource } from '@aws-sdk/client-cloudformation';
import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation';
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk refactor - detects refactoring changes and executes the refactor, overriding ambiguities',
withSpecificFixture('refactoring', async (fixture) => {
// First, deploy a stack
const stackArn = await fixture.cdkDeploy('basic', {
modEnv: {
BASIC_QUEUE_LOGICAL_ID: 'OldName',
ADDITIONAL_QUEUE_LOGICAL_ID: 'AdditionalOldName',
},
});

const stackInfo = getStackInfoFromArn(stackArn);
const stackName = stackInfo.name;

const overrides = {
environments: [
{
account: stackInfo.account,
region: stackInfo.region,
resources: {
[`${stackName}/OldName/Resource`]: `${stackName}/NewName/Resource`,
[`${stackName}/AdditionalOldName/Resource`]: `${stackName}/AdditionalNewName/Resource`,
},
},
],
};

const overridesPath = path.join(os.tmpdir(), `overrides-${Date.now()}.json`);
fs.writeFileSync(overridesPath, JSON.stringify(overrides));

// Then see if the refactoring tool detects the change
const stdErr = await fixture.cdkRefactor({
options: ['--unstable=refactor', '--force', `--override-file=${overridesPath}`],
allowErrExit: true,
// Making sure the synthesized stack has a queue with
// the new name so that a refactor is detected
modEnv: {
BASIC_QUEUE_LOGICAL_ID: 'NewName',
ADDITIONAL_QUEUE_LOGICAL_ID: 'AdditionalNewName',
},
});

expect(stdErr).toMatch('Stack refactor complete');

const stackDescription = await fixture.aws.cloudFormation.send(
new DescribeStackResourcesCommand({
StackName: stackName,
}),
);

const resources = Object.fromEntries(
(stackDescription.StackResources ?? []).map(
(resource) => [resource.LogicalResourceId!, resource] as [string, StackResource],
),
);

expect(resources.AdditionalNewNameE2FC5A4C).toBeDefined();
expect(resources.NewName57B171FE).toBeDefined();

// CloudFormation may complete the refactoring, while the stack is still in the "UPDATE_IN_PROGRESS" state.
// Give it a couple of seconds to finish the update.
await new Promise((resolve) => setTimeout(resolve, 2000));
}),
);

interface StackInfo {
readonly account: string;
readonly region: string;
readonly name: string;
}

export function getStackInfoFromArn(stackArn: string): StackInfo {
// Example ARN: arn:aws:cloudformation:region:account-id:stack/stack-name/guid
const arnParts = stackArn.split(':');
const resource = arnParts[5]; // "stack/stack-name/guid"
const resourceParts = resource.split('/');
// The stack name is the second part: ["stack", "stack-name", "guid"]
return {
region: arnParts[3],
account: arnParts[4],
name: resourceParts[1],
};
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/cloudformation-diff/lib/mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export function formatAmbiguousMappings(
formatter.print('Detected ambiguities:');
formatter.print(tables.join('\n\n'));
formatter.print(' ');
formatter.print(chalk.yellow('Please provide an override file to resolve these ambiguous mappings.'));
formatter.print(' ');

function renderTable([removed, added]: [string[], string[]]) {
return formatTable([['', 'Resource'], renderRemoval(removed), renderAddition(added)], undefined);
Expand Down
4 changes: 4 additions & 0 deletions packages/@aws-cdk/toolkit-lib/.projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/@aws-cdk/toolkit-lib/.projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/docs/message-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ Please let us know by [opening an issue](https://github.com/aws/aws-cdk-cli/issu
| `CDK_TOOLKIT_E7900` | Stack deletion failed | `error` | {@link ErrorPayload} |
| `CDK_TOOLKIT_E8900` | Stack refactor failed | `error` | {@link ErrorPayload} |
| `CDK_TOOLKIT_I8900` | Refactor result | `result` | {@link RefactorResult} |
| `CDK_TOOLKIT_I8910` | Confirm refactor | `info` | {@link ConfirmationRequest} |
| `CDK_TOOLKIT_W8010` | Refactor execution not yet supported | `warn` | n/a |
| `CDK_TOOLKIT_I9000` | Provides bootstrap times | `info` | {@link Duration} |
| `CDK_TOOLKIT_I9100` | Bootstrap progress | `info` | {@link BootstrapEnvironmentProgress} |
Expand Down
10 changes: 10 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/refactor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ export interface RefactorOptions {
* A list of names of additional deployed stacks to be included in the comparison.
*/
readonly additionalStackNames?: string[];

/**
* Whether to do the refactor without prompting the user for confirmation.
*/
force?: boolean;

/**
* Role to assume in the target environment before performing the refactor.
*/
roleArn?: string;
}

export interface MappingGroup {
Expand Down
Loading