From fe6b5b25a77424a6491b331515c502b51ef069c7 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 4 Feb 2019 23:47:14 +0200 Subject: [PATCH] feat(core): overrideLogicalId: override IDs of CFN elements Allow users to explicitly override the logical ID of a CloudFormation element (such as "Cfn" resources) by invoking `overrideLogicalId` on the resource object. For example: const bucket = new s3.CfnBucket(this, 'MyBucket'); bucket.overrideLogicalId('YourBucket'); The resulting template will use `YourBucket` as the logical ID. NOTE: the `logicalId` property will now return a stringified token instead of a concrete value. Fixes #1594 --- .../cdk/lib/cloudformation/resource.ts | 2 +- .../cdk/lib/cloudformation/stack-element.ts | 41 +++++++++++++------ .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 39 +++++++++--------- .../test/cloudformation/test.logical-id.ts | 10 ++--- .../cdk/test/cloudformation/test.output.ts | 9 ++-- .../cdk/test/cloudformation/test.parameter.ts | 4 +- .../cdk/test/cloudformation/test.resource.ts | 12 +++--- .../cdk/test/cloudformation/test.stack.ts | 24 +++++++++++ 8 files changed, 91 insertions(+), 50 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index f53cbc63bea02..ae06140eb4a32 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -117,7 +117,7 @@ export class Resource extends Referenceable { * @param attributeName The name of the attribute. */ public getAtt(attributeName: string) { - return new CfnReference({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`, this); + return new CfnReference({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.node.path.replace('/', '.')}.${attributeName}`, this); } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts index e4c1fc26daaec..39b0ebd292b60 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts @@ -1,4 +1,5 @@ import { Construct, IConstruct, PATH_SEP } from "../core/construct"; +import { Token } from '../core/tokens'; const LOGICAL_ID_MD = 'aws:cdk:logicalId'; @@ -15,24 +16,17 @@ export abstract class StackElement extends Construct { * * @returns The construct as a stack element or undefined if it is not a stack element. */ - public static _asStackElement(construct: IConstruct): StackElement | undefined { - if ('logicalId' in construct && 'toCloudFormation' in construct) { - return construct as StackElement; - } else { - return undefined; - } + public static isStackElement(construct: IConstruct): construct is StackElement { + return ('logicalId' in construct && 'toCloudFormation' in construct); } - /** - * The logical ID for this CloudFormation stack element - */ - public readonly logicalId: string; - /** * The stack this Construct has been made a part of */ protected stack: Stack; + private _logicalId: string; + /** * Creates an entity and binds it to a tree. * Note that the root of the tree must be a Stack object (not just any Root). @@ -50,7 +44,28 @@ export abstract class StackElement extends Construct { this.node.addMetadata(LOGICAL_ID_MD, new (require("../core/tokens/token").Token)(() => this.logicalId), this.constructor); - this.logicalId = this.stack.logicalIds.getLogicalId(this); + this._logicalId = this.stack.logicalIds.getLogicalId(this); + } + + /** + * The logical ID for this CloudFormation stack element. The logical ID of the element + * is calculated from the path of the resource node in the construct tree. + * + * To override this value, use `overrideLogicalId(newLogicalId)`. + * + * @returns the logical ID as a stringified token. This value will only get + * resolved during synthesis. + */ + public get logicalId(): string { + return new Token(() => this._logicalId).toString(); + } + + /** + * Overrides the auto-generated logical ID with a specific ID. + * @param newLogicalId The new logical ID to use for this stack element. + */ + public overrideLogicalId(newLogicalId: string) { + this._logicalId = newLogicalId; } /** @@ -127,7 +142,7 @@ import { CfnReference } from "./cfn-tokens"; */ export class Ref extends CfnReference { constructor(element: StackElement) { - super({ Ref: element.logicalId }, `${element.logicalId}.Ref`, element); + super({ Ref: element.logicalId }, `${element.node.path.replace('/', '.')}.Ref`, element); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index d0f85247e1e65..80d1c06655516 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -173,7 +173,7 @@ export class Stack extends Construct { // merge in all CloudFormation fragments collected from the tree for (const fragment of fragments) { - merge(template, fragment); + this.merge(template, fragment); } // resolve all tokens and remove all empties @@ -466,23 +466,25 @@ export class Stack extends Construct { } return false; } -} -function merge(template: any, part: any) { - for (const section of Object.keys(part)) { - const src = part[section]; - - // create top-level section if it doesn't exist - let dest = template[section]; - if (!dest) { - template[section] = dest = src; - } else { - // add all entities from source section to destination section - for (const id of Object.keys(src)) { - if (id in dest) { - throw new Error(`section '${section}' already contains '${id}'`); + private merge(template: any, part: any) { + part = this.node.resolve(part); + // console.error({ merge: { part: JSON.stringify(part) }}); + for (const section of Object.keys(part)) { + const src = part[section]; + + // create top-level section if it doesn't exist + let dest = template[section]; + if (!dest) { + template[section] = dest = src; + } else { + // add all entities from source section to destination section + for (const id of Object.keys(src)) { + if (id in dest) { + throw new Error(`section '${section}' already contains '${id}'`); + } + dest[id] = src[id]; } - dest[id] = src[id]; } } } @@ -522,9 +524,8 @@ export interface TemplateOptions { * @returns The same array as is being collected into */ function stackElements(node: IConstruct, into: StackElement[] = []): StackElement[] { - const element = StackElement._asStackElement(node); - if (element) { - into.push(element); + if (StackElement.isStackElement(node)) { + into.push(node); } for (const child of node.node.children) { diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.logical-id.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.logical-id.ts index 7c63ac07ea106..9fd0ecba1ebe8 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.logical-id.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.logical-id.ts @@ -29,7 +29,7 @@ const uniqueTests = { const r = new Resource(stack, 'MyAwesomeness', { type: 'Resource' }); // THEN - test.equal(r.logicalId, 'MyAwesomeness'); + test.equal(stack.node.resolve(r.logicalId), 'MyAwesomeness'); test.done(); }, @@ -204,13 +204,13 @@ const allSchemesTests: {[name: string]: (scheme: IAddressingScheme, test: Test) stack.node.prepareTree(); test.deepEqual(stack.toCloudFormation(), { Resources: { - [c1.logicalId]: { + NewName: { Type: 'R1' }, - [c2.logicalId]: { + Construct2: { Type: 'R2', Properties: { - ReferenceToR1: { Ref: c1.logicalId } }, - DependsOn: [ c1.logicalId ] } } }); + ReferenceToR1: { Ref: 'NewName' } }, + DependsOn: [ 'NewName' ] } } }); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts index b6ee26217d2d7..d84b5f75e7869 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts @@ -33,8 +33,9 @@ export = { const output = new Output(stack, 'MyOutput'); const child = new Construct(stack, 'MyConstruct'); const output2 = new Output(child, 'MyOutput2'); - test.equal(output.export, 'MyStack:MyOutput'); - test.equal(output2.export, 'MyStack:MyConstructMyOutput255322D15'); + + test.equal(stack.node.resolve(output.export), 'MyStack:MyOutput'); + test.equal(stack.node.resolve(output2.export), 'MyStack:MyConstructMyOutput255322D15'); test.done(); }, @@ -53,10 +54,10 @@ export = { test.done(); }, - 'is stack name is undefined, we will only use the logical ID for the export name'(test: Test) { + 'if stack name is undefined, we will only use the logical ID for the export name'(test: Test) { const stack = new Stack(); const output = new Output(stack, 'MyOutput'); - test.equal(output.export, 'MyOutput'); + test.equal(stack.node.resolve(output.export), 'MyOutput'); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts index 2a5526c4255f2..2424939dccbac 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts @@ -16,14 +16,14 @@ export = { test.deepEqual(stack.toCloudFormation(), { Parameters: { - [param.logicalId]: { + ChildMyParam3161BF5D: { Default: 10, Type: 'Integer', Description: 'My first parameter' } }, Resources: { Resource: { Type: 'Type', - Properties: { ReferenceToParam: { Ref: param.logicalId } } } } }); + Properties: { ReferenceToParam: { Ref: 'ChildMyParam3161BF5D' } } } } }); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 90e3cf0c5d9a9..71081d413e586 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -44,8 +44,8 @@ export = { const res1 = new Resource(level1, 'childoflevel1', { type: 'MyResourceType1' }); const res2 = new Resource(level3, 'childoflevel3', { type: 'MyResourceType2' }); - test.equal(withoutHash(res1.logicalId), 'level1childoflevel1'); - test.equal(withoutHash(res2.logicalId), 'level1level2level3childoflevel3'); + test.equal(withoutHash(stack.node.resolve(res1.logicalId)), 'level1childoflevel1'); + test.equal(withoutHash(stack.node.resolve(res2.logicalId)), 'level1level2level3childoflevel3'); test.done(); }, @@ -327,10 +327,10 @@ export = { MyResource: { Type: 'R', DependsOn: - [ 'MyC1R1FB2A562F', - 'MyC1R2AE2B5066', - 'MyC2R3809EEAD6', - 'MyC3C2R38CE6F9F7' ] } } }); + [ 'MyC1R2AE2B5066', + 'MyC1R1FB2A562F', + 'MyC2R3809EEAD6', + 'MyC3C2R38CE6F9F7' ] } } }); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index 880068e92d589..d56bedb34896d 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -345,6 +345,30 @@ export = { test.done(); }, + + 'overrideLogicalId(id) can be used to override the logical ID of a resource'(test: Test) { + // GIVEN + const stack = new Stack(); + const bonjour = new Resource(stack, 'BonjourResource', { type: 'Resource::Type' }); + + // { Ref } and { GetAtt } + new Resource(stack, 'RefToBonjour', { type: 'Other::Resource', properties: { + RefToBonjour: bonjour.ref.toString(), + GetAttBonjour: bonjour.getAtt('TheAtt').toString() + }}); + + bonjour.overrideLogicalId('BOOM'); + + // THEN + test.deepEqual(stack.toCloudFormation(), { Resources: + { BOOM: { Type: 'Resource::Type' }, + RefToBonjour: + { Type: 'Other::Resource', + Properties: + { RefToBonjour: { Ref: 'BOOM' }, + GetAttBonjour: { 'Fn::GetAtt': [ 'BOOM', 'TheAtt' ] } } } } }); + test.done(); + } }; class StackWithPostProcessor extends Stack {