Skip to content

Commit 452b868

Browse files
chore: add --from-stack to cdk migrate command (#27155)
This change includes a refactor to migrate to align more closely with how the other cli commands are implemented. `--from-stack` also allows users to set the account and region to get the stack template from. Not included in this PR: 1. Integ tests - this PR has been manually tested and automated tests will be added in a subsequent PR. 2. Go support - there are still some bugs to work out with go (in cdk-from-cfn) so implementation for it will be added after that's been worked out. 3. The option to compress the app to a zip file ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 0634c68 commit 452b868

File tree

12 files changed

+1505
-39
lines changed

12 files changed

+1505
-39
lines changed

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

+74
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { HotswapMode } from './api/hotswap/common';
1515
import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs';
1616
import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
1717
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
18+
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, validateSourceOptions } from './commands/migrate';
1819
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
1920
import { ResourceImporter } from './import';
2021
import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging';
@@ -698,6 +699,28 @@ export class CdkToolkit {
698699
}));
699700
}
700701

702+
/**
703+
* Migrates a CloudFormation stack/template to a CDK app
704+
* @param options Options for CDK app creation
705+
*/
706+
public async migrate(options: MigrateOptions): Promise<void> {
707+
warning('This is an experimental feature. We make no guarantees about the outcome or stability of the functionality.');
708+
const language = options.language ?? 'typescript';
709+
710+
try {
711+
validateSourceOptions(options.fromPath, options.fromStack);
712+
const template = readFromPath(options.fromPath) ||
713+
await readFromStack(options.stackName, this.props.sdkProvider, setEnvironment(options.account, options.region));
714+
const stack = generateStack(template!, options.stackName, language);
715+
success(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName));
716+
await generateCdkApp(options.stackName, stack!, language, options.outputPath);
717+
} catch (e) {
718+
error(' ❌ Migrate failed for `%s`: %s', chalk.blue(options.stackName), (e as Error).message);
719+
throw e;
720+
}
721+
722+
}
723+
701724
private async selectStacksForList(patterns: string[]) {
702725
const assembly = await this.assembly();
703726
const stacks = await assembly.selectStacks({ patterns }, { defaultBehavior: DefaultSelection.AllStacks });
@@ -1172,6 +1195,57 @@ export interface DestroyOptions {
11721195
readonly ci?: boolean;
11731196
}
11741197

1198+
export interface MigrateOptions {
1199+
/**
1200+
* The name assigned to the generated stack. This is also used to get
1201+
* the stack from the user's account if `--from-stack` is used.
1202+
*/
1203+
readonly stackName: string;
1204+
1205+
/**
1206+
* The target language for the generated the CDK app.
1207+
*
1208+
* @default typescript
1209+
*/
1210+
readonly language?: string;
1211+
1212+
/**
1213+
* The local path of the template used to generate the CDK app.
1214+
*
1215+
* @default - Local path is not used for the template source.
1216+
*/
1217+
readonly fromPath?: string;
1218+
1219+
/**
1220+
* Whether to get the template from an existing CloudFormation stack.
1221+
*
1222+
* @default false
1223+
*/
1224+
readonly fromStack?: boolean;
1225+
1226+
/**
1227+
* The output path at which to create the CDK app.
1228+
*
1229+
* @default - The current directory
1230+
*/
1231+
readonly outputPath?: string;
1232+
1233+
/**
1234+
* The account from which to retrieve the template of the CloudFormation stack.
1235+
*
1236+
* @default - Uses the account for the credentials in use by the user.
1237+
*/
1238+
readonly account?: string;
1239+
1240+
/**
1241+
* The region from which to retrieve the template of the CloudFormation stack.
1242+
*
1243+
* @default - Uses the default region for the credentials in use by the user.
1244+
*/
1245+
readonly region?: string;
1246+
1247+
}
1248+
11751249
/**
11761250
* @returns an array with the tags available in the stack metadata.
11771251
*/

packages/aws-cdk/lib/cli.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { CdkToolkit, AssetBuildTime } from '../lib/cdk-toolkit';
2222
import { realHandler as context } from '../lib/commands/context';
2323
import { realHandler as docs } from '../lib/commands/docs';
2424
import { realHandler as doctor } from '../lib/commands/doctor';
25-
import { MIGRATE_SUPPORTED_LANGUAGES, cliMigrate } from '../lib/commands/migrate';
25+
import { MIGRATE_SUPPORTED_LANGUAGES } from '../lib/commands/migrate';
2626
import { RequireApproval } from '../lib/diff';
2727
import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init';
2828
import { data, debug, error, print, setLogLevel, setCI } from '../lib/logging';
@@ -274,7 +274,10 @@ async function parseCommandLineArguments(args: string[]) {
274274
.command('migrate', false /* hidden from "cdk --help" */, (yargs: Argv) => yargs
275275
.option('stack-name', { type: 'string', alias: 'n', desc: 'The name assigned to the stack created in the new project. The name of the app will be based off this name as well.', requiresArg: true })
276276
.option('language', { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project', choices: MIGRATE_SUPPORTED_LANGUAGES })
277+
.option('account', { type: 'string', alias: 'a' })
278+
.option('region', { type: 'string' })
277279
.option('from-path', { type: 'string', alias: 'p', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' })
280+
.option('from-stack', { type: 'boolean', alias: 's', desc: 'USe this flag to retrieve the template for an existing CloudFormation stack' })
278281
.option('output-path', { type: 'string', alias: 'o', desc: 'The output path for the migrated cdk app' }),
279282
)
280283
.command('context', 'Manage cached context values', (yargs: Argv) => yargs
@@ -659,11 +662,14 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
659662
return cliInit(args.TEMPLATE, language, undefined, args.generateOnly);
660663
}
661664
case 'migrate':
662-
return cliMigrate({
665+
return cli.migrate({
663666
stackName: args['stack-name'],
664667
fromPath: args['from-path'],
668+
fromStack: args['from-stack'],
665669
language: args.language,
666670
outputPath: args['output-path'],
671+
account: args.account,
672+
region: args.region,
667673
});
668674
case 'version':
669675
return data(version.DISPLAY_VERSION);
+88-37
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3+
import { Environment, UNKNOWN_ACCOUNT, UNKNOWN_REGION } from '@aws-cdk/cx-api';
34
import * as cdk_from_cfn from 'cdk-from-cfn';
45
import { cliInit } from '../../lib/init';
5-
import { warning } from '../logging';
6+
import { Mode, SdkProvider } from '../api';
67

78
/* eslint-disable @typescript-eslint/no-var-requires */ // Packages don't have @types module
89
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -13,66 +14,116 @@ const decamelize = require('decamelize');
1314
/** The list of languages supported by the built-in noctilucent binary. */
1415
export const MIGRATE_SUPPORTED_LANGUAGES: readonly string[] = cdk_from_cfn.supported_languages();
1516

16-
export interface CliMigrateOptions {
17-
readonly stackName: string;
18-
readonly language?: string;
19-
readonly fromPath?: string;
20-
readonly outputPath?: string;
21-
}
22-
23-
export async function cliMigrate(options: CliMigrateOptions) {
24-
warning('This is an experimental feature. We make no guarantees about the outcome or stability of the functionality.');
25-
26-
// TODO: Validate stack name
27-
28-
const language = options.language ?? 'typescript';
29-
const outputPath = path.join(options.outputPath ?? process.cwd(), options.stackName);
30-
31-
const generatedStack = generateStack(options, language);
32-
const stackName = decamelize(options.stackName);
17+
/**
18+
* Generates a CDK app from a yaml or json template.
19+
*
20+
* @param stackName The name to assign to the stack in the generated app
21+
* @param stack The yaml or json template for the stack
22+
* @param language The language to generate the CDK app in
23+
* @param outputPath The path at which to generate the CDK app
24+
*/
25+
export async function generateCdkApp(stackName: string, stack: string, language: string, outputPath?: string) {
26+
const resolvedOutputPath = path.join(outputPath ?? process.cwd(), stackName);
27+
const formattedStackName = decamelize(stackName);
3328

3429
try {
35-
fs.rmSync(outputPath, { recursive: true, force: true });
36-
fs.mkdirSync(outputPath, { recursive: true });
37-
await cliInit('app', language, true, false, outputPath, options.stackName);
30+
fs.rmSync(resolvedOutputPath, { recursive: true, force: true });
31+
fs.mkdirSync(resolvedOutputPath, { recursive: true });
32+
await cliInit('app', language, true, false, resolvedOutputPath, stackName);
3833

3934
let stackFileName: string;
4035
switch (language) {
4136
case 'typescript':
42-
stackFileName = `${outputPath}/lib/${stackName}-stack.ts`;
37+
stackFileName = `${resolvedOutputPath}/lib/${formattedStackName}-stack.ts`;
4338
break;
4439
case 'java':
45-
stackFileName = `${outputPath}/src/main/java/com/myorg/${camelCase(stackName, { pascalCase: true })}Stack.java`;
40+
stackFileName = `${resolvedOutputPath}/src/main/java/com/myorg/${camelCase(formattedStackName, { pascalCase: true })}Stack.java`;
4641
break;
4742
case 'python':
48-
stackFileName = `${outputPath}/${stackName.replace(/-/g, '_')}/${stackName.replace(/-/g, '_')}_stack.py`;
43+
stackFileName = `${resolvedOutputPath}/${formattedStackName.replace(/-/g, '_')}/${formattedStackName.replace(/-/g, '_')}_stack.py`;
4944
break;
5045
case 'csharp':
51-
stackFileName = `${outputPath}/src/${camelCase(stackName, { pascalCase: true })}/${camelCase(stackName, { pascalCase: true })}Stack.cs`;
46+
stackFileName = `${resolvedOutputPath}/src/${camelCase(formattedStackName, { pascalCase: true })}/${camelCase(formattedStackName, { pascalCase: true })}Stack.cs`;
5247
break;
5348
// TODO: Add Go support
5449
default:
5550
throw new Error(`${language} is not supported by CDK Migrate. Please choose from: ${MIGRATE_SUPPORTED_LANGUAGES.join(', ')}`);
5651
}
57-
fs.writeFileSync(stackFileName!, generatedStack);
52+
fs.writeFileSync(stackFileName, stack);
5853
} catch (error) {
59-
fs.rmSync(outputPath, { recursive: true, force: true });
54+
fs.rmSync(resolvedOutputPath, { recursive: true, force: true });
6055
throw error;
6156
}
57+
}
6258

59+
/**
60+
* Generates a CDK stack file.
61+
* @param template The template to translate into a CDK stack
62+
* @param stackName The name to assign to the stack
63+
* @param language The language to generate the stack in
64+
* @returns A string representation of a CDK stack file
65+
*/
66+
export function generateStack(template: string, stackName: string, language: string) {
67+
try {
68+
const formattedStackName = `${camelCase(decamelize(stackName), { pascalCase: true })}Stack`;
69+
return cdk_from_cfn.transmute(template, language, formattedStackName);
70+
} catch (e) {
71+
throw new Error(`stack generation failed due to error '${(e as Error).message}'`);
72+
}
6373
}
6474

65-
function generateStack(options: CliMigrateOptions, language: string) {
66-
const stackName = `${camelCase(decamelize(options.stackName), { pascalCase: true })}Stack`;
67-
// We will add other options here in a future change.
68-
if (options.fromPath) {
69-
return fromPath(stackName, options.fromPath, language);
75+
/**
76+
* Reads and returns a stack template from a local path.
77+
*
78+
* @param inputPath The location of the template
79+
* @returns A string representation of the template if present, otherwise undefined
80+
*/
81+
export function readFromPath(inputPath?: string): string | undefined {
82+
try {
83+
return inputPath ? fs.readFileSync(inputPath, 'utf8') : undefined;
84+
} catch (e) {
85+
throw new Error(`'${inputPath}' is not a valid path.`);
7086
}
71-
// TODO: replace with actual output for other options.
72-
return '';
87+
7388
}
7489

75-
function fromPath(stackName: string, inputPath: string, language: string): string {
76-
const templateFile = fs.readFileSync(inputPath, 'utf8');
77-
return cdk_from_cfn.transmute(templateFile, language, stackName);
90+
/**
91+
* Reads and returns a stack template from a deployed CloudFormation stack.
92+
*
93+
* @param stackName The name of the stack
94+
* @param sdkProvider The sdk provider for making CloudFormation calls
95+
* @param environment The account and region where the stack is deployed
96+
* @returns A string representation of the template if present, otherwise undefined
97+
*/
98+
export async function readFromStack(stackName: string, sdkProvider: SdkProvider, environment: Environment): Promise<string | undefined> {
99+
const cloudFormation = (await sdkProvider.forEnvironment(environment, Mode.ForReading)).sdk.cloudFormation();
100+
101+
return (await cloudFormation.getTemplate({
102+
StackName: stackName,
103+
}).promise()).TemplateBody;
104+
}
105+
106+
/**
107+
* Sets the account and region for making CloudFormation calls.
108+
* @param account The account to use
109+
* @param region The region to use
110+
* @returns The environment object
111+
*/
112+
export function setEnvironment(account?: string, region?: string): Environment {
113+
return { account: account ?? UNKNOWN_ACCOUNT, region: region ?? UNKNOWN_REGION, name: 'cdk-migrate-env' };
114+
}
115+
116+
/**
117+
* Validates that exactly one source option has been provided.
118+
* @param fromPath The content of the flag `--from-path`
119+
* @param fromStack the content of the flag `--from-stack`
120+
*/
121+
export function validateSourceOptions(fromPath?: string, fromStack?: boolean) {
122+
if (fromPath && fromStack) {
123+
throw new Error('Only one of `--from-path` or `--from-stack` may be provided.');
124+
}
125+
126+
if (!fromPath && !fromStack) {
127+
throw new Error('Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.');
128+
}
78129
}

0 commit comments

Comments
 (0)