From 3cc2fff38b45ecfcc56f7c983d9eefdfc4967817 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 15 Nov 2018 20:30:09 +0200 Subject: [PATCH] feat: embed construct paths in CloudFormation metadata (#1183) In order to allow tools that read a CloudFormation template created by the CDK to be able to present the CDK path of resources in the template, the CDK now embeds the full path as CloudFormation metadata "aws:cdk:path" entry for each resource. To disable this behavior use the switch `--no-path-metadata` or set `pathMetadata` to `false` in `cdk.json` or `~/.cdk.json`. The toolkit can control this behavior by setting the "aws:cdk:enable-path-metadata" context key. It sets it to `true` by default. The default behavior of the *Resource class* is _not_ to include metadata. This is in order to maintain backwards compatibility for tests. `cdk-integ` also disables this by default. Fixes #1182 Related #1121 --- .gitignore | 4 +++- .../cdk/lib/cloudformation/resource.ts | 10 ++++++++++ .../cdk/test/cloudformation/test.resource.ts | 19 +++++++++++++++++++ packages/@aws-cdk/cx-api/lib/cxapi.ts | 11 +++++++++++ packages/aws-cdk/bin/cdk.ts | 14 ++++++++++---- packages/aws-cdk/lib/exec.ts | 11 +++++++++++ tools/cdk-integ-tools/bin/cdk-integ-assert.ts | 2 +- tools/cdk-integ-tools/bin/cdk-integ.ts | 15 +++++++++++---- 8 files changed, 76 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index b969fc3b38cb9..91e60255b44d5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ coverage .nyc_output .LAST_BUILD *.swp -tsconfig.json + +# we don't want tsconfig at the root +/tsconfig.json diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index e53d11112fbd8..06895929f647f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,3 +1,4 @@ +import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { CloudFormationToken } from './cloudformation-token'; @@ -86,6 +87,15 @@ export class Resource extends Referenceable { this.resourceType = props.type; this.properties = props.properties || { }; + + // if aws:cdk:enable-path-metadata is set, embed the current construct's + // path in the CloudFormation template, so it will be possible to trace + // back to the actual construct path. + if (this.getContext(cxapi.PATH_METADATA_ENABLE_CONTEXT)) { + this.options.metadata = { + [cxapi.PATH_METADATA_KEY]: this.path + }; + } } /** diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 7c9fa4a60c722..b1bd59e016b77 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -1,3 +1,4 @@ +import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { applyRemovalPolicy, Condition, Construct, DeletionPolicy, FnEquals, FnNot, HashedAddressingScheme, IDependable, @@ -570,6 +571,24 @@ export = { test.done(); } } + }, + + '"aws:cdk:path" metadata is added if "aws:cdk:path-metadata" context is set to true'(test: Test) { + const stack = new Stack(); + stack.setContext(cxapi.PATH_METADATA_ENABLE_CONTEXT, true); + + const parent = new Construct(stack, 'Parent'); + + new Resource(parent, 'MyResource', { + type: 'MyResourceType', + }); + + test.deepEqual(stack.toCloudFormation(), { Resources: + { ParentMyResource4B1FDBCC: + { Type: 'MyResourceType', + Metadata: { [cxapi.PATH_METADATA_KEY]: 'Parent/MyResource' } } } }); + + test.done(); } }; diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 20da4145f9172..32fc928ef7407 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -176,6 +176,17 @@ export const WARNING_METADATA_KEY = 'aws:cdk:warning'; */ export const ERROR_METADATA_KEY = 'aws:cdk:error'; +/** + * The key used when CDK path is embedded in **CloudFormation template** + * metadata. + */ +export const PATH_METADATA_KEY = 'aws:cdk:path'; + +/** + * Enables the embedding of the "aws:cdk:path" in CloudFormation template metadata. + */ +export const PATH_METADATA_ENABLE_CONTEXT = 'aws:cdk:enable-path-metadata'; + /** * Separator string that separates the prefix separator from the object key separator. * diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index e458f1e675889..17b9b3f27e21d 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -19,7 +19,7 @@ import { data, debug, error, highlight, print, setVerbose, success, warning } fr import { PluginHost } from '../lib/plugin'; import { parseRenames } from '../lib/renames'; import { deserializeStructure, serializeStructure } from '../lib/serialize'; -import { loadProjectConfig, loadUserConfig, PER_USER_DEFAULTS, saveProjectConfig, Settings } from '../lib/settings'; +import { DEFAULTS, loadProjectConfig, loadUserConfig, PER_USER_DEFAULTS, saveProjectConfig, Settings } from '../lib/settings'; import { VERSION } from '../lib/version'; // tslint:disable-next-line:no-var-requires @@ -50,7 +50,8 @@ async function parseCommandLineArguments() { .option('profile', { type: 'string', desc: 'Use the indicated AWS profile as the default environment' }) .option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified.' }) .option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status.' }) - .option('version-reporting', { type: 'boolean', desc: 'Disable insersion of the CDKMetadata resource in synthesized templates', default: undefined }) + .option('version-reporting', { type: 'boolean', desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', default: undefined }) + .option('path-metadata', { type: 'boolean', desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', default: true }) .option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined }) .command([ 'list', 'ls' ], 'Lists all stacks in the app', yargs => yargs .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'display environment information for each stack' })) @@ -137,7 +138,7 @@ async function initCommandLine() { ec2creds: argv.ec2creds, }); - const defaultConfig = new Settings({ versionReporting: true }); + const defaultConfig = new Settings({ versionReporting: true, pathMetadata: true }); const userConfig = await loadUserConfig(); const projectConfig = await loadProjectConfig(); const commandLineArguments = argumentsToSettings(); @@ -645,7 +646,11 @@ async function initCommandLine() { function logDefaults() { if (!userConfig.empty()) { - debug('Defaults loaded from ', PER_USER_DEFAULTS, ':', JSON.stringify(userConfig.settings, undefined, 2)); + debug(PER_USER_DEFAULTS + ':', JSON.stringify(userConfig.settings, undefined, 2)); + } + + if (!projectConfig.empty()) { + debug(DEFAULTS + ':', JSON.stringify(projectConfig.settings, undefined, 2)); } const combined = userConfig.merge(projectConfig); @@ -680,6 +685,7 @@ async function initCommandLine() { plugin: argv.plugin, toolkitStackName: argv.toolkitStackName, versionReporting: argv.versionReporting, + pathMetadata: argv.pathMetadata, }); } diff --git a/packages/aws-cdk/lib/exec.ts b/packages/aws-cdk/lib/exec.ts index 068be91460802..8a588d382a982 100644 --- a/packages/aws-cdk/lib/exec.ts +++ b/packages/aws-cdk/lib/exec.ts @@ -15,6 +15,17 @@ export async function execProgram(aws: SDK, config: Settings): Promise !argv.verbose ? args : [ '--verbose', ...args ]; + const args = new Array(); + + // inject "--no-path-metadata" so aws:cdk:path entries are not added to CFN metadata + args.push('--no-path-metadata'); + + // inject "--verbose" to the command line of "cdk" if we are in verbose mode + if (argv.verbose) { + args.push('--verbose'); + } try { - await test.invoke(makeArgs('deploy'), { verbose: argv.verbose }); // Note: no context, so use default user settings! + await test.invoke([ ...args, 'deploy' ], { verbose: argv.verbose }); // Note: no context, so use default user settings! console.error(`Success! Writing out reference synth.`); // If this all worked, write the new expectation file - const actual = await test.invoke(makeArgs('--json', 'synth'), { + const actual = await test.invoke([ ...args, '--json', 'synth' ], { json: true, context: STATIC_TEST_CONTEXT, verbose: argv.verbose