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): convert "/" in construct id to "--" and disallow tokens #1375

Merged
merged 1 commit into from
Dec 17, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-logs/test/test.loggroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,31 @@ export = {
test.done();
},

'extractMetric allows passing in namespaces with "/"'(test: Test) {
// GIVEN
const stack = new Stack();
const lg = new LogGroup(stack, 'LogGroup');

// WHEN
const metric = lg.extractMetric('$.myField', 'MyNamespace/MyService', 'Field');

// THEN
expect(stack).to(haveResource('AWS::Logs::MetricFilter', {
FilterPattern: "{ $.myField = \"*\" }",
MetricTransformations: [
{
MetricName: "Field",
MetricNamespace: "MyNamespace/MyService",
MetricValue: "$.myField"
}
]
}));
test.equal(metric.namespace, 'MyNamespace/MyService');
test.equal(metric.metricName, 'Field');

test.done();
},

'grant'(test: Test) {
// GIVEN
const stack = new Stack();
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/cdk/lib/cloudformation/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class Stack extends Construct {
this.env = this.parseEnvironment(props);

this.logicalIds = new LogicalIDs(props && props.namingScheme ? props.namingScheme : new HashedAddressingScheme());
this.name = name || 'Stack';
this.name = this.id;
}

/**
Expand Down Expand Up @@ -236,7 +236,7 @@ export class Stack extends Construct {
* character classes, and we don't allow one of the magic markers.
*/
protected _validateId(name: string) {
if (!Stack.VALID_STACK_NAME_REGEX.test(name)) {
if (name && !Stack.VALID_STACK_NAME_REGEX.test(name)) {
throw new Error(`Stack name must match the regular expression: ${Stack.VALID_STACK_NAME_REGEX.toString()}, got '${name}'`);
}
}
Expand Down
35 changes: 26 additions & 9 deletions packages/@aws-cdk/cdk/lib/core/construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ export class Construct {
* Creates a new construct node.
*
* @param parent The parent construct
* @param props Properties for this construct
* @param id The local logical ID of the construct. Must be unique amongst
* siblings. If the ID includes a path separator (`/`), then it will be
* replaced by double dash `--`.
*/
constructor(parent: Construct, id: string) {
id = id || ''; // if undefined, convert to empty string

this.id = id;
this.parent = parent;

Expand All @@ -68,10 +72,11 @@ export class Construct {
this.id = id;
}

// Validate the name we ended up with
if (this.id !== '') {
this._validateId(this.id);
}
// escape any path separators so they don't wreck havoc
this.id = this._escapePathSeparator(this.id);

// allow derived classes to validate the construct id
this._validateId(this.id);

const components = this.rootPath().map(c => c.id);
this.path = components.join(PATH_SEP);
Expand Down Expand Up @@ -105,6 +110,9 @@ export class Construct {
/**
* Return a descendant by path, or undefined
*
* Note that if the original ID of the construct you are looking for contained
* a '/', then it would have been replaced by '--'.
*
* @param name Relative name of a direct or indirect child
* @returns a child by path or undefined if not found.
*/
Expand All @@ -127,6 +135,9 @@ export class Construct {
*
* Throws an exception if the descendant is not found.
*
* Note that if the original ID of the construct you are looking for contained
* a '/', then it would have been replaced by '--'.
*
* @param name Relative name of a direct or indirect child
* @returns Child with the given path.
*/
Expand Down Expand Up @@ -292,10 +303,8 @@ export class Construct {
* Validate that the id of the construct legal.
* Construct IDs can be any characters besides the path separator.
*/
protected _validateId(id: string) {
if (id.indexOf(PATH_SEP) !== -1) {
throw new Error(`Construct names cannot include '${PATH_SEP}': ${id}`);
}
protected _validateId(_id: string) {
// can be used by derived classes to customize ID validation.
}

/**
Expand Down Expand Up @@ -387,6 +396,14 @@ export class Construct {

return false;
}

/**
* If the construct ID contains a path separator, it is replaced by double dash (`--`).
*/
private _escapePathSeparator(id: string) {
if (!id) { return id; }
return id.split(PATH_SEP).join('--');
}
}

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/@aws-cdk/cdk/lib/util/uniqueid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// tslint:disable-next-line:no-var-requires
import crypto = require('crypto');
import { unresolved } from '../core/tokens';

/**
* Resources with this ID are hidden from humans
Expand Down Expand Up @@ -35,6 +36,11 @@ export function makeUniqueId(components: string[]) {
throw new Error('Unable to calculate a unique id for an empty set of components');
}

const unresolvedTokens = components.filter(c => unresolved(c));
if (unresolvedTokens.length > 0) {
throw new Error(`ID components may not include unresolved tokens: ${unresolvedTokens.join(',')}`);
}

// top-level resources will simply use the `name` as-is in order to support
// transparent migration of cloudformation templates to the CDK without the
// need to rename all resources.
Expand Down
25 changes: 23 additions & 2 deletions packages/@aws-cdk/cdk/test/core/test.construct.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cxapi = require('@aws-cdk/cx-api');
import { Test } from 'nodeunit';
import { Construct, Root } from '../../lib';
import { Construct, Root, Token } from '../../lib';

// tslint:disable:variable-name
// tslint:disable:max-line-length
Expand Down Expand Up @@ -47,8 +47,29 @@ export = {
new Construct(root, 'in-Valid' );
new Construct(root, 'in\\Valid' );
new Construct(root, 'in.Valid' );
test.done();
},

'if construct id contains path seperators, they will be replaced by double-dash'(test: Test) {
const root = new Root();
const c = new Construct(root, 'Boom/Boom/Bam');
test.deepEqual(c.id, 'Boom--Boom--Bam');
test.done();
},

'if "undefined" is forcefully used as an "id", it will be treated as an empty string'(test: Test) {
const c = new Construct(undefined as any, undefined as any);
test.deepEqual(c.id, '');
test.done();
},

"dont allow unresolved tokens to be used in construct IDs"(test: Test) {
// GIVEN
const root = new Root();
const token = new Token(() => 'lazy');

test.throws(() => new Construct(root, 'in/Valid' ), Error, 'backslashes are not allowed');
// WHEN + THEN
test.throws(() => new Construct(root, `MyID: ${token}`), /ID components may not include unresolved tokens: MyID: \${Token/);
test.done();
},

Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/cdk/test/test.environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export = {
app.setContext(DEFAULT_ACCOUNT_CONTEXT_KEY, 'my-default-account');
app.setContext(DEFAULT_REGION_CONTEXT_KEY, 'my-default-region');

const stack = new Stack(app);
const stack = new Stack(app, 'my-stack');

test.equal(stack.env.account, 'my-default-account');
test.equal(stack.env.region, 'my-default-region');
Expand Down