diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index fb1675d9d579f..8bddfd07fcf1f 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -289,3 +289,32 @@ role.addToPolicy(new iam.PolicyStatement({ resources: [cfnBucket.attrArn], })); ``` + +## Vending CloudFormation templates as Constructs + +In many cases, there are existing CloudFormation templates that are not entire applications, +but more like specialized fragments, implementing a particular pattern or best practice. +If you have templates like that, +you can use the `CfnInclude` class to vend them as a CDK Constructs: + +```ts +import * as path from 'path'; + +export class MyConstruct extends Construct { + constructor(scope: Construct, id: string) { + super(scope, id); + + // include a template inside the Construct + new cfn_inc.CfnInclude(this, 'MyConstruct', { + templateFile: path.join(__dirname, 'my-template.json'), + preserveLogicalIds: false, // <--- !!! + }); + } +} +``` + +Notice the `preserveLogicalIds` parameter - +it makes sure the logical IDs of all the included template elements are re-named using CDK's algorithm, +guaranteeing they are unique within your application. +Without that parameter passed, +instantiating `MyConstruct` twice in the same Stack would result in duplicated logical IDs. diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index 2c135abba3377..ebad5a7de6b33 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -14,6 +14,20 @@ export interface CfnIncludeProps { */ readonly templateFile: string; + /** + * Whether the resources should have the same logical IDs in the resulting CDK template + * as they did in the original CloudFormation template file. + * If you're vending a Construct using an existing CloudFormation template, + * make sure to pass this as `false`. + * + * **Note**: regardless of whether this option is true or false, + * the {@link CfnInclude.getResource} and related methods always uses the original logical ID of the resource/element, + * as specified in the template file. + * + * @default true + */ + readonly preserveLogicalIds?: boolean; + /** * Specifies the template files that define nested stacks that should be included. * @@ -86,8 +100,7 @@ export class CfnInclude extends core.CfnElement { // read the template into a JS object this.template = futils.readYamlSync(props.templateFile); - // ToDo implement preserveLogicalIds=false - this.preserveLogicalIds = true; + this.preserveLogicalIds = props.preserveLogicalIds ?? true; // check if all user specified parameter values exist in the template for (const logicalId of Object.keys(this.parametersToReplace)) { @@ -326,7 +339,7 @@ export class CfnInclude extends core.CfnElement { mapping: cfnParser.parseValue(this.template.Mappings[mappingName]), }); this.mappings[mappingName] = cfnMapping; - cfnMapping.overrideLogicalId(mappingName); + this.overrideLogicalIdIfNeeded(cfnMapping, mappingName); } private createParameter(logicalId: string): void { @@ -357,7 +370,7 @@ export class CfnInclude extends core.CfnElement { noEcho: expression.NoEcho, }); - cfnParameter.overrideLogicalId(logicalId); + this.overrideLogicalIdIfNeeded(cfnParameter, logicalId); this.parameters[logicalId] = cfnParameter; } @@ -384,7 +397,7 @@ export class CfnInclude extends core.CfnElement { assertions: ruleProperties.Assertions, }); this.rules[ruleName] = rule; - rule.overrideLogicalId(ruleName); + this.overrideLogicalIdIfNeeded(rule, ruleName); } private createOutput(logicalId: string, scope: core.Construct): void { @@ -422,7 +435,7 @@ export class CfnInclude extends core.CfnElement { })(), }); - cfnOutput.overrideLogicalId(logicalId); + this.overrideLogicalIdIfNeeded(cfnOutput, logicalId); this.outputs[logicalId] = cfnOutput; } @@ -455,8 +468,7 @@ export class CfnInclude extends core.CfnElement { expression: cfnParser.parseValue(this.template.Conditions[conditionName]), }); - // ToDo handle renaming of the logical IDs of the conditions - cfnCondition.overrideLogicalId(conditionName); + this.overrideLogicalIdIfNeeded(cfnCondition, conditionName); this.conditions[conditionName] = cfnCondition; return cfnCondition; } @@ -533,11 +545,7 @@ export class CfnInclude extends core.CfnElement { } } - if (this.preserveLogicalIds) { - // override the logical ID to match the original template - l1Instance.overrideLogicalId(logicalId); - } - + this.overrideLogicalIdIfNeeded(l1Instance, logicalId); this.resources[logicalId] = l1Instance; return l1Instance; } @@ -585,4 +593,10 @@ export class CfnInclude extends core.CfnElement { } return ret; } + + private overrideLogicalIdIfNeeded(element: core.CfnElement, id: string): void { + if (this.preserveLogicalIds) { + element.overrideLogicalId(id); + } + } } diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 35127627410f5..0ab537e3b326a 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -114,7 +114,7 @@ describe('CDK Include', () => { ); }); - xtest('correctly changes the logical IDs, including references, if imported with preserveLogicalIds=false', () => { + test('correctly changes the logical IDs, including references, if imported with preserveLogicalIds=false', () => { const cfnTemplate = includeTestTemplate(stack, 'bucket-with-encryption-key.json', { preserveLogicalIds: false, }); @@ -177,6 +177,11 @@ describe('CDK Include', () => { ], }, }, + "Metadata": { + "Object1": "Location1", + "KeyRef": { "Ref": "MyScopeKey7673692F" }, + "KeyArn": { "Fn::GetAtt": ["MyScopeKey7673692F", "Arn"] }, + }, "DeletionPolicy": "Retain", "UpdateReplacePolicy": "Retain", }, @@ -918,7 +923,7 @@ function includeTestTemplate(scope: core.Construct, testTemplate: string, props: return new inc.CfnInclude(scope, 'MyScope', { templateFile: _testTemplateFilePath(testTemplate), parameters: props.parameters, - // preserveLogicalIds: props.preserveLogicalIds, + preserveLogicalIds: props.preserveLogicalIds, }); }