Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Implementation of Cloudformation's Stack Refactoring functionality #33463

Closed
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
12 changes: 6 additions & 6 deletions packages/@aws-cdk-testing/cli-integ/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,16 @@
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/cdk-build-tools": "0.0.0",
"@types/semver": "^7.5.8",
"@types/yargs": "^15.0.19",
"@aws-cdk/pkglint": "0.0.0",
"@types/fs-extra": "^9.0.13",
"@types/glob": "^7.2.0",
"@types/npm": "^7.19.3",
"@aws-cdk/pkglint": "0.0.0"
"@types/semver": "^7.5.8",
"@types/yargs": "^15.0.19"
},
"dependencies": {
"@octokit/rest": "^18.12.0",
"@aws-sdk/client-cloudformation": "^3.749.0",
"@aws-sdk/client-codeartifact": "3.632.0",
"@aws-sdk/client-cloudformation": "3.632.0",
"@aws-sdk/client-ecr": "3.632.0",
"@aws-sdk/client-ecs": "3.632.0",
"@aws-sdk/client-iam": "3.632.0",
Expand All @@ -51,8 +50,9 @@
"@aws-sdk/client-sts": "3.632.0",
"@aws-sdk/credential-providers": "3.632.0",
"@cdklabs/cdk-atmosphere-client": "0.0.1",
"@smithy/util-retry": "3.0.8",
"@octokit/rest": "^18.12.0",
"@smithy/types": "3.6.0",
"@smithy/util-retry": "3.0.8",
"axios": "1.7.7",
"chalk": "^4",
"fs-extra": "^9.1.0",
Expand Down
3,889 changes: 3,605 additions & 284 deletions packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/@aws-cdk/cloudformation-diff/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"devDependencies": {
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@aws-sdk/client-cloudformation": "3.662.0",
"@aws-sdk/client-cloudformation": "^3.749.0",
"@types/jest": "^29.5.14",
"@types/string-width": "^4.0.1",
"fast-check": "^3.22.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@aws-cdk/cx-api": "0.0.0",
"@aws-cdk/region-info": "0.0.0",
"@aws-sdk/client-appsync": "3.699.0",
"@aws-sdk/client-cloudformation": "3.699.0",
"@aws-sdk/client-cloudformation": "^3.749.0",
"@aws-sdk/client-cloudwatch-logs": "3.699.0",
"@aws-sdk/client-codebuild": "3.699.0",
"@aws-sdk/client-ec2": "3.699.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## CLI Commands

All CDK CLI Commands are defined in `lib/config.ts`. This file is translated
All CDK CLI Commands are defined in `lib/cli/config.ts`. This file is translated
into a valid `yargs` configuration by `bin/user-input-gen`, which is generated by `@aws-cdk/user-input-gen`.
The `yargs` configuration is generated into the function `parseCommandLineArguments()`,
in `lib/parse-command-line-arguments.ts`, and is checked into git for readability and
Expand Down
3,795 changes: 3,547 additions & 248 deletions packages/aws-cdk/THIRD_PARTY_LICENSES

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions packages/aws-cdk/lib/api/aws-auth/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ import {
UpdateTerminationProtectionCommand,
type UpdateTerminationProtectionCommandInput,
type UpdateTerminationProtectionCommandOutput,
CreateStackRefactorCommand,
type CreateStackRefactorCommandInput,
type CreateStackRefactorCommandOutput,
ExecuteStackRefactorCommand,
type ExecuteStackRefactorCommandInput,
type ExecuteStackRefactorCommandOutput,
waitUntilStackRefactorCreateComplete,
waitUntilStackRefactorExecuteComplete,
DescribeStackRefactorCommandInput,
} from '@aws-sdk/client-cloudformation';
import {
CloudWatchLogsClient,
Expand Down Expand Up @@ -408,6 +417,10 @@ export interface ICloudFormationClient {
// Pagination functions
describeStackEvents(input: DescribeStackEventsCommandInput): Promise<DescribeStackEventsCommandOutput>;
listStackResources(input: ListStackResourcesCommandInput): Promise<StackResourceSummary[]>;
createStackRefactor(input: CreateStackRefactorCommandInput): Promise<CreateStackRefactorCommandOutput>;
executeStackRefactor(input: ExecuteStackRefactorCommandInput): Promise<ExecuteStackRefactorCommandOutput>;
waitForStackRefactorCreateComplete(input: DescribeStackRefactorCommandInput): Promise<WaiterResult>;
waitForStackRefactorExecuteComplete(input: DescribeStackRefactorCommandInput): Promise<WaiterResult>;
}

export interface ICloudWatchLogsClient {
Expand Down Expand Up @@ -679,6 +692,30 @@ export class SDK {
}
return stackResources;
},
createStackRefactor: (input: CreateStackRefactorCommandInput): Promise<CreateStackRefactorCommandOutput> =>
client.send(new CreateStackRefactorCommand(input)),
executeStackRefactor: (input: ExecuteStackRefactorCommandInput): Promise<ExecuteStackRefactorCommandOutput> =>
client.send(new ExecuteStackRefactorCommand(input)),
waitForStackRefactorCreateComplete: (input: DescribeStackRefactorCommandInput): Promise<WaiterResult> =>
waitUntilStackRefactorCreateComplete(
{
client,
maxWaitTime: 600,
minDelay: 6,
maxDelay: 6,
},
input,
),
waitForStackRefactorExecuteComplete: (input: DescribeStackRefactorCommandInput): Promise<WaiterResult> =>
waitUntilStackRefactorExecuteComplete(
{
client,
maxWaitTime: 600,
minDelay: 6,
maxDelay: 6,
},
input,
),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,10 @@ Resources:
- sts:GetCallerIdentity
# `cdk import`
- cloudformation:GetTemplateSummary
# `cdk refactor`
- cloudformation:CreateStackRefactor
- cloudformation:DescribeStackRefactor
- cloudformation:ExecuteStackRefactor
Resource: "*"
Effect: Allow
- Sid: CliStagingBucket
Expand Down Expand Up @@ -657,7 +661,7 @@ Resources:
Type: String
Name:
Fn::Sub: '/cdk-bootstrap/${Qualifier}/version'
Value: '25'
Value: '26'
Outputs:
BucketName:
Description: The name of the S3 bucket owned by the CDK toolkit stack
Expand Down
65 changes: 65 additions & 0 deletions packages/aws-cdk/lib/api/deployments/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type RootTemplateWithNestedStacks,
} from './nested-stack-helpers';
import { debug, warning } from '../../logging';
import { toYAML } from '../../serialize';
import { ToolkitError } from '../../toolkit/error';
import { formatErrorMessage } from '../../util/error';
import type { SdkProvider } from '../aws-auth/sdk-provider';
Expand Down Expand Up @@ -327,6 +328,14 @@ export interface DeploymentsProps {
readonly quiet?: boolean;
}

export interface RefactorOptions {
sourceStack: cxapi.CloudFormationStackArtifact;
targetStack: cxapi.CloudFormationStackArtifact;
sourceResource: string;
targetResource: string;
dryRun: boolean;
}

/**
* Scope for a single set of deployments from a set of Cloud Assembly Artifacts
*
Expand Down Expand Up @@ -626,6 +635,62 @@ export class Deployments {
return stack.exists;
}

public async refactor(options: RefactorOptions): Promise<void> {
const env = await this.envs.accessStackForMutableStackOperations(options.sourceStack);
const cfn = env.sdk.cloudFormation();

const dryRun = options.dryRun;

let refactorId: string;
if (dryRun) {
debug('Will perform a dry-run refactor');
debug(
'Moving resource %s from stack %s to stack %s',
options.sourceResource,
options.sourceStack.stackName,
options.targetStack.stackName,
);
debug('Source resource template: %s', toYAML(options.sourceStack.template.Resources![options.sourceResource]));
debug('Target resource template: %s', toYAML(options.targetStack.template.Resources![options.targetResource]));
} else {
options.targetStack.template.Resources![options.targetResource] =
options.sourceStack.template.Resources![options.sourceResource];
delete options.sourceStack.template.Resources![options.sourceResource];

debug('Source resource template: %s', toYAML(options.sourceStack.template.Resources![options.sourceResource]));
debug('Target resource template: %s', toYAML(options.targetStack.template.Resources![options.targetResource]));
const createStackRefactor = await cfn.createStackRefactor({
EnableStackCreation: true,
StackDefinitions: [
{
StackName: options.sourceStack.stackName,
TemplateBody: toYAML(options.sourceStack.template),
},
{
StackName: options.targetStack.stackName,
TemplateBody: toYAML(options.targetStack.template),
},
],
});
refactorId = createStackRefactor.StackRefactorId!;
await cfn.waitForStackRefactorCreateComplete({
StackRefactorId: refactorId,
});
}

if (dryRun) {
debug('Dry-run refactor complete');
} else {
await cfn.executeStackRefactor({
StackRefactorId: refactorId!,
});

await cfn.waitForStackRefactorExecuteComplete({
StackRefactorId: refactorId!,
});
}
}

/**
* Build a single asset from an asset manifest
*
Expand Down
55 changes: 55 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 * as fs from 'fs-extra';
import * as promptly from 'promptly';
import * as uuid from 'uuid';
import { Configuration, PROJECT_CONFIG } from './user-configuration';
import { RefactorOptions } from './user-input';
import { DEFAULT_TOOLKIT_STACK_NAME } from '../api';
import { SdkProvider } from '../api/aws-auth';
import { Bootstrapper, BootstrapEnvironmentOptions } from '../api/bootstrap';
Expand Down Expand Up @@ -169,6 +170,39 @@ export class CdkToolkit {
await this.props.configuration.saveContext();
}

public async refactor(options: RefactorOptions) {
const stacks = await this.selectStacksForRefactor(options.fromStack!, options.toStack!);

const sourceStack = stacks.source;
const targetStack = stacks.target;

try {
await this.props.deployments.stackExists({
stack: targetStack,
deployName: targetStack.stackName,
tryLookupRole: true,
});
} catch (e: any) {
debug(formatErrorMessage(e));
throw new ToolkitError(
`Checking if the stack ${targetStack.stackName} exists before refactoring has failed. You need to deploy the destination stack first.`,
);
}

try {
await this.props.deployments.refactor({
sourceStack,
targetStack: targetStack,
sourceResource: options.sourceResource!,
targetResource: options.targetResource!,
dryRun: options.dryRun ?? false,
});
} catch (e: any) {
debug(formatErrorMessage(e));
throw new ToolkitError(`Refactoring failed: ${formatErrorMessage(e)}`);
}
}

public async diff(options: DiffOptions): Promise<number> {
const stacks = await this.selectStacksForDiff(options.stackNames, options.exclusively);

Expand Down Expand Up @@ -1142,6 +1176,27 @@ export class CdkToolkit {
return stacks;
}

private async selectStacksForRefactor(sourceStack: string, targetStack: string) {
const assembly = await this.assembly();
const sourceStacks = await assembly.selectStacks(
{ patterns: [sourceStack] },
{ defaultBehavior: DefaultSelection.None, extend: ExtendedStackSelection.Downstream },
);
const targetStacks = await assembly.selectStacks(
{ patterns: [targetStack] },
{ defaultBehavior: DefaultSelection.None, extend: ExtendedStackSelection.Downstream },
);

this.validateStacksSelected(sourceStacks, [sourceStack]);
this.validateStacksSelected(targetStacks, [targetStack]);
await this.validateStacks(sourceStacks.concat(targetStacks));

return {
source: sourceStacks.firstStack,
target: targetStacks.firstStack,
};
}

private async selectStacksForDeploy(
selector: StackSelector,
exclusively?: boolean,
Expand Down
10 changes: 10 additions & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,16 @@ export async function makeConfig(): Promise<CliConfig> {
},
},
},
refactor: {
description: 'Refactor a CloudFormation Stacks with CDK',
options: {
'from-stack': { type: 'string', desc: 'The name of the source stack to refactor', requiresArg: true },
'to-stack': { type: 'string', desc: 'The name of the target stack to refactor', requiresArg: true },
'source-resource': { type: 'string', desc: 'The name of the source resource to refactor', requiresArg: true },
'target-resource': { type: 'string', desc: 'The name of the target resource to refactor', requiresArg: true },
'dry-run': { type: 'boolean', desc: 'Run the refactor in dry-run mode', default: false },
},
},
doctor: {
description: 'Check your set-up for potential problems',
},
Expand Down
10 changes: 10 additions & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,16 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
ioHost.currentAction = 'version';
return result(version.displayVersion());

case 'refactor':
ioHost.currentAction = 'refactor';
return cli.refactor({
fromStack: args['from-stack'],
toStack: args['to-stack'],
sourceResource: args['source-resource'],
targetResource: args['target-resource'],
dryRun: args['dry-run'],
});

default:
throw new ToolkitError('Unknown command: ' + command);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ export function convertYargsToUserInput(args: any): UserInput {
};
break;

case 'refactor':
commandOptions = {
fromStack: args.fromStack,
toStack: args.toStack,
sourceResource: args.sourceResource,
targetResource: args.targetResource,
dryRun: args.dryRun,
};
break;

case 'doctor':
commandOptions = {};
break;
Expand Down Expand Up @@ -429,6 +439,13 @@ export function convertConfigToUserInput(config: any): UserInput {
const docsOptions = {
browser: config.docs?.browser,
};
const refactorOptions = {
fromStack: config.refactor?.fromStack,
toStack: config.refactor?.toStack,
sourceResource: config.refactor?.sourceResource,
targetResource: config.refactor?.targetResource,
dryRun: config.refactor?.dryRun,
};
const doctorOptions = {};
const userInput: UserInput = {
globalOptions,
Expand All @@ -449,6 +466,7 @@ export function convertConfigToUserInput(config: any): UserInput {
migrate: migrateOptions,
context: contextOptions,
docs: docsOptions,
refactor: refactorOptions,
doctor: doctorOptions,
};

Expand Down
Loading
Loading