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

chore: add --from-stack to cdk migrate command #27155

Merged
merged 2 commits into from
Sep 15, 2023
Merged
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
74 changes: 74 additions & 0 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { HotswapMode } from './api/hotswap/common';
import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs';
import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, validateSourceOptions } from './commands/migrate';
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
import { ResourceImporter } from './import';
import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging';
Expand Down Expand Up @@ -698,6 +699,28 @@ export class CdkToolkit {
}));
}

/**
* Migrates a CloudFormation stack/template to a CDK app
* @param options Options for CDK app creation
*/
public async migrate(options: MigrateOptions): Promise<void> {
warning('This is an experimental feature. We make no guarantees about the outcome or stability of the functionality.');
const language = options.language ?? 'typescript';

try {
validateSourceOptions(options.fromPath, options.fromStack);
const template = readFromPath(options.fromPath) ||
await readFromStack(options.stackName, this.props.sdkProvider, setEnvironment(options.account, options.region));
const stack = generateStack(template!, options.stackName, language);
success(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName));
await generateCdkApp(options.stackName, stack!, language, options.outputPath);
} catch (e) {
error(' ❌ Migrate failed for `%s`: %s', chalk.blue(options.stackName), (e as Error).message);
throw e;
}

}

private async selectStacksForList(patterns: string[]) {
const assembly = await this.assembly();
const stacks = await assembly.selectStacks({ patterns }, { defaultBehavior: DefaultSelection.AllStacks });
Expand Down Expand Up @@ -1172,6 +1195,57 @@ export interface DestroyOptions {
readonly ci?: boolean;
}

export interface MigrateOptions {
/**
* The name assigned to the generated stack. This is also used to get
* the stack from the user's account if `--from-stack` is used.
*/
readonly stackName: string;

/**
* The target language for the generated the CDK app.
*
* @default typescript
*/
readonly language?: string;

/**
* The local path of the template used to generate the CDK app.
*
* @default - Local path is not used for the template source.
*/
readonly fromPath?: string;

/**
* Whether to get the template from an existing CloudFormation stack.
*
* @default false
*/
readonly fromStack?: boolean;

/**
* The output path at which to create the CDK app.
*
* @default - The current directory
*/
readonly outputPath?: string;

/**
* The account from which to retrieve the template of the CloudFormation stack.
*
* @default - Uses the account for the credentials in use by the user.
*/
readonly account?: string;

/**
* The region from which to retrieve the template of the CloudFormation stack.
*
* @default - Uses the default region for the credentials in use by the user.
*/
readonly region?: string;

}

/**
* @returns an array with the tags available in the stack metadata.
*/
Expand Down
10 changes: 8 additions & 2 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { CdkToolkit, AssetBuildTime } from '../lib/cdk-toolkit';
import { realHandler as context } from '../lib/commands/context';
import { realHandler as docs } from '../lib/commands/docs';
import { realHandler as doctor } from '../lib/commands/doctor';
import { MIGRATE_SUPPORTED_LANGUAGES, cliMigrate } from '../lib/commands/migrate';
import { MIGRATE_SUPPORTED_LANGUAGES } from '../lib/commands/migrate';
import { RequireApproval } from '../lib/diff';
import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init';
import { data, debug, error, print, setLogLevel, setCI } from '../lib/logging';
Expand Down Expand Up @@ -274,7 +274,10 @@ async function parseCommandLineArguments(args: string[]) {
.command('migrate', false /* hidden from "cdk --help" */, (yargs: Argv) => yargs
.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 })
.option('language', { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project', choices: MIGRATE_SUPPORTED_LANGUAGES })
.option('account', { type: 'string', alias: 'a' })
.option('region', { type: 'string' })
.option('from-path', { type: 'string', alias: 'p', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' })
.option('from-stack', { type: 'boolean', alias: 's', desc: 'USe this flag to retrieve the template for an existing CloudFormation stack' })
.option('output-path', { type: 'string', alias: 'o', desc: 'The output path for the migrated cdk app' }),
)
.command('context', 'Manage cached context values', (yargs: Argv) => yargs
Expand Down Expand Up @@ -659,11 +662,14 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
return cliInit(args.TEMPLATE, language, undefined, args.generateOnly);
}
case 'migrate':
return cliMigrate({
return cli.migrate({
stackName: args['stack-name'],
fromPath: args['from-path'],
fromStack: args['from-stack'],
language: args.language,
outputPath: args['output-path'],
account: args.account,
region: args.region,
});
case 'version':
return data(version.DISPLAY_VERSION);
Expand Down
125 changes: 88 additions & 37 deletions packages/aws-cdk/lib/commands/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as fs from 'fs';
import * as path from 'path';
import { Environment, UNKNOWN_ACCOUNT, UNKNOWN_REGION } from '@aws-cdk/cx-api';
import * as cdk_from_cfn from 'cdk-from-cfn';
import { cliInit } from '../../lib/init';
import { warning } from '../logging';
import { Mode, SdkProvider } from '../api';

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

export interface CliMigrateOptions {
readonly stackName: string;
readonly language?: string;
readonly fromPath?: string;
readonly outputPath?: string;
}

export async function cliMigrate(options: CliMigrateOptions) {
warning('This is an experimental feature. We make no guarantees about the outcome or stability of the functionality.');

// TODO: Validate stack name

const language = options.language ?? 'typescript';
const outputPath = path.join(options.outputPath ?? process.cwd(), options.stackName);

const generatedStack = generateStack(options, language);
const stackName = decamelize(options.stackName);
/**
* Generates a CDK app from a yaml or json template.
*
* @param stackName The name to assign to the stack in the generated app
* @param stack The yaml or json template for the stack
* @param language The language to generate the CDK app in
* @param outputPath The path at which to generate the CDK app
*/
export async function generateCdkApp(stackName: string, stack: string, language: string, outputPath?: string) {
const resolvedOutputPath = path.join(outputPath ?? process.cwd(), stackName);
const formattedStackName = decamelize(stackName);

try {
fs.rmSync(outputPath, { recursive: true, force: true });
fs.mkdirSync(outputPath, { recursive: true });
await cliInit('app', language, true, false, outputPath, options.stackName);
fs.rmSync(resolvedOutputPath, { recursive: true, force: true });
fs.mkdirSync(resolvedOutputPath, { recursive: true });
await cliInit('app', language, true, false, resolvedOutputPath, stackName);

let stackFileName: string;
switch (language) {
case 'typescript':
stackFileName = `${outputPath}/lib/${stackName}-stack.ts`;
stackFileName = `${resolvedOutputPath}/lib/${formattedStackName}-stack.ts`;
break;
case 'java':
stackFileName = `${outputPath}/src/main/java/com/myorg/${camelCase(stackName, { pascalCase: true })}Stack.java`;
stackFileName = `${resolvedOutputPath}/src/main/java/com/myorg/${camelCase(formattedStackName, { pascalCase: true })}Stack.java`;
break;
case 'python':
stackFileName = `${outputPath}/${stackName.replace(/-/g, '_')}/${stackName.replace(/-/g, '_')}_stack.py`;
stackFileName = `${resolvedOutputPath}/${formattedStackName.replace(/-/g, '_')}/${formattedStackName.replace(/-/g, '_')}_stack.py`;
break;
case 'csharp':
stackFileName = `${outputPath}/src/${camelCase(stackName, { pascalCase: true })}/${camelCase(stackName, { pascalCase: true })}Stack.cs`;
stackFileName = `${resolvedOutputPath}/src/${camelCase(formattedStackName, { pascalCase: true })}/${camelCase(formattedStackName, { pascalCase: true })}Stack.cs`;
break;
// TODO: Add Go support
default:
throw new Error(`${language} is not supported by CDK Migrate. Please choose from: ${MIGRATE_SUPPORTED_LANGUAGES.join(', ')}`);
}
fs.writeFileSync(stackFileName!, generatedStack);
fs.writeFileSync(stackFileName, stack);
} catch (error) {
fs.rmSync(outputPath, { recursive: true, force: true });
fs.rmSync(resolvedOutputPath, { recursive: true, force: true });
throw error;
}
}

/**
* Generates a CDK stack file.
* @param template The template to translate into a CDK stack
* @param stackName The name to assign to the stack
* @param language The language to generate the stack in
* @returns A string representation of a CDK stack file
*/
export function generateStack(template: string, stackName: string, language: string) {
try {
const formattedStackName = `${camelCase(decamelize(stackName), { pascalCase: true })}Stack`;
return cdk_from_cfn.transmute(template, language, formattedStackName);
} catch (e) {
throw new Error(`stack generation failed due to error '${(e as Error).message}'`);
}
}

function generateStack(options: CliMigrateOptions, language: string) {
const stackName = `${camelCase(decamelize(options.stackName), { pascalCase: true })}Stack`;
// We will add other options here in a future change.
if (options.fromPath) {
return fromPath(stackName, options.fromPath, language);
/**
* Reads and returns a stack template from a local path.
*
* @param inputPath The location of the template
* @returns A string representation of the template if present, otherwise undefined
*/
export function readFromPath(inputPath?: string): string | undefined {
try {
return inputPath ? fs.readFileSync(inputPath, 'utf8') : undefined;
} catch (e) {
throw new Error(`'${inputPath}' is not a valid path.`);
}
// TODO: replace with actual output for other options.
return '';

}

function fromPath(stackName: string, inputPath: string, language: string): string {
const templateFile = fs.readFileSync(inputPath, 'utf8');
return cdk_from_cfn.transmute(templateFile, language, stackName);
/**
* Reads and returns a stack template from a deployed CloudFormation stack.
*
* @param stackName The name of the stack
* @param sdkProvider The sdk provider for making CloudFormation calls
* @param environment The account and region where the stack is deployed
* @returns A string representation of the template if present, otherwise undefined
*/
export async function readFromStack(stackName: string, sdkProvider: SdkProvider, environment: Environment): Promise<string | undefined> {
const cloudFormation = (await sdkProvider.forEnvironment(environment, Mode.ForReading)).sdk.cloudFormation();

return (await cloudFormation.getTemplate({
StackName: stackName,
}).promise()).TemplateBody;
}

/**
* Sets the account and region for making CloudFormation calls.
* @param account The account to use
* @param region The region to use
* @returns The environment object
*/
export function setEnvironment(account?: string, region?: string): Environment {
return { account: account ?? UNKNOWN_ACCOUNT, region: region ?? UNKNOWN_REGION, name: 'cdk-migrate-env' };
}

/**
* Validates that exactly one source option has been provided.
* @param fromPath The content of the flag `--from-path`
* @param fromStack the content of the flag `--from-stack`
*/
export function validateSourceOptions(fromPath?: string, fromStack?: boolean) {
if (fromPath && fromStack) {
throw new Error('Only one of `--from-path` or `--from-stack` may be provided.');
}

if (!fromPath && !fromStack) {
throw new Error('Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.');
}
}
Loading