diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts index 66a39b250ca1c..167d589753b63 100644 --- a/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -114,12 +114,19 @@ export class CloudFormationStackArtifact extends CloudArtifact { this.originalName = this.stackName; } + /** + * Full path to the template file + */ + public get templateFullPath() { + return path.join(this.assembly.directory, this.templateFile); + } + /** * The CloudFormation template for this stack. */ public get template(): any { if (this._template === undefined) { - this._template = JSON.parse(fs.readFileSync(path.join(this.assembly.directory, this.templateFile), 'utf-8')); + this._template = JSON.parse(fs.readFileSync(this.templateFullPath, 'utf-8')); } return this._template; } diff --git a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts index 6ab4b595ae36b..8155d8a2c33ca 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts @@ -172,13 +172,22 @@ export class CloudAssembly { * @returns all the CloudFormation stack artifacts that are included in this assembly. */ public get stacks(): CloudFormationStackArtifact[] { - const result = new Array(); - for (const a of this.artifacts) { - if (a instanceof CloudFormationStackArtifact) { - result.push(a); - } + return this.artifacts.filter(isCloudFormationStackArtifact); + + function isCloudFormationStackArtifact(x: any): x is CloudFormationStackArtifact { + return x instanceof CloudFormationStackArtifact; + } + } + + /** + * The nested assembly artifacts in this assembly + */ + public get nestedAssemblies(): NestedCloudAssemblyArtifact[] { + return this.artifacts.filter(isNestedCloudAssemblyArtifact); + + function isNestedCloudAssemblyArtifact(x: any): x is NestedCloudAssemblyArtifact { + return x instanceof NestedCloudAssemblyArtifact; } - return result; } private validateDeps() { diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts index 9a6bd42593b44..859bf10c6e2e6 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts @@ -1,3 +1,4 @@ +import { promises as fs } from 'fs'; import * as cxapi from '@aws-cdk/cx-api'; import { RegionInfo } from '@aws-cdk/region-info'; import * as contextproviders from '../../context-providers'; @@ -89,54 +90,76 @@ export class CloudExecutable { } } - if (trackVersions && assembly.runtime) { - const modules = formatModules(assembly.runtime); - for (const stack of assembly.stacks) { - if (!stack.template.Resources) { - stack.template.Resources = {}; - } - const resourcePresent = stack.environment.region === cxapi.UNKNOWN_REGION - || RegionInfo.get(stack.environment.region).cdkMetadataResourceAvailable; - if (resourcePresent) { - if (!stack.template.Resources.CDKMetadata) { - stack.template.Resources.CDKMetadata = { - Type: 'AWS::CDK::Metadata', - Properties: { - Modules: modules, - }, - }; - if (stack.environment.region === cxapi.UNKNOWN_REGION) { - stack.template.Conditions = stack.template.Conditions || {}; - const condName = 'CDKMetadataAvailable'; - if (!stack.template.Conditions[condName]) { - stack.template.Conditions[condName] = _makeCdkMetadataAvailableCondition(); - stack.template.Resources.CDKMetadata.Condition = condName; - } else { - warning(`The stack ${stack.id} already includes a ${condName} condition`); - } - } - } else { - warning(`The stack ${stack.id} already includes a CDKMetadata resource`); - } - } - } + if (trackVersions) { + // @deprecated(v2): remove this 'if' block and all code referenced by it. + // This should honestly not be done here. The framework + // should (and will, shortly) synthesize this information directly into + // the template. However, in order to support old framework versions + // that don't synthesize this info yet, we can only remove this code + // once we break backwards compatibility. + await this.addMetadataResource(assembly); } return new CloudAssembly(assembly); + } + } + + /** + * Modify the templates in the assembly in-place to add metadata resource declarations + */ + private async addMetadataResource(rootAssembly: cxapi.CloudAssembly) { + if (!rootAssembly.runtime) { return; } + + const modules = formatModules(rootAssembly.runtime); + await processAssembly(rootAssembly); + + async function processAssembly(assembly: cxapi.CloudAssembly) { + for (const stack of assembly.stacks) { + await processStack(stack); + } + for (const nested of assembly.nestedAssemblies) { + await processAssembly(nested.nestedAssembly); + } + } - function formatModules(runtime: cxapi.RuntimeInfo): string { - const modules = new Array(); + async function processStack(stack: cxapi.CloudFormationStackArtifact) { + const resourcePresent = stack.environment.region === cxapi.UNKNOWN_REGION + || RegionInfo.get(stack.environment.region).cdkMetadataResourceAvailable; + if (!resourcePresent) { return; } - // inject toolkit version to list of modules - // eslint-disable-next-line @typescript-eslint/no-require-imports - const toolkitVersion = require('../../../package.json').version; - modules.push(`aws-cdk=${toolkitVersion}`); + if (!stack.template.Resources) { + stack.template.Resources = {}; + } + if (stack.template.Resources.CDKMetadata) { + warning(`The stack ${stack.id} already includes a CDKMetadata resource`); + return; + } - for (const key of Object.keys(runtime.libraries).sort()) { - modules.push(`${key}=${runtime.libraries[key]}`); + stack.template.Resources.CDKMetadata = { + Type: 'AWS::CDK::Metadata', + Properties: { + Modules: modules, + }, + }; + + if (stack.environment.region === cxapi.UNKNOWN_REGION) { + stack.template.Conditions = stack.template.Conditions || {}; + const condName = 'CDKMetadataAvailable'; + if (!stack.template.Conditions[condName]) { + stack.template.Conditions[condName] = _makeCdkMetadataAvailableCondition(); + stack.template.Resources.CDKMetadata.Condition = condName; + } else { + warning(`The stack ${stack.id} already includes a ${condName} condition`); } - return modules.join(','); } + + // The template has changed in-memory, but the file on disk remains unchanged so far. + // The CLI *might* later on deploy the in-memory version (if it's <50kB) or use the + // on-disk version (if it's >50kB). + // + // Be sure to flush the changes we just made back to disk. The on-disk format is always + // JSON. + await fs.writeFile(stack.templateFullPath, JSON.stringify(stack.template, undefined, 2), { encoding: 'utf-8' }); } } } @@ -185,4 +208,18 @@ function _inGroupsOf(array: T[], maxGroup: number): T[][] { result.push(array.slice(i, i + maxGroup)); } return result; -} \ No newline at end of file +} + +function formatModules(runtime: cxapi.RuntimeInfo): string { + const modules = new Array(); + + // inject toolkit version to list of modules + // eslint-disable-next-line @typescript-eslint/no-require-imports + const toolkitVersion = require('../../../package.json').version; + modules.push(`aws-cdk=${toolkitVersion}`); + + for (const key of Object.keys(runtime.libraries).sort()) { + modules.push(`${key}=${runtime.libraries[key]}`); + } + return modules.join(','); +} diff --git a/packages/aws-cdk/test/integ/cli/app/app.js b/packages/aws-cdk/test/integ/cli/app/app.js index ab5efffbded80..175b658949fc4 100644 --- a/packages/aws-cdk/test/integ/cli/app/app.js +++ b/packages/aws-cdk/test/integ/cli/app/app.js @@ -239,6 +239,14 @@ class ConditionalResourceStack extends cdk.Stack { } } +class SomeStage extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + + new YourStack(this, 'StackInStage'); + } +} + const app = new cdk.App(); const defaultEnv = { @@ -286,4 +294,6 @@ new YourStack(app, `${stackPrefix}-termination-protection`, { terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false, }); +new SomeStage(app, `${stackPrefix}-stage`); + app.synth(); diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index e030f27799dc4..43a882b5e5950 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -679,6 +679,25 @@ integTest('generating and loading assembly', async () => { await cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); }); +integTest('templates on disk contain metadata resource, also in nested assemblies', async () => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await cdk(['synth', '--version-reporting=true']); + + // Load template from disk from root assembly + const templateContents = await shell(['cat', 'cdk.out/*-lambda.template.json'], { + cwd: INTEG_TEST_DIR, + }); + + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + + // Load template from nested assembly + const nestedTemplateContents = await shell(['cat', 'cdk.out/assembly-*-stage/*-stage-StackInStage.template.json'], { + cwd: INTEG_TEST_DIR, + }); + + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); +}); + async function listChildren(parent: string, pred: (x: string) => Promise) { const ret = new Array(); for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) {