From 4f4da72741939d7cef790a7e399b474aea210f62 Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:09:41 -0800 Subject: [PATCH] chore(migrate): add migrate information to AWS::CDK::Metadata (#28958) This change adds a new context key to the `cdk.json` file when an app is generated by the `cdk migrate` cli command. If the context key `"cdk-migrate"` is `true`, then that information is added to the end of the analytics string in the AWS::CDK::Metadata resource. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../core/lib/private/metadata-resource.ts | 33 ++++++++++++++----- packages/aws-cdk/lib/commands/migrate.ts | 1 + packages/aws-cdk/lib/init.ts | 22 ++++++++++++- .../aws-cdk/test/commands/migrate.test.ts | 12 +++++++ 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/aws-cdk-lib/core/lib/private/metadata-resource.ts b/packages/aws-cdk-lib/core/lib/private/metadata-resource.ts index 06b0d3f3aefef..36b205bf11648 100644 --- a/packages/aws-cdk-lib/core/lib/private/metadata-resource.ts +++ b/packages/aws-cdk-lib/core/lib/private/metadata-resource.ts @@ -16,7 +16,6 @@ import { Token } from '../token'; export class MetadataResource extends Construct { constructor(scope: Stack, id: string) { super(scope, id); - const metadataServiceExists = Token.isUnresolved(scope.region) || RegionInfo.get(scope.region).cdkMetadataResourceAvailable; if (metadataServiceExists) { const resource = new CfnResource(this, 'Default', { @@ -51,22 +50,30 @@ function makeCdkMetadataAvailableCondition() { class Trie extends Map { } /** - * Formats a list of construct fully-qualified names (FQNs) and versions into a (possibly compressed) prefix-encoded string. + * Formats the analytics string which has 3 or 4 sections separated by colons (:) + * + * version:encoding:constructinfo OR version:encoding:constructinfo:appinfo * - * The list of ConstructInfos is logically formatted into: - * ${version}!${fqn} (e.g., "1.90.0!aws-cdk-lib.Stack") - * and then all of the construct-versions are grouped with common prefixes together, grouping common parts in '{}' and separating items with ','. + * The constructinfo section is a list of construct fully-qualified names (FQNs) + * and versions into a (possibly compressed) prefix-encoded string. + * + * The list of ConstructInfos is logically formatted into: ${version}!${fqn} + * (e.g., "1.90.0!aws-cdk-lib.Stack") and then all of the construct-versions are + * grouped with common prefixes together, grouping common parts in '{}' and + * separating items with ','. * * Example: * [1.90.0!aws-cdk-lib.Stack, 1.90.0!aws-cdk-lib.Construct, 1.90.0!aws-cdk-lib.service.Resource, 0.42.1!aws-cdk-lib-experiments.NewStuff] * Becomes: * 1.90.0!aws-cdk-lib.{Stack,Construct,service.Resource},0.42.1!aws-cdk-lib-experiments.NewStuff * - * The whole thing is then either included directly as plaintext as: - * v2:plaintext:{prefixEncodedList} - * Or is compressed and base64-encoded, and then formatted as: + * The whole thing is then compressed and base64-encoded, and then formatted as: * v2:deflate64:{prefixEncodedListCompressedAndEncoded} * + * The appinfo section is optional, and currently only added if the app was generated using `cdk migrate` + * It is also compressed and base64-encoded. In this case, the string will be formatted as: + * v2:deflate64:{prefixEncodedListCompressedAndEncoded}:{'cdk-migrate'CompressedAndEncoded} + * * Exported/visible for ease of testing. */ export function formatAnalytics(infos: ConstructInfo[]) { @@ -81,7 +88,15 @@ export function formatAnalytics(infos: ConstructInfo[]) { setGzipOperatingSystemToUnknown(compressedConstructsBuffer); const compressedConstructs = compressedConstructsBuffer.toString('base64'); - return `v2:deflate64:${compressedConstructs}`; + const analyticsString = `v2:deflate64:${compressedConstructs}`; + + if (process.env.CDK_CONTEXT_JSON && JSON.parse(process.env.CDK_CONTEXT_JSON)['cdk-migrate']) { + const compressedAppInfoBuffer = zlib.gzipSync(Buffer.from('cdk-migrate')); + const compressedAppInfo = compressedAppInfoBuffer.toString('base64'); + analyticsString.concat(':', compressedAppInfo); + } + + return analyticsString; } /** diff --git a/packages/aws-cdk/lib/commands/migrate.ts b/packages/aws-cdk/lib/commands/migrate.ts index 020f473cb62e6..43414807a49e4 100644 --- a/packages/aws-cdk/lib/commands/migrate.ts +++ b/packages/aws-cdk/lib/commands/migrate.ts @@ -38,6 +38,7 @@ export async function generateCdkApp(stackName: string, stack: string, language: generateOnly, workDir: resolvedOutputPath, stackName, + migrate: true, }); let stackFileName: string; diff --git a/packages/aws-cdk/lib/init.ts b/packages/aws-cdk/lib/init.ts index cbbcc940eae4f..faf323100bf56 100644 --- a/packages/aws-cdk/lib/init.ts +++ b/packages/aws-cdk/lib/init.ts @@ -21,6 +21,7 @@ export interface CliInitOptions { readonly generateOnly?: boolean; readonly workDir?: string; readonly stackName?: string; + readonly migrate?: boolean; } /** @@ -51,7 +52,7 @@ export async function cliInit(options: CliInitOptions) { throw new Error('No language was selected'); } - await initializeProject(template, options.language, canUseNetwork, generateOnly, workDir, options.stackName); + await initializeProject(template, options.language, canUseNetwork, generateOnly, workDir, options.stackName, options.migrate); } /** @@ -203,6 +204,21 @@ export class InitTemplate { await fs.writeJson(cdkJson, config, { spaces: 2 }); } + + public async addMigrateContext(projectDir: string) { + const cdkJson = path.join(projectDir, 'cdk.json'); + if (!await fs.pathExists(cdkJson)) { + return; + } + + const config = await fs.readJson(cdkJson); + config.context = { + ...config.context, + 'cdk-migrate': true, + }; + + await fs.writeJson(cdkJson, config, { spaces: 2 }); + } } interface ProjectInfo { @@ -271,10 +287,14 @@ async function initializeProject( generateOnly: boolean, workDir: string, stackName?: string, + migrate?: boolean, ) { await assertIsEmptyDirectory(workDir); print(`Applying project template ${chalk.green(template.name)} for ${chalk.blue(language)}`); await template.install(language, workDir, stackName); + if (migrate) { + await template.addMigrateContext(workDir); + } if (await fs.pathExists('README.md')) { print(chalk.green(await fs.readFile('README.md', { encoding: 'utf-8' }))); } diff --git a/packages/aws-cdk/test/commands/migrate.test.ts b/packages/aws-cdk/test/commands/migrate.test.ts index c26870bc85b7b..342f513fa46a4 100644 --- a/packages/aws-cdk/test/commands/migrate.test.ts +++ b/packages/aws-cdk/test/commands/migrate.test.ts @@ -152,6 +152,18 @@ describe('Migrate Function Tests', () => { expect(replacedStack).toEqual(fs.readFileSync(path.join(...stackPath, 's3-stack.ts'), 'utf8')); }); + cliTest('generateCdkApp adds cdk-migrate key in context', async (workDir) => { + const stack = generateStack(validTemplate, 'GoodTypeScript', 'typescript'); + await generateCdkApp('GoodTypeScript', stack, 'typescript', workDir); + + // cdk.json exist in the correct spot + expect(fs.pathExistsSync(path.join(workDir, 'GoodTypeScript', 'cdk.json'))).toBeTruthy(); + + // cdk.json has "cdk-migrate" : true in context + const cdkJson = fs.readJsonSync(path.join(workDir, 'GoodTypeScript', 'cdk.json'), 'utf8'); + expect(cdkJson.context['cdk-migrate']).toBeTruthy(); + }); + cliTest('generateCdkApp generates the expected cdk app when called for python', async (workDir) => { const stack = generateStack(validTemplate, 'GoodPython', 'python'); await generateCdkApp('GoodPython', stack, 'python', workDir);