Skip to content

Commit bbc14b3

Browse files
HBobertzTheRealAmazonKendra
authored andcommitted
feat(migrate): Add CDK Migrate --from-scan functionality (#28962)
### `cdk migrate` ⚠️**CAUTION**⚠️: CDK Migrate is currently experimental and may have breaking changes in the future. CDK Migrate generates a CDK app from deployed AWS resources using `--from-scan`, deployed AWS CloudFormation stacks using `--from-stack`, and local AWS CloudFormation templates using `--from-path`. To learn more about the CDK Migrate feature, see [Migrate to AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/migrate.html). For more information on `cdk migrate` command options, see [cdk migrate command reference](https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cdk-migrate.html). The new CDK app will be initialized in the current working directory and will include a single stack that is named with the value you provide using `--stack-name`. The new stack, app, and directory will all use this name. To specify a different output directory, use `--output-path`. You can create the new CDK app in any CDK supported programming language using `--language`. #### Migrate from an AWS CloudFormation stack Migrate from a deployed AWS CloudFormation stack in a specific AWS account and AWS Region using `--from-stack`. Provide `--stack-name` to identify the name of your stack. Account and Region information are retrieved from default CDK CLI sources. Use `--account` and `--region` options to provide other values. The following is an example that migrates **myCloudFormationStack** to a new CDK app using TypeScript: ```console $ cdk migrate --language typescript --from-stack --stack-name 'myCloudFormationStack' ``` #### Migrate from a local AWS CloudFormation template Migrate from a local `YAML` or `JSON` AWS CloudFormation template using `--from-path`. Provide a name for the stack that will be created in your new CDK app using `--stack-name`. Account and Region information are retrieved from default CDK CLI sources. Use `--account` and `--region` options to provide other values. The following is an example that creates a new CDK app using TypeScript that includes a **myCloudFormationStack** stack from a local `template.json` file: ```console $ cdk migrate --language typescript --from-path "./template.json" --stack-name "myCloudFormationStack" ``` #### Migrate from deployed AWS resources Migrate from deployed AWS resources in a specific AWS account and Region that are not associated with an AWS CloudFormation stack using `--from-scan`. These would be resources that were provisioned outside of an IaC tool. CDK Migrate utilizes the IaC generator service to scan for resources and generate a template. Then, the CDK CLI references the template to create a new CDK app. To learn more about IaC generator, see [Generating templates for existing resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/generate-IaC.html). Account and Region information are retrieved from default CDK CLI sources. Use `--account` and `--region` options to provide other values. The following is an example that creates a new CDK app using TypeScript that includes a new **myCloudFormationStack** stack from deployed resources: ```console $ cdk migrate --language typescript --from-scan --stack-name "myCloudFormationStack" ``` Since CDK Migrate relies on the IaC generator service, any limitations of IaC generator will apply to CDK Migrate. For general limitations, see [Considerations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/generate-IaC.html#generate-template-considerations). IaC generator limitations with discovering resource and property values will also apply here. As a result, CDK Migrate will only migrate resources supported by IaC generator. Some of your resources may not be supported and some property values may not be accessible. For more information, see [Iac generator and write-only properties](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/generate-IaC-write-only-properties.html) and [Supported resource types](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/generate-IaC-supported-resources.html). You can specify filters using `--filter` to specify which resources to migrate. This is a good option to use if you are over the IaC generator total resource limit. After migration, you must resolve any write-only properties that were detected by IaC generator from your deployed resources. To learn more, see [Resolve write-only properties](https://docs.aws.amazon.com/cdk/v2/guide/migrate.html#migrate-resources-writeonly). #### Examples ##### Generate a TypeScript CDK app from a local AWS CloudFormation template.json file ```console $ # template.json is a valid cloudformation template in the local directory $ cdk migrate --stack-name MyAwesomeApplication --language typescript --from-path MyTemplate.json ``` This command generates a new directory named `MyAwesomeApplication` within your current working directory, and then initializes a new CDK application within that directory. The CDK app contains a `MyAwesomeApplication` stack with resources configured to match those in your local CloudFormation template. This results in a CDK application with the following structure, where the lib directory contains a stack definition with the same resource configuration as the provided template.json. ```console ├── README.md ├── bin │ └── my_awesome_application.ts ├── cdk.json ├── jest.config.js ├── lib │ └── my_awesome_application-stack.ts ├── package.json ├── tsconfig.json ``` ##### Generate a Python CDK app from a deployed stack If you already have a CloudFormation stack deployed in your account and would like to manage it with CDK, you can migrate the deployed stack to a new CDK app. The value provided with `--stack-name` must match the name of the deployed stack. ```console $ # generate a Python application from MyDeployedStack in your account $ cdk migrate --stack-name MyDeployedStack --language python --from-stack ``` This will generate a Python CDK app which will synthesize the same configuration of resources as the deployed stack. ##### Generate a TypeScript CDK app from deployed AWS resources that are not associated with a stack If you have resources in your account that were provisioned outside AWS IaC tools and would like to manage them with the CDK, you can use the `--from-scan` option to generate the application. In this example, we use the `--filter` option to specify which resources to migrate. You can filter resources to limit the number of resources migrated to only those specified by the `--filter` option, including any resources they depend on, or resources that depend on them (for example A filter which specifies a single Lambda Function, will find that specific table and any alarms that may monitor it). The `--filter` argument offers both AND as well as OR filtering. OR filtering can be specified by passing multiple `--filter` options, and AND filtering can be specified by passing a single `--filter` option with multiple comma separated key/value pairs as seen below (see below for examples). It is recommended to use the `--filter` option to limit the number of resources returned as some resource types provide sample resources by default in all accounts which can add to the resource limits. `--from-scan` takes 3 potential arguments: `--new`, `most-recent`, and undefined. If `--new` is passed, CDK Migrate will initiate a new scan of the account and use that new scan to discover resources. If `--most-recent` is passed, CDK Migrate will use the most recent scan of the account to discover resources. If neither `--new` nor `--most-recent` are passed, CDK Migrate will take the most recent scan of the account to discover resources, unless there is no recent scan, in which case it will initiate a new scan. ``` # Filtering options identifier|id|resource-identifier=<resource-specific-resource-identifier-value> type|resource-type-prefix=<resource-type-prefix> tag-key=<tag-key> tag-value=<tag-value> ``` ##### Additional examples of migrating from deployed resources ```console $ # Generate a typescript application from all un-managed resources in your account $ cdk migrate --stack-name MyAwesomeApplication --language typescript --from-scan $ # Generate a typescript application from all un-managed resources in your account with the tag key "Environment" AND the tag value "Production" $ cdk migrate --stack-name MyAwesomeApplication --language typescript --from-scan --filter tag-key=Environment,tag-value=Production $ # Generate a python application from any dynamoDB resources with the tag-key "dev" AND the tag-value "true" OR any SQS::Queue $ cdk migrate --stack-name MyAwesomeApplication --language python --from-scan --filter type=AWS::DynamoDb::,tag-key=dev,tag-value=true --filter type=SQS::Queue $ # Generate a typescript application from a specific lambda function by providing it's specific resource identifier $ cdk migrate --stack-name MyAwesomeApplication --language typescript --from-scan --filter identifier=myAwesomeLambdaFunction ``` #### **CDK Migrate Limitations** - CDK Migrate does not currently support nested stacks, custom resources, or the `Fn::ForEach` intrinsic function. - CDK Migrate will only generate L1 constructs and does not currently support any higher level abstractions. - CDK Migrate successfully generating an application does *not* guarantee the application is immediately deployable. It simply generates a CDK application which will synthesize a template that has identical resource configurations to the provided template. - CDK Migrate does not interact with the CloudFormation service to verify the template provided can deploy on its own. This means CDK Migrate will not verify that any resources in the provided template are already managed in other CloudFormation templates, nor will it verify that the resources in the provided template are available in the desired regions, which may impact ADC or Opt-In regions. - If the provided template has parameters without default values, those will need to be provided before deploying the generated application. In practice this is how CDK Migrate generated applications will operate in the following scenarios: | Situation | Result | | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | Provided template + stack-name is from a deployed stack in the account/region | The CDK application will deploy as a changeset to the existing stack | | Provided template has no overlap with resources already in the account/region | The CDK application will deploy a new stack successfully | | Provided template has overlap with Cloudformation managed resources already in the account/region | The CDK application will not be deployable unless those resources are removed | | Provided template has overlap with un-managed resources already in the account/region | The CDK application will not be deployable until those resources are adopted with [`cdk import`](#cdk-import) | | No template has been provided and resources exist in the region the scan is done | The CDK application will be immediatly deployable and will import those resources into a new cloudformation stack upon deploy | ##### **The provided template is already deployed to CloudFormation in the account/region** If the provided template came directly from a deployed CloudFormation stack, and that stack has not experienced any drift, then the generated application will be immediately deployable, and will not cause any changes to the deployed resources. Drift might occur if a resource in your template was modified outside of CloudFormation, namely via the AWS Console or AWS CLI. ##### **The provided template is not deployed to CloudFormation in the account/region, and there *is not* overlap with existing resources in the account/region** If the provided template represents a set of resources that have no overlap with resources already deployed in the account/region, then the generated application will be immediately deployable. This could be because the stack has never been deployed, or the application was generated from a stack deployed in another account/region. In practice this means for any resource in the provided template, for example, ```Json "S3Bucket": { "Type": "AWS::S3::Bucket", "Properties": { "BucketName": "MyBucket", "AccessControl": "PublicRead", }, "DeletionPolicy": "Retain" } ``` There must not exist a resource of that type with the same identifier in the desired region. In this example that identfier would be "MyBucket" ##### **The provided template is not deployed to CloudFormation in the account/region, and there *is* overlap with existing resources in the account/region** If the provided template represents a set of resources that overlap with resources already deployed in the account/region, then the generated application will not be immediately deployable. If those overlapped resources are already managed by another CloudFormation stack in that account/region, then those resources will need to be manually removed from the provided template. Otherwise, if the overlapped resources are not managed by another CloudFormation stack, then first remove those resources from your CDK Application Stack, deploy the cdk application successfully, then re-add them and run `cdk import` to import them into your deployed stack. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 64d263c commit bbc14b3

File tree

9 files changed

+1262
-39
lines changed

9 files changed

+1262
-39
lines changed

packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AwsContext, withAws } from './with-aws';
1212
import { withTimeout } from './with-timeout';
1313

1414
export const DEFAULT_TEST_TIMEOUT_S = 10 * 60;
15+
export const EXTENDED_TEST_TIMEOUT_S = 30 * 60;
1516

1617
/**
1718
* Higher order function to execute a block with a CDK app fixture
@@ -185,6 +186,10 @@ export function withDefaultFixture(block: (context: TestFixture) => Promise<void
185186
return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withCdkApp(block)));
186187
}
187188

189+
export function withExtendedTimeoutFixture(block: (context: TestFixture) => Promise<void>) {
190+
return withAws(withTimeout(EXTENDED_TEST_TIMEOUT_S, withCdkApp(block)));
191+
}
192+
188193
export function withCDKMigrateFixture(language: string, block: (content: TestFixture) => Promise<void>) {
189194
return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withCdkMigrateApp(language, block)));
190195
}

packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js

+28
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,35 @@ class MigrateStack extends cdk.Stack {
8686
value: queue.node.defaultChild.logicalId,
8787
});
8888
}
89+
if (process.env.SAMPLE_RESOURCES) {
90+
const myTopic = new sns.Topic(this, 'migratetopic1', {
91+
removalPolicy: cdk.RemovalPolicy.DESTROY,
92+
});
93+
cdk.Tags.of(myTopic).add('tag1', 'value1');
94+
const myTopic2 = new sns.Topic(this, 'migratetopic2', {
95+
removalPolicy: cdk.RemovalPolicy.DESTROY,
96+
});
97+
cdk.Tags.of(myTopic2).add('tag2', 'value2');
98+
const myQueue = new sqs.Queue(this, 'migratequeue1', {
99+
removalPolicy: cdk.RemovalPolicy.DESTROY,
100+
});
101+
cdk.Tags.of(myQueue).add('tag3', 'value3');
102+
}
103+
if (process.env.LAMBDA_RESOURCES) {
104+
const myFunction = new lambda.Function(this, 'migratefunction1', {
105+
code: lambda.Code.fromInline('console.log("hello world")'),
106+
handler: 'index.handler',
107+
runtime: lambda.Runtime.NODEJS_18_X,
108+
});
109+
cdk.Tags.of(myFunction).add('lambda-tag', 'lambda-value');
89110

111+
const myFunction2 = new lambda.Function(this, 'migratefunction2', {
112+
code: lambda.Code.fromInline('console.log("hello world2")'),
113+
handler: 'index.handler',
114+
runtime: lambda.Runtime.NODEJS_18_X,
115+
});
116+
cdk.Tags.of(myFunction2).add('lambda-tag', 'lambda-value');
117+
}
90118
}
91119
}
92120

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

+115-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { promises as fs, existsSync } from 'fs';
22
import * as os from 'os';
33
import * as path from 'path';
4-
import { integTest, cloneDirectory, shell, withDefaultFixture, retry, sleep, randomInteger, withSamIntegrationFixture, RESOURCES_DIR, withCDKMigrateFixture } from '../../lib';
4+
import { integTest, cloneDirectory, shell, withDefaultFixture, retry, sleep, randomInteger, withSamIntegrationFixture, RESOURCES_DIR, withCDKMigrateFixture, withExtendedTimeoutFixture } from '../../lib';
55

66
jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime
77

@@ -571,9 +571,9 @@ integTest('deploy with role', withDefaultFixture(async (fixture) => {
571571
}
572572
}));
573573

574-
// TODO add go back in when template synths properly
574+
// TODO add more testing that ensures the symmetry of the generated constructs to the resources.
575575
['typescript', 'python', 'csharp', 'java'].forEach(language => {
576-
integTest(`cdk migrate ${language}`, withCDKMigrateFixture(language, async (fixture) => {
576+
integTest(`cdk migrate ${language} deploys successfully`, withCDKMigrateFixture(language, async (fixture) => {
577577
if (language === 'python') {
578578
await fixture.shell(['pip', 'install', '-r', 'requirements.txt']);
579579
}
@@ -588,6 +588,118 @@ integTest('deploy with role', withDefaultFixture(async (fixture) => {
588588
}));
589589
});
590590

591+
integTest('cdk migrate generates migrate.json', withCDKMigrateFixture('typescript', async (fixture) => {
592+
593+
const migrateFile = await fs.readFile(path.join(fixture.integTestDir, 'migrate.json'), 'utf8');
594+
const expectedFile = `{
595+
\"//\": \"This file is generated by cdk migrate. It will be automatically deleted after the first successful deployment of this app to the environment of the original resources.\",
596+
\"Source\": \"localfile\"
597+
}`;
598+
expect(JSON.parse(migrateFile)).toEqual(JSON.parse(expectedFile));
599+
await fixture.cdkDestroy(fixture.stackNamePrefix);
600+
}));
601+
602+
integTest('cdk migrate --from-scan with AND/OR filters correctly filters resources', withExtendedTimeoutFixture(async (fixture) => {
603+
const stackName = `cdk-migrate-integ-${fixture.randomString}`;
604+
605+
await fixture.cdkDeploy('migrate-stack', {
606+
modEnv: { SAMPLE_RESOURCES: '1' },
607+
});
608+
await fixture.cdk(
609+
['migrate', '--stack-name', stackName, '--from-scan', 'new', '--filter', 'type=AWS::SNS::Topic,tag-key=tag1', 'type=AWS::SQS::Queue,tag-key=tag3'],
610+
{ modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false },
611+
);
612+
613+
try {
614+
const response = await fixture.aws.cloudFormation('describeGeneratedTemplate', {
615+
GeneratedTemplateName: stackName,
616+
});
617+
const resourceNames = [];
618+
for (const resource of response.Resources || []) {
619+
if (resource.LogicalResourceId) {
620+
resourceNames.push(resource.LogicalResourceId);
621+
}
622+
}
623+
fixture.log(`Resources: ${resourceNames}`);
624+
expect(resourceNames.some(ele => ele && ele.includes('migratetopic1'))).toBeTruthy();
625+
expect(resourceNames.some(ele => ele && ele.includes('migratequeue1'))).toBeTruthy();
626+
} finally {
627+
await fixture.cdkDestroy('migrate-stack');
628+
await fixture.aws.cloudFormation('deleteGeneratedTemplate', {
629+
GeneratedTemplateName: stackName,
630+
});
631+
}
632+
}));
633+
634+
integTest('cdk migrate --from-scan for resources with Write Only Properties generates warnings', withExtendedTimeoutFixture(async (fixture) => {
635+
const stackName = `cdk-migrate-integ-${fixture.randomString}`;
636+
637+
await fixture.cdkDeploy('migrate-stack', {
638+
modEnv: {
639+
LAMBDA_RESOURCES: '1',
640+
},
641+
});
642+
await fixture.cdk(
643+
['migrate', '--stack-name', stackName, '--from-scan', 'new', '--filter', 'type=AWS::Lambda::Function,tag-key=lambda-tag'],
644+
{ modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false },
645+
);
646+
647+
try {
648+
649+
const response = await fixture.aws.cloudFormation('describeGeneratedTemplate', {
650+
GeneratedTemplateName: stackName,
651+
});
652+
const resourceNames = [];
653+
for (const resource of response.Resources || []) {
654+
if (resource.LogicalResourceId && resource.ResourceType === 'AWS::Lambda::Function') {
655+
resourceNames.push(resource.LogicalResourceId);
656+
}
657+
}
658+
fixture.log(`Resources: ${resourceNames}`);
659+
const readmePath = path.join(fixture.integTestDir, stackName, 'README.md');
660+
const readme = await fs.readFile(readmePath, 'utf8');
661+
expect(readme).toContain('## Warnings');
662+
for (const resourceName of resourceNames) {
663+
expect(readme).toContain(`### ${resourceName}`);
664+
}
665+
} finally {
666+
await fixture.cdkDestroy('migrate-stack');
667+
await fixture.aws.cloudFormation('deleteGeneratedTemplate', {
668+
GeneratedTemplateName: stackName,
669+
});
670+
}
671+
}));
672+
673+
['typescript', 'python', 'csharp', 'java'].forEach(language => {
674+
integTest(`cdk migrate --from-stack creates deployable ${language} app`, withExtendedTimeoutFixture(async (fixture) => {
675+
const migrateStackName = fixture.fullStackName('migrate-stack');
676+
await fixture.aws.cloudFormation('createStack', {
677+
StackName: migrateStackName,
678+
TemplateBody: await fs.readFile(path.join(__dirname, '..', '..', 'resources', 'templates', 'sqs-template.json'), 'utf8'),
679+
});
680+
try {
681+
let stackStatus = 'CREATE_IN_PROGRESS';
682+
while (stackStatus === 'CREATE_IN_PROGRESS') {
683+
stackStatus = await (await (fixture.aws.cloudFormation('describeStacks', { StackName: migrateStackName }))).Stacks?.[0].StackStatus!;
684+
await sleep(1000);
685+
}
686+
await fixture.cdk(
687+
['migrate', '--stack-name', migrateStackName, '--from-stack'],
688+
{ modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false },
689+
);
690+
await fixture.shell(['cd', path.join(fixture.integTestDir, migrateStackName)]);
691+
await fixture.cdk(['deploy', migrateStackName], { neverRequireApproval: true, verbose: true, captureStderr: false });
692+
const response = await fixture.aws.cloudFormation('describeStacks', {
693+
StackName: migrateStackName,
694+
});
695+
696+
expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE');
697+
} finally {
698+
await fixture.cdkDestroy('migrate-stack');
699+
}
700+
}));
701+
});
702+
591703
integTest('cdk diff', withDefaultFixture(async (fixture) => {
592704
const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]);
593705
expect(diff1).toContain('AWS::SNS::Topic');

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

+82-7
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs';
1717
import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
1818
import { createDiffChangeSet, ResourcesToImport } from './api/util/cloudformation';
1919
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
20-
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, validateSourceOptions } from './commands/migrate';
20+
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, parseSourceOptions, generateTemplate, FromScan, TemplateSourceOptions, GenerateTemplateOutput, CfnTemplateGeneratorProvider, writeMigrateJsonFile, buildGenertedTemplateOutput, buildCfnClient, appendWarningsToReadme, isThereAWarning } from './commands/migrate';
2121
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
2222
import { ResourceImporter, removeNonImportResources } from './import';
2323
import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging';
@@ -735,19 +735,80 @@ export class CdkToolkit {
735735
public async migrate(options: MigrateOptions): Promise<void> {
736736
warning('This is an experimental feature and development on it is still in progress. We make no guarantees about the outcome or stability of the functionality.');
737737
const language = options.language?.toLowerCase() ?? 'typescript';
738+
const environment = setEnvironment(options.account, options.region);
739+
let generateTemplateOutput: GenerateTemplateOutput | undefined;
740+
let cfn: CfnTemplateGeneratorProvider | undefined;
741+
let templateToDelete: string | undefined;
738742

739743
try {
740-
validateSourceOptions(options.fromPath, options.fromStack);
741-
const template = readFromPath(options.fromPath) ||
742-
await readFromStack(options.stackName, this.props.sdkProvider, setEnvironment(options.account, options.region));
743-
const stack = generateStack(template!, options.stackName, language);
744+
// if neither fromPath nor fromStack is provided, generate a template using cloudformation
745+
const scanType = parseSourceOptions(options.fromPath, options.fromStack, options.stackName).source;
746+
if (scanType == TemplateSourceOptions.SCAN) {
747+
generateTemplateOutput = await generateTemplate({
748+
stackName: options.stackName,
749+
filters: options.filter,
750+
fromScan: options.fromScan,
751+
sdkProvider: this.props.sdkProvider,
752+
environment: environment,
753+
});
754+
templateToDelete = generateTemplateOutput.templateId;
755+
} else if (scanType == TemplateSourceOptions.PATH) {
756+
const templateBody = readFromPath(options.fromPath!);
757+
758+
const parsedTemplate = deserializeStructure(templateBody);
759+
const templateId = parsedTemplate.Metadata?.TemplateId?.toString();
760+
if (templateId) {
761+
// if we have a template id, we can call describe generated template to get the resource identifiers
762+
// resource metadata, and template source to generate the template
763+
cfn = new CfnTemplateGeneratorProvider(await buildCfnClient(this.props.sdkProvider, environment));
764+
const generatedTemplateSummary = await cfn.describeGeneratedTemplate(templateId);
765+
generateTemplateOutput = buildGenertedTemplateOutput(generatedTemplateSummary, templateBody, generatedTemplateSummary.GeneratedTemplateId!);
766+
} else {
767+
generateTemplateOutput = {
768+
migrateJson: {
769+
templateBody: templateBody,
770+
source: 'localfile',
771+
},
772+
};
773+
}
774+
} else if (scanType == TemplateSourceOptions.STACK) {
775+
const template = await readFromStack(options.stackName, this.props.sdkProvider, environment);
776+
if (!template) {
777+
throw new Error(`No template found for stack-name: ${options.stackName}`);
778+
}
779+
generateTemplateOutput = {
780+
migrateJson: {
781+
templateBody: template,
782+
source: options.stackName,
783+
},
784+
};
785+
} else {
786+
// We shouldn't ever get here, but just in case.
787+
throw new Error(`Invalid source option provided: ${scanType}`);
788+
}
789+
const stack = generateStack(generateTemplateOutput.migrateJson.templateBody, options.stackName, language);
744790
success(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName));
745791
await generateCdkApp(options.stackName, stack!, language, options.outputPath, options.compress);
792+
if (generateTemplateOutput) {
793+
writeMigrateJsonFile(options.outputPath, options.stackName, generateTemplateOutput.migrateJson);
794+
}
795+
if (isThereAWarning(generateTemplateOutput)) {
796+
warning(' ⚠️ Some resources could not be migrated completely. Please review the README.md file for more information.');
797+
appendWarningsToReadme(`${path.join(options.outputPath ?? process.cwd(), options.stackName)}/README.md`, generateTemplateOutput.resources!);
798+
}
746799
} catch (e) {
747-
error(' ❌ Migrate failed for `%s`: %s', chalk.blue(options.stackName), (e as Error).message);
800+
error(' ❌ Migrate failed for `%s`: %s', options.stackName, (e as Error).message);
748801
throw e;
802+
} finally {
803+
if (templateToDelete) {
804+
if (!cfn) {
805+
cfn = new CfnTemplateGeneratorProvider(await buildCfnClient(this.props.sdkProvider, environment));
806+
}
807+
if (!process.env.MIGRATE_INTEG_TEST) {
808+
await cfn.deleteGeneratedTemplate(templateToDelete);
809+
}
810+
}
749811
}
750-
751812
}
752813

753814
private async selectStacksForList(patterns: string[]) {
@@ -1353,6 +1414,20 @@ export interface MigrateOptions {
13531414
*/
13541415
readonly region?: string;
13551416

1417+
/**
1418+
* Filtering criteria used to select the resources to be included in the generated CDK app.
1419+
*
1420+
* @default - Include all resources
1421+
*/
1422+
readonly filter?: string[];
1423+
1424+
/**
1425+
* Whether to initiate a new account scan for generating the CDK app.
1426+
*
1427+
* @default false
1428+
*/
1429+
readonly fromScan?: FromScan;
1430+
13561431
/**
13571432
* Whether to zip the generated cdk app folder.
13581433
*

packages/aws-cdk/lib/cli.ts

+18-1
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 } from '../lib/commands/migrate';
25+
import { MIGRATE_SUPPORTED_LANGUAGES, getMigrateScanType } 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';
@@ -281,6 +281,21 @@ async function parseCommandLineArguments(args: string[]) {
281281
.option('from-path', { type: 'string', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' })
282282
.option('from-stack', { type: 'boolean', desc: 'Use this flag to retrieve the template for an existing CloudFormation stack' })
283283
.option('output-path', { type: 'string', desc: 'The output path for the migrated CDK app' })
284+
.option('from-scan', {
285+
type: 'string',
286+
desc: 'Determines if a new scan should be created, or the last successful existing scan should be used ' +
287+
'\n options are "new" or "most-recent"',
288+
})
289+
.option('filter', {
290+
type: 'array',
291+
desc: 'Filters the resource scan based on the provided criteria in the following format: "key1=value1,key2=value2"' +
292+
'\n This field can be passed multiple times for OR style filtering: ' +
293+
'\n filtering options: ' +
294+
'\n resource-identifier: A key-value pair that identifies the target resource. i.e. {"ClusterName", "myCluster"}' +
295+
'\n resource-type-prefix: A string that represents a type-name prefix. i.e. "AWS::DynamoDB::"' +
296+
'\n tag-key: a string that matches resources with at least one tag with the provided key. i.e. "myTagKey"' +
297+
'\n tag-value: a string that matches resources with at least one tag with the provided value. i.e. "myTagValue"',
298+
})
284299
.option('compress', { type: 'boolean', desc: 'Use this flag to zip the generated CDK app' }),
285300
)
286301
.command('context', 'Manage cached context values', (yargs: Argv) => yargs
@@ -679,6 +694,8 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
679694
fromStack: args['from-stack'],
680695
language: args.language,
681696
outputPath: args['output-path'],
697+
fromScan: getMigrateScanType(args['from-scan']),
698+
filter: args.filter,
682699
account: args.account,
683700
region: args.region,
684701
compress: args.compress,

0 commit comments

Comments
 (0)