Skip to content

Commit

Permalink
feat(migrate): Add template generator integration to CDK Migrate (#204)
Browse files Browse the repository at this point in the history
* initial jazz integration

* cdk migrate with jazz integration

* delete unused tests

* move comment to appropriate test

* refactor + comments to make it more readable

* add filter ts doc

* fix the test

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>

* minor refactors

* make the progress bar safer

* fix test

* changes to managed flag pruning

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

Co-authored-by: Madeline Kusters <80541297+madeline-k@users.noreply.github.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Madeline Kusters <80541297+madeline-k@users.noreply.github.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Madeline Kusters <80541297+madeline-k@users.noreply.github.com>

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>

* resolve conflicts

* or/and functionality

* refactor user experience

* display time since last scan

* add user agent to cfn calls

* make resources more statically typed

* update and fix flitering

* refactor template generator experience

* refactor code

* fix cli options

* adjust apis to reflect template generator changes

* update model

* update tests

* updated tests

* fixes for migrate tests

* fix generate template test mock

* add failure test for most-recent

* add filter failure test

* feat(migrate): add migrate.json file creation to migrate workflow for imports (#212)

* write file migrate.json to output

* simplified source options

* fixing tests

* fix tests

* fix test conflict and refactor tests

* remove commented out test

* add filter fail test

* add default start test

* add test for successful filters

* remove duplicate test

* update testing for import

* Update packages/aws-cdk/lib/commands/migrate.ts

Co-authored-by: Kendra Neil <53584728+TheRealAmazonKendra@users.noreply.github.com>

* add test for migrate.json

* add tests for migrate.json creation

* remove dumb comment I put in

---------

Co-authored-by: Hogan Bobertz <bobertzh@amazon.com>
Co-authored-by: Kendra Neil <53584728+TheRealAmazonKendra@users.noreply.github.com>

* readd comment

* add additional tests and address feedback

* update error message

* update test to no longer clean up generated templates

* comment out test for now

* address feedback

* address feedback and refactor

* remove bangs

---------

Co-authored-by: Hogan Bobertz <bobertzh@amazon.com>
Co-authored-by: Rico Hermans <rix0rrr@gmail.com>
Co-authored-by: Madeline Kusters <80541297+madeline-k@users.noreply.github.com>
Co-authored-by: Kendra Neil <53584728+TheRealAmazonKendra@users.noreply.github.com>
  • Loading branch information
5 people authored Jan 26, 2024
1 parent b1e3dfd commit a86a710
Show file tree
Hide file tree
Showing 8 changed files with 1,126 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,20 @@ class MigrateStack extends cdk.Stack {
value: queue.node.defaultChild.logicalId,
});
}

if (process.env.SAMPLE_RESOURCES) {
const myTopic = new sns.Topic(this, 'migratetopic1', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
cdk.Tags.of(myTopic).add('tag1', 'value1');
const myTopic2 = new sns.Topic(this, 'migratetopic2', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
cdk.Tags.of(myTopic2).add('tag2', 'value2');
const myQueue = new sqs.Queue(this, 'migratequeue1', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
cdk.Tags.of(myQueue).add('tag3', 'value3');
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { promises as fs, existsSync } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { integTest, cloneDirectory, shell, withDefaultFixture, retry, sleep, randomInteger, withSamIntegrationFixture, RESOURCES_DIR, withCDKMigrateFixture } from '../../lib';
import { AwsClients, TestFixture, integTest, cloneDirectory, shell, withDefaultFixture, retry, sleep, randomInteger, withSamIntegrationFixture, RESOURCES_DIR, withCDKMigrateFixture } from '../../lib';

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

Expand Down Expand Up @@ -571,9 +571,9 @@ integTest('deploy with role', withDefaultFixture(async (fixture) => {
}
}));

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

['typescript', 'python', 'csharp', 'java'].forEach(language => {
integTest(`cdk migrate generates migrate.json in ${language}`, withCDKMigrateFixture(language, async (fixture) => {

const migrateFile = await fs.readFile(path.join(fixture.integTestDir, 'migrate.json'), 'utf8');
const expectedFile = `{
\"//\": \"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.\",
\"Source\": \"localfile\"
}`;
expect(JSON.parse(migrateFile)).toEqual(JSON.parse(expectedFile));
await fixture.cdkDestroy(fixture.stackNamePrefix);
}));
});

// TODO: Uncomment this out before launch. Commenting this for now so pipelines won't complain.
// integTest('cdk migrate --from-scan with AND/OR filters correctly filters resources', withDefaultFixture(async (fixture) => {
// const migrateStackJr = 'migrate-stack-jr';

// await fixture.cdkDeploy('migrate-stack', {
// modEnv: { SAMPLE_RESOURCES: '1' },
// });
// await fixture.cdk(
// ['migrate', '--stack-name', migrateStackJr, '--from-scan', 'new', '--filter', 'type=AWS::SNS::Topic,tag-key=tag1', 'type=AWS::SQS::Queue,tag-key=tag3'],
// { modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false },
// );

// try {

// const response = await fixture.aws.cloudFormation('describeGeneratedTemplate', {
// GeneratedTemplateName: migrateStackJr,
// });
// const resourceNames = [];
// for (const resource of response.Resources || []) {
// if (resource.LogicalResourceId) {
// resourceNames.push(resource.LogicalResourceId);
// }
// }
// fixture.log(`Resources: ${resourceNames}`);
// expect(resourceNames.some(ele => ele && ele.includes('migratetopic1'))).toBeTruthy();
// expect(resourceNames.some(ele => ele && ele.includes('migratequeue1'))).toBeTruthy();
// expect(response.Resources?.length).toEqual(2);
// } finally {
// await fixture.cdkDestroy('migrate-stack');
// }
// }));

['typescript', 'python', 'csharp', 'java'].forEach(language => {
integTest(`cdk migrate --from-stack creates deployable ${language} app`, withDefaultFixture(async (fixture) => {
const migrateStackName = fixture.fullStackName('migrate-stack');
await fixture.aws.cloudFormation('createStack', {
StackName: migrateStackName,
TemplateBody: await fs.readFile(path.join(__dirname, '..', '..', 'resources', 'templates', 'sqs-template.json'), 'utf8').toString(),
});
try {
let stackStatus = 'CREATE_IN_PROGRESS';
while (stackStatus === 'CREATE_IN_PROGRESS') {
stackStatus = await (await (fixture.aws.cloudFormation('describeStacks', { StackName: migrateStackName }))).Stacks?.[0].StackStatus!;
await sleep(1000);
}
await fixture.cdk(
['migrate', '--stack-name', migrateStackName, '--from-stack'],
{ modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false },
);
const awsClients = await AwsClients.default(fixture.output);
const fixtureJr = new TestFixture(
path.join(fixture.integTestDir, migrateStackName),
fixture.stackNamePrefix,
fixture.output,
awsClients,
'',
);
await fixtureJr.cdkDeploy('migrate-stack', { neverRequireApproval: true, verbose: true, captureStderr: false });
const response = await fixture.aws.cloudFormation('describeStacks', {
StackName: migrateStackName,
});

expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE');
} finally {
await fixture.cdkDestroy(migrateStackName);
}
}));
});

integTest('cdk diff', withDefaultFixture(async (fixture) => {
const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]);
expect(diff1).toContain('AWS::SNS::Topic');
Expand Down
69 changes: 62 additions & 7 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs';
import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
import { createDiffChangeSet, ResourcesToImport } from './api/util/cloudformation';
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, validateSourceOptions } from './commands/migrate';
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, parseSourceOptions, generateTemplate, FromScan, TemplateSourceOptions, GenerateTemplateOutput, CfnTemplateGeneratorProvider, writeMigrateJsonFile, buildGenertedTemplateOutput, buildCfnClient } 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 @@ -721,17 +721,58 @@ export class CdkToolkit {
const language = options.language?.toLowerCase() ?? '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);
const environment = setEnvironment(options.account, options.region);
let generateTemplateOutput: GenerateTemplateOutput | undefined;
// if neither fromPath nor fromStack is provided, generate a template using cloudformation
const scanType = parseSourceOptions(options.fromPath, options.fromStack, options.stackName).source;
if (scanType == TemplateSourceOptions.SCAN) {
generateTemplateOutput = await generateTemplate({
stackName: options.stackName,
filters: options.filter,
fromScan: options.fromScan,
sdkProvider: this.props.sdkProvider,
environment: environment,
});
} else if (scanType == TemplateSourceOptions.PATH) {
const templateBody = readFromPath(options.fromPath!);

const parsedTemplate = deserializeStructure(templateBody);
const templateId = parsedTemplate.Metadata?.TemplateId?.toString();
if (templateId) {
// if we have a template id, we can call describe generated template to get the resource identifiers
// resource metadata, and template source to generate the template
const cfn = new CfnTemplateGeneratorProvider(await buildCfnClient(this.props.sdkProvider, environment));
const generatedTemplateSummary = await cfn.describeGeneratedTemplate(templateId);
generateTemplateOutput = buildGenertedTemplateOutput(generatedTemplateSummary, templateBody, generatedTemplateSummary.GeneratedTemplateId!);
} else {
generateTemplateOutput = {
templateBody: templateBody,
source: 'localfile',
};
}
} else if (scanType == TemplateSourceOptions.STACK) {
const template = await readFromStack(options.stackName, this.props.sdkProvider, environment);
if (!template) {
throw new Error(`No template found for stack-name: ${options.stackName}`);
}
generateTemplateOutput = {
templateBody: template,
source: options.stackName,
};
} else {
// We shouldn't ever get here, but just in case.
throw new Error(`Invalid source option provided: ${scanType}`);
}
const stack = generateStack(generateTemplateOutput!.templateBody, options.stackName, language);
success(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName));
await generateCdkApp(options.stackName, stack!, language, options.outputPath, options.compress);
if (generateTemplateOutput) {
writeMigrateJsonFile(options.outputPath, options.stackName, generateTemplateOutput);
}
} catch (e) {
error(' ❌ Migrate failed for `%s`: %s', chalk.blue(options.stackName), (e as Error).message);
error(' ❌ Migrate failed for `%s`: %s', options.stackName, (e as Error).message);
throw e;
}

}

private async selectStacksForList(patterns: string[]) {
Expand Down Expand Up @@ -1336,6 +1377,20 @@ export interface MigrateOptions {
*/
readonly region?: string;

/**
* Filtering criteria used to select the resources to be included in the generated CDK app.
*
* @default - Include all resources
*/
readonly filter?: string[];

/**
* Whether to initiate a new account scan for generating the CDK app.
*
* @default false
*/
readonly fromScan?: FromScan;

/**
* Whether to zip the generated cdk app folder.
*
Expand Down
19 changes: 18 additions & 1 deletion 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 } from '../lib/commands/migrate';
import { MIGRATE_SUPPORTED_LANGUAGES, getMigrateScanType } 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 @@ -281,6 +281,21 @@ async function parseCommandLineArguments(args: string[]) {
.option('from-path', { type: 'string', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' })
.option('from-stack', { type: 'boolean', desc: 'Use this flag to retrieve the template for an existing CloudFormation stack' })
.option('output-path', { type: 'string', desc: 'The output path for the migrated CDK app' })
.option('from-scan', {
type: 'string',
desc: 'Determines if a new scan should be created, or the last successful existing scan should be used ' +
'\n options are "new" or "most-recent"',
})
.option('filter', {
type: 'array',
desc: 'Filters the resource scan based on the provided criteria in the following format: "key1=value1,key2=value2"' +
'\n This field can be passed multiple times for OR style filtering: ' +
'\n filtering options: ' +
'\n resource-identifier: A key-value pair that identifies the target resource. i.e. {"ClusterName", "myCluster"}' +
'\n resource-type-prefix: A string that represents a type-name prefix. i.e. "AWS::DynamoDB::"' +
'\n tag-key: a string that matches resources with at least one tag with the provided key. i.e. "myTagKey"' +
'\n tag-value: a string that matches resources with at least one tag with the provided value. i.e. "myTagValue"',
})
.option('compress', { type: 'boolean', desc: 'Use this flag to zip the generated CDK app' }),
)
.command('context', 'Manage cached context values', (yargs: Argv) => yargs
Expand Down Expand Up @@ -679,6 +694,8 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
fromStack: args['from-stack'],
language: args.language,
outputPath: args['output-path'],
fromScan: getMigrateScanType(args['from-scan']),
filter: args.filter,
account: args.account,
region: args.region,
compress: args.compress,
Expand Down
Loading

0 comments on commit a86a710

Please sign in to comment.