Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): overrideLogicalId: override IDs of CFN elements #1670

Merged
merged 22 commits into from
Feb 6, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions packages/@aws-cdk/assets/test/test.asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export = {
// the correct information
const entry = asset.node.metadata.find(m => m.type === 'aws:cdk:asset');
test.ok(entry, 'found metadata entry');
test.deepEqual(entry!.data, {
test.deepEqual(stack.node.resolve(entry!.data), {
path: dirPath,
id: 'MyAsset',
packaging: 'zip',
Expand All @@ -34,13 +34,35 @@ export = {
test.done();
},

'verify that the app resolves tokens in metadata'(test: Test) {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'my-stack');
const dirPath = path.join(__dirname, 'sample-asset-directory');

new ZipDirectoryAsset(stack, 'MyAsset', {
path: dirPath
});

const synth = app.synthesizeStack(stack.name);

test.deepEqual(synth.metadata['/my-stack/MyAsset'][0].data, {
path: "/Users/benisrae/code/cdk/aws-cdk/packages/@aws-cdk/assets/test/sample-asset-directory",
id: "mystackMyAssetD6B1B593",
packaging: "zip",
s3BucketParameter: "MyAssetS3Bucket68C9B344",
s3KeyParameter: "MyAssetS3VersionKey68E1A45D"
});

test.done();
},

'"file" assets'(test: Test) {
const stack = new cdk.Stack();
const filePath = path.join(__dirname, 'file-asset.txt');
const asset = new FileAsset(stack, 'MyAsset', { path: filePath });
const entry = asset.node.metadata.find(m => m.type === 'aws:cdk:asset');
test.ok(entry, 'found metadata entry');
test.deepEqual(entry!.data, {
test.deepEqual(stack.node.resolve(entry!.data), {
path: filePath,
packaging: 'file',
id: 'MyAsset',
Expand Down
4 changes: 4 additions & 0 deletions packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class CfnReference extends Token {
if (typeof(value) === 'function') {
throw new Error('CfnReference can only hold CloudFormation intrinsics (not a function)');
}
// prepend scope path to display name
if (displayName && scope) {
displayName = `${scope.node.path}.${displayName}`;
}
super(value, displayName);
this.replacementTokens = new Map<Stack, Token>();
this.isReference = true;
Expand Down
26 changes: 17 additions & 9 deletions packages/@aws-cdk/cdk/lib/cloudformation/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class Resource extends Referenceable {
*
* Is filled during prepare().
*/
private readonly dependsOn = new Set<string>();
private readonly dependsOn = new Set<Resource>();

/**
* Creates a resource construct.
Expand Down Expand Up @@ -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] }, attributeName, this);
}

/**
Expand Down Expand Up @@ -179,8 +179,12 @@ export class Resource extends Referenceable {
this.addPropertyOverride(propertyPath, undefined);
}

/**
* Indicates that this resource depends on another resource and cannot be provisioned
* unless the other resource has been successfully provisioned.
*/
public addDependsOn(resource: Resource) {
this.dependsOn.add(resource.logicalId);
this.dependsOn.add(resource);
}

/**
Expand All @@ -197,7 +201,7 @@ export class Resource extends Referenceable {
Type: this.resourceType,
Properties: ignoreEmpty(this, properties),
// Return a sorted set of dependencies to be consistent across tests
DependsOn: ignoreEmpty(this, sortedSet(this.dependsOn)),
DependsOn: ignoreEmpty(this, renderDependsOn(this.dependsOn)),
CreationPolicy: capitalizePropertyNames(this, this.options.creationPolicy),
UpdatePolicy: capitalizePropertyNames(this, this.options.updatePolicy),
UpdateReplacePolicy: capitalizePropertyNames(this, this.options.updateReplacePolicy),
Expand All @@ -217,6 +221,15 @@ export class Resource extends Referenceable {
// Re-throw
throw e;
}

// returns the set of logical ID (tokens) this resource depends on
// sorted by construct paths to ensure test determinism
function renderDependsOn(dependsOn: Set<Resource>) {
return Array
.from(dependsOn)
.sort((x, y) => x.node.path.localeCompare(y.node.path))
.map(r => r.logicalId);
}
}

protected renderProperties(properties: any): { [key: string]: any } {
Expand Down Expand Up @@ -305,8 +318,3 @@ export function deepMerge(target: any, source: any) {

return target;
}
function sortedSet<T>(xs: Set<T>): T[] {
const ret = Array.from(xs);
ret.sort();
return ret;
}
32 changes: 23 additions & 9 deletions packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Construct, IConstruct, PATH_SEP } from "../core/construct";
import { Token } from '../core/tokens';

const LOGICAL_ID_MD = 'aws:cdk:logicalId';

Expand All @@ -15,16 +16,18 @@ 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
* 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 readonly logicalId: string;

Expand All @@ -33,6 +36,8 @@ export abstract class StackElement extends Construct {
*/
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).
Expand All @@ -50,7 +55,16 @@ 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);
this.logicalId = 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;
}

/**
Expand Down Expand Up @@ -127,7 +141,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 }, 'Ref', element);
}
}

Expand Down
10 changes: 4 additions & 6 deletions packages/@aws-cdk/cdk/lib/cloudformation/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export class Stack extends Construct {
};

const elements = stackElements(this);
const fragments = elements.map(e => e.toCloudFormation());
const fragments = elements.map(e => this.node.resolve(e.toCloudFormation()));

// merge in all CloudFormation fragments collected from the tree
for (const fragment of fragments) {
Expand Down Expand Up @@ -239,7 +239,6 @@ export class Stack extends Construct {
* Rename a generated logical identities
*/
public renameLogical(oldId: string, newId: string) {
// tslint:disable-next-line:no-console
if (this.node.children.length > 0) {
throw new Error("All renames must be set up before adding elements to the stack");
}
Expand Down Expand Up @@ -522,9 +521,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) {
Expand All @@ -549,4 +547,4 @@ function findResources(roots: Iterable<IConstruct>): Resource[] {
ret.push(...root.node.findAll().filter(Resource.isResource));
}
return ret;
}
}
7 changes: 1 addition & 6 deletions packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,8 @@ export class TokenMap {

private register(token: Token, representationHint?: string): string {
const counter = Object.keys(this.tokenMap).length;
const representation = representationHint || `TOKEN`;

const representation = (representationHint || `TOKEN`).replace(new RegExp(`[^${VALID_KEY_CHARS}]`, 'g'), '.');
eladb marked this conversation as resolved.
Show resolved Hide resolved
const key = `${representation}.${counter}`;
if (new RegExp(`[^${VALID_KEY_CHARS}]`).exec(key)) {
throw new Error(`Invalid characters in token representation: ${key}`);
}

this.tokenMap[key] = token;
return key;
}
Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/cdk/test/cloudformation/test.logical-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
Expand Down Expand Up @@ -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();
},
Expand Down
9 changes: 5 additions & 4 deletions packages/@aws-cdk/cdk/test/cloudformation/test.output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},

Expand All @@ -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();
},

Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
Expand Down
37 changes: 35 additions & 2 deletions packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
Expand Down Expand Up @@ -152,6 +152,39 @@ export = {
test.done();
},

'if addDependency is called multiple times with the same resource, it will only appear once'(test: Test) {
// GIVEN
const stack = new Stack();
const r1 = new Counter(stack, 'Counter1', { Count: 1 });
const dependent = new Resource(stack, 'Dependent', { type: 'R' });

// WHEN
dependent.addDependsOn(r1);
dependent.addDependsOn(r1);
dependent.addDependsOn(r1);
dependent.addDependsOn(r1);
dependent.addDependsOn(r1);

// THEN
test.deepEqual(stack.toCloudFormation(), {
Resources: {
Counter1: {
Type: "My::Counter",
Properties: {
Count: 1
}
},
Dependent: {
Type: "R",
DependsOn: [
"Counter1"
]
}
}
});
test.done();
},

'conditions can be attached to a resource'(test: Test) {
const stack = new Stack();
const r1 = new Resource(stack, 'Resource', { type: 'Type' });
Expand Down
24 changes: 24 additions & 0 deletions packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down