From 7b97ebb84851a7817e35bc34966d2e079ed39d2b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sat, 29 Sep 2018 14:17:05 +0200 Subject: [PATCH 01/39] Fix tag manager tests to use resolve(token) instead of token.resolve() --- .../cdk/test/core/test.tag-manager.ts | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts index 5c096ba76a724..ae48ef6bdb41f 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -1,6 +1,7 @@ import { Test } from 'nodeunit'; import { Construct, Root } from '../../lib/core/construct'; import { ITaggable, TagManager } from '../../lib/core/tag-manager'; +import { resolve } from '../../lib/core/tokens'; class ChildTagger extends Construct implements ITaggable { public readonly tags: TagManager; @@ -32,10 +33,10 @@ export = { const tagArray = [tag]; for (const construct of [ctagger, ctagger1]) { - test.deepEqual(construct.tags.resolve(), tagArray); + test.deepEqual(resolve(construct.tags), tagArray); } - test.deepEqual(ctagger2.tags.resolve().length, 0); + test.deepEqual(resolve(ctagger2.tags).length, 0); test.done(); }, 'setTag with propagate false tags do not propagate'(test: Test) { @@ -51,10 +52,10 @@ export = { ctagger.tags.setTag(tag.key, tag.value, {propagate: false}); for (const construct of [ctagger1, ctagger2]) { - test.deepEqual(construct.tags.resolve().length, 0); + test.deepEqual(resolve(construct.tags).length, 0); } - test.deepEqual(ctagger.tags.resolve()[0].key, 'Name'); - test.deepEqual(ctagger.tags.resolve()[0].value, 'TheCakeIsALie'); + test.deepEqual(resolve(ctagger.tags)[0].key, 'Name'); + test.deepEqual(resolve(ctagger.tags)[0].value, 'TheCakeIsALie'); test.done(); }, 'setTag with overwrite false does not overwrite a tag'(test: Test) { @@ -62,7 +63,7 @@ export = { const ctagger = new ChildTagger(root, 'one'); ctagger.tags.setTag('Env', 'Dev'); ctagger.tags.setTag('Env', 'Prod', {overwrite: false}); - const result = ctagger.tags.resolve(); + const result = resolve(ctagger.tags); test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); test.done(); }, @@ -72,8 +73,8 @@ export = { const ctagger1 = new ChildTagger(ctagger, 'two'); ctagger.tags.setTag('Parent', 'Is always right'); ctagger1.tags.setTag('Parent', 'Is wrong', {sticky: false}); - const parent = ctagger.tags.resolve(); - const child = ctagger1.tags.resolve(); + const parent = resolve(ctagger.tags); + const child = resolve(ctagger1.tags); test.deepEqual(parent, child); test.done(); @@ -86,7 +87,7 @@ export = { const ctagger2 = new ChildTagger(cNoTag, 'four'); const tag = {key: 'Name', value: 'TheCakeIsALie'}; ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(ctagger2.tags.resolve(), [tag]); + test.deepEqual(resolve(ctagger2.tags), [tag]); test.done(); }, 'a tag can be removed and added back'(test: Test) { @@ -94,11 +95,11 @@ export = { const ctagger = new ChildTagger(root, 'one'); const tag = {key: 'Name', value: 'TheCakeIsALie'}; ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(ctagger.tags.resolve(), [tag]); + test.deepEqual(resolve(ctagger.tags), [tag]); ctagger.tags.removeTag(tag.key); - test.deepEqual(ctagger.tags.resolve(), []); + test.deepEqual(resolve(ctagger.tags), []); ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(ctagger.tags.resolve(), [tag]); + test.deepEqual(resolve(ctagger.tags), [tag]); test.done(); }, 'removeTag removes a tag by key'(test: Test) { @@ -115,7 +116,7 @@ export = { ctagger.tags.removeTag('Name'); for (const construct of [ctagger, ctagger1, ctagger2]) { - test.deepEqual(construct.tags.resolve().length, 0); + test.deepEqual(resolve(construct.tags).length, 0); } test.done(); }, @@ -125,9 +126,9 @@ export = { const ctagger1 = new ChildTagger(ctagger, 'two'); ctagger.tags.setTag('Env', 'Dev'); ctagger1.tags.removeTag('Env', {blockPropagate: true}); - const result = ctagger.tags.resolve(); + const result = resolve(ctagger.tags); test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); - test.deepEqual(ctagger1.tags.resolve(), []); + test.deepEqual(resolve(ctagger1.tags), []); test.done(); }, 'children can override parent propagated tags'(test: Test) { @@ -139,8 +140,8 @@ export = { ctagger.tags.setTag(tag2.key, tag2.value); ctagger.tags.setTag(tag.key, tag.value); ctagChild.tags.setTag(tag2.key, tag2.value); - const parentTags = ctagger.tags.resolve(); - const childTags = ctagChild.tags.resolve(); + const parentTags = resolve(ctagger.tags); + const childTags = resolve(ctagChild.tags); test.deepEqual(parentTags, [tag]); test.deepEqual(childTags, [tag2]); test.done(); @@ -167,11 +168,11 @@ export = { const cAll = ctagger.tags; const cProp = ctagChild.tags; - for (const tag of cAll.resolve()) { + for (const tag of resolve(cAll)) { const expectedTag = allTags.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } - for (const tag of cProp.resolve()) { + for (const tag of resolve(cProp)) { const expectedTag = tagsProp.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } From 1f938cfcdaee7b6ceaacbd27e57715d2ccd650d2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sat, 29 Sep 2018 14:19:45 +0200 Subject: [PATCH 02/39] Introduce StackAwareToken, make all pseudos Stack-Aware. Introduce context in resolve(), so that StackAwareToken can do something else if it's a Token from a different stack. Introduce a two-phase Freeze protocol which is a definite moment at which all lazy tokens must be resolved (so that the StackAwareTokens can build cross-Stack links). Change lock() into the freezing protocol, remove unlock as this was a speculative feature that is unused. --- packages/@aws-cdk/aws-s3/lib/bucket.ts | 2 +- packages/@aws-cdk/cdk/lib/app.ts | 16 +++ .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 36 ++++-- .../cloudformation/cloudformation-token.ts | 29 ++++- .../cdk/lib/cloudformation/condition.ts | 2 +- .../cdk/lib/cloudformation/include.ts | 2 +- .../cdk/lib/cloudformation/mapping.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 2 +- .../cdk/lib/cloudformation/parameter.ts | 20 +-- .../cdk/lib/cloudformation/permission.ts | 21 ++-- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 45 +++---- .../cdk/lib/cloudformation/resource.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/rule.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 116 ++++++++++++++---- packages/@aws-cdk/cdk/lib/core/construct.ts | 57 +++++---- packages/@aws-cdk/cdk/lib/core/tag-manager.ts | 4 +- packages/@aws-cdk/cdk/lib/core/tokens.ts | 31 +++-- .../cdk/test/cloudformation/test.arn.ts | 10 +- .../cdk/test/cloudformation/test.perms.ts | 10 +- .../cdk/test/cloudformation/test.stack.ts | 33 +++-- .../@aws-cdk/cdk/test/core/test.construct.ts | 12 +- 21 files changed, 299 insertions(+), 155 deletions(-) diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 1a55df6afd323..6b824e1d70bdc 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -153,7 +153,7 @@ export abstract class BucketRef extends cdk.Construct { * @returns an ObjectS3Url token */ public urlForObject(key?: any): string { - const components = [ 'https://', 's3.', new cdk.AwsRegion(), '.', new cdk.AwsURLSuffix(), '/', this.bucketName ]; + const components = [ 'https://', 's3.', new cdk.AwsRegion(this), '.', new cdk.AwsURLSuffix(this), '/', this.bucketName ]; if (key) { // trim prepending '/' if (typeof key === 'string' && key.startsWith('/')) { diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index bdf22d987c230..5ddfda8722766 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -62,6 +62,8 @@ export class App extends Root { return this.usage; } + this.freezeConstructTree(); + const result = this.runCommand(); return JSON.stringify(result, undefined, 2); } @@ -159,6 +161,20 @@ export class App extends Root { } } + /** + * Freeze all constructs in the tree + */ + public freezeConstructTree() { + // This is a two-step operation since you generally want the guarantee + // that at "frozen=true" a construct does not change anymore, but it may + // still be changed by siblings calling mutating operations. + // + // Only after all constructs in the tree have finished freezing do we + // consider a construct truly frozen (and can it be sythesized). + this.freeze(); + this.markFrozen(); + } + private collectRuntimeInformation(): cxapi.AppRuntime { const libraries: { [name: string]: string } = {}; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 01ce8c7a7b53a..a8ebff2b61969 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,4 +1,4 @@ -import { AwsAccountId, AwsPartition, AwsRegion, FnConcat, Token } from '..'; +import { AwsAccountId, AwsPartition, AwsRegion, Construct, FnConcat, Token } from '..'; import { FnSelect, FnSplit } from '../cloudformation/fn'; import { unresolved } from '../core/tokens'; import { CloudFormationToken } from './cloudformation-token'; @@ -22,16 +22,30 @@ export class ArnUtils { * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} * */ - public static fromComponents(components: ArnComponents): string { - const partition = components.partition == null - ? new AwsPartition() - : components.partition; - const region = components.region == null - ? new AwsRegion() - : components.region; - const account = components.account == null - ? new AwsAccountId() - : components.account; + public static fromComponents(components: ArnComponents, anchor?: Construct): string { + let partition = components.partition; + if (partition == null) { + if (!anchor) { + throw new Error('Must provide anchor when using current partition'); + } + partition = new AwsPartition(anchor).toString(); + } + + let region = components.region; + if (region == null) { + if (!anchor) { + throw new Error('Must provide anchor when using current region'); + } + region = new AwsRegion(anchor).toString(); + } + + let account = components.account; + if (account == null) { + if (!anchor) { + throw new Error('Must provide anchor when using current account'); + } + account = new AwsAccountId(anchor).toString(); + } const values = [ 'arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource ]; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 024414218e3ce..203521b70655d 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -1,4 +1,5 @@ -import { resolve, Token } from "../core/tokens"; +import { Construct } from "../core/construct"; +import { ContextMap, resolve, Token } from "../core/tokens"; /** * Base class for CloudFormation built-ins @@ -13,7 +14,28 @@ export class CloudFormationToken extends Token { } } -import { FnConcat } from "./fn"; +export class StackAwareCloudFormationToken extends CloudFormationToken { + private readonly tokenStack: Stack; + + constructor(anchor: Construct, value: any, displayName?: string) { + if (typeof(value) === 'function') { + throw new Error('StackAwareCloudFormationToken can only contain eager values'); + } + super(value, displayName); + this.tokenStack = Stack.find(anchor); + } + + public resolve(context: ContextMap): any { + const consumingStack = context.stack; + if (consumingStack && this.tokenStack !== consumingStack) { + // We're trying to resolve a cross-stack reference + consumingStack.addStackDependency(this.tokenStack); + return this.tokenStack.exportValue(this, consumingStack); + } + // Stack-local resolution + return super.resolve(context); + } +} /** * Return whether the given value represents a CloudFormation intrinsic @@ -26,3 +48,6 @@ export function isIntrinsic(x: any) { return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); } + +import { FnConcat } from "./fn"; +import { Stack } from "./stack"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index e21558225a424..4c152d9ad40aa 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -25,7 +25,7 @@ export class Condition extends Referenceable { this.expression = props && props.expression; } - public toCloudFormation(): object { + protected renderCloudFormation(): object { return { Conditions: { [this.logicalId]: this.expression diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index 18756222de393..1db60630f5779 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -31,7 +31,7 @@ export class Include extends StackElement { this.template = props.template; } - public toCloudFormation() { + protected renderCloudFormation() { return this.template; } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index b77b3af8f1673..286cb22a3c258 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -43,7 +43,7 @@ export class Mapping extends Referenceable { return new FnFindInMap(this.logicalId, key1, key2); } - public toCloudFormation(): object { + protected renderCloudFormation(): object { return { Mappings: { [this.logicalId]: this.mapping diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index fa39cf0990f0c..677be7ef4852f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -107,7 +107,7 @@ export class Output extends StackElement { return new FnImportValue(this.export); } - public toCloudFormation(): object { + protected renderCloudFormation(): object { return { Outputs: { [this.logicalId]: { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index a7ec01fb989d0..24ecea94e4d17 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { Token } from '../core/tokens'; +import { ContextMap, Token } from '../core/tokens'; import { Ref, Referenceable } from './stack'; export interface ParameterProps { @@ -92,7 +92,15 @@ export class Parameter extends Referenceable { this.value = new Ref(this); } - public toCloudFormation(): object { + /** + * Allows using parameters as tokens without the need to dereference them. + * This implicitly implements Token, until we make it an interface. + */ + public resolve(_context: ContextMap): any { + return this.value; + } + + protected renderCloudFormation(): object { return { Parameters: { [this.logicalId]: { @@ -111,12 +119,4 @@ export class Parameter extends Referenceable { } }; } - - /** - * Allows using parameters as tokens without the need to dereference them. - * This implicitly implements Token, until we make it an interface. - */ - public resolve(): any { - return this.value; - } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/permission.ts b/packages/@aws-cdk/cdk/lib/cloudformation/permission.ts index 33ad7339f1881..3a9e9d3848877 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/permission.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/permission.ts @@ -1,4 +1,5 @@ -import { Token } from '../core/tokens'; +import { Construct } from '../core/construct'; +import { ContextMap, Token } from '../core/tokens'; import { AwsAccountId, AwsPartition } from './pseudo'; export class PolicyDocument extends Token { @@ -82,8 +83,8 @@ export class ArnPrincipal extends PolicyPrincipal { } export class AccountPrincipal extends ArnPrincipal { - constructor(public readonly accountId: any) { - super(`arn:${new AwsPartition()}:iam::${accountId}:root`); + constructor(public readonly anchor: Construct, public readonly accountId: any) { + super(`arn:${new AwsPartition(anchor)}:iam::${accountId}:root`); } } @@ -137,8 +138,8 @@ export class FederatedPrincipal extends PolicyPrincipal { } export class AccountRootPrincipal extends AccountPrincipal { - constructor() { - super(new AwsAccountId()); + constructor(anchor: Construct) { + super(anchor, new AwsAccountId(anchor)); } } @@ -212,8 +213,8 @@ export class PolicyStatement extends Token { return this.addPrincipal(new ArnPrincipal(arn)); } - public addAwsAccountPrincipal(accountId: string): PolicyStatement { - return this.addPrincipal(new AccountPrincipal(accountId)); + public addAwsAccountPrincipal(anchor: Construct, accountId: string): PolicyStatement { + return this.addPrincipal(new AccountPrincipal(anchor, accountId)); } public addServicePrincipal(service: string): PolicyStatement { @@ -224,8 +225,8 @@ export class PolicyStatement extends Token { return this.addPrincipal(new FederatedPrincipal(federated, conditions)); } - public addAccountRootPrincipal(): PolicyStatement { - return this.addPrincipal(new AccountRootPrincipal()); + public addAccountRootPrincipal(anchor: Construct): PolicyStatement { + return this.addPrincipal(new AccountRootPrincipal(anchor)); } // @@ -322,7 +323,7 @@ export class PolicyStatement extends Token { // Serialization // - public resolve(): any { + public resolve(_context: ContextMap): any { return this.toJson(); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index e576320655ec2..4f4c84a9e7dde 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,61 +1,62 @@ -import { CloudFormationToken } from './cloudformation-token'; +import { Construct } from '../core/construct'; +import { CloudFormationToken, StackAwareCloudFormationToken } from './cloudformation-token'; -export class PseudoParameter extends CloudFormationToken { - constructor(name: string) { - super({ Ref: name }, name); +export class PseudoParameter extends StackAwareCloudFormationToken { + constructor(anchor: Construct, name: string) { + super(anchor, { Ref: name }, name); } } export class AwsAccountId extends PseudoParameter { - constructor() { - super('AWS::AccountId'); + constructor(anchor: Construct) { + super(anchor, 'AWS::AccountId'); } } export class AwsDomainSuffix extends PseudoParameter { - constructor() { - super('AWS::DomainSuffix'); + constructor(anchor: Construct) { + super(anchor, 'AWS::DomainSuffix'); } } export class AwsURLSuffix extends PseudoParameter { - constructor() { - super('AWS::URLSuffix'); + constructor(anchor: Construct) { + super(anchor, 'AWS::URLSuffix'); } } export class AwsNotificationARNs extends PseudoParameter { - constructor() { - super('AWS::NotificationARNs'); + constructor(anchor: Construct) { + super(anchor, 'AWS::NotificationARNs'); } } -export class AwsNoValue extends PseudoParameter { +export class AwsNoValue extends CloudFormationToken { constructor() { - super('AWS::NoValue'); + super({ Ref: 'AWS::NoValue' }); } } export class AwsPartition extends PseudoParameter { - constructor() { - super('AWS::Partition'); + constructor(anchor: Construct) { + super(anchor, 'AWS::Partition'); } } export class AwsRegion extends PseudoParameter { - constructor() { - super('AWS::Region'); + constructor(anchor: Construct) { + super(anchor, 'AWS::Region'); } } export class AwsStackId extends PseudoParameter { - constructor() { - super('AWS::StackId'); + constructor(anchor: Construct) { + super(anchor, 'AWS::StackId'); } } export class AwsStackName extends PseudoParameter { - constructor() { - super('AWS::StackName'); + constructor(anchor: Construct) { + super(anchor, 'AWS::StackName'); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index a8c90de6e6348..1c5d733b67158 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -174,7 +174,7 @@ export class Resource extends Referenceable { /** * Emits CloudFormation for this resource. */ - public toCloudFormation(): object { + protected renderCloudFormation(): object { try { // merge property overrides onto properties and then render (and validate). const properties = this.renderProperties(deepMerge(this.properties || { }, this.untypedPropertyOverrides)); diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index 29a881032809e..79e40296f1fd9 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -92,7 +92,7 @@ export class Rule extends Referenceable { }); } - public toCloudFormation(): object { + protected renderCloudFormation(): object { return { Rules: { [this.logicalId]: { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 82cce8b4f1538..3447ea9a19f9f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -92,6 +92,11 @@ export class Stack extends Construct { */ public readonly name: string; + /** + * Other stacks this stack depends on + */ + private readonly dependsOnStacks = new Set(); + /** * Creates a new stack. * @@ -130,35 +135,33 @@ export class Stack extends Construct { * the tree and invoking toCloudFormation() on all Entity objects. */ public toCloudFormation() { - // before we begin synthesis, we shall lock this stack, so children cannot be added - this.lock(); - - try { - const template: any = { - Description: this.templateOptions.description, - Transform: this.templateOptions.transform, - AWSTemplateFormatVersion: this.templateOptions.templateFormatVersion, - Metadata: this.templateOptions.metadata - }; - - const elements = stackElements(this); - const fragments = elements.map(e => e.toCloudFormation()); - - // merge in all CloudFormation fragments collected from the tree - for (const fragment of fragments) { - merge(template, fragment); - } + // We must double-check to see that our stack is frozen here, and + // perform the freezing if not. This is to support unit tests that only + // work at the level of Stacks. + if (!this.frozen) { + this.freeze(); + this.markFrozen(); + } - // resolve all tokens and remove all empties - const ret = resolve(template) || { }; + const template: any = { + Description: this.templateOptions.description, + Transform: this.templateOptions.transform, + AWSTemplateFormatVersion: this.templateOptions.templateFormatVersion, + Metadata: this.templateOptions.metadata + }; - this.logicalIds.assertAllRenamesApplied(); + const elements = stackElements(this); + const fragments = elements.map(e => e.toCloudFormation()); - return ret; - } finally { - // allow mutations after synthesis is finished. - this.unlock(); + // merge in all CloudFormation fragments collected from the tree + for (const fragment of fragments) { + merge(template, fragment); } + + this.logicalIds.assertAllRenamesApplied(); + + // FIXME: should use removeEmpty() instead of resolve() + return resolve(template); } /** @@ -197,6 +200,35 @@ export class Stack extends Construct { this.logicalIds.renameLogical(oldId, newId); } + /** + * Add a dependency between this stack and another stack + */ + public addStackDependency(stack: Stack) { + if (stack.dependsOnStack(this)) { + // tslint:disable-next-line:max-line-length + throw new Error(`Stack '${this.name}' already depends on stack '${stack.name}'. Adding this dependency would create a cyclic reference.`); + } + this.dependsOnStacks.add(stack); + } + + /** + * Export a Token value for use in another stack + */ + public exportValue(tokenValue: Token, consumingStack: Stack): Token { + if (this.env.account !== consumingStack.env.account || this.env.region !== consumingStack.env.region) { + throw new Error('Can only reference cross stacks in the same region and account.'); + } + + // Ensure a singleton Output for this value + const resolved = resolve(tokenValue); + const id = 'Output' + JSON.stringify(resolved); + let output = this.tryFindChild(id) as Output; + if (!output) { + output = new Output(this, id, { value: tokenValue }); + } + return output.makeImportValue(); + } + /** * Validate stack name * @@ -228,6 +260,17 @@ export class Stack extends Construct { return env; } + + /** + * Check whether this stack has a (transitive) dependency on another stack + */ + private dependsOnStack(other: Stack) { + if (this === other) { return true; } + for (const dep of this.dependsOnStacks) { + if (dep.dependsOnStack(other)) { return true; } + } + return false; + } } function merge(template: any, part: any) { @@ -294,6 +337,8 @@ export abstract class StackElement extends Construct implements IDependable { */ protected stack: Stack; + private frozenRepresentation?: object; + /** * 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). @@ -362,7 +407,23 @@ export abstract class StackElement extends Construct implements IDependable { * } * } */ - public abstract toCloudFormation(): object; + public toCloudFormation(): object { + if (!this.frozen) { + throw new Error('StackElement must be frozen before it is synthesized'); + } + return this.frozenRepresentation!; + } + + protected abstract renderCloudFormation(): object; + + protected freeze() { + this.freezeChildren(); + + const stack = Stack.find(this); + this.frozenRepresentation = resolve(this.renderCloudFormation(), { + context: { stack } + }); + } } /** @@ -447,3 +508,6 @@ export class Ref extends CloudFormationToken { super({ Ref: element.logicalId }, `${element.logicalId}.Ref`); } } + +// Has to be at the end to prevent circular imports +import { Output } from './output'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 66b720aec5d0a..608c202fd17c1 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -42,7 +42,7 @@ export class Construct { * If this is set to 'true'. addChild() calls for this construct and any child * will fail. This is used to prevent tree mutations during synthesis. */ - private _locked = false; + private _frozen = false; /** * Creates a new construct node. @@ -288,6 +288,30 @@ export class Construct { return ret; } + /** + * Signal that tree construction has finished + * + * Subclasses can override this to take mutating actions + * at the last moment. After this method, no mutation + * should occur anymore. + */ + protected freeze(): void { + this.freezeChildren(); + } + + protected markFrozen() { + this._frozen = true; + for (const child of this.children) { + child.markFrozen(); + } + } + + protected freezeChildren() { + for (const child of this.children) { + child.freeze(); + } + } + /** * Validate that the id of the construct legal. * Construct IDs can be any characters besides the path separator. @@ -331,14 +355,13 @@ export class Construct { * @returns The resolved path part name of the child */ protected addChild(child: Construct, childName: string) { - if (this.locked) { - + if (this.frozen) { // special error if root is locked if (!this.path) { - throw new Error('Cannot add children during synthesis'); + throw new Error('Cannot add children after freezing'); } - throw new Error(`Cannot add children to "${this.path}" during synthesis`); + throw new Error(`Cannot add children to "${this.path}" after freezing`); } if (childName in this._children) { @@ -353,14 +376,7 @@ export class Construct { * call, no more children can be added to this construct or to any children. */ protected lock() { - this._locked = true; - } - - /** - * Unlocks this costruct and allows mutations (adding children). - */ - protected unlock() { - this._locked = false; + this._frozen = true; } /** @@ -373,19 +389,10 @@ export class Construct { } /** - * Returns true if this construct or any of it's parent constructs are - * locked. + * Returns true if this construct is frozen. */ - protected get locked() { - if (this._locked) { - return true; - } - - if (this.parent && this.parent.locked) { - return true; - } - - return false; + protected get frozen() { + return this._frozen; } } diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts index 7a25100eb47e5..69fea1691b2e0 100644 --- a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -1,5 +1,5 @@ import { Construct } from './construct'; -import { Token } from './tokens'; +import { ContextMap, Token } from './tokens'; /** * ITaggable indicates a entity manages tags via the `tags` property @@ -171,7 +171,7 @@ export class TagManager extends Token { /** * Converts the `tags` to a Token for use in lazy evaluation */ - public resolve(): any { + public resolve(_context: ContextMap): any { // need this for scoping const blockedTags = this.blockedTags; function filterTags(_tags: FullTags, filter: TagProps = {}): Tags { diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index ebdbb5f671e09..4db01feca1d82 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -6,6 +6,14 @@ import { Construct } from "./construct"; */ export const RESOLVE_METHOD = 'resolve'; +export type ContextMap = {[key: string]: any}; + +export interface ResolveOptions { + context?: ContextMap; + + prefix?: string[]; +} + /** * Represents a special or lazily-evaluated value. * @@ -44,10 +52,10 @@ export class Token { /** * @returns The resolved value for this token. */ - public resolve(): any { + public resolve(context: ContextMap): any { let value = this.valueOrFunction; if (typeof(value) === 'function') { - value = value(); + value = value(context); } return value; @@ -119,14 +127,15 @@ export function unresolved(obj: any): obj is Token { * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. * * @param obj The object to resolve. - * @param prefix Prefix key path components for diagnostics. + * @param options Resolution options (prefix and context) */ -export function resolve(obj: any, prefix?: string[]): any { - const path = prefix || [ ]; - const pathName = '/' + path.join('/'); +export function resolve(obj: any, options: ResolveOptions = {}): any { + const prefix = options.prefix || [ ]; + const context = options.context || {}; + const pathName = '/' + prefix.join('/'); // protect against cyclic references by limiting depth. - if (path.length > 200) { + if (prefix.length > 200) { throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); } @@ -174,8 +183,8 @@ export function resolve(obj: any, prefix?: string[]): any { // if (unresolved(obj)) { - const value = obj[RESOLVE_METHOD](); - return resolve(value, path); + const value = obj[RESOLVE_METHOD](context); + return resolve(value, { context, prefix }); } // @@ -184,7 +193,7 @@ export function resolve(obj: any, prefix?: string[]): any { if (Array.isArray(obj)) { const arr = obj - .map((x, i) => resolve(x, path.concat(i.toString()))) + .map((x, i) => resolve(x, { context, prefix: prefix.concat(i.toString()) })) .filter(x => typeof(x) !== 'undefined'); return arr; @@ -208,7 +217,7 @@ export function resolve(obj: any, prefix?: string[]): any { throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); } - const value = resolve(obj[key], path.concat(key)); + const value = resolve(obj[key], { context, prefix: prefix.concat(key) }); // skip undefined if (typeof(value) === 'undefined') { diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index c8879d5a119c9..404f39327bfe9 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,12 +1,14 @@ import { Test } from 'nodeunit'; -import { ArnComponents, ArnUtils, resolve, Token } from '../../lib'; +import { ArnComponents, ArnUtils, resolve, Stack, Token } from '../../lib'; export = { 'create from components with defaults'(test: Test) { + const stack = new Stack(); + const arn = ArnUtils.fromComponents({ service: 'sqs', resource: 'myqueuename' - }); + }, stack); test.deepEqual(resolve(arn), { 'Fn::Join': [ '', @@ -84,12 +86,14 @@ export = { }, 'resourcePathSep can be set to ":" instead of the default "/"'(test: Test) { + const stack = new Stack(); + const arn = ArnUtils.fromComponents({ service: 'codedeploy', resource: 'application', sep: ':', resourceName: 'WordPress_App' - }); + }, stack); test.deepEqual(resolve(arn), { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.perms.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.perms.ts index 24fcfdf60fbd0..377e72211df95 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.perms.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.perms.ts @@ -1,8 +1,10 @@ import { Test } from 'nodeunit'; -import { CanonicalUserPrincipal, FnConcat, PolicyDocument, PolicyStatement, resolve } from '../../lib'; +import { CanonicalUserPrincipal, FnConcat, PolicyDocument, PolicyStatement, resolve, Stack } from '../../lib'; export = { 'the Permission class is a programming model for iam'(test: Test) { + const stack = new Stack(); + const p = new PolicyStatement(); p.addAction('sqs:SendMessage'); p.addActions('dynamodb:CreateTable', 'dynamodb:DeleteTable'); @@ -10,7 +12,7 @@ export = { p.addResource('yourQueue'); p.addAllResources(); - p.addAwsAccountPrincipal(new FnConcat('my', 'account', 'name').toString()); + p.addAwsAccountPrincipal(stack, new FnConcat('my', 'account', 'name').toString()); p.limitToAccount('12221121221'); test.deepEqual(resolve(p), { Action: @@ -105,8 +107,10 @@ export = { }, 'addAccountRootPrincipal adds a principal with the current account root'(test: Test) { + const stack = new Stack(); + const p = new PolicyStatement(); - p.addAccountRootPrincipal(); + p.addAccountRootPrincipal(stack); test.deepEqual(resolve(p), { Effect: "Allow", Principal: { diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index 8e27bc94f5818..95b13771b9337 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { App, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack, Token } from '../../lib'; +import { App, AwsAccountId, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack } from '../../lib'; export = { 'a stack can be serialized into a CloudFormation template, initially it\'s empty'(test: Test) { @@ -172,20 +172,29 @@ export = { test.done(); }, - 'Can\'t add children during synthesis'(test: Test) { - const stack = new Stack(); + 'Pseudo values attached to one stack can be referenced in another'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new AwsAccountId(stack1); + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another stack + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); - // add a construct with a token that when resolved adds a child. this - // means that this child is going to be added during synthesis and this - // is a no-no. - new Resource(stack, 'Resource', { type: 'T', properties: { - foo: new Token(() => new Construct(stack, 'Foo')) - }}); + // THEN + // Need to freeze manually now, because we want to test using JUST the stacks. + app.freezeConstructTree(); - test.throws(() => stack.toCloudFormation(), /Cannot add children during synthesis/); + test.deepEqual(stack1.toCloudFormation(), { + Output: { + } + }); - // okay to add after synthesis - new Construct(stack, 'C1'); + test.deepEqual(stack2.toCloudFormation(), { + Parameters: { + } + }); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/core/test.construct.ts b/packages/@aws-cdk/cdk/test/core/test.construct.ts index 59e9d5c63a38e..6ea984a1c64db 100644 --- a/packages/@aws-cdk/cdk/test/core/test.construct.ts +++ b/packages/@aws-cdk/cdk/test/core/test.construct.ts @@ -350,11 +350,7 @@ export = { class LockableConstruct extends Construct { public lockMe() { - this.lock(); - } - - public unlockMe() { - this.unlock(); + this.markFrozen(); } } @@ -374,12 +370,6 @@ export = { test.throws(() => new Construct(c1a, 'fail2'), /Cannot add children to "c0a\/c1a" during synthesis/); test.throws(() => new Construct(c1b, 'fail3'), /Cannot add children to "c0a\/c1b" during synthesis/); - c0a.unlockMe(); - - new Construct(c0a, 'c0aZ'); - new Construct(c1a, 'c1aZ'); - new Construct(c1b, 'c1bZ'); - test.done(); } }; From d02e16da2e4b573aa7e42a9eb39e2c1d437edb31 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 18 Dec 2018 12:21:22 +0100 Subject: [PATCH 03/39] Remove concept of freezing --- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 2 +- packages/@aws-cdk/cdk/lib/app.ts | 17 +--- .../cdk/lib/cloudformation/condition.ts | 2 +- .../cdk/lib/cloudformation/include.ts | 2 +- .../cdk/lib/cloudformation/mapping.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 2 +- .../cdk/lib/cloudformation/parameter.ts | 12 +-- .../cdk/lib/cloudformation/resource.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/rule.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 85 +++++++++---------- packages/@aws-cdk/cdk/lib/core/construct.ts | 57 ++++++------- .../cdk/test/cloudformation/test.stack.ts | 2 +- .../@aws-cdk/cdk/test/core/test.construct.ts | 12 ++- 13 files changed, 88 insertions(+), 111 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 7fe96f9732f1e..e5a371650e141 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -159,7 +159,7 @@ class LatestDeploymentResource extends CfnDeployment { public addToLogicalId(data: unknown) { // if the construct is locked, it means we are already synthesizing and then // we can't modify the hash because we might have already calculated it. - if (this.frozen) { + if (this.locked) { throw new Error('Cannot modify the logical ID when the construct is locked'); } diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index 205f3765bca58..6d6337e76a9a1 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -42,8 +42,6 @@ export class App extends Root { return; } - this.freezeConstructTree(); - const result: cxapi.SynthesizeResponse = { version: cxapi.PROTO_RESPONSE_VERSION, stacks: this.synthesizeStacks(Object.keys(this.stacks)), @@ -125,18 +123,7 @@ export class App extends Root { } } - /** - * Freeze all constructs in the tree - */ - public freezeConstructTree() { - // This is a two-step operation since you generally want the guarantee - // that at "frozen=true" a construct does not change anymore, but it may - // still be changed by siblings calling mutating operations. - // - // Only after all constructs in the tree have finished freezing do we - // consider a construct truly frozen (and can it be sythesized). - this.freeze(); - this.markFrozen(); + public applyCrossEnvironmentReferences() { } private collectRuntimeInformation(): cxapi.AppRuntime { @@ -242,4 +229,4 @@ function getJsiiAgentVersion() { } return jsiiAgent; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index 4c152d9ad40aa..e21558225a424 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -25,7 +25,7 @@ export class Condition extends Referenceable { this.expression = props && props.expression; } - protected renderCloudFormation(): object { + public toCloudFormation(): object { return { Conditions: { [this.logicalId]: this.expression diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index 1db60630f5779..18756222de393 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -31,7 +31,7 @@ export class Include extends StackElement { this.template = props.template; } - protected renderCloudFormation() { + public toCloudFormation() { return this.template; } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index 286cb22a3c258..b77b3af8f1673 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -43,7 +43,7 @@ export class Mapping extends Referenceable { return new FnFindInMap(this.logicalId, key1, key2); } - protected renderCloudFormation(): object { + public toCloudFormation(): object { return { Mappings: { [this.logicalId]: this.mapping diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index 677be7ef4852f..fa39cf0990f0c 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -107,7 +107,7 @@ export class Output extends StackElement { return new FnImportValue(this.export); } - protected renderCloudFormation(): object { + public toCloudFormation(): object { return { Outputs: { [this.logicalId]: { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index 24ecea94e4d17..20533e7bfcc89 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { ContextMap, Token } from '../core/tokens'; +import { Token } from '../core/tokens'; import { Ref, Referenceable } from './stack'; export interface ParameterProps { @@ -92,15 +92,7 @@ export class Parameter extends Referenceable { this.value = new Ref(this); } - /** - * Allows using parameters as tokens without the need to dereference them. - * This implicitly implements Token, until we make it an interface. - */ - public resolve(_context: ContextMap): any { - return this.value; - } - - protected renderCloudFormation(): object { + public toCloudFormation(): object { return { Parameters: { [this.logicalId]: { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 3255070b29f9f..06895929f647f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -178,7 +178,7 @@ export class Resource extends Referenceable { /** * Emits CloudFormation for this resource. */ - protected renderCloudFormation(): object { + public toCloudFormation(): object { try { // merge property overrides onto properties and then render (and validate). const properties = this.renderProperties(deepMerge(this.properties || { }, this.untypedPropertyOverrides)); diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index 79e40296f1fd9..29a881032809e 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -92,7 +92,7 @@ export class Rule extends Referenceable { }); } - protected renderCloudFormation(): object { + public toCloudFormation(): object { return { Rules: { [this.logicalId]: { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index f4fb2aebf523b..f61595f7a8ec3 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -106,6 +106,14 @@ export class Stack extends Construct { */ private readonly dependsOnStacks = new Set(); + /** + * A construct to hold cross-stack exports + * + * This mostly exists to trigger LogicalID munging, which would be + * disabled if we parented constructs directly under Stack. + */ + private crossStackExports?: Construct; + /** * Creates a new stack. * @@ -144,33 +152,35 @@ export class Stack extends Construct { * the tree and invoking toCloudFormation() on all Entity objects. */ public toCloudFormation() { - // We must double-check to see that our stack is frozen here, and - // perform the freezing if not. This is to support unit tests that only - // work at the level of Stacks. - if (!this.frozen) { - this.freeze(); - this.markFrozen(); - } + // before we begin synthesis, we shall lock this stack, so children cannot be added + this.lock(); + + try { + const template: any = { + Description: this.templateOptions.description, + Transform: this.templateOptions.transform, + AWSTemplateFormatVersion: this.templateOptions.templateFormatVersion, + Metadata: this.templateOptions.metadata + }; + + const elements = stackElements(this); + const fragments = elements.map(e => e.toCloudFormation()); + + // merge in all CloudFormation fragments collected from the tree + for (const fragment of fragments) { + merge(template, fragment); + } - const template: any = { - Description: this.templateOptions.description, - Transform: this.templateOptions.transform, - AWSTemplateFormatVersion: this.templateOptions.templateFormatVersion, - Metadata: this.templateOptions.metadata - }; + // resolve all tokens and remove all empties + const ret = resolve(template) || { }; - const elements = stackElements(this); - const fragments = elements.map(e => e.toCloudFormation()); + this.logicalIds.assertAllRenamesApplied(); - // merge in all CloudFormation fragments collected from the tree - for (const fragment of fragments) { - merge(template, fragment); + return ret; + } finally { + // allow mutations after synthesis is finished. + this.unlock(); } - - this.logicalIds.assertAllRenamesApplied(); - - // FIXME: should use removeEmpty() instead of resolve() - return resolve(template); } /** @@ -254,9 +264,12 @@ export class Stack extends Construct { // Ensure a singleton Output for this value const resolved = resolve(tokenValue); const id = 'Output' + JSON.stringify(resolved); - let output = this.tryFindChild(id) as Output; + if (this.crossStackExports === undefined) { + this.crossStackExports = new Construct(this, 'Exports'); + } + let output = this.crossStackExports.tryFindChild(id) as Output; if (!output) { - output = new Output(this, id, { value: tokenValue }); + output = new Output(this.crossStackExports, id, { value: tokenValue }); } return output.makeImportValue(); } @@ -369,8 +382,6 @@ export abstract class StackElement extends Construct implements IDependable { */ protected stack: Stack; - private frozenRepresentation?: object; - /** * 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). @@ -439,23 +450,7 @@ export abstract class StackElement extends Construct implements IDependable { * } * } */ - public toCloudFormation(): object { - if (!this.frozen) { - throw new Error('StackElement must be frozen before it is synthesized'); - } - return this.frozenRepresentation!; - } - - protected abstract renderCloudFormation(): object; - - protected freeze() { - this.freezeChildren(); - - const stack = Stack.find(this); - this.frozenRepresentation = resolve(this.renderCloudFormation(), { - context: { stack } - }); - } + public abstract toCloudFormation(): object; } /** @@ -533,4 +528,4 @@ export class Ref extends CloudFormationToken { } // Has to be at the end to prevent circular imports -import { Output } from './output'; +import { Output } from './output'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 727069ba0cacc..1fff75c333bbe 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -42,7 +42,7 @@ export class Construct { * If this is set to 'true'. addChild() calls for this construct and any child * will fail. This is used to prevent tree mutations during synthesis. */ - private _frozen = false; + private _locked = false; /** * Creates a new construct node. @@ -299,30 +299,6 @@ export class Construct { return ret; } - /** - * Signal that tree construction has finished - * - * Subclasses can override this to take mutating actions - * at the last moment. After this method, no mutation - * should occur anymore. - */ - protected freeze(): void { - this.freezeChildren(); - } - - protected markFrozen() { - this._frozen = true; - for (const child of this.children) { - child.markFrozen(); - } - } - - protected freezeChildren() { - for (const child of this.children) { - child.freeze(); - } - } - /** * Validate that the id of the construct legal. * Construct IDs can be any characters besides the path separator. @@ -364,13 +340,14 @@ export class Construct { * @returns The resolved path part name of the child */ protected addChild(child: Construct, childName: string) { - if (this.frozen) { + if (this.locked) { + // special error if root is locked if (!this.path) { - throw new Error('Cannot add children after freezing'); + throw new Error('Cannot add children during synthesis'); } - throw new Error(`Cannot add children to "${this.path}" after freezing`); + throw new Error(`Cannot add children to "${this.path}" during synthesis`); } if (childName in this._children) { @@ -385,7 +362,14 @@ export class Construct { * call, no more children can be added to this construct or to any children. */ protected lock() { - this._frozen = true; + this._locked = true; + } + + /** + * Unlocks this costruct and allows mutations (adding children). + */ + protected unlock() { + this._locked = false; } /** @@ -398,10 +382,19 @@ export class Construct { } /** - * Returns true if this construct is frozen. + * Returns true if this construct or any of it's parent constructs are + * locked. */ - protected get frozen() { - return this._frozen; + protected get locked() { + if (this._locked) { + return true; + } + + if (this.parent && this.parent.locked) { + return true; + } + + return false; } /** diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index 8bba584758ae3..6890581622fb1 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -184,7 +184,7 @@ export = { // THEN // Need to freeze manually now, because we want to test using JUST the stacks. - app.freezeConstructTree(); + app.applyCrossEnvironmentReferences(); test.deepEqual(stack1.toCloudFormation(), { Output: { diff --git a/packages/@aws-cdk/cdk/test/core/test.construct.ts b/packages/@aws-cdk/cdk/test/core/test.construct.ts index 932a1856e66e9..22146f69fd077 100644 --- a/packages/@aws-cdk/cdk/test/core/test.construct.ts +++ b/packages/@aws-cdk/cdk/test/core/test.construct.ts @@ -371,7 +371,11 @@ export = { class LockableConstruct extends Construct { public lockMe() { - this.markFrozen(); + this.lock(); + } + + public unlockMe() { + this.unlock(); } } @@ -391,6 +395,12 @@ export = { test.throws(() => new Construct(c1a, 'fail2'), /Cannot add children to "c0a\/c1a" during synthesis/); test.throws(() => new Construct(c1b, 'fail3'), /Cannot add children to "c0a\/c1b" during synthesis/); + c0a.unlockMe(); + + new Construct(c0a, 'c0aZ'); + new Construct(c1a, 'c1aZ'); + new Construct(c1b, 'c1bZ'); + test.done(); } }; From ac062fc0121955a5ab93c50af7d5079be8dec205 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 18 Dec 2018 12:49:30 +0100 Subject: [PATCH 04/39] Add explicit preprocessor step for references --- packages/@aws-cdk/cdk/lib/app.ts | 5 +++ .../cloudformation/cloudformation-token.ts | 21 ++++++++---- .../cdk/lib/cloudformation/condition.ts | 6 +++- .../cdk/lib/cloudformation/include.ts | 5 ++- .../cdk/lib/cloudformation/mapping.ts | 6 +++- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 24 +++++++++----- .../cdk/lib/cloudformation/parameter.ts | 16 ++++++++-- .../cdk/lib/cloudformation/resource.ts | 6 +++- .../@aws-cdk/cdk/lib/cloudformation/rule.ts | 5 ++- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 32 +++++++++++++++++-- packages/@aws-cdk/cdk/lib/core/tokens.ts | 2 +- .../cdk/test/cloudformation/test.stack.ts | 13 ++++++-- .../cdk/test/core/test.tag-manager.ts | 2 +- 13 files changed, 116 insertions(+), 27 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index 6d6337e76a9a1..1c0eef70cdb01 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -42,6 +42,8 @@ export class App extends Root { return; } + this.applyCrossEnvironmentReferences(); + const result: cxapi.SynthesizeResponse = { version: cxapi.PROTO_RESPONSE_VERSION, stacks: this.synthesizeStacks(Object.keys(this.stacks)), @@ -124,6 +126,9 @@ export class App extends Root { } public applyCrossEnvironmentReferences() { + for (const stack of Object.values(this.stacks)) { + stack.applyCrossEnvironmentReferences(); + } } private collectRuntimeInformation(): cxapi.AppRuntime { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 91019c40eef80..5a60b76885515 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -1,5 +1,5 @@ import { Construct } from "../core/construct"; -import { ContextMap, resolve, Token } from "../core/tokens"; +import { resolve, Token } from "../core/tokens"; /** * Base class for CloudFormation built-ins @@ -15,6 +15,12 @@ export class CloudFormationToken extends Token { } export class StackAwareCloudFormationToken extends CloudFormationToken { + public static isInstance(x: any): x is StackAwareCloudFormationToken { + return x && x._isStackAwareCloudFormationToken; + } + + protected readonly _isStackAwareCloudFormationToken: boolean; + private readonly tokenStack?: Stack; constructor(anchor: Construct | undefined, value: any, displayName?: string) { @@ -22,21 +28,24 @@ export class StackAwareCloudFormationToken extends CloudFormationToken { throw new Error('StackAwareCloudFormationToken can only contain eager values'); } super(value, displayName); + this._isStackAwareCloudFormationToken = true; if (anchor !== undefined) { this.tokenStack = Stack.find(anchor); } } - public resolve(context: ContextMap): any { - const consumingStack = context.stack; - if (this.tokenStack && consumingStack && this.tokenStack !== consumingStack) { + /** + * In a consuming context, potentially substitute this Token with a different one + */ + public substituteToken(consumingStack: Stack): Token { + if (this.tokenStack && this.tokenStack !== consumingStack) { // We're trying to resolve a cross-stack reference consumingStack.addStackDependency(this.tokenStack); return this.tokenStack.exportValue(this, consumingStack); } - // Stack-local resolution - return super.resolve(context); + // In case of doubt, return same Token + return this; } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index e21558225a424..2b13ade354972 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { FnCondition } from './fn'; -import { Referenceable } from './stack'; +import { Referenceable, Stack } from './stack'; export interface ConditionProps { expression?: FnCondition; @@ -32,4 +32,8 @@ export class Condition extends Referenceable { } }; } + + public substituteCrossStackReferences(sourceStack: Stack): void { + this.expression = this.deepSubCrossStackReferences(sourceStack, this.expression); + } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index 18756222de393..a8e49f4084867 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { StackElement } from './stack'; +import { StackElement, Stack } from './stack'; export interface IncludeProps { /** @@ -34,4 +34,7 @@ export class Include extends StackElement { public toCloudFormation() { return this.template; } + + public substituteCrossStackReferences(_sourceStack: Stack): void { + } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index b77b3af8f1673..8fd60c9c25a8d 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { FnFindInMap } from './fn'; -import { Referenceable } from './stack'; +import { Referenceable, Stack } from './stack'; export interface MappingProps { mapping?: { [k1: string]: { [k2: string]: any } }; @@ -50,4 +50,8 @@ export class Mapping extends Referenceable { } }; } + + public substituteCrossStackReferences(sourceStack: Stack): void { + this.mapping = this.deepSubCrossStackReferences(sourceStack, this.mapping); + } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index fa39cf0990f0c..be11c1787bbba 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -52,13 +52,6 @@ export class Output extends StackElement { */ public readonly description?: string; - /** - * The value of the property returned by the aws cloudformation describe-stacks command. - * The value of an output can include literals, parameter references, pseudo-parameters, - * a mapping value, or intrinsic functions. - */ - public readonly value?: any; - /** * The name of the resource output to be exported for a cross-stack reference. * By default, the logical ID of the Output element is used as it's export name. @@ -72,6 +65,8 @@ export class Output extends StackElement { */ public readonly condition?: Condition; + private _value?: any; + /** * Creates an Output value for this stack. * @param parent The parent construct. @@ -81,7 +76,7 @@ export class Output extends StackElement { super(parent, name); this.description = props.description; - this.value = props.value; + this._value = props.value; this.condition = props.condition; if (props.export) { @@ -97,6 +92,15 @@ export class Output extends StackElement { } } + /** + * The value of the property returned by the aws cloudformation describe-stacks command. + * The value of an output can include literals, parameter references, pseudo-parameters, + * a mapping value, or intrinsic functions. + */ + public get value(): any { + return this._value; + } + /** * Returns an FnImportValue bound to this export name. */ @@ -120,6 +124,10 @@ export class Output extends StackElement { }; } + public substituteCrossStackReferences(sourceStack: Stack): void { + this._value = this.deepSubCrossStackReferences(sourceStack, this._value); + } + public get ref(): string { throw new Error('Outputs cannot be referenced'); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index 20533e7bfcc89..4d82f03a1ff2c 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; -import { Token } from '../core/tokens'; -import { Ref, Referenceable } from './stack'; +import { Token, ContextMap } from '../core/tokens'; +import { Ref, Referenceable, Stack } from './stack'; export interface ParameterProps { /** @@ -111,4 +111,16 @@ export class Parameter extends Referenceable { } }; } + + public substituteCrossStackReferences(sourceStack: Stack): void { + this.properties = this.deepSubCrossStackReferences(sourceStack, this.properties); + } + + /** + * Allows using parameters as tokens without the need to dereference them. + * This implicitly implements Token, until we make it an interface. + */ + public resolve(_context: ContextMap): any { + return this.value; + } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 06895929f647f..1b00fc92fe30c 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -4,7 +4,7 @@ import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { CloudFormationToken } from './cloudformation-token'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; -import { IDependable, Referenceable, StackElement } from './stack'; +import { IDependable, Referenceable, StackElement, Stack } from './stack'; export interface ResourceProps { /** @@ -209,6 +209,10 @@ export class Resource extends Referenceable { } } + public substituteCrossStackReferences(sourceStack: Stack): void { + this.deepSubCrossStackReferences(sourceStack, this.properties); + } + protected renderProperties(properties: any): { [key: string]: any } { return properties; } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index 29a881032809e..04c353a6f88d9 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -1,7 +1,7 @@ import { Construct } from '../core/construct'; import { capitalizePropertyNames } from '../core/util'; import { FnCondition } from './fn'; -import { Referenceable } from './stack'; +import { Referenceable, Stack } from './stack'; /** * A rule can include a RuleCondition property and must include an Assertions property. @@ -102,6 +102,9 @@ export class Rule extends Referenceable { } }; } + + public substituteCrossStackReferences(_sourceStack: Stack): void { + } } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index f61595f7a8ec3..4b29c96cf7305 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -1,9 +1,9 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; import { Construct, PATH_SEP } from '../core/construct'; -import { resolve, Token } from '../core/tokens'; +import { resolve, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; -import { CloudFormationToken } from './cloudformation-token'; +import { CloudFormationToken, StackAwareCloudFormationToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -274,6 +274,11 @@ export class Stack extends Construct { return output.makeImportValue(); } + public applyCrossEnvironmentReferences() { + const elements = stackElements(this); + elements.forEach(e => e.substituteCrossStackReferences(this)); + } + /** * Validate stack name * @@ -451,6 +456,29 @@ export abstract class StackElement extends Construct implements IDependable { * } */ public abstract toCloudFormation(): object; + + public abstract substituteCrossStackReferences(sourceStack: Stack): void; + + protected deepSubCrossStackReferences(sourceStack: Stack, x: any): any { + if (StackAwareCloudFormationToken.isInstance(x)) { + return x.substituteToken(sourceStack); + } + + if (unresolved(x)) { + x = resolve(x); + } + + if (Array.isArray(x)) { + return x.map(e => this.deepSubCrossStackReferences(sourceStack, e)); + } + if (typeof x === 'object' && x !== null) { + for (const [key, value] of Object.entries(x)) { + x[key] = this.deepSubCrossStackReferences(sourceStack, value); + } + return x; + } + return x; + } } /** diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index 9c0fa7efbf4a3..d8171a27da1d2 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -146,7 +146,7 @@ export function unresolved(obj: any): boolean { } else if (Array.isArray(obj) && obj.length === 1) { return isListToken(obj[0]); } else { - return typeof(obj[RESOLVE_METHOD]) === 'function'; + return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; } } diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index 6890581622fb1..ffa3b0cd79bb3 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -183,16 +183,25 @@ export = { new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); // THEN - // Need to freeze manually now, because we want to test using JUST the stacks. + // Need to do this manually now, since we're in testing mode. In a normal CDK app, + // this happens as part of app.run(). app.applyCrossEnvironmentReferences(); test.deepEqual(stack1.toCloudFormation(), { - Output: { + Outputs: { + ExportsOutputRefAWSAccountIdAD568057: { + Value: { Ref: 'AWS::AccountId' }, + Export: { Name: 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } } }); test.deepEqual(stack2.toCloudFormation(), { Parameters: { + SomeParameter: { + Type: 'String', + Default: { 'Fn::ImportValue': 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } } }); diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts index 45d5d29a37df5..d2e186262557b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -52,7 +52,7 @@ export = { ctagger.tags.setTag(tag.key, tag.value, {propagate: false}); for (const construct of [ctagger1, ctagger2]) { - test.deepEqual(resolve(construct.tags).length, undefined); + test.deepEqual(resolve(construct.tags), undefined); } test.deepEqual(resolve(ctagger.tags)[0].key, 'Name'); test.deepEqual(resolve(ctagger.tags)[0].value, 'TheCakeIsALie'); From 935fd82948e8c985439aa4f0969a8b4086cd6d9a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 18 Dec 2018 12:53:42 +0100 Subject: [PATCH 05/39] Get rid of "context" parameter for Token resolution --- .../@aws-cdk/aws-iam/lib/policy-document.ts | 4 +- .../@aws-cdk/cdk/lib/cloudformation/fn.ts | 6 +-- .../cdk/lib/cloudformation/parameter.ts | 4 +- packages/@aws-cdk/cdk/lib/core/tag-manager.ts | 4 +- packages/@aws-cdk/cdk/lib/core/tokens.ts | 38 +++++-------------- 5 files changed, 19 insertions(+), 37 deletions(-) diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index 48e44f9fb739c..d0d6a98341cbb 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -1,5 +1,5 @@ import { Construct } from '@aws-cdk/cdk'; -import { ContextMap, Token } from '@aws-cdk/cdk'; +import { Token } from '@aws-cdk/cdk'; import { AwsAccountId, AwsPartition } from '@aws-cdk/cdk'; export class PolicyDocument extends Token { @@ -325,7 +325,7 @@ export class PolicyStatement extends Token { // Serialization // - public resolve(_context: ContextMap): any { + public resolve(): any { return this.toJson(); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index f29b665bf1520..5b9a705e355c1 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -1,4 +1,4 @@ -import { resolve, Token, unresolved, ContextMap } from '../core/tokens'; +import { resolve, Token, unresolved } from '../core/tokens'; import { CloudFormationToken, isIntrinsic } from './cloudformation-token'; // tslint:disable:max-line-length @@ -107,12 +107,12 @@ export class FnJoin extends Fn { this.canOptimize = true; } - public resolve(context: ContextMap): any { + public resolve(): any { const resolved = this.resolveValues(); if (this.canOptimize && resolved.length === 1) { return resolved[0]; } - return super.resolve(context); + return super.resolve(); } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index 4d82f03a1ff2c..bb3a1fca59501 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { Token, ContextMap } from '../core/tokens'; +import { Token } from '../core/tokens'; import { Ref, Referenceable, Stack } from './stack'; export interface ParameterProps { @@ -120,7 +120,7 @@ export class Parameter extends Referenceable { * Allows using parameters as tokens without the need to dereference them. * This implicitly implements Token, until we make it an interface. */ - public resolve(_context: ContextMap): any { + public resolve(): any { return this.value; } } diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts index 79c9d855acb3a..3b5ebf053320d 100644 --- a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -1,5 +1,5 @@ import { Construct } from './construct'; -import { ContextMap, Token } from './tokens'; +import { Token } from './tokens'; /** * ITaggable indicates a entity manages tags via the `tags` property @@ -171,7 +171,7 @@ export class TagManager extends Token { /** * Converts the `tags` to a Token for use in lazy evaluation */ - public resolve(_context: ContextMap): any { + public resolve(): any { // need this for scoping const blockedTags = this.blockedTags; function filterTags(_tags: FullTags, filter: TagProps = {}): Tags { diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index d8171a27da1d2..ef8190559871d 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -6,14 +6,6 @@ import { Construct } from "./construct"; */ export const RESOLVE_METHOD = 'resolve'; -export type ContextMap = {[key: string]: any}; - -export interface ResolveOptions { - context?: ContextMap; - - prefix?: string[]; -} - /** * Represents a special or lazily-evaluated value. * @@ -53,10 +45,10 @@ export class Token { /** * @returns The resolved value for this token. */ - public resolve(context: ContextMap): any { + public resolve(): any { let value = this.valueOrFunction; if (typeof(value) === 'function') { - value = value(context); + value = value(); } return value; @@ -155,15 +147,14 @@ export function unresolved(obj: any): boolean { * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. * * @param obj The object to resolve. - * @param options Resolution options (prefix and context) + * @param prefix Prefix key path components for diagnostics. */ -export function resolve(obj: any, options: ResolveOptions = {}): any { - const prefix = options.prefix || [ ]; - const context = options.context || {}; - const pathName = '/' + prefix.join('/'); +export function resolve(obj: any, prefix?: string[]): any { + const path = prefix || [ ]; + const pathName = '/' + path.join('/'); // protect against cyclic references by limiting depth. - if (prefix.length > 200) { + if (path.length > 200) { throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); } @@ -206,15 +197,6 @@ export function resolve(obj: any, options: ResolveOptions = {}): any { return obj; } - // - // tokens - invoke 'resolve' and continue to resolve recursively - // - - if (unresolved(obj)) { - const value = obj[RESOLVE_METHOD](context); - return resolve(value, { context, prefix }); - } - // // arrays - resolve all values, remove undefined and remove empty arrays // @@ -225,7 +207,7 @@ export function resolve(obj: any, options: ResolveOptions = {}): any { } const arr = obj - .map((x, i) => resolve(x, { context, prefix: prefix.concat(i.toString()) })) + .map((x, i) => resolve(x, path.concat(i.toString()))) .filter(x => typeof(x) !== 'undefined'); return arr; @@ -237,7 +219,7 @@ export function resolve(obj: any, options: ResolveOptions = {}): any { if (unresolved(obj)) { const value = obj[RESOLVE_METHOD](); - return resolve(value, options); + return resolve(value, path); } // @@ -258,7 +240,7 @@ export function resolve(obj: any, options: ResolveOptions = {}): any { throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); } - const value = resolve(obj[key], { context, prefix: prefix.concat(key) }); + const value = resolve(obj[key], path.concat(key)); // skip undefined if (typeof(value) === 'undefined') { From 9f40aa2149d77d715cae4143d7d22ba0ecb9cd9d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 21 Dec 2018 07:40:02 -0500 Subject: [PATCH 06/39] Make refs and attributes Stack-aware --- packages/@aws-cdk/cdk/lib/cloudformation/resource.ts | 6 +++--- packages/@aws-cdk/cdk/lib/cloudformation/stack.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 1b00fc92fe30c..694ac485a1cbf 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,10 +1,10 @@ import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; -import { CloudFormationToken } from './cloudformation-token'; +import { StackAwareCloudFormationToken } from './cloudformation-token'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; -import { IDependable, Referenceable, StackElement, Stack } from './stack'; +import { IDependable, Referenceable, Stack, StackElement } from './stack'; export interface ResourceProps { /** @@ -105,7 +105,7 @@ export class Resource extends Referenceable { * @param attributeName The name of the attribute. */ public getAtt(attributeName: string) { - return new CloudFormationToken({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`); + return new StackAwareCloudFormationToken(this, { 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`); } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 4b29c96cf7305..4bd5930ac8b95 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -3,7 +3,7 @@ import { App } from '../app'; import { Construct, PATH_SEP } from '../core/construct'; import { resolve, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; -import { CloudFormationToken, StackAwareCloudFormationToken } from './cloudformation-token'; +import { StackAwareCloudFormationToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -108,7 +108,7 @@ export class Stack extends Construct { /** * A construct to hold cross-stack exports - * + * * This mostly exists to trigger LogicalID munging, which would be * disabled if we parented constructs directly under Stack. */ @@ -549,9 +549,9 @@ function stackElements(node: Construct, into: StackElement[] = []): StackElement /** * A generic, untyped reference to a Stack Element */ -export class Ref extends CloudFormationToken { +export class Ref extends StackAwareCloudFormationToken { constructor(element: StackElement) { - super({ Ref: element.logicalId }, `${element.logicalId}.Ref`); + super(element, { Ref: element.logicalId }, `${element.logicalId}.Ref`); } } From c1d4dc15ec8315376cf7210cdc0f7ce5da2fd16f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 21 Dec 2018 15:06:22 -0500 Subject: [PATCH 07/39] Create tests for things I'll have to do later --- .../cdk/test/cloudformation/test.stack.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index ffa3b0cd79bb3..384dffc8943a8 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -172,7 +172,7 @@ export = { test.done(); }, - 'Pseudo values attached to one stack can be referenced in another'(test: Test) { + 'Pseudo values attached to one stack can be referenced in another stack'(test: Test) { // GIVEN const app = new App(); const stack1 = new Stack(app, 'Stack1'); @@ -207,6 +207,26 @@ export = { test.done(); }, + + 'references in strings work'(test: Test) { + test.assert(false, 'NIMPL'); + test.done(); + }, + + 'cannot create cyclic reference between stacks'(test: Test) { + test.assert(false, 'NIMPL'); + test.done(); + }, + + 'stacks with references have an order'(test: Test) { + test.assert(false, 'NIMPL'); + test.done(); + }, + + 'cannot create references to stacks in other regions/accounts'(test: Test) { + test.assert(false, 'NIMPL'); + test.done(); + }, }; class StackWithPostProcessor extends Stack { From 5d853187ec4bf27c978c75b95079cae95b6a84ff Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 26 Dec 2018 16:45:40 -0500 Subject: [PATCH 08/39] Fix core tests --- packages/@aws-cdk/cdk/lib/app.ts | 3 +- .../cloudformation/cloudformation-token.ts | 10 +- .../cdk/lib/cloudformation/include.ts | 3 +- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 29 ++- packages/@aws-cdk/cdk/lib/core/tokens.ts | 216 ++++++++++++------ .../cdk/test/cloudformation/test.arn.ts | 5 +- .../test.cloudformation-json.ts | 5 +- .../cloudformation/test.dynamic-reference.ts | 5 +- .../cdk/test/cloudformation/test.fn.ts | 5 +- .../cdk/test/cloudformation/test.output.ts | 5 +- .../cdk/test/cloudformation/test.parameter.ts | 5 +- .../cdk/test/cloudformation/test.resource.ts | 5 +- .../cdk/test/cloudformation/test.secret.ts | 5 +- .../cdk/test/cloudformation/test.stack.ts | 107 ++++++++- .../cdk/test/core/test.tag-manager.ts | 5 +- .../@aws-cdk/cdk/test/core/test.tokens.ts | 5 +- packages/@aws-cdk/cdk/test/core/test.util.ts | 5 +- packages/@aws-cdk/cdk/test/test.app.ts | 7 +- packages/@aws-cdk/cdk/test/test.context.ts | 5 +- packages/@aws-cdk/cdk/test/util.ts | 21 ++ packages/@aws-cdk/cx-api/lib/cxapi.ts | 5 + 22 files changed, 349 insertions(+), 114 deletions(-) create mode 100644 packages/@aws-cdk/cdk/test/util.ts diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index 1c0eef70cdb01..f82c079d56516 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -83,7 +83,8 @@ export class App extends Root { environment, missing, template: stack.toCloudFormation(), - metadata: this.collectMetadata(stack) + metadata: this.collectMetadata(stack), + dependsOn: stack.dependencyStackIds(), }; } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 5a60b76885515..312b4967dac93 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -1,15 +1,19 @@ import { Construct } from "../core/construct"; -import { resolve, Token } from "../core/tokens"; +import { Token } from "../core/tokens"; /** * Base class for CloudFormation built-ins */ export class CloudFormationToken extends Token { - public concat(left: any | undefined, right: any | undefined): Token { + public static cloudFormationConcat(left: any | undefined, right: any | undefined): any { + if (left === undefined && right === undefined) { return ''; } + const parts = new Array(); if (left !== undefined) { parts.push(left); } - parts.push(resolve(this)); if (right !== undefined) { parts.push(right); } + + if (parts.length === 1) { return parts[0]; } + return new FnConcat(...parts); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index a8e49f4084867..6d23ee8a197f8 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { StackElement, Stack } from './stack'; +import { Stack, StackElement } from './stack'; export interface IncludeProps { /** @@ -36,5 +36,6 @@ export class Include extends StackElement { } public substituteCrossStackReferences(_sourceStack: Stack): void { + // Left empty on purpose } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index be11c1787bbba..bac534ccd1837 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -127,7 +127,7 @@ export class Output extends StackElement { public substituteCrossStackReferences(sourceStack: Stack): void { this._value = this.deepSubCrossStackReferences(sourceStack, this._value); } - + public get ref(): string { throw new Error('Outputs cannot be referenced'); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 4bd5930ac8b95..e27c26ced84aa 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -1,9 +1,9 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; import { Construct, PATH_SEP } from '../core/construct'; -import { resolve, Token, unresolved } from '../core/tokens'; +import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; -import { StackAwareCloudFormationToken } from './cloudformation-token'; +import { CloudFormationToken, StackAwareCloudFormationToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -154,6 +154,7 @@ export class Stack extends Construct { public toCloudFormation() { // before we begin synthesis, we shall lock this stack, so children cannot be added this.lock(); + const options = RESOLVE_OPTIONS.push({ concat: CloudFormationToken.cloudFormationConcat }); try { const template: any = { @@ -172,7 +173,7 @@ export class Stack extends Construct { } // resolve all tokens and remove all empties - const ret = resolve(template) || { }; + const ret = resolve(template) || {}; this.logicalIds.assertAllRenamesApplied(); @@ -180,6 +181,7 @@ export class Stack extends Construct { } finally { // allow mutations after synthesis is finished. this.unlock(); + options.pop(); } } @@ -253,6 +255,10 @@ export class Stack extends Construct { this.dependsOnStacks.add(stack); } + public dependencyStackIds(): string[] { + return Array.from(this.dependsOnStacks.values()).map(s => s.id); + } + /** * Export a Token value for use in another stack */ @@ -275,8 +281,13 @@ export class Stack extends Construct { } public applyCrossEnvironmentReferences() { - const elements = stackElements(this); - elements.forEach(e => e.substituteCrossStackReferences(this)); + const options = RESOLVE_OPTIONS.push({ concat: CloudFormationToken.cloudFormationConcat }); + try { + const elements = stackElements(this); + elements.forEach(e => e.substituteCrossStackReferences(this)); + } finally { + options.pop(); + } } /** @@ -465,12 +476,18 @@ export abstract class StackElement extends Construct implements IDependable { } if (unresolved(x)) { - x = resolve(x); + const options = RESOLVE_OPTIONS.push({ recurse: (y: any) => this.deepSubCrossStackReferences(sourceStack, y) }); + try { + x = resolve(x); + } finally { + options.pop(); + } } if (Array.isArray(x)) { return x.map(e => this.deepSubCrossStackReferences(sourceStack, e)); } + if (typeof x === 'object' && x !== null) { for (const [key, value] of Object.entries(x)) { x[key] = this.deepSubCrossStackReferences(sourceStack, value); diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index ef8190559871d..4af66d338743e 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -113,17 +113,6 @@ export class Token { } return this.tokenListification; } - - /** - * Return a concated version of this Token in a string context - * - * The default implementation of this combines strings, but specialized - * implements of Token can return a more appropriate value. - */ - public concat(left: any | undefined, right: any | undefined): Token { - const parts = [left, resolve(this), right].filter(x => x !== undefined); - return new Token(parts.map(x => `${x}`).join('')); - } } /** @@ -150,11 +139,12 @@ export function unresolved(obj: any): boolean { * @param prefix Prefix key path components for diagnostics. */ export function resolve(obj: any, prefix?: string[]): any { - const path = prefix || [ ]; - const pathName = '/' + path.join('/'); + prefix = prefix || [ ]; + const pathName = '/' + prefix.join('/'); + const recurse = RESOLVE_OPTIONS.recurse || resolve; // protect against cyclic references by limiting depth. - if (path.length > 200) { + if (prefix.length > 200) { throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); } @@ -186,7 +176,11 @@ export function resolve(obj: any, prefix?: string[]): any { // string - potentially replace all stringified Tokens // if (typeof(obj) === 'string') { - return TOKEN_MAP.resolveStringTokens(obj as string); + const concat = RESOLVE_OPTIONS.concatFunc; + if (!concat) { + throw new Error('Cannot resolve a string with Tokens; no concatenation function given'); + } + return TOKEN_MAP.resolveStringTokens(obj as string, recurse, concat); } // @@ -207,7 +201,7 @@ export function resolve(obj: any, prefix?: string[]): any { } const arr = obj - .map((x, i) => resolve(x, path.concat(i.toString()))) + .map((x, i) => recurse(x, prefix!.concat(i.toString()))) .filter(x => typeof(x) !== 'undefined'); return arr; @@ -219,7 +213,7 @@ export function resolve(obj: any, prefix?: string[]): any { if (unresolved(obj)) { const value = obj[RESOLVE_METHOD](); - return resolve(value, path); + return recurse(value, prefix); } // @@ -235,12 +229,12 @@ export function resolve(obj: any, prefix?: string[]): any { const result: any = { }; for (const key of Object.keys(obj)) { - const resolvedKey = resolve(key); + const resolvedKey = recurse(key, prefix); if (typeof(resolvedKey) !== 'string') { throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); } - const value = resolve(obj[key], path.concat(key)); + const value = recurse(obj[key], prefix.concat(key)); // skip undefined if (typeof(value) === 'undefined') { @@ -271,12 +265,7 @@ function containsListToken(xs: any[]) { * works even when different copies of the library are loaded. */ class TokenMap { - private readonly tokenMap: {[key: string]: Token}; - - constructor() { - const glob = global as any; - this.tokenMap = glob.__cdkTokenMap = glob.__cdkTokenMap || {}; - } + private readonly tokenMap: {[key: string]: Token} = {}; /** * Generate a unique string for this Token, returning a key @@ -319,10 +308,14 @@ class TokenMap { /** * Replace any Token markers in this string with their resolved values */ - public resolveStringTokens(s: string): any { + public resolveStringTokens(s: string, resolver: ResolveFunc, concat: ConcatFunc): any { const str = this.createStringTokenString(s); const fragments = str.split(this.lookupToken.bind(this)); - return fragments.join(); + const ret = fragments.mapUnresolved(resolver).join(concat); + if (unresolved(ret)) { + return resolve(ret); + } + return ret; } public resolveListTokens(xs: string[]): any { @@ -369,11 +362,6 @@ const BEGIN_LIST_TOKEN_MARKER = '#{Token['; const END_TOKEN_MARKER = ']}'; const VALID_KEY_CHARS = 'a-zA-Z0-9:._-'; -/** - * Singleton instance of the token string map - */ -const TOKEN_MAP = new TokenMap(); - /** * Interface that Token joiners implement */ @@ -409,25 +397,25 @@ class TokenString { /** * Split string on markers, substituting markers with Tokens */ - public split(lookup: (id: string) => Token): TokenStringFragments { + public split(lookup: (id: string) => Token): TokenizedStringFragments { const re = new RegExp(this.pattern, 'g'); - const ret = new TokenStringFragments(); + const ret = new TokenizedStringFragments(); let rest = 0; let m = re.exec(this.str); while (m) { if (m.index > rest) { - ret.addString(this.str.substring(rest, m.index)); + ret.addLiteral(this.str.substring(rest, m.index)); } - ret.addToken(lookup(m[1])); + ret.addUnresolved(lookup(m[1])); rest = re.lastIndex; m = re.exec(this.str); } if (rest < this.str.length) { - ret.addString(this.str.substring(rest)); + ret.addLiteral(this.str.substring(rest)); } return ret; @@ -447,14 +435,14 @@ class TokenString { * * Either a literal part of the string, or an unresolved Token. */ -type StringFragment = { type: 'string'; str: string }; -type TokenFragment = { type: 'token'; token: Token }; -type Fragment = StringFragment | TokenFragment; +type LiteralFragment = { type: 'literal'; lit: any; }; +type UnresolvedFragment = { type: 'unresolved'; token: any; }; +type Fragment = LiteralFragment | UnresolvedFragment; /** * Fragments of a string with markers */ -class TokenStringFragments { +class TokenizedStringFragments { private readonly fragments = new Array(); public get length() { @@ -462,15 +450,38 @@ class TokenStringFragments { } public values(): any[] { - return this.fragments.map(f => f.type === 'token' ? resolve(f.token) : f.str); + return this.fragments.map(f => f.type === 'unresolved' ? resolve(f.token) : f.lit); + } + + public addLiteral(lit: any) { + this.fragments.push({ type: 'literal', lit }); } - public addString(str: string) { - this.fragments.push({ type: 'string', str }); + public addUnresolved(token: Token) { + this.fragments.push({ type: 'unresolved', token }); } - public addToken(token: Token) { - this.fragments.push({ type: 'token', token }); + public mapUnresolved(fn: (t: any) => any): TokenizedStringFragments { + const ret = new TokenizedStringFragments(); + + for (const f of this.fragments) { + switch (f.type) { + case 'literal': + ret.addLiteral(f.lit); + break; + case 'unresolved': + const mappedToken = fn(f.token); + + if (unresolved(mappedToken)) { + ret.addUnresolved(mappedToken); + } else { + ret.addLiteral(mappedToken); + } + break; + } + } + + return ret; } /** @@ -478,38 +489,25 @@ class TokenStringFragments { * * Resolves the result. */ - public join(): any { - if (this.fragments.length === 0) { return ''; } - if (this.fragments.length === 1) { return resolveFragment(this.fragments[0]); } - - const first = this.fragments[0]; - - let i; - let token: Token; - - if (first.type === 'token') { - token = first.token; - i = 1; - } else { - // We never have two strings in a row - token = (this.fragments[1] as TokenFragment).token.concat(first.str, undefined); - i = 2; - } + public join(concat: ConcatFunc): any { + if (this.fragments.length === 0) { return concat(undefined, undefined); } + + const values = this.fragments.map(fragmentValue); - while (i < this.fragments.length) { - token = token.concat(undefined, resolveFragment(this.fragments[i])); - i++; + while (values.length > 1) { + const prefix = values.splice(0, 2); + values.splice(0, 0, concat(prefix[0], prefix[1])); } - return resolve(token); + return values[0]; } } /** * Resolve the value from a single fragment */ -function resolveFragment(fragment: Fragment): any { - return fragment.type === 'string' ? fragment.str : resolve(fragment.token); +function fragmentValue(fragment: Fragment): any { + return fragment.type === 'literal' ? fragment.lit : fragment.token; } /** @@ -518,3 +516,83 @@ function resolveFragment(fragment: Fragment): any { function regexQuote(s: string) { return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); } + +/** + * Global options for resolve() + * + * Because there are many independent calls to resolve(), some losing context, + * we cannot simply pass through options at each individual call. Instead, + * we configure global context at the stack synthesis level. + */ +export class ResolveConfiguration { + private readonly options = new Array(); + + public push(options: ResolveOptions): OptionsContext { + this.options.push(options); + + return { + pop: () => { + if (this.options.length === 0 || this.options[this.options.length - 1] !== options) { + throw new Error('ResolveConfiguration push/pop mismatch'); + } + this.options.pop(); + } + }; + } + + public get concatFunc(): ConcatFunc | undefined { + for (let i = this.options.length - 1; i >= 0; i--) { + if (this.options[i].concat) { + return this.options[i].concat; + } + } + return undefined; + } + + public get recurse(): ResolveFunc | undefined { + for (let i = this.options.length - 1; i >= 0; i--) { + if (this.options[i].recurse) { + return this.options[i].recurse; + } + } + return undefined; + } +} + +export interface OptionsContext { + pop(): void; +} + +export interface ResolveOptions { + /** + * Function to use for concatenating symbols in the target document language + */ + concat?: ConcatFunc; + + /** + * What function to use for recursing into deeper resolutions + */ + recurse?: ResolveFunc; +} + +/** + * Function used to resolve Tokens + */ +export type ResolveFunc = (obj: any) => any; + +/** + * Function used to concatenate symbols in the target document language + */ +export type ConcatFunc = (left: any | undefined, right: any | undefined) => any; + +const glob = global as any; + +/** + * Singleton instance of the token string map + */ +const TOKEN_MAP: TokenMap = glob.__cdkTokenMap = glob.__cdkTokenMap || new TokenMap(); + +/** + * Singleton instance of resolver options + */ +export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index e4e16c298061c..4ca5337777825 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,7 +1,8 @@ import { Test } from 'nodeunit'; import { ArnComponents, ArnUtils, FnConcat, resolve, Stack, Token } from '../../lib'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'create from components with defaults'(test: Test) { const stack = new Stack(); @@ -214,4 +215,4 @@ export = { } }, -}; +}); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts index 1ddfd6269dacb..f1ea472c4bb30 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts @@ -1,8 +1,9 @@ import { Test } from 'nodeunit'; import { CloudFormationJSON, CloudFormationToken, FnConcat, resolve, Token } from '../../lib'; +import { makeCloudformationTestSuite } from '../util'; import { evaluateCFN } from './evaluate-cfn'; -export = { +export = makeCloudformationTestSuite({ 'plain JSON.stringify() on a Token fails'(test: Test) { // GIVEN const token = new Token(() => 'value'); @@ -162,7 +163,7 @@ export = { test.done(); }, -}; +}); /** * Return two Tokens, one of which evaluates to a Token directly, one which evaluates to it lazily diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts index 4dc2f2767f40d..71e3708bf57cf 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts @@ -1,7 +1,8 @@ import { Test } from 'nodeunit'; import { DynamicReference, DynamicReferenceService, resolve, Stack } from '../../lib'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'can create dynamic references with service and key with colons'(test: Test) { // GIVEN const stack = new Stack(); @@ -17,4 +18,4 @@ export = { test.done(); }, -}; +}); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts index 509834e7f2e72..3ea1ebb4fd90f 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts @@ -3,6 +3,7 @@ import _ = require('lodash'); import nodeunit = require('nodeunit'); import fn = require('../../lib/cloudformation/fn'); import { resolve } from '../../lib/core/tokens'; +import { makeCloudformationTestSuite } from '../util'; function asyncTest(cb: (test: nodeunit.Test) => Promise): (test: nodeunit.Test) => void { return async (test: nodeunit.Test) => { @@ -24,7 +25,7 @@ const nonEmptyString = fc.string(1, 16); const tokenish = fc.array(nonEmptyString, 2, 2).map(arr => ({ [arr[0]]: arr[1] })); const anyValue = fc.oneof(nonEmptyString, tokenish); -export = nodeunit.testCase({ +export = nodeunit.testCase(makeCloudformationTestSuite({ FnJoin: { 'rejects empty list of arguments to join'(test: nodeunit.Test) { test.throws(() => new fn.FnJoin('.', [])); @@ -93,4 +94,4 @@ export = nodeunit.testCase({ ); }), }, -}); +})); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts index 3e9de87245942..7e8e8f63d52d7 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts @@ -1,7 +1,8 @@ import { Test } from 'nodeunit'; import { Construct, Output, Ref, resolve, Resource, Stack } from '../../lib'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'outputs can be added to the stack'(test: Test) { const stack = new Stack(); const res = new Resource(stack, 'MyResource', { type: 'R' }); @@ -66,4 +67,4 @@ export = { test.deepEqual(resolve(output.makeImportValue()), { 'Fn::ImportValue': 'MyStack:MyOutput' }); test.done(); } -}; +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts index d866af8099a8a..537834c9a768d 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts @@ -1,7 +1,8 @@ import { Test } from 'nodeunit'; import { Construct, Parameter, resolve, Resource, Stack } from '../../lib'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'parameters can be used and referenced using param.ref'(test: Test) { const stack = new Stack(); @@ -35,4 +36,4 @@ export = { test.deepEqual(resolve(param), { Ref: 'MyParam' }); test.done(); } -}; +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 9b961a4de0cd4..0c8d13044010b 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -3,8 +3,9 @@ import { Test } from 'nodeunit'; import { applyRemovalPolicy, Condition, Construct, DeletionPolicy, FnEquals, FnNot, HashedAddressingScheme, IDependable, RemovalPolicy, resolve, Resource, Root, Stack } from '../../lib'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'all resources derive from Resource, which derives from Entity'(test: Test) { const stack = new Stack(); @@ -603,7 +604,7 @@ export = { test.done(); } -}; +}); interface CounterProps { // tslint:disable-next-line:variable-name diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts index 33422251adaa9..cf31aaef0aad3 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts @@ -1,7 +1,8 @@ import { Test } from 'nodeunit'; import { resolve, Secret, SecretParameter, Stack } from '../../lib'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'Secret is merely a token'(test: Test) { const foo = new Secret('Foo'); const bar = new Secret(() => 'Bar'); @@ -47,4 +48,4 @@ export = { 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 384dffc8943a8..dcb17e4a22f89 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { App, AwsAccountId, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack } from '../../lib'; +import { App, AwsAccountId, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack, Token } from '../../lib'; export = { 'a stack can be serialized into a CloudFormation template, initially it\'s empty'(test: Test) { @@ -208,23 +208,116 @@ export = { test.done(); }, - 'references in strings work'(test: Test) { - test.assert(false, 'NIMPL'); + 'cross-stack references in lazy tokens work'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new AwsAccountId(stack1); + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another stack + new Parameter(stack2, 'SomeParameter', { type: 'String', default: new Token(() => account1) }); + + app.applyCrossEnvironmentReferences(); + + // THEN + test.deepEqual(stack1.toCloudFormation(), { + Outputs: { + ExportsOutputRefAWSAccountIdAD568057: { + Value: { Ref: 'AWS::AccountId' }, + Export: { Name: 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } + } + }); + + test.deepEqual(stack2.toCloudFormation(), { + Parameters: { + SomeParameter: { + Type: 'String', + Default: { 'Fn::ImportValue': 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } + } + }); + + test.done(); + }, + + 'cross-stack references in strings work'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new AwsAccountId(stack1); + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another stack + new Parameter(stack2, 'SomeParameter', { type: 'String', default: `TheAccountIs${account1}` }); + + app.applyCrossEnvironmentReferences(); + + // THEN + test.deepEqual(stack2.toCloudFormation(), { + Parameters: { + SomeParameter: { + Type: 'String', + Default: { 'Fn::Join': [ '', [ 'TheAccountIs', { 'Fn::ImportValue': 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } ]] } + } + } + }); + test.done(); }, 'cannot create cyclic reference between stacks'(test: Test) { - test.assert(false, 'NIMPL'); + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new AwsAccountId(stack1); + const stack2 = new Stack(app, 'Stack2'); + const account2 = new AwsAccountId(stack2); + + // WHEN + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); + new Parameter(stack1, 'SomeParameter', { type: 'String', default: account2 }); + + test.throws(() => { + app.applyCrossEnvironmentReferences(); + }, /Adding this dependency would create a cyclic reference/); + test.done(); }, - 'stacks with references have an order'(test: Test) { - test.assert(false, 'NIMPL'); + 'stacks know about their dependencies'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new AwsAccountId(stack1); + const stack2 = new Stack(app, 'Stack2'); + + // WHEN + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); + + app.applyCrossEnvironmentReferences(); + + // THEN + test.deepEqual(stack2.dependencyStackIds(), ['Stack1']); + test.done(); }, 'cannot create references to stacks in other regions/accounts'(test: Test) { - test.assert(false, 'NIMPL'); + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1', { env: { account: '123456789012', region: 'es-norst-1' }}); + const account1 = new AwsAccountId(stack1); + const stack2 = new Stack(app, 'Stack2', { env: { account: '123456789012', region: 'es-norst-2' }}); + + // WHEN + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); + + test.throws(() => { + app.applyCrossEnvironmentReferences(); + }, /Can only reference cross stacks in the same region and account/); + test.done(); }, }; diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts index d2e186262557b..54d334786a3a0 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -2,6 +2,7 @@ import { Test } from 'nodeunit'; import { Construct, Root } from '../../lib/core/construct'; import { ITaggable, TagManager } from '../../lib/core/tag-manager'; import { resolve } from '../../lib/core/tokens'; +import { makeCloudformationTestSuite as testSuiteWithCloudFormationResolve } from '../util'; class ChildTagger extends Construct implements ITaggable { public readonly tags: TagManager; @@ -17,7 +18,7 @@ class Child extends Construct { } } -export = { +export = testSuiteWithCloudFormationResolve({ 'TagManger handles tags for a Contruct Tree': { 'setTag by default propagates to children'(test: Test) { const root = new Root(); @@ -179,4 +180,4 @@ export = { test.done(); }, }, -}; +}); diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index badab37cd1b47..c5f123be23213 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -1,8 +1,9 @@ import { Test } from 'nodeunit'; import { CloudFormationToken, FnJoin, FnSelect, resolve, Token, unresolved } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'resolve a plain old object should just return the object'(test: Test) { const obj = { PlainOldObject: 123, Array: [ 1, 2, 3 ] }; test.deepEqual(resolve(obj), obj); @@ -351,7 +352,7 @@ export = { test.done(); } } -}; +}); class Promise2 extends Token { public resolve() { diff --git a/packages/@aws-cdk/cdk/test/core/test.util.ts b/packages/@aws-cdk/cdk/test/core/test.util.ts index 2a7cb94ec87c5..d10ae42580aa7 100644 --- a/packages/@aws-cdk/cdk/test/core/test.util.ts +++ b/packages/@aws-cdk/cdk/test/core/test.util.ts @@ -1,7 +1,8 @@ import { Test } from 'nodeunit'; import { capitalizePropertyNames, ignoreEmpty } from '../../lib/core/util'; +import { makeCloudformationTestSuite } from '../util'; -export = { +export = makeCloudformationTestSuite({ 'capitalizeResourceProperties capitalizes all keys of an object (recursively) from camelCase to PascalCase'(test: Test) { test.equal(capitalizePropertyNames(undefined), undefined); @@ -64,7 +65,7 @@ export = { test.done(); } } -}; +}); class SomeToken { public foo = 60; diff --git a/packages/@aws-cdk/cdk/test/test.app.ts b/packages/@aws-cdk/cdk/test/test.app.ts index 1f3712dfefc87..a5badbb9b75e7 100644 --- a/packages/@aws-cdk/cdk/test/test.app.ts +++ b/packages/@aws-cdk/cdk/test/test.app.ts @@ -5,6 +5,7 @@ import os = require('os'); import path = require('path'); import { Construct, Resource, Stack, StackProps } from '../lib'; import { App } from '../lib/app'; +import { makeCloudformationTestSuite } from './util'; function withApp(context: { [key: string]: any } | undefined, block: (app: App) => void) { const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-app-test')); @@ -62,7 +63,7 @@ function synthStack(name: string, includeMetadata: boolean = false, context?: an return stack; } -export = { +export = makeCloudformationTestSuite({ 'synthesizes all stacks and returns synthesis result'(test: Test) { const response = synth(); @@ -78,6 +79,7 @@ export = { { name: '12345/us-east-1', account: '12345', region: 'us-east-1' }, + dependsOn: [], template: { Resources: { s1c1: { Type: 'DummyResource', Properties: { Prop1: 'Prop1' } }, @@ -87,6 +89,7 @@ export = { { name: 'unknown-account/unknown-region', account: 'unknown-account', region: 'unknown-region' }, + dependsOn: [], template: { Resources: { s2c1: { Type: 'DummyResource', Properties: { Prog2: 'Prog2' } }, @@ -315,7 +318,7 @@ export = { test.done(); }, -}; +}); class MyConstruct extends Construct { constructor(parent: Construct, name: string) { diff --git a/packages/@aws-cdk/cdk/test/test.context.ts b/packages/@aws-cdk/cdk/test/test.context.ts index f061a16c4b443..9a52dd8f7f14d 100644 --- a/packages/@aws-cdk/cdk/test/test.context.ts +++ b/packages/@aws-cdk/cdk/test/test.context.ts @@ -2,8 +2,9 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { App, AvailabilityZoneProvider, Construct, ContextProvider, MetadataEntry, resolve, SSMParameterProvider, Stack } from '../lib'; +import { makeCloudformationTestSuite } from './util'; -export = { +export = makeCloudformationTestSuite({ 'AvailabilityZoneProvider returns a list with dummy values if the context is not available'(test: Test) { const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); const azs = new AvailabilityZoneProvider(stack).availabilityZones; @@ -111,7 +112,7 @@ export = { test.done(); }, -}; +}); function firstKey(obj: any): string { return Object.keys(obj)[0]; diff --git a/packages/@aws-cdk/cdk/test/util.ts b/packages/@aws-cdk/cdk/test/util.ts new file mode 100644 index 0000000000000..e1efdc11eda51 --- /dev/null +++ b/packages/@aws-cdk/cdk/test/util.ts @@ -0,0 +1,21 @@ +import { ITestGroup } from 'nodeunit'; +import { CloudFormationToken, RESOLVE_OPTIONS } from "../lib"; + +/** + * Update a nodeunit test suite so that we set up and tear down the proper CloudFormation token concatenator + */ +export function makeCloudformationTestSuite(tests: T): T { + let options: any; + + tests.setUp = (callback: () => void) => { + options = RESOLVE_OPTIONS.push({ concat: CloudFormationToken.cloudFormationConcat }); + callback(); + }; + + tests.tearDown = (callback: () => void) => { + options.pop(); + callback(); + }; + + return tests; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 51bf4388dff31..eef3c5f7d607a 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -56,6 +56,11 @@ export interface SynthesizedStack { missing?: { [key: string]: MissingContext }; metadata: StackMetadata; template: any; + + /** + * Other stacks this stack depends on + */ + dependsOn?: string[]; } /** From d8876bf887d4d091e6ade3703d05e417c713adbb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Dec 2018 13:53:27 +0100 Subject: [PATCH 09/39] Remove CloudFormationToken --- .../lib/cloudformation/cloudformation-json.ts | 4 ++-- .../cloudformation/cloudformation-token.ts | 23 ++++++++----------- .../cdk/lib/cloudformation/condition.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/fn.ts | 7 +++--- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 5 ++-- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 6 ++--- .../test.cloudformation-json.ts | 8 +++---- .../cdk/test/cloudformation/test.fn.ts | 7 +++--- .../@aws-cdk/cdk/test/core/test.tokens.ts | 20 ++++++++-------- packages/@aws-cdk/cdk/test/util.ts | 4 ++-- 10 files changed, 42 insertions(+), 46 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts index 6075f5650352a..fb21a56c663d9 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts @@ -1,5 +1,5 @@ import { resolve, Token } from "../core/tokens"; -import { CloudFormationToken, isIntrinsic } from "./cloudformation-token"; +import { isIntrinsic } from "./cloudformation-token"; /** * Class for JSON routines that are framework-aware @@ -65,7 +65,7 @@ export class CloudFormationJSON { /** * Token that also stringifies in the toJSON() operation. */ -class IntrinsicToken extends CloudFormationToken { +class IntrinsicToken extends Token { /** * Special handler that gets called when JSON.stringify() is used. */ diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 048bb293b78b1..4b36db34fe155 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -2,24 +2,19 @@ import { Construct } from "../core/construct"; import { resolve, Token, unresolved } from "../core/tokens"; import { Stack } from "./stack"; -/** - * Base class for CloudFormation built-ins - */ -export class CloudFormationToken extends Token { - public static cloudFormationConcat(left: any | undefined, right: any | undefined): any { - if (left === undefined && right === undefined) { return ''; } +export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { + if (left === undefined && right === undefined) { return ''; } - const parts = new Array(); - if (left !== undefined) { parts.push(left); } - if (right !== undefined) { parts.push(right); } + const parts = new Array(); + if (left !== undefined) { parts.push(left); } + if (right !== undefined) { parts.push(right); } - if (parts.length === 1) { return parts[0]; } + if (parts.length === 1) { return parts[0]; } - return new FnJoin('', parts); - } + return new FnJoin('', parts); } -export class StackAwareCloudFormationToken extends CloudFormationToken { +export class StackAwareCloudFormationToken extends Token { public static isInstance(x: any): x is StackAwareCloudFormationToken { return x && x._isStackAwareCloudFormationToken; } @@ -71,7 +66,7 @@ export function isIntrinsic(x: any) { * the specified delimiter. If a delimiter is the empty string, the set of values are concatenated * with no delimiter. */ -export class FnJoin extends CloudFormationToken { +export class FnJoin extends Token { private readonly delimiter: string; private readonly listOfValues: any[]; // Cache for the result of resolveValues() - since it otherwise would be computed several times diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index d35d5edd35de3..75ac6cd545f3e 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { CloudFormationToken } from './cloudformation-token'; +import { Token } from '../core/tokens'; import { Referenceable, Stack } from './stack'; export interface ConditionProps { @@ -57,7 +57,7 @@ export class Condition extends Referenceable { * conditions, you can define which resources are created and how they're configured for each * environment type. */ -export class FnCondition extends CloudFormationToken { +export class FnCondition extends Token { constructor(type: string, value: any) { super({ [type]: value }); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index 44b0f440da057..b94734917b8c0 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -1,4 +1,5 @@ -import { CloudFormationToken, FnJoin } from './cloudformation-token'; +import { Token } from '../core/tokens'; +import { FnJoin } from './cloudformation-token'; import { FnCondition } from './condition'; // tslint:disable:max-line-length @@ -19,7 +20,7 @@ export class Fn { * attributes available for that resource type. * @returns a CloudFormationToken object */ - public static getAtt(logicalNameOfResource: string, attributeName: string): CloudFormationToken { + public static getAtt(logicalNameOfResource: string, attributeName: string): Token { return new FnGetAtt(logicalNameOfResource, attributeName); } @@ -285,7 +286,7 @@ export class Fn { /** * Base class for tokens that represent CloudFormation intrinsic functions. */ -class FnBase extends CloudFormationToken { +class FnBase extends Token { constructor(name: string, value: any) { super({ [name]: value }); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 6e51156096da3..4bf2a98adea57 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,5 +1,6 @@ import { Construct } from '../core/construct'; -import { CloudFormationToken, StackAwareCloudFormationToken } from './cloudformation-token'; +import { Token } from '../core/tokens'; +import { StackAwareCloudFormationToken } from './cloudformation-token'; export class PseudoParameter extends StackAwareCloudFormationToken { constructor(anchor: Construct | undefined, name: string) { @@ -31,7 +32,7 @@ export class AwsNotificationARNs extends PseudoParameter { } } -export class AwsNoValue extends CloudFormationToken { +export class AwsNoValue extends Token { constructor() { super({ Ref: 'AWS::NoValue' }); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 36a6a6e655085..be0641f8b92d1 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -3,7 +3,7 @@ import { App } from '../app'; import { Construct, PATH_SEP } from '../core/construct'; import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; -import { CloudFormationToken, StackAwareCloudFormationToken } from './cloudformation-token'; +import { cloudFormationConcat, StackAwareCloudFormationToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -154,7 +154,7 @@ export class Stack extends Construct { public toCloudFormation() { // before we begin synthesis, we shall lock this stack, so children cannot be added this.lock(); - const options = RESOLVE_OPTIONS.push({ concat: CloudFormationToken.cloudFormationConcat }); + const options = RESOLVE_OPTIONS.push({ concat: cloudFormationConcat }); try { const template: any = { @@ -284,7 +284,7 @@ export class Stack extends Construct { } public applyCrossEnvironmentReferences() { - const options = RESOLVE_OPTIONS.push({ concat: CloudFormationToken.cloudFormationConcat }); + const options = RESOLVE_OPTIONS.push({ concat: cloudFormationConcat }); try { const elements = stackElements(this); elements.forEach(e => e.substituteCrossStackReferences(this)); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts index bca269a951696..27f3316093112 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationJSON, CloudFormationToken, Fn, resolve, Token } from '../../lib'; +import { CloudFormationJSON, Fn, resolve, Token } from '../../lib'; import { makeCloudformationTestSuite } from '../util'; import { evaluateCFN } from './evaluate-cfn'; @@ -74,7 +74,7 @@ export = makeCloudformationTestSuite({ 'intrinsic Tokens embed correctly in JSONification'(test: Test) { // GIVEN - const bucketName = new CloudFormationToken({ Ref: 'MyBucket' }); + const bucketName = new Token({ Ref: 'MyBucket' }); // WHEN const resolved = resolve(CloudFormationJSON.stringify({ theBucket: bucketName })); @@ -105,7 +105,7 @@ export = makeCloudformationTestSuite({ 'Tokens in Tokens are handled correctly'(test: Test) { // GIVEN - const bucketName = new CloudFormationToken({ Ref: 'MyBucket' }); + const bucketName = new Token({ Ref: 'MyBucket' }); const combinedName = Fn.join('', [ 'The bucket name is ', bucketName.toString() ]); // WHEN @@ -135,7 +135,7 @@ export = makeCloudformationTestSuite({ 'Doubly nested intrinsics evaluate correctly in JSON context'(test: Test) { // WHEN - const fidoSays = new CloudFormationToken(() => ({ Ref: 'Something' })); + const fidoSays = new Token(() => ({ Ref: 'Something' })); // WHEN const resolved = resolve(CloudFormationJSON.stringify({ diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts index 7057de62e3cab..846411bf9a0e9 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts @@ -1,9 +1,8 @@ import fc = require('fast-check'); import _ = require('lodash'); import nodeunit = require('nodeunit'); -import { CloudFormationToken } from '../../lib/cloudformation/cloudformation-token'; import { Fn } from '../../lib/cloudformation/fn'; -import { resolve } from '../../lib/core/tokens'; +import { resolve, Token } from '../../lib/core/tokens'; import { makeCloudformationTestSuite } from '../util'; function asyncTest(cb: (test: nodeunit.Test) => Promise): (test: nodeunit.Test) => void { @@ -98,8 +97,8 @@ export = nodeunit.testCase(makeCloudformationTestSuite({ })); function stringListToken(o: any): string[] { - return new CloudFormationToken(o).toList(); + return new Token(o).toList(); } function stringToken(o: any): string { - return new CloudFormationToken(o).toString(); + return new Token(o).toString(); } diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index 577140dc55c1b..cd54e8db718c5 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationToken, Fn, resolve, Token, unresolved } from '../../lib'; +import { Fn, resolve, Token, unresolved } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; import { makeCloudformationTestSuite } from '../util'; @@ -160,7 +160,7 @@ export = makeCloudformationTestSuite({ 'Tokens stringification and reversing of CloudFormation Tokens is implemented using Fn::Join'(test: Test) { // GIVEN - const token = new CloudFormationToken(() => ({ woof: 'woof' })); + const token = new Token(() => ({ woof: 'woof' })); // WHEN const stringified = `The dog says: ${token}`; @@ -260,7 +260,7 @@ export = makeCloudformationTestSuite({ 'fails if token in a hash key resolves to a non-string'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const s = { @@ -275,7 +275,7 @@ export = makeCloudformationTestSuite({ 'list encoding': { 'can encode Token to string and resolve the encoding'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const struct = { @@ -292,7 +292,7 @@ export = makeCloudformationTestSuite({ 'cannot add to encoded list'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const encoded: string[] = token.toList(); @@ -308,7 +308,7 @@ export = makeCloudformationTestSuite({ 'cannot add to strings in encoded list'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const encoded: string[] = token.toList(); @@ -324,7 +324,7 @@ export = makeCloudformationTestSuite({ 'can pass encoded lists to FnSelect'(test: Test) { // GIVEN - const encoded: string[] = new CloudFormationToken({ Ref: 'Other' }).toList(); + const encoded: string[] = new Token({ Ref: 'Other' }).toList(); // WHEN const struct = Fn.select(1, encoded); @@ -339,7 +339,7 @@ export = makeCloudformationTestSuite({ 'can pass encoded lists to FnJoin'(test: Test) { // GIVEN - const encoded: string[] = new CloudFormationToken({ Ref: 'Other' }).toList(); + const encoded: string[] = new Token({ Ref: 'Other' }).toList(); // WHEN const struct = Fn.join('/', encoded); @@ -402,8 +402,8 @@ function literalTokensThatResolveTo(value: any): Token[] { */ function cloudFormationTokensThatResolveTo(value: any): Token[] { return [ - new CloudFormationToken(value), - new CloudFormationToken(() => value) + new Token(value), + new Token(() => value) ]; } diff --git a/packages/@aws-cdk/cdk/test/util.ts b/packages/@aws-cdk/cdk/test/util.ts index e1efdc11eda51..85acc6f4ab7c5 100644 --- a/packages/@aws-cdk/cdk/test/util.ts +++ b/packages/@aws-cdk/cdk/test/util.ts @@ -1,5 +1,5 @@ import { ITestGroup } from 'nodeunit'; -import { CloudFormationToken, RESOLVE_OPTIONS } from "../lib"; +import { cloudFormationConcat, RESOLVE_OPTIONS } from "../lib"; /** * Update a nodeunit test suite so that we set up and tear down the proper CloudFormation token concatenator @@ -8,7 +8,7 @@ export function makeCloudformationTestSuite(tests: T): T { let options: any; tests.setUp = (callback: () => void) => { - options = RESOLVE_OPTIONS.push({ concat: CloudFormationToken.cloudFormationConcat }); + options = RESOLVE_OPTIONS.push({ concat: cloudFormationConcat }); callback(); }; From 0a724b487873345ba6e0d362e28a30f79923c641 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Dec 2018 13:57:30 +0100 Subject: [PATCH 10/39] Rename StackAwareCloudFormationToken -> StackAwareToken --- .../cdk/lib/cloudformation/cloudformation-token.ts | 8 ++++---- packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 4 ++-- packages/@aws-cdk/cdk/lib/cloudformation/resource.ts | 4 ++-- packages/@aws-cdk/cdk/lib/cloudformation/stack.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 4b36db34fe155..370902d22ecad 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -14,12 +14,12 @@ export function cloudFormationConcat(left: any | undefined, right: any | undefin return new FnJoin('', parts); } -export class StackAwareCloudFormationToken extends Token { - public static isInstance(x: any): x is StackAwareCloudFormationToken { +export class StackAwareToken extends Token { + public static isInstance(x: any): x is StackAwareToken { return x && x._isStackAwareCloudFormationToken; } - protected readonly _isStackAwareCloudFormationToken: boolean; + protected readonly _isStackAwareToken: boolean; private readonly tokenStack?: Stack; @@ -28,7 +28,7 @@ export class StackAwareCloudFormationToken extends Token { throw new Error('StackAwareCloudFormationToken can only contain eager values'); } super(value, displayName); - this._isStackAwareCloudFormationToken = true; + this._isStackAwareToken = true; if (anchor !== undefined) { this.tokenStack = Stack.find(anchor); diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 4bf2a98adea57..23f14b0d60f52 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,8 +1,8 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { StackAwareCloudFormationToken } from './cloudformation-token'; +import { StackAwareToken } from './cloudformation-token'; -export class PseudoParameter extends StackAwareCloudFormationToken { +export class PseudoParameter extends StackAwareToken { constructor(anchor: Construct | undefined, name: string) { super(anchor, { Ref: name }, name); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 694ac485a1cbf..3518f3ee6091a 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; -import { StackAwareCloudFormationToken } from './cloudformation-token'; +import { StackAwareToken } from './cloudformation-token'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; import { IDependable, Referenceable, Stack, StackElement } from './stack'; @@ -105,7 +105,7 @@ export class Resource extends Referenceable { * @param attributeName The name of the attribute. */ public getAtt(attributeName: string) { - return new StackAwareCloudFormationToken(this, { 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`); + return new StackAwareToken(this, { 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`); } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index be0641f8b92d1..5f6da1865988f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -3,7 +3,7 @@ import { App } from '../app'; import { Construct, PATH_SEP } from '../core/construct'; import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; -import { cloudFormationConcat, StackAwareCloudFormationToken } from './cloudformation-token'; +import { cloudFormationConcat, StackAwareToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -474,7 +474,7 @@ export abstract class StackElement extends Construct implements IDependable { public abstract substituteCrossStackReferences(sourceStack: Stack): void; protected deepSubCrossStackReferences(sourceStack: Stack, x: any): any { - if (StackAwareCloudFormationToken.isInstance(x)) { + if (StackAwareToken.isInstance(x)) { return x.substituteToken(sourceStack); } @@ -569,7 +569,7 @@ function stackElements(node: Construct, into: StackElement[] = []): StackElement /** * A generic, untyped reference to a Stack Element */ -export class Ref extends StackAwareCloudFormationToken { +export class Ref extends StackAwareToken { constructor(element: StackElement) { super(element, { Ref: element.logicalId }, `${element.logicalId}.Ref`); } From 445805fc94df9c09df107c3d2f277e77c4785714 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Dec 2018 14:03:56 +0100 Subject: [PATCH 11/39] CloudFormationConcat is no longer configurable, but just the default --- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 13 +++------- packages/@aws-cdk/cdk/lib/core/tokens.ts | 25 +++---------------- .../cdk/test/cloudformation/test.arn.ts | 5 ++-- .../test.cloudformation-json.ts | 5 ++-- .../cloudformation/test.dynamic-reference.ts | 5 ++-- .../cdk/test/cloudformation/test.fn.ts | 5 ++-- .../cdk/test/cloudformation/test.output.ts | 5 ++-- .../cdk/test/cloudformation/test.parameter.ts | 5 ++-- .../cdk/test/cloudformation/test.resource.ts | 5 ++-- .../cdk/test/cloudformation/test.secret.ts | 5 ++-- .../cdk/test/core/test.tag-manager.ts | 5 ++-- .../@aws-cdk/cdk/test/core/test.tokens.ts | 5 ++-- packages/@aws-cdk/cdk/test/core/test.util.ts | 5 ++-- packages/@aws-cdk/cdk/test/test.app.ts | 5 ++-- packages/@aws-cdk/cdk/test/test.context.ts | 5 ++-- packages/@aws-cdk/cdk/test/util.ts | 21 ---------------- 16 files changed, 33 insertions(+), 91 deletions(-) delete mode 100644 packages/@aws-cdk/cdk/test/util.ts diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 5f6da1865988f..5283367e445ee 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -3,7 +3,7 @@ import { App } from '../app'; import { Construct, PATH_SEP } from '../core/construct'; import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; -import { cloudFormationConcat, StackAwareToken } from './cloudformation-token'; +import { StackAwareToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -154,7 +154,6 @@ export class Stack extends Construct { public toCloudFormation() { // before we begin synthesis, we shall lock this stack, so children cannot be added this.lock(); - const options = RESOLVE_OPTIONS.push({ concat: cloudFormationConcat }); try { const template: any = { @@ -181,7 +180,6 @@ export class Stack extends Construct { } finally { // allow mutations after synthesis is finished. this.unlock(); - options.pop(); } } @@ -284,13 +282,8 @@ export class Stack extends Construct { } public applyCrossEnvironmentReferences() { - const options = RESOLVE_OPTIONS.push({ concat: cloudFormationConcat }); - try { - const elements = stackElements(this); - elements.forEach(e => e.substituteCrossStackReferences(this)); - } finally { - options.pop(); - } + const elements = stackElements(this); + elements.forEach(e => e.substituteCrossStackReferences(this)); } /** diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index 4af66d338743e..158716b3a7172 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -1,3 +1,4 @@ +import { cloudFormationConcat } from "../cloudformation/cloudformation-token"; import { Construct } from "./construct"; /** @@ -176,11 +177,7 @@ export function resolve(obj: any, prefix?: string[]): any { // string - potentially replace all stringified Tokens // if (typeof(obj) === 'string') { - const concat = RESOLVE_OPTIONS.concatFunc; - if (!concat) { - throw new Error('Cannot resolve a string with Tokens; no concatenation function given'); - } - return TOKEN_MAP.resolveStringTokens(obj as string, recurse, concat); + return TOKEN_MAP.resolveStringTokens(obj as string, recurse); } // @@ -308,10 +305,10 @@ class TokenMap { /** * Replace any Token markers in this string with their resolved values */ - public resolveStringTokens(s: string, resolver: ResolveFunc, concat: ConcatFunc): any { + public resolveStringTokens(s: string, resolver: ResolveFunc): any { const str = this.createStringTokenString(s); const fragments = str.split(this.lookupToken.bind(this)); - const ret = fragments.mapUnresolved(resolver).join(concat); + const ret = fragments.mapUnresolved(resolver).join(cloudFormationConcat); if (unresolved(ret)) { return resolve(ret); } @@ -540,15 +537,6 @@ export class ResolveConfiguration { }; } - public get concatFunc(): ConcatFunc | undefined { - for (let i = this.options.length - 1; i >= 0; i--) { - if (this.options[i].concat) { - return this.options[i].concat; - } - } - return undefined; - } - public get recurse(): ResolveFunc | undefined { for (let i = this.options.length - 1; i >= 0; i--) { if (this.options[i].recurse) { @@ -564,11 +552,6 @@ export interface OptionsContext { } export interface ResolveOptions { - /** - * Function to use for concatenating symbols in the target document language - */ - concat?: ConcatFunc; - /** * What function to use for recursing into deeper resolutions */ diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index 5fe8e3e4d0356..623dc2ab01a89 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,8 +1,7 @@ import { Test } from 'nodeunit'; import { ArnComponents, ArnUtils, AwsAccountId, AwsPartition, AwsRegion, resolve, Stack, Token } from '../../lib'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'create from components with defaults'(test: Test) { const stack = new Stack(); @@ -171,4 +170,4 @@ export = makeCloudformationTestSuite({ } }, -}); +}; diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts index 27f3316093112..8366e7c4084d5 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts @@ -1,9 +1,8 @@ import { Test } from 'nodeunit'; import { CloudFormationJSON, Fn, resolve, Token } from '../../lib'; -import { makeCloudformationTestSuite } from '../util'; import { evaluateCFN } from './evaluate-cfn'; -export = makeCloudformationTestSuite({ +export = { 'plain JSON.stringify() on a Token fails'(test: Test) { // GIVEN const token = new Token(() => 'value'); @@ -163,7 +162,7 @@ export = makeCloudformationTestSuite({ test.done(); }, -}); +}; /** * Return two Tokens, one of which evaluates to a Token directly, one which evaluates to it lazily diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts index 71e3708bf57cf..4dc2f2767f40d 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts @@ -1,8 +1,7 @@ import { Test } from 'nodeunit'; import { DynamicReference, DynamicReferenceService, resolve, Stack } from '../../lib'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'can create dynamic references with service and key with colons'(test: Test) { // GIVEN const stack = new Stack(); @@ -18,4 +17,4 @@ export = makeCloudformationTestSuite({ test.done(); }, -}); +}; diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts index 846411bf9a0e9..a4dd6a3976f5f 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts @@ -3,7 +3,6 @@ import _ = require('lodash'); import nodeunit = require('nodeunit'); import { Fn } from '../../lib/cloudformation/fn'; import { resolve, Token } from '../../lib/core/tokens'; -import { makeCloudformationTestSuite } from '../util'; function asyncTest(cb: (test: nodeunit.Test) => Promise): (test: nodeunit.Test) => void { return async (test: nodeunit.Test) => { @@ -25,7 +24,7 @@ const nonEmptyString = fc.string(1, 16); const tokenish = fc.array(nonEmptyString, 2, 2).map(arr => ({ [arr[0]]: arr[1] })); const anyValue = fc.oneof(nonEmptyString, tokenish); -export = nodeunit.testCase(makeCloudformationTestSuite({ +export = nodeunit.testCase({ FnJoin: { 'rejects empty list of arguments to join'(test: nodeunit.Test) { test.throws(() => Fn.join('.', [])); @@ -94,7 +93,7 @@ export = nodeunit.testCase(makeCloudformationTestSuite({ ); }), }, -})); +}); function stringListToken(o: any): string[] { return new Token(o).toList(); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts index 7e8e8f63d52d7..9121b85287169 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts @@ -1,8 +1,7 @@ import { Test } from 'nodeunit'; import { Construct, Output, Ref, resolve, Resource, Stack } from '../../lib'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'outputs can be added to the stack'(test: Test) { const stack = new Stack(); const res = new Resource(stack, 'MyResource', { type: 'R' }); @@ -67,4 +66,4 @@ export = makeCloudformationTestSuite({ test.deepEqual(resolve(output.makeImportValue()), { 'Fn::ImportValue': 'MyStack:MyOutput' }); test.done(); } -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts index 537834c9a768d..5bccaec77161a 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts @@ -1,8 +1,7 @@ import { Test } from 'nodeunit'; import { Construct, Parameter, resolve, Resource, Stack } from '../../lib'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'parameters can be used and referenced using param.ref'(test: Test) { const stack = new Stack(); @@ -36,4 +35,4 @@ export = makeCloudformationTestSuite({ test.deepEqual(resolve(param), { Ref: 'MyParam' }); test.done(); } -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 66c00f43ebf86..c4feb2ee6d714 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -3,9 +3,8 @@ import { Test } from 'nodeunit'; import { applyRemovalPolicy, Condition, Construct, DeletionPolicy, Fn, HashedAddressingScheme, IDependable, RemovalPolicy, resolve, Resource, Root, Stack } from '../../lib'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'all resources derive from Resource, which derives from Entity'(test: Test) { const stack = new Stack(); @@ -604,7 +603,7 @@ export = makeCloudformationTestSuite({ test.done(); } -}); +}; interface CounterProps { // tslint:disable-next-line:variable-name diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts index cf31aaef0aad3..33422251adaa9 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts @@ -1,8 +1,7 @@ import { Test } from 'nodeunit'; import { resolve, Secret, SecretParameter, Stack } from '../../lib'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'Secret is merely a token'(test: Test) { const foo = new Secret('Foo'); const bar = new Secret(() => 'Bar'); @@ -48,4 +47,4 @@ export = makeCloudformationTestSuite({ test.done(); } -}); +}; diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts index 54d334786a3a0..d2e186262557b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -2,7 +2,6 @@ import { Test } from 'nodeunit'; import { Construct, Root } from '../../lib/core/construct'; import { ITaggable, TagManager } from '../../lib/core/tag-manager'; import { resolve } from '../../lib/core/tokens'; -import { makeCloudformationTestSuite as testSuiteWithCloudFormationResolve } from '../util'; class ChildTagger extends Construct implements ITaggable { public readonly tags: TagManager; @@ -18,7 +17,7 @@ class Child extends Construct { } } -export = testSuiteWithCloudFormationResolve({ +export = { 'TagManger handles tags for a Contruct Tree': { 'setTag by default propagates to children'(test: Test) { const root = new Root(); @@ -180,4 +179,4 @@ export = testSuiteWithCloudFormationResolve({ test.done(); }, }, -}); +}; diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index cd54e8db718c5..50f57e12cf405 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -1,9 +1,8 @@ import { Test } from 'nodeunit'; import { Fn, resolve, Token, unresolved } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'resolve a plain old object should just return the object'(test: Test) { const obj = { PlainOldObject: 123, Array: [ 1, 2, 3 ] }; test.deepEqual(resolve(obj), obj); @@ -352,7 +351,7 @@ export = makeCloudformationTestSuite({ test.done(); } } -}); +}; class Promise2 extends Token { public resolve() { diff --git a/packages/@aws-cdk/cdk/test/core/test.util.ts b/packages/@aws-cdk/cdk/test/core/test.util.ts index d10ae42580aa7..2a7cb94ec87c5 100644 --- a/packages/@aws-cdk/cdk/test/core/test.util.ts +++ b/packages/@aws-cdk/cdk/test/core/test.util.ts @@ -1,8 +1,7 @@ import { Test } from 'nodeunit'; import { capitalizePropertyNames, ignoreEmpty } from '../../lib/core/util'; -import { makeCloudformationTestSuite } from '../util'; -export = makeCloudformationTestSuite({ +export = { 'capitalizeResourceProperties capitalizes all keys of an object (recursively) from camelCase to PascalCase'(test: Test) { test.equal(capitalizePropertyNames(undefined), undefined); @@ -65,7 +64,7 @@ export = makeCloudformationTestSuite({ test.done(); } } -}); +}; class SomeToken { public foo = 60; diff --git a/packages/@aws-cdk/cdk/test/test.app.ts b/packages/@aws-cdk/cdk/test/test.app.ts index a5badbb9b75e7..fab2ff1678603 100644 --- a/packages/@aws-cdk/cdk/test/test.app.ts +++ b/packages/@aws-cdk/cdk/test/test.app.ts @@ -5,7 +5,6 @@ import os = require('os'); import path = require('path'); import { Construct, Resource, Stack, StackProps } from '../lib'; import { App } from '../lib/app'; -import { makeCloudformationTestSuite } from './util'; function withApp(context: { [key: string]: any } | undefined, block: (app: App) => void) { const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-app-test')); @@ -63,7 +62,7 @@ function synthStack(name: string, includeMetadata: boolean = false, context?: an return stack; } -export = makeCloudformationTestSuite({ +export = { 'synthesizes all stacks and returns synthesis result'(test: Test) { const response = synth(); @@ -318,7 +317,7 @@ export = makeCloudformationTestSuite({ test.done(); }, -}); +}; class MyConstruct extends Construct { constructor(parent: Construct, name: string) { diff --git a/packages/@aws-cdk/cdk/test/test.context.ts b/packages/@aws-cdk/cdk/test/test.context.ts index 9a52dd8f7f14d..f061a16c4b443 100644 --- a/packages/@aws-cdk/cdk/test/test.context.ts +++ b/packages/@aws-cdk/cdk/test/test.context.ts @@ -2,9 +2,8 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { App, AvailabilityZoneProvider, Construct, ContextProvider, MetadataEntry, resolve, SSMParameterProvider, Stack } from '../lib'; -import { makeCloudformationTestSuite } from './util'; -export = makeCloudformationTestSuite({ +export = { 'AvailabilityZoneProvider returns a list with dummy values if the context is not available'(test: Test) { const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); const azs = new AvailabilityZoneProvider(stack).availabilityZones; @@ -112,7 +111,7 @@ export = makeCloudformationTestSuite({ test.done(); }, -}); +}; function firstKey(obj: any): string { return Object.keys(obj)[0]; diff --git a/packages/@aws-cdk/cdk/test/util.ts b/packages/@aws-cdk/cdk/test/util.ts deleted file mode 100644 index 85acc6f4ab7c5..0000000000000 --- a/packages/@aws-cdk/cdk/test/util.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ITestGroup } from 'nodeunit'; -import { cloudFormationConcat, RESOLVE_OPTIONS } from "../lib"; - -/** - * Update a nodeunit test suite so that we set up and tear down the proper CloudFormation token concatenator - */ -export function makeCloudformationTestSuite(tests: T): T { - let options: any; - - tests.setUp = (callback: () => void) => { - options = RESOLVE_OPTIONS.push({ concat: cloudFormationConcat }); - callback(); - }; - - tests.tearDown = (callback: () => void) => { - options.pop(); - callback(); - }; - - return tests; -} \ No newline at end of file From cc928f84b38d321d5433dbcde958c9454b3a5e15 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Dec 2018 14:36:05 +0100 Subject: [PATCH 12/39] Remove sourceStack argument to substitution --- packages/@aws-cdk/aws-apigateway/lib/restapi.ts | 2 +- .../aws-codebuild/test/test.codebuild.ts | 2 +- .../aws-codepipeline-api/lib/artifact.ts | 6 +++--- packages/@aws-cdk/aws-iam/lib/util.ts | 6 +++--- packages/@aws-cdk/aws-s3/test/test.util.ts | 4 ++-- packages/@aws-cdk/cdk/lib/app.ts | 2 +- .../lib/cloudformation/cloudformation-token.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/condition.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/include.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/mapping.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/parameter.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/resource.ts | 4 ++-- packages/@aws-cdk/cdk/lib/cloudformation/rule.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 16 ++++++++-------- packages/@aws-cdk/cdk/lib/core/tokens.ts | 2 +- .../cdk/test/cloudformation/test.stack.ts | 2 +- 17 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 35304e1b88766..5a92cf2e9fcac 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -404,7 +404,7 @@ export enum EndpointType { Private = 'PRIVATE' } -export class RestApiUrl extends cdk.CloudFormationToken { } +export class RestApiUrl extends cdk.Token { } class ImportedRestApi extends cdk.Construct implements IRestApi { public restApiId: string; diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 187b72cbcacb7..f4a58d9d7d40e 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -857,7 +857,7 @@ export = { environment: { environmentVariables: { FOO: { value: '1234' }, - BAR: { value: `111${new cdk.CloudFormationToken({ twotwotwo: '222' })}`, type: codebuild.BuildEnvironmentVariableType.ParameterStore } + BAR: { value: `111${new cdk.Token({ twotwotwo: '222' })}`, type: codebuild.BuildEnvironmentVariableType.ParameterStore } } }, environmentVariables: { diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts index b31da4dc4884a..d351d44a7f155 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts @@ -1,4 +1,4 @@ -import { CloudFormationToken, Construct } from "@aws-cdk/cdk"; +import { Construct, Token } from "@aws-cdk/cdk"; import { Action } from "./action"; /** @@ -72,9 +72,9 @@ export class ArtifactPath { } function artifactAttribute(artifact: Artifact, attributeName: string) { - return new CloudFormationToken(() => ({ 'Fn::GetArtifactAtt': [artifact.name, attributeName] })).toString(); + return new Token(() => ({ 'Fn::GetArtifactAtt': [artifact.name, attributeName] })).toString(); } function artifactGetParam(artifact: Artifact, jsonFile: string, keyName: string) { - return new CloudFormationToken(() => ({ 'Fn::GetParam': [artifact.name, jsonFile, keyName] })).toString(); + return new Token(() => ({ 'Fn::GetParam': [artifact.name, jsonFile, keyName] })).toString(); } diff --git a/packages/@aws-cdk/aws-iam/lib/util.ts b/packages/@aws-cdk/aws-iam/lib/util.ts index 9acb4a71747bc..aeb0f2c39652f 100644 --- a/packages/@aws-cdk/aws-iam/lib/util.ts +++ b/packages/@aws-cdk/aws-iam/lib/util.ts @@ -1,10 +1,10 @@ -import { CloudFormationToken } from '@aws-cdk/cdk'; +import { Token } from '@aws-cdk/cdk'; import { Policy } from './policy'; const MAX_POLICY_NAME_LEN = 128; -export function undefinedIfEmpty(f: () => T[]): CloudFormationToken { - return new CloudFormationToken(() => { +export function undefinedIfEmpty(f: () => T[]): Token { + return new Token(() => { const array = f(); return (array && array.length > 0) ? array : undefined; }); diff --git a/packages/@aws-cdk/aws-s3/test/test.util.ts b/packages/@aws-cdk/aws-s3/test/test.util.ts index 2f33e12c81a02..4116984d16250 100644 --- a/packages/@aws-cdk/aws-s3/test/test.util.ts +++ b/packages/@aws-cdk/aws-s3/test/test.util.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { CloudFormationToken } from '@aws-cdk/cdk'; +import { Token } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { parseBucketArn, parseBucketName } from '../lib/util'; @@ -42,7 +42,7 @@ export = { }, 'undefined if cannot extract name from a non-string arn'(test: Test) { - const bucketArn = `arn:aws:s3:::${new CloudFormationToken({ Ref: 'my-bucket' })}`; + const bucketArn = `arn:aws:s3:::${new Token({ Ref: 'my-bucket' })}`; test.deepEqual(cdk.resolve(parseBucketName({ bucketArn })), undefined); test.done(); }, diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index f82c079d56516..2bdfcaaecc8ef 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -84,7 +84,7 @@ export class App extends Root { missing, template: stack.toCloudFormation(), metadata: this.collectMetadata(stack), - dependsOn: stack.dependencyStackIds(), + dependsOn: stack.dependencies().map(s => s.id), }; } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 370902d22ecad..24b14f5f9739a 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -41,7 +41,7 @@ export class StackAwareToken extends Token { public substituteToken(consumingStack: Stack): Token { if (this.tokenStack && this.tokenStack !== consumingStack) { // We're trying to resolve a cross-stack reference - consumingStack.addStackDependency(this.tokenStack); + consumingStack.addDependency(this.tokenStack); return this.tokenStack.exportValue(this, consumingStack); } // In case of doubt, return same Token diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index 75ac6cd545f3e..5b20ceaca3cfb 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -33,8 +33,8 @@ export class Condition extends Referenceable { }; } - public substituteCrossStackReferences(sourceStack: Stack): void { - this.expression = this.deepSubCrossStackReferences(sourceStack, this.expression); + public substituteCrossStackReferences(): void { + this.expression = this.deepSubCrossStackReferences(Stack.find(this), this.expression); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index 6d23ee8a197f8..c77f17aaff4fb 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { Stack, StackElement } from './stack'; +import { StackElement } from './stack'; export interface IncludeProps { /** @@ -35,7 +35,7 @@ export class Include extends StackElement { return this.template; } - public substituteCrossStackReferences(_sourceStack: Stack): void { + public substituteCrossStackReferences(): void { // Left empty on purpose } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index 4e21eb911bee4..6ac4c5a8fdb5a 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -51,7 +51,7 @@ export class Mapping extends Referenceable { }; } - public substituteCrossStackReferences(sourceStack: Stack): void { - this.mapping = this.deepSubCrossStackReferences(sourceStack, this.mapping); + public substituteCrossStackReferences(): void { + this.mapping = this.deepSubCrossStackReferences(Stack.find(this), this.mapping); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index 97855a1dd9ae1..513dbe6b21289 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -123,8 +123,8 @@ export class Output extends StackElement { }; } - public substituteCrossStackReferences(sourceStack: Stack): void { - this._value = this.deepSubCrossStackReferences(sourceStack, this._value); + public substituteCrossStackReferences(): void { + this._value = this.deepSubCrossStackReferences(Stack.find(this), this._value); } public get ref(): string { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index a4d8b09b22de2..b076b5cfd8aa5 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -124,8 +124,8 @@ export class Parameter extends Referenceable { }; } - public substituteCrossStackReferences(sourceStack: Stack): void { - this.properties = this.deepSubCrossStackReferences(sourceStack, this.properties); + public substituteCrossStackReferences(): void { + this.properties = this.deepSubCrossStackReferences(Stack.find(this), this.properties); } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 3518f3ee6091a..8e7212421460f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -209,8 +209,8 @@ export class Resource extends Referenceable { } } - public substituteCrossStackReferences(sourceStack: Stack): void { - this.deepSubCrossStackReferences(sourceStack, this.properties); + public substituteCrossStackReferences(): void { + this.deepSubCrossStackReferences(Stack.find(this), this.properties); } protected renderProperties(properties: any): { [key: string]: any } { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index 262541e0f2499..4e83a482c12dd 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -1,7 +1,7 @@ import { Construct } from '../core/construct'; import { capitalizePropertyNames } from '../core/util'; import { FnCondition } from './condition'; -import { Referenceable, Stack } from './stack'; +import { Referenceable } from './stack'; /** * A rule can include a RuleCondition property and must include an Assertions property. @@ -103,7 +103,7 @@ export class Rule extends Referenceable { }; } - public substituteCrossStackReferences(_sourceStack: Stack): void { + public substituteCrossStackReferences(): void { // Empty on purpose } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 5283367e445ee..dc6ed36e3f539 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -104,7 +104,7 @@ export class Stack extends Construct { /** * Other stacks this stack depends on */ - private readonly dependsOnStacks = new Set(); + private readonly stackDependencies = new Set(); /** * A construct to hold cross-stack exports @@ -245,16 +245,16 @@ export class Stack extends Construct { /** * Add a dependency between this stack and another stack */ - public addStackDependency(stack: Stack) { + public addDependency(stack: Stack) { if (stack.dependsOnStack(this)) { // tslint:disable-next-line:max-line-length throw new Error(`Stack '${this.name}' already depends on stack '${stack.name}'. Adding this dependency would create a cyclic reference.`); } - this.dependsOnStacks.add(stack); + this.stackDependencies.add(stack); } - public dependencyStackIds(): string[] { - return Array.from(this.dependsOnStacks.values()).map(s => s.id); + public dependencies(): Stack[] { + return Array.from(this.stackDependencies.values()); } /** @@ -283,7 +283,7 @@ export class Stack extends Construct { public applyCrossEnvironmentReferences() { const elements = stackElements(this); - elements.forEach(e => e.substituteCrossStackReferences(this)); + elements.forEach(e => e.substituteCrossStackReferences()); } /** @@ -323,7 +323,7 @@ export class Stack extends Construct { */ private dependsOnStack(other: Stack) { if (this === other) { return true; } - for (const dep of this.dependsOnStacks) { + for (const dep of this.stackDependencies) { if (dep.dependsOnStack(other)) { return true; } } return false; @@ -464,7 +464,7 @@ export abstract class StackElement extends Construct implements IDependable { */ public abstract toCloudFormation(): object; - public abstract substituteCrossStackReferences(sourceStack: Stack): void; + public abstract substituteCrossStackReferences(): void; protected deepSubCrossStackReferences(sourceStack: Stack, x: any): any { if (StackAwareToken.isInstance(x)) { diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index 158716b3a7172..4f94bfc3e57ee 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -177,7 +177,7 @@ export function resolve(obj: any, prefix?: string[]): any { // string - potentially replace all stringified Tokens // if (typeof(obj) === 'string') { - return TOKEN_MAP.resolveStringTokens(obj as string, recurse); + return TOKEN_MAP.resolveStringTokens(obj, recurse); } // diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index dcb17e4a22f89..12c4c2382ac0d 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -299,7 +299,7 @@ export = { app.applyCrossEnvironmentReferences(); // THEN - test.deepEqual(stack2.dependencyStackIds(), ['Stack1']); + test.deepEqual(stack2.dependencies().map(s => s.id), ['Stack1']); test.done(); }, From 3cb539f9f1c182f6e672b093f68c315cad281e43 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Dec 2018 14:52:03 +0100 Subject: [PATCH 13/39] Make JSII happy --- .../cloudformation/cloudformation-token.ts | 12 +++++++++-- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 20 +++++++++---------- .../cdk/lib/cloudformation/resource.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 2 +- packages/@aws-cdk/cdk/lib/core/tokens.ts | 8 ++++---- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 24b14f5f9739a..7adce26ac6db6 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -2,6 +2,9 @@ import { Construct } from "../core/construct"; import { resolve, Token, unresolved } from "../core/tokens"; import { Stack } from "./stack"; +/** + * Produce a CloudFormation expression to concat two arbitrary expressions + */ export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { if (left === undefined && right === undefined) { return ''; } @@ -9,8 +12,13 @@ export function cloudFormationConcat(left: any | undefined, right: any | undefin if (left !== undefined) { parts.push(left); } if (right !== undefined) { parts.push(right); } + // Some case analysis to produce minimal expressions if (parts.length === 1) { return parts[0]; } + if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { + return parts[0] + parts[1]; + } + // Otherwise return a Join intrinsic return new FnJoin('', parts); } @@ -23,9 +31,9 @@ export class StackAwareToken extends Token { private readonly tokenStack?: Stack; - constructor(anchor: Construct | undefined, value: any, displayName?: string) { + constructor(value: any, displayName?: string, anchor?: Construct) { if (typeof(value) === 'function') { - throw new Error('StackAwareCloudFormationToken can only contain eager values'); + throw new Error('StackAwareToken can only contain eager values'); } super(value, displayName); this._isStackAwareToken = true; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 23f14b0d60f52..f1b9b0d73ad54 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -3,32 +3,32 @@ import { Token } from '../core/tokens'; import { StackAwareToken } from './cloudformation-token'; export class PseudoParameter extends StackAwareToken { - constructor(anchor: Construct | undefined, name: string) { - super(anchor, { Ref: name }, name); + constructor(name: string, anchor?: Construct) { + super({ Ref: name }, name, anchor); } } export class AwsAccountId extends PseudoParameter { constructor(anchor: Construct) { - super(anchor, 'AWS::AccountId'); + super('AWS::AccountId', anchor); } } export class AwsDomainSuffix extends PseudoParameter { constructor(anchor: Construct) { - super(anchor, 'AWS::DomainSuffix'); + super('AWS::DomainSuffix', anchor); } } export class AwsURLSuffix extends PseudoParameter { constructor(anchor: Construct) { - super(anchor, 'AWS::URLSuffix'); + super('AWS::URLSuffix', anchor); } } export class AwsNotificationARNs extends PseudoParameter { constructor(anchor: Construct) { - super(anchor, 'AWS::NotificationARNs'); + super('AWS::NotificationARNs', anchor); } } @@ -40,24 +40,24 @@ export class AwsNoValue extends Token { export class AwsPartition extends PseudoParameter { constructor(anchor: Construct) { - super(anchor, 'AWS::Partition'); + super('AWS::Partition', anchor); } } export class AwsRegion extends PseudoParameter { constructor(anchor: Construct | undefined) { - super(anchor, 'AWS::Region'); + super('AWS::Region', anchor); } } export class AwsStackId extends PseudoParameter { constructor(anchor: Construct) { - super(anchor, 'AWS::StackId'); + super('AWS::StackId', anchor); } } export class AwsStackName extends PseudoParameter { constructor(anchor: Construct) { - super(anchor, 'AWS::StackName'); + super('AWS::StackName', anchor); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 8e7212421460f..18dbe577ef4a5 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -105,7 +105,7 @@ export class Resource extends Referenceable { * @param attributeName The name of the attribute. */ public getAtt(attributeName: string) { - return new StackAwareToken(this, { 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`); + return new StackAwareToken({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`, this); } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index dc6ed36e3f539..a40ca049fbe0b 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -564,7 +564,7 @@ function stackElements(node: Construct, into: StackElement[] = []): StackElement */ export class Ref extends StackAwareToken { constructor(element: StackElement) { - super(element, { Ref: element.logicalId }, `${element.logicalId}.Ref`); + super({ Ref: element.logicalId }, `${element.logicalId}.Ref`, element); } } diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index 4f94bfc3e57ee..0fbc6d36cb598 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -521,10 +521,10 @@ function regexQuote(s: string) { * we cannot simply pass through options at each individual call. Instead, * we configure global context at the stack synthesis level. */ -export class ResolveConfiguration { +class ResolveConfiguration { private readonly options = new Array(); - public push(options: ResolveOptions): OptionsContext { + public push(options: ResolveOptions): IOptionsContext { this.options.push(options); return { @@ -547,11 +547,11 @@ export class ResolveConfiguration { } } -export interface OptionsContext { +interface IOptionsContext { pop(): void; } -export interface ResolveOptions { +interface ResolveOptions { /** * What function to use for recursing into deeper resolutions */ From 0840d761317f7d234e419a61359bd31e0f790d09 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Dec 2018 15:55:07 +0100 Subject: [PATCH 14/39] Make Pseudo parameters (StackId etc.) attributes of Stack - stack.accountId - stack.region - stack.partition - stack.urlSuffix etc. BREAKING CHANGE: pseudo parameters have disappeared and should now be retrieved as attributes of Stack. Some account-granting functions now have an additional parameter, 'anchor', in which you should pass the current scope (typically 'this'). --- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 4 +- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 4 +- packages/@aws-cdk/aws-cloudwatch/lib/graph.ts | 8 +-- .../@aws-cdk/aws-codecommit/lib/repository.ts | 3 +- .../aws-codedeploy/lib/deployment-group.ts | 10 +-- packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 4 +- .../aws-ecs/lib/log-drivers/aws-log-driver.ts | 3 +- .../@aws-cdk/aws-iam/lib/policy-document.ts | 22 +++--- packages/@aws-cdk/aws-kinesis/lib/stream.ts | 3 +- packages/@aws-cdk/aws-kms/test/integ.key.ts | 4 +- .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 6 +- .../@aws-cdk/aws-lambda/test/test.lambda.ts | 2 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 3 +- .../aws-stepfunctions/lib/state-machine.ts | 3 +- .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 39 ++++------ .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 64 +++++++++++++---- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 71 +++++++++++++++++++ .../cdk/test/cloudformation/test.arn.ts | 10 ++- .../cdk/test/cloudformation/test.stack.ts | 16 ++--- packages/@aws-cdk/runtime-values/lib/rtv.ts | 15 ++-- .../@aws-cdk/runtime-values/test/test.rtv.ts | 4 +- 21 files changed, 206 insertions(+), 92 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index 9707a6d8d7f2b..881e3183ca200 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { Stack } from '@aws-cdk/cdk'; import { CfnStage } from './apigateway.generated'; import { Deployment } from './deployment'; import { IRestApi } from './restapi'; @@ -173,7 +174,8 @@ export class Stage extends cdk.Construct implements cdk.IDependable { if (!path.startsWith('/')) { throw new Error(`Path must begin with "/": ${path}`); } - return `https://${this.restApi.restApiId}.execute-api.${new cdk.AwsRegion(this)}.amazonaws.com/${this.stageName}${path}`; + const stack = Stack.find(this); + return `https://${this.restApi.restApiId}.execute-api.${stack.region}.${stack.urlSuffix}/${this.stageName}${path}`; } private renderMethodSettings(props: StageProps): CfnStage.MethodSettingProperty[] | undefined { diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 6ba16f65b8a56..39ab98a2ef2d2 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -132,13 +132,15 @@ export class CloudTrail extends cdk.Construct { const s3bucket = new s3.Bucket(this, 'S3', {encryption: s3.BucketEncryption.Unencrypted}); const cloudTrailPrincipal = "cloudtrail.amazonaws.com"; + const stack = cdk.Stack.find(this); + s3bucket.addToResourcePolicy(new iam.PolicyStatement() .addResource(s3bucket.bucketArn) .addActions('s3:GetBucketAcl') .addServicePrincipal(cloudTrailPrincipal)); s3bucket.addToResourcePolicy(new iam.PolicyStatement() - .addResource(s3bucket.arnForObjects(`AWSLogs/${new cdk.AwsAccountId(this)}/*`)) + .addResource(s3bucket.arnForObjects(`AWSLogs/${stack.accountId}/*`)) .addActions("s3:PutObject") .addServicePrincipal(cloudTrailPrincipal) .setCondition("StringEquals", {'s3:x-amz-acl': "bucket-owner-full-control"})); diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts index a9f79a538a129..89ee6bd233994 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts @@ -1,4 +1,4 @@ -import { AwsRegion } from "@aws-cdk/cdk"; +import cdk = require('@aws-cdk/cdk'); import { Alarm } from "./alarm"; import { Metric } from "./metric"; import { parseStatistic } from './util.statistic'; @@ -73,7 +73,7 @@ export class AlarmWidget extends ConcreteWidget { properties: { view: 'timeSeries', title: this.props.title, - region: this.props.region || new AwsRegion(undefined), + region: this.props.region || new cdk.Token({ Ref: 'AWS::Region' }), annotations: { alarms: [this.props.alarm.alarmArn] }, @@ -150,7 +150,7 @@ export class GraphWidget extends ConcreteWidget { properties: { view: 'timeSeries', title: this.props.title, - region: this.props.region || new AwsRegion(undefined), + region: this.props.region || new cdk.Token({ Ref: 'AWS::Region' }), metrics: (this.props.left || []).map(m => metricJson(m, 'left')).concat( (this.props.right || []).map(m => metricJson(m, 'right'))), annotations: { @@ -197,7 +197,7 @@ export class SingleValueWidget extends ConcreteWidget { properties: { view: 'singleValue', title: this.props.title, - region: this.props.region || new AwsRegion(undefined), + region: this.props.region || new cdk.Token({ Ref: 'AWS::Region' }), metrics: this.props.metrics.map(m => metricJson(m, 'left')) } }]; diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 7972b017cc202..1a82a4ad8b3ca 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -264,7 +264,8 @@ class ImportedRepository extends RepositoryBase { } private repositoryCloneUrl(protocol: 'https' | 'ssh'): string { - return `${protocol}://git-codecommit.${new cdk.AwsRegion(this)}.${new cdk.AwsURLSuffix(this)}/v1/repos/${this.repositoryName}`; + const stack = cdk.Stack.find(this); + return `${protocol}://git-codecommit.${stack.region}.${stack.urlSuffix}/v1/repos/${this.repositoryName}`; } } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index 6694ff7353dc2..e046ecc27b906 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -310,9 +310,9 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { this._autoScalingGroups = props.autoScalingGroups || []; this.installAgent = props.installAgent === undefined ? true : props.installAgent; - const region = new cdk.AwsRegion(this).toString(); + const stack = cdk.Stack.find(this); this.codeDeployBucket = s3.Bucket.import(this, 'CodeDeployBucket', { - bucketName: `aws-codedeploy-${region}`, + bucketName: `aws-codedeploy-${stack.region}`, }); for (const asg of this._autoScalingGroups) { this.addCodeDeployAgentInstallUserData(asg); @@ -387,7 +387,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { this.codeDeployBucket.grantRead(asg.role, 'latest/*'); - const region = (new cdk.AwsRegion(this)).toString(); + const stack = cdk.Stack.find(this); switch (asg.osType) { case ec2.OperatingSystemType.Linux: asg.addUserData( @@ -405,7 +405,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { '$PKG_CMD install -y awscli', 'TMP_DIR=`mktemp -d`', 'cd $TMP_DIR', - `aws s3 cp s3://aws-codedeploy-${region}/latest/install . --region ${region}`, + `aws s3 cp s3://aws-codedeploy-${stack.region}/latest/install . --region ${stack.region}`, 'chmod +x ./install', './install auto', 'rm -fr $TMP_DIR', @@ -414,7 +414,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { case ec2.OperatingSystemType.Windows: asg.addUserData( 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName', - `aws s3 cp s3://aws-codedeploy-${region}/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi`, + `aws s3 cp s3://aws-codedeploy-${stack.region}/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi`, '$TEMPDIR\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', ); break; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index 3717491e9152d..c55e2efd0bfea 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -1,4 +1,4 @@ -import { AwsRegion, Construct, IDependable } from "@aws-cdk/cdk"; +import { Construct, IDependable, Stack } from "@aws-cdk/cdk"; import { subnetName } from './util'; export interface IVpcSubnet extends IDependable { @@ -260,7 +260,7 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork { * The region where this VPC is defined */ public get vpcRegion(): string { - return new AwsRegion(this).toString(); + return Stack.find(this).region; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts index 709c63a979e08..b5f48548c4e68 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -73,12 +73,13 @@ export class AwsLogDriver extends LogDriver { * Return the log driver CloudFormation JSON */ public renderLogDriver(): CfnTaskDefinition.LogConfigurationProperty { + const stack = cdk.Stack.find(this); return { logDriver: 'awslogs', options: removeEmpty({ 'awslogs-group': this.logGroup.logGroupName, 'awslogs-stream-prefix': this.props.streamPrefix, - 'awslogs-region': `${new cdk.AwsRegion(this)}`, + 'awslogs-region': stack.region, 'awslogs-datetime-format': this.props.datetimeFormat, 'awslogs-multiline-pattern': this.props.multilinePattern, }), diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index f4c8f08d7e424..fbfe83e343a9c 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -1,8 +1,6 @@ -import { Construct } from '@aws-cdk/cdk'; -import { Token } from '@aws-cdk/cdk'; -import { AwsAccountId, AwsPartition } from '@aws-cdk/cdk'; +import cdk = require('@aws-cdk/cdk'); -export class PolicyDocument extends Token { +export class PolicyDocument extends cdk.Token { private statements = new Array(); /** @@ -83,8 +81,8 @@ export class ArnPrincipal extends PolicyPrincipal { } export class AccountPrincipal extends ArnPrincipal { - constructor(public readonly anchor: Construct, public readonly accountId: any) { - super(`arn:${new AwsPartition(anchor)}:iam::${accountId}:root`); + constructor(public readonly anchor: cdk.Construct, public readonly accountId: any) { + super(`arn:${new cdk.Aws(anchor).partition}:iam::${accountId}:root`); } } @@ -138,8 +136,8 @@ export class FederatedPrincipal extends PolicyPrincipal { } export class AccountRootPrincipal extends AccountPrincipal { - constructor(anchor: Construct) { - super(anchor, new AwsAccountId(anchor)); + constructor(anchor: cdk.Construct) { + super(anchor, new cdk.Aws(anchor).accountId); } } @@ -203,7 +201,7 @@ export class CompositePrincipal extends PolicyPrincipal { /** * Represents a statement in an IAM policy document. */ -export class PolicyStatement extends Token { +export class PolicyStatement extends cdk.Token { private action = new Array(); private principal: { [key: string]: any[] } = {}; private resource = new Array(); @@ -252,7 +250,7 @@ export class PolicyStatement extends Token { return this.addPrincipal(new ArnPrincipal(arn)); } - public addAwsAccountPrincipal(anchor: Construct, accountId: string): this { + public addAwsAccountPrincipal(anchor: cdk.Construct, accountId: string): this { return this.addPrincipal(new AccountPrincipal(anchor, accountId)); } @@ -268,7 +266,7 @@ export class PolicyStatement extends Token { return this.addPrincipal(new FederatedPrincipal(federated, conditions)); } - public addAccountRootPrincipal(anchor: Construct): this { + public addAccountRootPrincipal(anchor: cdk.Construct): this { return this.addPrincipal(new AccountRootPrincipal(anchor)); } @@ -365,7 +363,7 @@ export class PolicyStatement extends Token { } public limitToAccount(accountId: string): PolicyStatement { - return this.addCondition('StringEquals', new Token(() => { + return this.addCondition('StringEquals', new cdk.Token(() => { return { 'sts:ExternalId': accountId }; })); } diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index aed046ded8989..fe634069d94c7 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -197,9 +197,10 @@ export abstract class StreamBase extends cdk.Construct implements IStream { public logSubscriptionDestination(sourceLogGroup: logs.ILogGroup): logs.LogSubscriptionDestination { // Following example from https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#DestinationKinesisExample if (!this.cloudWatchLogsRole) { + const stack = cdk.Stack.find(this); // Create a role to be assumed by CWL that can write to this stream and pass itself. this.cloudWatchLogsRole = new iam.Role(this, 'CloudWatchLogsCanPutRecords', { - assumedBy: new iam.ServicePrincipal(`logs.${new cdk.AwsRegion(this)}.amazonaws.com`) + assumedBy: new iam.ServicePrincipal(`logs.${stack.region}.amazonaws.com`) }); this.cloudWatchLogsRole.addToPolicy(new iam.PolicyStatement().addAction('kinesis:PutRecord').addResource(this.streamArn)); this.cloudWatchLogsRole.addToPolicy(new iam.PolicyStatement().addAction('iam:PassRole').addResource(this.cloudWatchLogsRole.roleArn)); diff --git a/packages/@aws-cdk/aws-kms/test/integ.key.ts b/packages/@aws-cdk/aws-kms/test/integ.key.ts index 8ea3e07de916e..370a696fbaab0 100644 --- a/packages/@aws-cdk/aws-kms/test/integ.key.ts +++ b/packages/@aws-cdk/aws-kms/test/integ.key.ts @@ -1,5 +1,5 @@ import { PolicyStatement } from '@aws-cdk/aws-iam'; -import { App, AwsAccountId, Stack } from '@aws-cdk/cdk'; +import { App, Stack } from '@aws-cdk/cdk'; import { EncryptionKey } from '../lib'; const app = new App(); @@ -11,7 +11,7 @@ const key = new EncryptionKey(stack, 'MyKey'); key.addToResourcePolicy(new PolicyStatement() .addAllResources() .addAction('kms:encrypt') - .addAwsPrincipal(new AwsAccountId(stack).toString())); + .addAwsPrincipal(stack.accountId)); key.addAlias('alias/bar'); diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 1a261551d8517..95612d13ceb12 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -323,12 +323,13 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction { const arn = sourceLogGroup.logGroupArn; if (this.logSubscriptionDestinationPolicyAddedFor.indexOf(arn) === -1) { + const stack = cdk.Stack.find(this); // NOTE: the use of {AWS::Region} limits this to the same region, which shouldn't really be an issue, // since the Lambda must be in the same region as the SubscriptionFilter anyway. // // (Wildcards in principals are unfortunately not supported. this.addPermission('InvokedByCloudWatchLogs', { - principal: new iam.ServicePrincipal(`logs.${new cdk.AwsRegion(this)}.amazonaws.com`), + principal: new iam.ServicePrincipal(`logs.${stack.region}.amazonaws.com`), sourceArn: arn }); this.logSubscriptionDestinationPolicyAddedFor.push(arn); @@ -347,9 +348,10 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction { */ public asBucketNotificationDestination(bucketArn: string, bucketId: string): s3n.BucketNotificationDestinationProps { const permissionId = `AllowBucketNotificationsFrom${bucketId}`; + const stack = cdk.Stack.find(this); if (!this.tryFindChild(permissionId)) { this.addPermission(permissionId, { - sourceAccount: new cdk.AwsAccountId(this).toString(), + sourceAccount: stack.accountId, principal: new iam.ServicePrincipal('s3.amazonaws.com'), sourceArn: bucketArn, }); diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 4f2bc269942bc..b36a0fe1be606 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -118,7 +118,7 @@ export = { fn.addPermission('S3Permission', { action: 'lambda:*', principal: new iam.ServicePrincipal('s3.amazonaws.com'), - sourceAccount: new cdk.AwsAccountId(stack).toString(), + sourceAccount: stack.accountId, sourceArn: 'arn:aws:s3:::my_bucket' }); diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 3d665ac79d498..2d71628fcc5e7 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -310,7 +310,8 @@ export abstract class BucketBase extends cdk.Construct implements IBucket { * @returns an ObjectS3Url token */ public urlForObject(key?: string): string { - const components = [ `https://s3.${new cdk.AwsRegion(this)}.${new cdk.AwsURLSuffix(this)}/${this.bucketName}` ]; + const stack = cdk.Stack.find(this); + const components = [ `https://s3.${stack.region}.${stack.urlSuffix}/${this.bucketName}` ]; if (key) { // trim prepending '/' if (typeof key === 'string' && key.startsWith('/')) { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 315bcceb88e62..48b4fa4efe7c0 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -71,8 +71,9 @@ export class StateMachine extends cdk.Construct implements IStateMachine { constructor(parent: cdk.Construct, id: string, props: StateMachineProps) { super(parent, id); + const stack = cdk.Stack.find(this); this.role = props.role || new iam.Role(this, 'Role', { - assumedBy: new iam.ServicePrincipal(`states.${new cdk.AwsRegion(this)}.amazonaws.com`), + assumedBy: new iam.ServicePrincipal(`states.${stack.region}.amazonaws.com`), }); const graph = new StateGraph(props.definition.startState, `State Machine ${id} definition`); diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index d3b66d6d1b6e8..7e6593485c3db 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,4 +1,4 @@ -import { AwsAccountId, AwsPartition, AwsRegion, Construct } from '..'; +import { Construct, Stack } from '..'; import { Fn } from '../cloudformation/fn'; import { unresolved } from '../core/tokens'; @@ -22,29 +22,9 @@ export class ArnUtils { * */ public static fromComponents(components: ArnComponents, anchor?: Construct): string { - let partition = components.partition; - if (partition == null) { - if (!anchor) { - throw new Error('Must provide anchor when using current partition'); - } - partition = new AwsPartition(anchor).toString(); - } - - let region = components.region; - if (region == null) { - if (!anchor) { - throw new Error('Must provide anchor when using current region'); - } - region = new AwsRegion(anchor).toString(); - } - - let account = components.account; - if (account == null) { - if (!anchor) { - throw new Error('Must provide anchor when using current account'); - } - account = new AwsAccountId(anchor).toString(); - } + const partition = components.partition || theStack('partition').partition; + const region = components.region || theStack('region').region; + const account = components.account || theStack('account').accountId; const values = [ 'arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource ]; @@ -59,6 +39,17 @@ export class ArnUtils { } return values.join(''); + + /** + * Return the anchored stack (so the caller can get an attribute from it), throw a descriptive error if we don't have an anchor + */ + function theStack(attribute: string) { + if (!anchor) { + throw new Error(`Must provide anchor when using implicit ${attribute}`); + } + return Stack.find(anchor); + + } } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index f1b9b0d73ad54..1af5cc4d721a1 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -2,31 +2,65 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; import { StackAwareToken } from './cloudformation-token'; -export class PseudoParameter extends StackAwareToken { - constructor(name: string, anchor?: Construct) { - super({ Ref: name }, name, anchor); +/** + * Accessor for pseudo parameters + * + * Since pseudo parameters need to be anchored to a stack somewhere in the + * construct tree, this class takes an anchor parameter; the pseudo parameter + * values can be obtained as properties from an anchored object. + */ +export class Aws { + constructor(private readonly anchor: Construct) { + } + + public get accountId(): string { + return new AwsAccountId(this.anchor).toString(); + } + + public get urlSuffix(): string { + return new AwsURLSuffix(this.anchor).toString(); + } + + public get notificationArns(): string[] { + return new AwsNotificationARNs(this.anchor).toList(); + } + + public get partition(): string { + return new AwsPartition(this.anchor).toString(); + } + + public get region(): string { + return new AwsRegion(this.anchor).toString(); + } + + public get stackId(): string { + return new AwsStackId(this.anchor).toString(); + } + + public get stackName(): string { + return new AwsStackName(this.anchor).toString(); } } -export class AwsAccountId extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::AccountId', anchor); +class PseudoParameter extends StackAwareToken { + constructor(name: string, anchor: Construct) { + super({ Ref: name }, name, anchor); } } -export class AwsDomainSuffix extends PseudoParameter { +class AwsAccountId extends PseudoParameter { constructor(anchor: Construct) { - super('AWS::DomainSuffix', anchor); + super('AWS::AccountId', anchor); } } -export class AwsURLSuffix extends PseudoParameter { +class AwsURLSuffix extends PseudoParameter { constructor(anchor: Construct) { super('AWS::URLSuffix', anchor); } } -export class AwsNotificationARNs extends PseudoParameter { +class AwsNotificationARNs extends PseudoParameter { constructor(anchor: Construct) { super('AWS::NotificationARNs', anchor); } @@ -38,25 +72,25 @@ export class AwsNoValue extends Token { } } -export class AwsPartition extends PseudoParameter { +class AwsPartition extends PseudoParameter { constructor(anchor: Construct) { super('AWS::Partition', anchor); } } -export class AwsRegion extends PseudoParameter { - constructor(anchor: Construct | undefined) { +class AwsRegion extends PseudoParameter { + constructor(anchor: Construct) { super('AWS::Region', anchor); } } -export class AwsStackId extends PseudoParameter { +class AwsStackId extends PseudoParameter { constructor(anchor: Construct) { super('AWS::StackId', anchor); } } -export class AwsStackName extends PseudoParameter { +class AwsStackName extends PseudoParameter { constructor(anchor: Construct) { super('AWS::StackName', anchor); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index a40ca049fbe0b..77fc7abaeebd0 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -5,6 +5,7 @@ import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; import { StackAwareToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; +import { Aws } from './pseudo'; import { Resource } from './resource'; export interface StackProps { @@ -281,6 +282,76 @@ export class Stack extends Construct { return new Token({ 'Fn::ImportValue': output.export }); } + /** + * The account in which this stack is defined + * + * Either returns the literal account for this stack, or a symbolic value + * that will evaluate to the correct account at deployment time. + */ + public get accountId(): string { + if (this.env.account) { + return this.env.account; + } + return new Aws(this).accountId; + } + + /** + * The region in which this stack is defined + * + * Either returns the literal region for this stack, or a symbolic value + * that will evaluate to the correct region at deployment time. + */ + public get region(): string { + if (this.env.region) { + return this.env.region; + } + return new Aws(this).region; + } + + /** + * The partition in which this stack is defined + */ + public get partition(): string { + return new Aws(this).partition; + } + + /** + * The Amazon domain suffix for the region in which this stack is defined + */ + public get urlSuffix(): string { + return new Aws(this).urlSuffix; + } + + /** + * The ID of the stack + * + * @example After resolving, looks like arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123 + */ + public get stackId(): string { + return new Aws(this).stackId; + } + + /** + * The name of the stack currently being deployed + * + * Only available at deployment time. + */ + public get stackName(): string { + return new Aws(this).stackName; + } + + /** + * Returns the list of notification Amazon Resource Names (ARNs) for the current stack. + */ + public get notificationArns(): string[] { + return new Aws(this).notificationArns; + } + + /** + * Find cross-stack references embedded in the stack's content and replace them + * + * Do not call this as an app author; this is automatically called as part of synthesis. + */ public applyCrossEnvironmentReferences() { const elements = stackElements(this); elements.forEach(e => e.substituteCrossStackReferences()); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index 623dc2ab01a89..1d5f50ba6f66b 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { ArnComponents, ArnUtils, AwsAccountId, AwsPartition, AwsRegion, resolve, Stack, Token } from '../../lib'; +import { ArnComponents, ArnUtils, Aws, resolve, Stack, Token } from '../../lib'; export = { 'create from components with defaults'(test: Test) { @@ -10,8 +10,10 @@ export = { resource: 'myqueuename' }, stack); + const pseudo = new Aws(stack); + test.deepEqual(resolve(arn), - resolve(`arn:${new AwsPartition(stack)}:sqs:${new AwsRegion(stack)}:${new AwsAccountId(stack)}:myqueuename`)); + resolve(`arn:${pseudo.partition}:sqs:${pseudo.region}:${pseudo.accountId}:myqueuename`)); test.done(); }, @@ -55,8 +57,10 @@ export = { resourceName: 'WordPress_App' }, stack); + const pseudo = new Aws(stack); + test.deepEqual(resolve(arn), - resolve(`arn:${new AwsPartition(stack)}:codedeploy:${new AwsRegion(stack)}:${new AwsAccountId(stack)}:application:WordPress_App`)); + resolve(`arn:${pseudo.partition}:codedeploy:${pseudo.region}:${pseudo.accountId}:application:WordPress_App`)); 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 12c4c2382ac0d..3ad92d9860e52 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { App, AwsAccountId, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack, Token } from '../../lib'; +import { App, Aws, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack, Token } from '../../lib'; export = { 'a stack can be serialized into a CloudFormation template, initially it\'s empty'(test: Test) { @@ -176,7 +176,7 @@ export = { // GIVEN const app = new App(); const stack1 = new Stack(app, 'Stack1'); - const account1 = new AwsAccountId(stack1); + const account1 = new Aws(stack1).accountId; const stack2 = new Stack(app, 'Stack2'); // WHEN - used in another stack @@ -212,7 +212,7 @@ export = { // GIVEN const app = new App(); const stack1 = new Stack(app, 'Stack1'); - const account1 = new AwsAccountId(stack1); + const account1 = new Aws(stack1).accountId; const stack2 = new Stack(app, 'Stack2'); // WHEN - used in another stack @@ -246,7 +246,7 @@ export = { // GIVEN const app = new App(); const stack1 = new Stack(app, 'Stack1'); - const account1 = new AwsAccountId(stack1); + const account1 = new Aws(stack1).accountId; const stack2 = new Stack(app, 'Stack2'); // WHEN - used in another stack @@ -271,9 +271,9 @@ export = { // GIVEN const app = new App(); const stack1 = new Stack(app, 'Stack1'); - const account1 = new AwsAccountId(stack1); + const account1 = new Aws(stack1).accountId; const stack2 = new Stack(app, 'Stack2'); - const account2 = new AwsAccountId(stack2); + const account2 = new Aws(stack2).accountId; // WHEN new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); @@ -290,7 +290,7 @@ export = { // GIVEN const app = new App(); const stack1 = new Stack(app, 'Stack1'); - const account1 = new AwsAccountId(stack1); + const account1 = new Aws(stack1).accountId; const stack2 = new Stack(app, 'Stack2'); // WHEN @@ -308,7 +308,7 @@ export = { // GIVEN const app = new App(); const stack1 = new Stack(app, 'Stack1', { env: { account: '123456789012', region: 'es-norst-1' }}); - const account1 = new AwsAccountId(stack1); + const account1 = new Aws(stack1).accountId; const stack2 = new Stack(app, 'Stack2', { env: { account: '123456789012', region: 'es-norst-2' }}); // WHEN diff --git a/packages/@aws-cdk/runtime-values/lib/rtv.ts b/packages/@aws-cdk/runtime-values/lib/rtv.ts index 91855f8ee0ea9..1d5ed679039d1 100644 --- a/packages/@aws-cdk/runtime-values/lib/rtv.ts +++ b/packages/@aws-cdk/runtime-values/lib/rtv.ts @@ -27,11 +27,6 @@ export class RuntimeValue extends cdk.Construct { */ public static readonly ENV_NAME = 'RTV_STACK_NAME'; - /** - * The value to assign to the `RTV_STACK_NAME` environment variable. - */ - public readonly envValue = new cdk.AwsStackName(this); - /** * IAM actions needed to read a value from an SSM parameter. */ @@ -41,6 +36,11 @@ export class RuntimeValue extends cdk.Construct { 'ssm:GetParameter' ]; + /** + * The value to assign to the `RTV_STACK_NAME` environment variable. + */ + public readonly envValue: string; + /** * The name of the runtime parameter. */ @@ -54,7 +54,10 @@ export class RuntimeValue extends cdk.Construct { constructor(parent: cdk.Construct, name: string, props: RuntimeValueProps) { super(parent, name); - this.parameterName = `/rtv/${new cdk.AwsStackName(this)}/${props.package}/${name}`; + const stack = cdk.Stack.find(this); + + this.parameterName = `/rtv/${stack.stackName}/${props.package}/${name}`; + this.envValue = stack.stackName; new ssm.CfnParameter(this, 'Parameter', { name: this.parameterName, diff --git a/packages/@aws-cdk/runtime-values/test/test.rtv.ts b/packages/@aws-cdk/runtime-values/test/test.rtv.ts index d5034ae20d327..409bc008d6ae6 100644 --- a/packages/@aws-cdk/runtime-values/test/test.rtv.ts +++ b/packages/@aws-cdk/runtime-values/test/test.rtv.ts @@ -23,6 +23,8 @@ class RuntimeValueTest extends cdk.Construct { constructor(parent: cdk.Construct, name: string) { super(parent, name); + const stack = cdk.Stack.find(this); + const queue = new sqs.CfnQueue(this, 'Queue', {}); const role = new iam.Role(this, 'Role', { @@ -42,7 +44,7 @@ class RuntimeValueTest extends cdk.Construct { role: role.roleArn, environment: { variables: { - [RuntimeValue.ENV_NAME]: new cdk.AwsStackName(this) + [RuntimeValue.ENV_NAME]: stack.stackName, } } }); From 73353c07635c3d76eda2fa99a66df4660396b088 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Dec 2018 17:31:21 +0100 Subject: [PATCH 15/39] Add cdk deployment ordering; WIP - fighting with dependency cycles --- .../cloudformation/cloudformation-token.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 6 +-- packages/@aws-cdk/cdk/lib/core/tokens.ts | 15 +++++-- packages/@aws-cdk/cdk/lib/util/uniqueid.ts | 4 +- packages/aws-cdk/bin/cdk.ts | 14 ++++-- packages/aws-cdk/integ-tests/app/app.js | 16 +++++++ .../aws-cdk/integ-tests/test-cdk-order.sh | 15 +++++++ packages/aws-cdk/lib/api/cxapp/stacks.ts | 13 +++++- .../lib/api/util/string-manipulation.ts | 7 +++ packages/aws-cdk/lib/api/util/toposort.ts | 44 +++++++++++++++++++ 10 files changed, 121 insertions(+), 15 deletions(-) create mode 100755 packages/aws-cdk/integ-tests/test-cdk-order.sh create mode 100644 packages/aws-cdk/lib/api/util/string-manipulation.ts create mode 100644 packages/aws-cdk/lib/api/util/toposort.ts diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 7adce26ac6db6..821c26bf5f308 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -147,4 +147,4 @@ export class FnJoin extends Token { return typeof obj === 'string' && !unresolved(obj); } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 77fc7abaeebd0..191a14dcbea17 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -5,7 +5,6 @@ import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; import { StackAwareToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; -import { Aws } from './pseudo'; import { Resource } from './resource'; export interface StackProps { @@ -639,5 +638,6 @@ export class Ref extends StackAwareToken { } } -// Has to be at the end to prevent circular imports -import { Output } from './output'; \ No newline at end of file +// These imports have to be at the end to prevent circular imports +import { Output } from './output'; +import { Aws } from './pseudo'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index 0fbc6d36cb598..18dc4b364eb3d 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -1,5 +1,4 @@ import { cloudFormationConcat } from "../cloudformation/cloudformation-token"; -import { Construct } from "./construct"; /** * If objects has a function property by this name, they will be considered tokens, and this @@ -220,7 +219,7 @@ export function resolve(obj: any, prefix?: string[]): any { // Must not be a Construct at this point, otherwise you probably made a type // mistake somewhere and resolve will get into an infinite loop recursing into // child.parent <---> parent.children - if (obj instanceof Construct) { + if (isConstruct(obj)) { throw new Error('Trying to resolve() a Construct at ' + pathName); } @@ -578,4 +577,14 @@ const TOKEN_MAP: TokenMap = glob.__cdkTokenMap = glob.__cdkTokenMap || new Token /** * Singleton instance of resolver options */ -export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); \ No newline at end of file +export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); + +/** + * Determine whether an object is a Construct + * + * Not in 'construct.ts' because that would lead to a dependency cycle via 'uniqueid.ts', + * and this is a best-effort protection against a common programming mistake anyway. + */ +function isConstruct(x: any): boolean { + return x._children !== undefined && x._metadata !== undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/util/uniqueid.ts b/packages/@aws-cdk/cdk/lib/util/uniqueid.ts index b9a9e34eb97fe..6f58d44bd7471 100644 --- a/packages/@aws-cdk/cdk/lib/util/uniqueid.ts +++ b/packages/@aws-cdk/cdk/lib/util/uniqueid.ts @@ -1,6 +1,5 @@ // tslint:disable-next-line:no-var-requires import crypto = require('crypto'); -import { unresolved } from '../core/tokens'; /** * Resources with this ID are hidden from humans @@ -36,7 +35,8 @@ 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)); + // Lazy require in order to break a module dependency cycle + const unresolvedTokens = components.filter(c => require('../core/tokens').unresolved(c)); if (unresolvedTokens.length > 0) { throw new Error(`ID components may not include unresolved tokens: ${unresolvedTokens.join(',')}`); } diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index c91f10123ab8e..b399b08c8e731 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -10,6 +10,7 @@ import yargs = require('yargs'); import { bootstrapEnvironment, deployStack, destroyStack, loadToolkitInfo, Mode, SDK } from '../lib'; import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments'; import { AppStacks, listStackNames } from '../lib/api/cxapp/stacks'; +import { leftPad } from '../lib/api/util/string-manipulation'; import { printSecurityDiff, printStackDiff, RequireApproval } from '../lib/diff'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { interactive } from '../lib/interactive'; @@ -52,7 +53,8 @@ async function parseCommandLineArguments() { .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'display environment information for each stack' })) .command([ 'synthesize [STACKS..]', 'synth [STACKS..]' ], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs .option('interactive', { type: 'boolean', alias: 'i', desc: 'interactively watch and show template updates' }) - .option('output', { type: 'string', alias: 'o', desc: 'write CloudFormation template for requested stacks to the given directory' })) + .option('output', { type: 'string', alias: 'o', desc: 'write CloudFormation template for requested stacks to the given directory' }) + .option('numbered', { type: 'boolean', alias: 'n', desc: 'Prefix filenames with numbers to indicate deployment ordering' })) .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment') .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' })) @@ -170,7 +172,7 @@ async function initCommandLine() { case 'synthesize': case 'synth': - return await cliSynthesize(args.STACKS, args.interactive, args.output, args.json); + return await cliSynthesize(args.STACKS, args.interactive, args.output, args.json, args.numbered); case 'metadata': return await cliMetadata(await findStack(args.STACK)); @@ -237,7 +239,8 @@ async function initCommandLine() { async function cliSynthesize(stackNames: string[], doInteractive: boolean, outputDir: string|undefined, - json: boolean): Promise { + json: boolean, + numbered: boolean): Promise { const stacks = await appStacks.selectStacks(...stackNames); renames.validateSelectedStacks(stacks); @@ -259,11 +262,14 @@ async function initCommandLine() { fs.mkdirpSync(outputDir); + let i = 0; for (const stack of stacks) { const finalName = renames.finalName(stack.name); - const fileName = `${outputDir}/${finalName}.template.${json ? 'json' : 'yaml'}`; + const prefix = numbered ? leftPad(`${i}`, 3, '0') + '.' : ''; + const fileName = `${outputDir}/${prefix}${finalName}.template.${json ? 'json' : 'yaml'}`; highlight(fileName); await fs.writeFile(fileName, toJsonOrYaml(stack.template)); + i++; } return undefined; // Nothing to print diff --git a/packages/aws-cdk/integ-tests/app/app.js b/packages/aws-cdk/integ-tests/app/app.js index 8da2b8aa6507b..b8f3365eaf49b 100644 --- a/packages/aws-cdk/integ-tests/app/app.js +++ b/packages/aws-cdk/integ-tests/app/app.js @@ -30,6 +30,20 @@ class IamStack extends cdk.Stack { } } +class ProvidingStack extends cdk.Stack { + constructor(parent, id) { + super(parent, id); + } +} + +class ConsumingStack extends cdk.Stack { + constructor(parent, id, providingStack) { + super(parent, id); + + new cdk.Output(this, 'IConsumedSomething', { value: providingStack.accountId }); + } +} + const app = new cdk.App(); // Deploy all does a wildcard cdk-toolkit-integration-test-* @@ -37,5 +51,7 @@ new MyStack(app, 'cdk-toolkit-integration-test-1'); new YourStack(app, 'cdk-toolkit-integration-test-2'); // Not included in wildcard new IamStack(app, 'cdk-toolkit-integration-iam-test'); +const providing = new ProvidingStack(app, 'cdk-toolkit-integration-providing'); +new ConsumingStack(app, 'cdk-toolkit-integration-consuming', providing); app.run(); diff --git a/packages/aws-cdk/integ-tests/test-cdk-order.sh b/packages/aws-cdk/integ-tests/test-cdk-order.sh new file mode 100755 index 0000000000000..d00502d8de6c9 --- /dev/null +++ b/packages/aws-cdk/integ-tests/test-cdk-order.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) +source ${scriptdir}/common.bash +# ---------------------------------------------------------- + +setup + +# ls order == synthesis order == provider before consumer +assert "cdk list cdk-toolkit-integration-consuming cdk-toolkit-integration-providing" < matched.has(s.name)); } + /** + * Return all stacks in the CX + * + * If the stacks have dependencies between them, they will be returned in + * topologically sorted order. If there are dependencies that are not in the + * set, they will be ignored; it is the user's responsibility that the + * non-selected stacks have already been deployed previously. + */ public async listStacks(): Promise { const response = await this.synthesizeStacks(); - return response.stacks; + return topologicalSort(response.stacks, s => s.name, s => s.dependsOn || []); } /** @@ -196,4 +205,4 @@ export class AppStacks { */ export function listStackNames(stacks: cxapi.SynthesizedStack[]): string { return stacks.map(s => s.name).join(', '); -} +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/util/string-manipulation.ts b/packages/aws-cdk/lib/api/util/string-manipulation.ts new file mode 100644 index 0000000000000..aa51679967ba3 --- /dev/null +++ b/packages/aws-cdk/lib/api/util/string-manipulation.ts @@ -0,0 +1,7 @@ +/** + * Pad 's' on the left with 'char' until it is n characters wide + */ +export function leftPad(s: string, n: number, char: string) { + const padding = Math.max(0, n - s.length); + return char.repeat(padding) + s; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/util/toposort.ts b/packages/aws-cdk/lib/api/util/toposort.ts new file mode 100644 index 0000000000000..97dd35ea8bb46 --- /dev/null +++ b/packages/aws-cdk/lib/api/util/toposort.ts @@ -0,0 +1,44 @@ +export type KeyFunc = (x: T) => string; +export type DepFunc = (x: T) => string[]; + +/** + * Return a topological sort of all elements of xs, according to the given dependency functions + * + * Dependencies outside the referenced set are ignored. + * + * Not a stable sort, but in order to keep the order as stable as possible, we'll sort by key + * among elements of equal precedence. + */ +export function topologicalSort(xs: Iterable, keyFn: KeyFunc, depFn: DepFunc): T[] { + const remaining = new Map>(); + for (const element of xs) { + const key = keyFn(element); + remaining.set(key, { key, element, dependencies: depFn(element) }); + } + + const ret = new Array(); + while (remaining.size > 0) { + // All elements with no more deps in the set can be ordered + const selectable = Array.from(remaining.values()).filter(e => e.dependencies.every(d => !remaining.has(d))); + + selectable.sort((a, b) => a.key < b.key ? -1 : b.key < a.key ? 1 : 0); + + for (const selected of selectable) { + ret.push(selected.element); + remaining.delete(selected.key); + } + + // If we didn't make any progress, we got stuck + if (selectable.length === 0) { + throw new Error(`Could not determine ordering between: ${Array.from(remaining.keys()).join(', ')}`); + } + } + + return ret; +} + +interface TopoElement { + key: string; + dependencies: string[]; + element: T; +} \ No newline at end of file From f161a4d5d041b23ffe0de652ec36d11b58669b25 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 31 Dec 2018 12:44:58 +0100 Subject: [PATCH 16/39] Reorganizing to get rid of cycles, add integ test --- .../lib/cloudformation/cloudformation-json.ts | 2 +- .../cloudformation/cloudformation-token.ts | 150 ------------------ .../@aws-cdk/cdk/lib/cloudformation/fn.ts | 84 +++++++++- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 2 +- .../cdk/lib/cloudformation/resolve-concat.ts | 20 +++ .../cdk/lib/cloudformation/resource.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/tokens.ts | 51 ++++++ packages/@aws-cdk/cdk/lib/core/tokens.ts | 2 +- packages/@aws-cdk/cdk/lib/index.ts | 2 +- packages/aws-cdk/integ-tests/app/app.js | 6 +- packages/aws-cdk/integ-tests/common.bash | 2 +- .../aws-cdk/integ-tests/test-cdk-order.sh | 6 +- 13 files changed, 166 insertions(+), 165 deletions(-) delete mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts create mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts create mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts index fb21a56c663d9..4ba8cd2f1e7c9 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts @@ -1,5 +1,5 @@ import { resolve, Token } from "../core/tokens"; -import { isIntrinsic } from "./cloudformation-token"; +import { isIntrinsic } from "./tokens"; /** * Class for JSON routines that are framework-aware diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts deleted file mode 100644 index 821c26bf5f308..0000000000000 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Construct } from "../core/construct"; -import { resolve, Token, unresolved } from "../core/tokens"; -import { Stack } from "./stack"; - -/** - * Produce a CloudFormation expression to concat two arbitrary expressions - */ -export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { - if (left === undefined && right === undefined) { return ''; } - - const parts = new Array(); - if (left !== undefined) { parts.push(left); } - if (right !== undefined) { parts.push(right); } - - // Some case analysis to produce minimal expressions - if (parts.length === 1) { return parts[0]; } - if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { - return parts[0] + parts[1]; - } - - // Otherwise return a Join intrinsic - return new FnJoin('', parts); -} - -export class StackAwareToken extends Token { - public static isInstance(x: any): x is StackAwareToken { - return x && x._isStackAwareCloudFormationToken; - } - - protected readonly _isStackAwareToken: boolean; - - private readonly tokenStack?: Stack; - - constructor(value: any, displayName?: string, anchor?: Construct) { - if (typeof(value) === 'function') { - throw new Error('StackAwareToken can only contain eager values'); - } - super(value, displayName); - this._isStackAwareToken = true; - - if (anchor !== undefined) { - this.tokenStack = Stack.find(anchor); - } - } - - /** - * In a consuming context, potentially substitute this Token with a different one - */ - public substituteToken(consumingStack: Stack): Token { - if (this.tokenStack && this.tokenStack !== consumingStack) { - // We're trying to resolve a cross-stack reference - consumingStack.addDependency(this.tokenStack); - return this.tokenStack.exportValue(this, consumingStack); - } - // In case of doubt, return same Token - return this; - } -} - -/** - * Return whether the given value represents a CloudFormation intrinsic - */ -export function isIntrinsic(x: any) { - if (Array.isArray(x) || x === null || typeof x !== 'object') { return false; } - - const keys = Object.keys(x); - if (keys.length !== 1) { return false; } - - return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); -} - -/** - * The intrinsic function ``Fn::Join`` appends a set of values into a single value, separated by - * the specified delimiter. If a delimiter is the empty string, the set of values are concatenated - * with no delimiter. - */ -export class FnJoin extends Token { - private readonly delimiter: string; - private readonly listOfValues: any[]; - // Cache for the result of resolveValues() - since it otherwise would be computed several times - private _resolvedValues?: any[]; - private canOptimize: boolean; - - /** - * Creates an ``Fn::Join`` function. - * @param delimiter The value you want to occur between fragments. The delimiter will occur between fragments only. - * It will not terminate the final value. - * @param listOfValues The list of values you want combined. - */ - constructor(delimiter: string, listOfValues: any[]) { - if (listOfValues.length === 0) { - throw new Error(`FnJoin requires at least one value to be provided`); - } - // Passing the values as a token, optimization requires resolving stringified tokens, we should be deferred until - // this token is itself being resolved. - super({ 'Fn::Join': [ delimiter, new Token(() => this.resolveValues()) ] }); - this.delimiter = delimiter; - this.listOfValues = listOfValues; - this.canOptimize = true; - } - - public resolve(): any { - const resolved = this.resolveValues(); - if (this.canOptimize && resolved.length === 1) { - return resolved[0]; - } - return super.resolve(); - } - - /** - * Optimization: if an Fn::Join is nested in another one and they share the same delimiter, then flatten it up. Also, - * if two concatenated elements are literal strings (not tokens), then pre-concatenate them with the delimiter, to - * generate shorter output. - */ - private resolveValues() { - if (this._resolvedValues) { return this._resolvedValues; } - - if (unresolved(this.listOfValues)) { - // This is a list token, don't resolve and also don't optimize. - this.canOptimize = false; - return this._resolvedValues = this.listOfValues; - } - - const resolvedValues = [...this.listOfValues.map(e => resolve(e))]; - let i = 0; - while (i < resolvedValues.length) { - const el = resolvedValues[i]; - if (isFnJoinIntrinsicWithSameDelimiter.call(this, el)) { - resolvedValues.splice(i, 1, ...el['Fn::Join'][1]); - } else if (i > 0 && isPlainString(resolvedValues[i - 1]) && isPlainString(resolvedValues[i])) { - resolvedValues[i - 1] += this.delimiter + resolvedValues[i]; - resolvedValues.splice(i, 1); - } else { - i += 1; - } - } - - return this._resolvedValues = resolvedValues; - - function isFnJoinIntrinsicWithSameDelimiter(this: FnJoin, obj: any): boolean { - return isIntrinsic(obj) - && Object.keys(obj)[0] === 'Fn::Join' - && obj['Fn::Join'][0] === this.delimiter; - } - - function isPlainString(obj: any): boolean { - return typeof obj === 'string' && !unresolved(obj); - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index b94734917b8c0..bc17c2c9127b7 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -1,6 +1,6 @@ -import { Token } from '../core/tokens'; -import { FnJoin } from './cloudformation-token'; +import { resolve, Token, unresolved } from '../core/tokens'; import { FnCondition } from './condition'; +import { isIntrinsic } from './tokens'; // tslint:disable:max-line-length @@ -602,3 +602,83 @@ class FnValueOfAll extends FnBase { super('Fn::ValueOfAll', [ parameterType, attribute ]); } } + +/** + * The intrinsic function ``Fn::Join`` appends a set of values into a single value, separated by + * the specified delimiter. If a delimiter is the empty string, the set of values are concatenated + * with no delimiter. + */ +class FnJoin extends Token { + private readonly delimiter: string; + private readonly listOfValues: any[]; + // Cache for the result of resolveValues() - since it otherwise would be computed several times + private _resolvedValues?: any[]; + private canOptimize: boolean; + + /** + * Creates an ``Fn::Join`` function. + * @param delimiter The value you want to occur between fragments. The delimiter will occur between fragments only. + * It will not terminate the final value. + * @param listOfValues The list of values you want combined. + */ + constructor(delimiter: string, listOfValues: any[]) { + if (listOfValues.length === 0) { + throw new Error(`FnJoin requires at least one value to be provided`); + } + // Passing the values as a token, optimization requires resolving stringified tokens, we should be deferred until + // this token is itself being resolved. + super({ 'Fn::Join': [ delimiter, new Token(() => this.resolveValues()) ] }); + this.delimiter = delimiter; + this.listOfValues = listOfValues; + this.canOptimize = true; + } + + public resolve(): any { + const resolved = this.resolveValues(); + if (this.canOptimize && resolved.length === 1) { + return resolved[0]; + } + return super.resolve(); + } + + /** + * Optimization: if an Fn::Join is nested in another one and they share the same delimiter, then flatten it up. Also, + * if two concatenated elements are literal strings (not tokens), then pre-concatenate them with the delimiter, to + * generate shorter output. + */ + private resolveValues() { + if (this._resolvedValues) { return this._resolvedValues; } + + if (unresolved(this.listOfValues)) { + // This is a list token, don't resolve and also don't optimize. + this.canOptimize = false; + return this._resolvedValues = this.listOfValues; + } + + const resolvedValues = [...this.listOfValues.map(e => resolve(e))]; + let i = 0; + while (i < resolvedValues.length) { + const el = resolvedValues[i]; + if (isFnJoinIntrinsicWithSameDelimiter.call(this, el)) { + resolvedValues.splice(i, 1, ...el['Fn::Join'][1]); + } else if (i > 0 && isPlainString(resolvedValues[i - 1]) && isPlainString(resolvedValues[i])) { + resolvedValues[i - 1] += this.delimiter + resolvedValues[i]; + resolvedValues.splice(i, 1); + } else { + i += 1; + } + } + + return this._resolvedValues = resolvedValues; + + function isFnJoinIntrinsicWithSameDelimiter(this: FnJoin, obj: any): boolean { + return isIntrinsic(obj) + && Object.keys(obj)[0] === 'Fn::Join' + && obj['Fn::Join'][0] === this.delimiter; + } + + function isPlainString(obj: any): boolean { + return typeof obj === 'string' && !unresolved(obj); + } + } +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 1af5cc4d721a1..941af28bdd655 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { StackAwareToken } from './cloudformation-token'; +import { StackAwareToken } from './tokens'; /** * Accessor for pseudo parameters diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts new file mode 100644 index 0000000000000..19f9b64c95efb --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts @@ -0,0 +1,20 @@ +/** + * Produce a CloudFormation expression to concat two arbitrary expressions when resolving + */ +export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { + if (left === undefined && right === undefined) { return ''; } + + const parts = new Array(); + if (left !== undefined) { parts.push(left); } + if (right !== undefined) { parts.push(right); } + + // Some case analysis to produce minimal expressions + if (parts.length === 1) { return parts[0]; } + if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { + return parts[0] + parts[1]; + } + + // Otherwise return a Join intrinsic (already in the target document language to avoid taking + // circular dependencies on FnJoin & friends) + return { 'Fn::Join': ['', parts] }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 18dbe577ef4a5..b13fae2490f79 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,10 +1,10 @@ import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; -import { StackAwareToken } from './cloudformation-token'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; import { IDependable, Referenceable, Stack, StackElement } from './stack'; +import { StackAwareToken } from './tokens'; export interface ResourceProps { /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 191a14dcbea17..7bf9ee953af52 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -3,9 +3,9 @@ import { App } from '../app'; import { Construct, PATH_SEP } from '../core/construct'; import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; -import { StackAwareToken } from './cloudformation-token'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; +import { StackAwareToken } from './tokens'; export interface StackProps { /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts b/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts new file mode 100644 index 0000000000000..c8dd27517f33d --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts @@ -0,0 +1,51 @@ +import { Construct } from "../core/construct"; +import { Token } from "../core/tokens"; + +export class StackAwareToken extends Token { + public static isInstance(x: any): x is StackAwareToken { + return x && x._isStackAwareToken; + } + + protected readonly _isStackAwareToken: boolean; + + private readonly tokenStack?: Stack; + + constructor(value: any, displayName?: string, anchor?: Construct) { + if (typeof(value) === 'function') { + throw new Error('StackAwareToken can only contain eager values'); + } + super(value, displayName); + this._isStackAwareToken = true; + + if (anchor !== undefined) { + this.tokenStack = Stack.find(anchor); + } + } + + /** + * In a consuming context, potentially substitute this Token with a different one + */ + public substituteToken(consumingStack: Stack): Token { + if (this.tokenStack && this.tokenStack !== consumingStack) { + // We're trying to resolve a cross-stack reference + consumingStack.addDependency(this.tokenStack); + return this.tokenStack.exportValue(this, consumingStack); + } + // In case of doubt, return same Token + return this; + } +} + +/** + * Return whether the given value represents a CloudFormation intrinsic + */ +export function isIntrinsic(x: any) { + if (Array.isArray(x) || x === null || typeof x !== 'object') { return false; } + + const keys = Object.keys(x); + if (keys.length !== 1) { return false; } + + return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); +} + +import { Stack } from "./stack"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index 18dc4b364eb3d..1e7a7e864d0bc 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -1,4 +1,4 @@ -import { cloudFormationConcat } from "../cloudformation/cloudformation-token"; +import { cloudFormationConcat } from "../cloudformation/resolve-concat"; /** * If objects has a function property by this name, they will be considered tokens, and this diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index e04b06a8812d8..8f02e9ab8a201 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -3,7 +3,7 @@ export * from './core/tokens'; export * from './core/tag-manager'; export * from './cloudformation/cloudformation-json'; -export * from './cloudformation/cloudformation-token'; +export * from './cloudformation/tokens'; export * from './cloudformation/condition'; export * from './cloudformation/fn'; export * from './cloudformation/include'; diff --git a/packages/aws-cdk/integ-tests/app/app.js b/packages/aws-cdk/integ-tests/app/app.js index b8f3365eaf49b..c07d5a43551c1 100644 --- a/packages/aws-cdk/integ-tests/app/app.js +++ b/packages/aws-cdk/integ-tests/app/app.js @@ -40,7 +40,7 @@ class ConsumingStack extends cdk.Stack { constructor(parent, id, providingStack) { super(parent, id); - new cdk.Output(this, 'IConsumedSomething', { value: providingStack.accountId }); + new cdk.Output(this, 'IConsumedSomething', { value: providingStack.stackName }); } } @@ -51,7 +51,7 @@ new MyStack(app, 'cdk-toolkit-integration-test-1'); new YourStack(app, 'cdk-toolkit-integration-test-2'); // Not included in wildcard new IamStack(app, 'cdk-toolkit-integration-iam-test'); -const providing = new ProvidingStack(app, 'cdk-toolkit-integration-providing'); -new ConsumingStack(app, 'cdk-toolkit-integration-consuming', providing); +const providing = new ProvidingStack(app, 'cdk-toolkit-integration-order-providing'); +new ConsumingStack(app, 'cdk-toolkit-integration-order-consuming', providing); app.run(); diff --git a/packages/aws-cdk/integ-tests/common.bash b/packages/aws-cdk/integ-tests/common.bash index 97733316dd841..6af9d8bec095c 100644 --- a/packages/aws-cdk/integ-tests/common.bash +++ b/packages/aws-cdk/integ-tests/common.bash @@ -77,7 +77,7 @@ function assert() { echo "| running ${command}" - $command > ${actual} || { + eval "$command" > ${actual} || { fail "command ${command} non-zero exit code" } diff --git a/packages/aws-cdk/integ-tests/test-cdk-order.sh b/packages/aws-cdk/integ-tests/test-cdk-order.sh index d00502d8de6c9..9aa46451fbcfa 100755 --- a/packages/aws-cdk/integ-tests/test-cdk-order.sh +++ b/packages/aws-cdk/integ-tests/test-cdk-order.sh @@ -7,9 +7,9 @@ source ${scriptdir}/common.bash setup # ls order == synthesis order == provider before consumer -assert "cdk list cdk-toolkit-integration-consuming cdk-toolkit-integration-providing" < Date: Fri, 4 Jan 2019 10:46:51 +0100 Subject: [PATCH 17/39] Rename StackAwareToken => CfnReference --- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 4 ++-- .../@aws-cdk/cdk/lib/cloudformation/resource.ts | 4 ++-- packages/@aws-cdk/cdk/lib/cloudformation/stack.ts | 6 +++--- .../@aws-cdk/cdk/lib/cloudformation/tokens.ts | 15 +++++++++------ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 941af28bdd655..5fd1a1a024310 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { StackAwareToken } from './tokens'; +import { CfnReference } from './tokens'; /** * Accessor for pseudo parameters @@ -42,7 +42,7 @@ export class Aws { } } -class PseudoParameter extends StackAwareToken { +class PseudoParameter extends CfnReference { constructor(name: string, anchor: Construct) { super({ Ref: name }, name, anchor); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 3ac079affd91c..1af6a1ba4a5fd 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -4,7 +4,7 @@ import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; import { IDependable, Referenceable, Stack, StackElement } from './stack'; -import { StackAwareToken } from './tokens'; +import { CfnReference } from './tokens'; export interface ResourceProps { /** @@ -105,7 +105,7 @@ export class Resource extends Referenceable { * @param attributeName The name of the attribute. */ public getAtt(attributeName: string) { - return new StackAwareToken({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`, this); + return new CfnReference({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`, this); } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 1633240016a15..6b8789b386acd 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -5,7 +5,7 @@ import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; import { Environment } from '../environment'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; -import { StackAwareToken } from './tokens'; +import { CfnReference } from './tokens'; export interface StackProps { /** @@ -545,7 +545,7 @@ export abstract class StackElement extends Construct implements IDependable { public abstract substituteCrossStackReferences(): void; protected deepSubCrossStackReferences(sourceStack: Stack, x: any): any { - if (StackAwareToken.isInstance(x)) { + if (CfnReference.isInstance(x)) { return x.substituteToken(sourceStack); } @@ -640,7 +640,7 @@ function stackElements(node: IConstruct, into: StackElement[] = []): StackElemen /** * A generic, untyped reference to a Stack Element */ -export class Ref extends StackAwareToken { +export class Ref extends CfnReference { constructor(element: StackElement) { super({ Ref: element.logicalId }, `${element.logicalId}.Ref`, element); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts b/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts index c8dd27517f33d..1dd6097fa2063 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts @@ -1,21 +1,24 @@ import { Construct } from "../core/construct"; import { Token } from "../core/tokens"; -export class StackAwareToken extends Token { - public static isInstance(x: any): x is StackAwareToken { - return x && x._isStackAwareToken; +/** + * A Token that represents a CloudFormation reference to another resource + */ +export class CfnReference extends Token { + public static isInstance(x: any): x is CfnReference { + return x && x._isCfnReference; } - protected readonly _isStackAwareToken: boolean; + protected readonly _isCfnReference: boolean; private readonly tokenStack?: Stack; constructor(value: any, displayName?: string, anchor?: Construct) { if (typeof(value) === 'function') { - throw new Error('StackAwareToken can only contain eager values'); + throw new Error('CfnReference can only contain eager values'); } super(value, displayName); - this._isStackAwareToken = true; + this._isCfnReference = true; if (anchor !== undefined) { this.tokenStack = Stack.find(anchor); From 809336143eb7a18079261b7ea11813f101bf97a8 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 4 Jan 2019 13:53:41 +0100 Subject: [PATCH 18/39] Move resolve() into the context of a Construct, and hide the global method --- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 2 +- .../aws-apigateway/test/test.method.ts | 4 +- .../aws-apigateway/test/test.restapi.ts | 12 +- .../lib/pipeline-actions.ts | 2 +- .../test/test.pipeline-actions.ts | 24 +- .../@aws-cdk/aws-cloudwatch/lib/dashboard.ts | 2 +- .../aws-cloudwatch/test/test.graphs.ts | 18 +- packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 10 +- packages/@aws-cdk/aws-ecr/lib/repository.ts | 2 +- .../@aws-cdk/aws-ecr/test/test.repository.ts | 24 +- .../test/ec2/test.ec2-task-definition.ts | 2 +- .../test/alb/test.listener.ts | 2 +- .../test/alb/test.load-balancer.ts | 2 +- .../@aws-cdk/aws-events/test/test.rule.ts | 6 +- .../aws-iam/test/test.managed-policy.ts | 3 +- .../aws-iam/test/test.policy-document.ts | 46 +- packages/@aws-cdk/aws-iam/test/test.role.ts | 8 +- packages/@aws-cdk/aws-kms/lib/key.ts | 4 +- .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 2 +- .../@aws-cdk/aws-lambda/test/test.alias.ts | 10 +- .../aws-logs/lib/cross-account-destination.ts | 2 +- .../@aws-cdk/aws-rds/test/test.cluster.ts | 4 +- .../test/test.hosted-zone-provider.ts | 2 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 2 +- packages/@aws-cdk/aws-s3/lib/util.ts | 4 +- packages/@aws-cdk/aws-s3/test/test.bucket.ts | 14 +- packages/@aws-cdk/aws-s3/test/test.util.ts | 24 +- .../test/test.secret-string.ts | 4 +- packages/@aws-cdk/aws-sns/test/test.sns.ts | 13 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 10 +- .../test/test.parameter-store-string.ts | 4 +- .../aws-stepfunctions/lib/state-machine.ts | 2 +- .../aws-stepfunctions/test/test.activity.ts | 4 +- .../test/test.state-machine-resources.ts | 4 +- .../test/test.states-language.ts | 2 +- packages/@aws-cdk/cdk/lib/app.ts | 3 +- .../lib/cloudformation/cloudformation-json.ts | 16 +- .../@aws-cdk/cdk/lib/cloudformation/fn.ts | 22 +- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 2 +- .../cdk/lib/cloudformation/resolve-concat.ts | 20 - .../cdk/lib/cloudformation/resource.ts | 14 +- .../@aws-cdk/cdk/lib/cloudformation/rule.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 34 +- packages/@aws-cdk/cdk/lib/core/construct.ts | 31 +- packages/@aws-cdk/cdk/lib/core/tokens.ts | 590 ------------------ .../tokens.ts => core/tokens/cfn-tokens.ts} | 27 +- .../@aws-cdk/cdk/lib/core/tokens/encoding.ts | 287 +++++++++ .../@aws-cdk/cdk/lib/core/tokens/index.ts | 3 + .../@aws-cdk/cdk/lib/core/tokens/options.ts | 55 ++ .../@aws-cdk/cdk/lib/core/tokens/resolve.ts | 124 ++++ .../@aws-cdk/cdk/lib/core/tokens/token.ts | 141 +++++ packages/@aws-cdk/cdk/lib/core/util.ts | 14 +- packages/@aws-cdk/cdk/lib/index.ts | 1 - .../cdk/test/cloudformation/test.arn.ts | 36 +- .../test.cloudformation-json.ts | 54 +- .../cloudformation/test.dynamic-reference.ts | 4 +- .../cdk/test/cloudformation/test.fn.ts | 20 +- .../cdk/test/cloudformation/test.output.ts | 4 +- .../cdk/test/cloudformation/test.parameter.ts | 4 +- .../cdk/test/cloudformation/test.resource.ts | 4 +- .../cdk/test/cloudformation/test.secret.ts | 9 +- .../cdk/test/core/test.tag-manager.ts | 42 +- .../@aws-cdk/cdk/test/core/test.tokens.ts | 11 +- packages/@aws-cdk/cdk/test/core/test.util.ts | 48 +- packages/@aws-cdk/cdk/test/test.context.ts | 4 +- tools/cfn2ts/lib/codegen.ts | 2 +- 66 files changed, 994 insertions(+), 914 deletions(-) delete mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts delete mode 100644 packages/@aws-cdk/cdk/lib/core/tokens.ts rename packages/@aws-cdk/cdk/lib/{cloudformation/tokens.ts => core/tokens/cfn-tokens.ts} (61%) create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/index.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/options.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/token.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 2e7b45c73533c..addf085cf134a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -178,7 +178,7 @@ class LatestDeploymentResource extends CfnDeployment { } else { const md5 = crypto.createHash('md5'); this.hashComponents - .map(c => cdk.resolve(c)) + .map(c => this.node.resolve(c)) .forEach(c => md5.update(JSON.stringify(c))); this.lazyLogicalId = this.originalLogicalId + md5.digest("hex"); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index 2f2d94f59d55d..2a377a139f661 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -123,7 +123,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(method.methodArn), { + test.deepEqual(method.node.resolve(method.methodArn), { "Fn::Join": [ "", [ @@ -157,7 +157,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(method.testMethodArn), { + test.deepEqual(method.node.resolve(method.testMethodArn), { "Fn::Join": [ "", [ diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index a0917ec673025..0eb64c31291e2 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -391,8 +391,8 @@ export = { Value: { Ref: 'MyRestApi2D1F47A9' }, Export: { Name: 'MyRestApiRestApiIdB93C5C2D' } }); - test.deepEqual(cdk.resolve(imported.restApiId), 'api-rxt4498f'); - test.deepEqual(cdk.resolve(exported), { restApiId: { 'Fn::ImportValue': 'MyRestApiRestApiIdB93C5C2D' } }); + test.deepEqual(imported.node.resolve(imported.restApiId), 'api-rxt4498f'); + test.deepEqual(imported.node.resolve(exported), { restApiId: { 'Fn::ImportValue': 'MyRestApiRestApiIdB93C5C2D' } }); test.done(); }, @@ -403,7 +403,7 @@ export = { api.root.addMethod('GET'); // THEN - test.deepEqual(cdk.resolve(api.url), { 'Fn::Join': + test.deepEqual(api.node.resolve(api.url), { 'Fn::Join': [ '', [ 'https://', { Ref: 'apiC8550315' }, @@ -412,7 +412,7 @@ export = { '.amazonaws.com/', { Ref: 'apiDeploymentStageprod896C8101' }, '/' ] ] }); - test.deepEqual(cdk.resolve(api.urlForPath('/foo/bar')), { 'Fn::Join': + test.deepEqual(api.node.resolve(api.urlForPath('/foo/bar')), { 'Fn::Join': [ '', [ 'https://', { Ref: 'apiC8550315' }, @@ -457,7 +457,7 @@ export = { const arn = api.executeApiArn('method', '/path', 'stage'); // THEN - test.deepEqual(cdk.resolve(arn), { 'Fn::Join': + test.deepEqual(api.node.resolve(arn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -490,7 +490,7 @@ export = { const method = api.root.addMethod('ANY'); // THEN - test.deepEqual(cdk.resolve(method.methodArn), { 'Fn::Join': + test.deepEqual(api.node.resolve(method.methodArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index f38df0a6c4d22..af813f657b5af 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -203,7 +203,7 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo // None evaluates to empty string which is falsey and results in undefined Capabilities: (capabilities && capabilities.toString()) || undefined, RoleArn: new cdk.Token(() => this.role.roleArn), - ParameterOverrides: cdk.CloudFormationJSON.stringify(props.parameterOverrides), + ParameterOverrides: new cdk.Token(() => cdk.CloudFormationJSON.stringify(props.parameterOverrides, this)), TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined, StackName: props.stackName, }); diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 001799de59a13..76536df8a26e8 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -64,7 +64,7 @@ export = nodeunit.testCase({ }); test.deepEqual( - cdk.resolve(pipelineRole.statements), + stack.node.resolve(pipelineRole.statements), [ { Action: 'iam:PassRole', @@ -138,7 +138,7 @@ export = nodeunit.testCase({ }); test.deepEqual( - cdk.resolve(pipelineRole.statements), + stack.node.resolve(pipelineRole.statements), [ { Action: 'cloudformation:ExecuteChangeSet', @@ -213,10 +213,10 @@ function _assertActionMatches(test: nodeunit.Test, category: string, configuration?: { [key: string]: any }) { const configurationStr = configuration - ? `configuration including ${JSON.stringify(cdk.resolve(configuration), null, 2)}` + ? `configuration including ${JSON.stringify(resolve(configuration), null, 2)}` : ''; const actionsStr = JSON.stringify(actions.map(a => - ({ owner: a.owner, provider: a.provider, category: a.category, configuration: cdk.resolve(a.configuration) }) + ({ owner: a.owner, provider: a.provider, category: a.category, configuration: resolve(a.configuration) }) ), null, 2); test.ok(_hasAction(actions, owner, provider, category, configuration), `Expected to find an action with owner ${owner}, provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); @@ -230,7 +230,7 @@ function _hasAction(actions: cpapi.Action[], owner: string, provider: string, ca if (configuration && !action.configuration) { continue; } if (configuration) { for (const key of Object.keys(configuration)) { - if (!_.isEqual(cdk.resolve(action.configuration[key]), cdk.resolve(configuration[key]))) { + if (!_.isEqual(resolve(action.configuration[key]), resolve(configuration[key]))) { continue; } } @@ -242,12 +242,12 @@ function _hasAction(actions: cpapi.Action[], owner: string, provider: string, ca function _assertPermissionGranted(test: nodeunit.Test, statements: iam.PolicyStatement[], action: string, resource: string, conditions?: any) { const conditionStr = conditions - ? ` with condition(s) ${JSON.stringify(cdk.resolve(conditions))}` + ? ` with condition(s) ${JSON.stringify(resolve(conditions))}` : ''; - const resolvedStatements = cdk.resolve(statements); + const resolvedStatements = resolve(statements); const statementsStr = JSON.stringify(resolvedStatements, null, 2); test.ok(_grantsPermission(resolvedStatements, action, resource, conditions), - `Expected to find a statement granting ${action} on ${JSON.stringify(cdk.resolve(resource))}${conditionStr}, found:\n${statementsStr}`); + `Expected to find a statement granting ${action} on ${JSON.stringify(resolve(resource))}${conditionStr}, found:\n${statementsStr}`); } function _grantsPermission(statements: PolicyStatementJson[], action: string, resource: string, conditions?: any) { @@ -261,8 +261,8 @@ function _grantsPermission(statements: PolicyStatementJson[], action: string, re } function _isOrContains(entity: string | string[], value: string): boolean { - const resolvedValue = cdk.resolve(value); - const resolvedEntity = cdk.resolve(entity); + const resolvedValue = resolve(value); + const resolvedEntity = resolve(entity); if (_.isEqual(resolvedEntity, resolvedValue)) { return true; } if (!Array.isArray(resolvedEntity)) { return false; } for (const tested of entity) { @@ -352,3 +352,7 @@ class RoleDouble extends iam.Role { this.statements.push(statement); } } + +function resolve(x: any): any { + return new cdk.Root().node.resolve(x); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts index 0f08dde461215..a06046f9260cf 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts @@ -33,7 +33,7 @@ export class Dashboard extends Construct { dashboardBody: new Token(() => { const column = new Column(...this.rows); column.position(0, 0); - return CloudFormationJSON.stringify({ widgets: column.toJson() }); + return CloudFormationJSON.stringify({ widgets: column.toJson() }, this); }) }); } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts index 3b70ad7ca60df..8a3073ffd0750 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts @@ -1,10 +1,11 @@ -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { AlarmWidget, GraphWidget, Metric, Shading, SingleValueWidget } from '../lib'; export = { 'add metrics to graphs on either axis'(test: Test) { // WHEN + const stack = new Stack(); const widget = new GraphWidget({ title: 'My fancy graph', left: [ @@ -16,7 +17,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -38,12 +39,13 @@ export = { 'label and color are respected in constructor'(test: Test) { // WHEN + const stack = new Stack(); const widget = new GraphWidget({ left: [new Metric({ namespace: 'CDK', metricName: 'Test', label: 'MyMetric', color: '000000' }) ], }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -63,6 +65,7 @@ export = { 'singlevalue widget'(test: Test) { // GIVEN + const stack = new Stack(); const metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); // WHEN @@ -71,7 +74,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 3, @@ -102,7 +105,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -121,6 +124,7 @@ export = { 'add annotations to graph'(test: Test) { // WHEN + const stack = new Stack(); const widget = new GraphWidget({ title: 'My fancy graph', left: [ @@ -135,7 +139,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -178,7 +182,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index dcc3ecd5d3827..afa4c91f45177 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -1,5 +1,5 @@ import { countResources, expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; -import { AvailabilityZoneProvider, Construct, resolve, Stack, Tags } from '@aws-cdk/cdk'; +import { AvailabilityZoneProvider, Construct, Stack, Tags } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { DefaultInstanceTenancy, IVpcNetwork, SubnetType, VpcNetwork } from '../lib'; @@ -10,7 +10,7 @@ export = { "vpc.vpcId returns a token to the VPC ID"(test: Test) { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); - test.deepEqual(resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); + test.deepEqual(stack.node.resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); test.done(); }, @@ -68,7 +68,7 @@ export = { const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; test.equal(vpc.publicSubnets.length, zones); test.equal(vpc.privateSubnets.length, zones); - test.deepEqual(resolve(vpc.vpcId), { Ref: 'TheVPC92636AB0' }); + test.deepEqual(stack.node.resolve(vpc.vpcId), { Ref: 'TheVPC92636AB0' }); test.done(); }, @@ -442,7 +442,7 @@ export = { }); // THEN - test.deepEqual(resolve(vpc2.vpcId), { + test.deepEqual(vpc2.node.resolve(vpc2.vpcId), { 'Fn::ImportValue': 'TestStack:TheVPCVpcIdD346CDBA' }); @@ -461,7 +461,7 @@ export = { }); // THEN - test.deepEqual(resolve(imported.vpcId), { + test.deepEqual(imported.node.resolve(imported.vpcId), { 'Fn::ImportValue': 'TestStack:TheVPCVpcIdD346CDBA' }); diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 98ea1f4697752..c7346889b8177 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -126,7 +126,7 @@ export class Repository extends RepositoryBase { if (this.lifecycleRules.length === 0 && !this.registryId) { return undefined; } if (this.lifecycleRules.length > 0) { - lifecyclePolicyText = JSON.stringify(cdk.resolve({ + lifecyclePolicyText = JSON.stringify(this.node.resolve({ rules: this.orderedLifecycleRules().map(renderLifecycleRule), })); } diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index e3bfa4ffcc657..d27f5724721a5 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -137,7 +137,7 @@ export = { // THEN const arnSplit = { 'Fn::Split': [ ':', { 'Fn::GetAtt': [ 'Repo02AC86CF', 'Arn' ] } ] }; - test.deepEqual(cdk.resolve(uri), { 'Fn::Join': [ '', [ + test.deepEqual(repo.node.resolve(uri), { 'Fn::Join': [ '', [ { 'Fn::Select': [ 4, arnSplit ] }, '.dkr.ecr.', { 'Fn::Select': [ 3, arnSplit ] }, @@ -159,11 +159,11 @@ export = { const repo2 = ecr.Repository.import(stack2, 'Repo', repo1.export()); // THEN - test.deepEqual(cdk.resolve(repo2.repositoryArn), { + test.deepEqual(repo2.node.resolve(repo2.repositoryArn), { 'Fn::ImportValue': 'RepoRepositoryArn7F2901C9' }); - test.deepEqual(cdk.resolve(repo2.repositoryName), { + test.deepEqual(repo2.node.resolve(repo2.repositoryName), { 'Fn::ImportValue': 'RepoRepositoryName58A7E467' }); @@ -182,9 +182,9 @@ export = { const exportImport = repo2.export(); // THEN - test.deepEqual(cdk.resolve(repo2.repositoryArn), 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo'); - test.deepEqual(cdk.resolve(repo2.repositoryName), 'foo/bar/foo/fooo'); - test.deepEqual(cdk.resolve(exportImport), { repositoryArn: 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo' }); + test.deepEqual(repo2.node.resolve(repo2.repositoryArn), 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo'); + test.deepEqual(repo2.node.resolve(repo2.repositoryName), 'foo/bar/foo/fooo'); + test.deepEqual(repo2.node.resolve(exportImport), { repositoryArn: 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo' }); test.done(); }, @@ -212,8 +212,8 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(repo.repositoryArn), { 'Fn::GetAtt': [ 'Boom', 'Arn' ] }); - test.deepEqual(cdk.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); + test.deepEqual(repo.node.resolve(repo.repositoryArn), { 'Fn::GetAtt': [ 'Boom', 'Arn' ] }); + test.deepEqual(repo.node.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); test.done(); }, @@ -227,7 +227,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(repo.repositoryArn), { + test.deepEqual(repo.node.resolve(repo.repositoryArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -238,7 +238,7 @@ export = { ':repository/my-repo' ] ] }); - test.deepEqual(cdk.resolve(repo.repositoryName), 'my-repo'); + test.deepEqual(repo.node.resolve(repo.repositoryName), 'my-repo'); test.done(); }, @@ -254,8 +254,8 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); - test.deepEqual(cdk.resolve(repo.repositoryArn), { + test.deepEqual(repo.node.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); + test.deepEqual(repo.node.resolve(repo.repositoryArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 04042794afbc2..feee1b254f7b0 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -232,7 +232,7 @@ export = { // THEN expect(stack).to(haveResourceLike("AWS::ECS::TaskDefinition", { - TaskRoleArn: cdk.resolve(taskDefinition.taskRole.roleArn) + TaskRoleArn: stack.node.resolve(taskDefinition.taskRole.roleArn) })); test.done(); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts index 2bc6f79866622..2311081a79f3a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts @@ -412,7 +412,7 @@ export = { test.equal('AWS/ApplicationELB', metric.namespace); const loadBalancerArn = { Ref: "LBSomeListenerCA01F1A0" }; - test.deepEqual(cdk.resolve(metric.dimensions), { + test.deepEqual(lb.node.resolve(metric.dimensions), { TargetGroup: { 'Fn::GetAtt': [ 'TargetGroup3D7CD9B8', 'TargetGroupFullName' ] }, LoadBalancer: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts index ea995361ae531..c6a387deeb536 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts @@ -179,7 +179,7 @@ export = { for (const metric of metrics) { test.equal('AWS/ApplicationELB', metric.namespace); - test.deepEqual(cdk.resolve(metric.dimensions), { + test.deepEqual(stack.node.resolve(metric.dimensions), { LoadBalancer: { 'Fn::GetAtt': ['LB8A12904C', 'LoadBalancerFullName'] } }); } diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 3e0001c455d9d..870194bde5ead 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -1,6 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { IEventRuleTarget } from '../lib'; import { EventRule } from '../lib/rule'; @@ -329,7 +329,7 @@ export = { const rule = new EventRule(stack, 'EventRule'); rule.addTarget(t1); - test.deepEqual(resolve(receivedRuleArn), resolve(rule.ruleArn)); + test.deepEqual(stack.node.resolve(receivedRuleArn), stack.node.resolve(rule.ruleArn)); test.deepEqual(receivedRuleId, rule.node.uniqueId); test.done(); }, @@ -347,7 +347,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(exportedRule), { eventRuleArn: { 'Fn::ImportValue': 'MyRuleRuleArnDB13ADB1' } }); + test.deepEqual(stack.node.resolve(exportedRule), { eventRuleArn: { 'Fn::ImportValue': 'MyRuleRuleArnDB13ADB1' } }); test.deepEqual(importedRule.ruleArn, 'arn:of:rule'); test.done(); diff --git a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts index 49989eecc870a..23a0d83d815c0 100644 --- a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts @@ -4,9 +4,10 @@ import { AwsManagedPolicy } from '../lib'; export = { 'simple managed policy'(test: Test) { + const stack = new cdk.Stack(); const mp = new AwsManagedPolicy("service-role/SomePolicy"); - test.deepEqual(cdk.resolve(mp.policyArn), { + test.deepEqual(stack.node.resolve(mp.policyArn), { "Fn::Join": ['', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts index 9c8210df3414d..2694df9538380 100644 --- a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts +++ b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts @@ -1,4 +1,4 @@ -import { resolve, Stack, Token } from '@aws-cdk/cdk'; +import { Stack, Token } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { Anyone, AnyPrincipal, CanonicalUserPrincipal, PolicyDocument, PolicyPrincipal, PolicyStatement } from '../lib'; import { ArnPrincipal, CompositePrincipal, FederatedPrincipal, PrincipalPolicyFragment, ServicePrincipal } from '../lib'; @@ -17,7 +17,7 @@ export = { p.addAwsAccountPrincipal(stack, `my${new Token({ account: 'account' })}name`); p.limitToAccount('12221121221'); - test.deepEqual(resolve(p), { Action: + test.deepEqual(stack.node.resolve(p), { Action: [ 'sqs:SendMessage', 'dynamodb:CreateTable', 'dynamodb:DeleteTable' ], @@ -38,6 +38,7 @@ export = { }, 'the PolicyDocument class is a dom for iam policy documents'(test: Test) { + const stack = new Stack(); const doc = new PolicyDocument(); const p1 = new PolicyStatement(); p1.addAction('sqs:SendMessage'); @@ -50,7 +51,7 @@ export = { doc.addStatement(p1); doc.addStatement(p2); - test.deepEqual(resolve(doc), { + test.deepEqual(stack.node.resolve(doc), { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: 'sqs:SendMessage', Resource: '*' }, @@ -60,6 +61,7 @@ export = { }, 'A PolicyDocument can be initialized with an existing policy, which is merged upon serialization'(test: Test) { + const stack = new Stack(); const base = { Version: 'Foo', Something: 123, @@ -71,7 +73,7 @@ export = { const doc = new PolicyDocument(base); doc.addStatement(new PolicyStatement().addResource('resource').addAction('action')); - test.deepEqual(resolve(doc), { Version: 'Foo', + test.deepEqual(stack.node.resolve(doc), { Version: 'Foo', Something: 123, Statement: [ { Statement1: 1 }, @@ -81,8 +83,9 @@ export = { }, 'Permission allows specifying multiple actions upon construction'(test: Test) { + const stack = new Stack(); const perm = new PolicyStatement().addResource('MyResource').addActions('Action1', 'Action2', 'Action3'); - test.deepEqual(resolve(perm), { + test.deepEqual(stack.node.resolve(perm), { Effect: 'Allow', Action: [ 'Action1', 'Action2', 'Action3' ], Resource: 'MyResource' }); @@ -90,16 +93,18 @@ export = { }, 'PolicyDoc resolves to undefined if there are no permissions'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); - test.deepEqual(resolve(p), undefined); + test.deepEqual(stack.node.resolve(p), undefined); test.done(); }, 'canonicalUserPrincipal adds a principal to a policy with the passed canonical user id'(test: Test) { + const stack = new Stack(); const p = new PolicyStatement(); const canoncialUser = "averysuperduperlongstringfor"; p.addPrincipal(new CanonicalUserPrincipal(canoncialUser)); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: "Allow", Principal: { CanonicalUser: canoncialUser @@ -113,7 +118,7 @@ export = { const p = new PolicyStatement(); p.addAccountRootPrincipal(stack); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: "Allow", Principal: { AWS: { @@ -134,9 +139,10 @@ export = { }, 'addFederatedPrincipal adds a Federated principal with the passed value'(test: Test) { + const stack = new Stack(); const p = new PolicyStatement(); p.addFederatedPrincipal("com.amazon.cognito", { StringEquals: { key: 'value' }}); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: "Allow", Principal: { Federated: "com.amazon.cognito" @@ -154,7 +160,7 @@ export = { const p = new PolicyStatement(); p.addAwsAccountPrincipal(stack, '1234'); p.addAwsAccountPrincipal(stack, '5678'); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: 'Allow', Principal: { AWS: [ @@ -214,13 +220,14 @@ export = { }, 'the { AWS: "*" } principal is represented as `Anyone` or `AnyPrincipal`'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); p.addStatement(new PolicyStatement().addPrincipal(new Anyone())); p.addStatement(new PolicyStatement().addPrincipal(new AnyPrincipal())); p.addStatement(new PolicyStatement().addAnyPrincipal()); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Statement: [ { Effect: 'Allow', Principal: '*' }, { Effect: 'Allow', Principal: '*' }, @@ -232,13 +239,14 @@ export = { }, 'addAwsPrincipal/addArnPrincipal are the aliases'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); p.addStatement(new PolicyStatement().addAwsPrincipal('111222-A')); p.addStatement(new PolicyStatement().addArnPrincipal('111222-B')); p.addStatement(new PolicyStatement().addPrincipal(new ArnPrincipal('111222-C'))); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Statement: [ { Effect: 'Allow', Principal: { AWS: '111222-A' } }, { Effect: 'Allow', Principal: { AWS: '111222-B' } }, @@ -263,12 +271,13 @@ export = { }, 'addCanonicalUserPrincipal can be used to add cannonical user principals'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); p.addStatement(new PolicyStatement().addCanonicalUserPrincipal('cannonical-user-1')); p.addStatement(new PolicyStatement().addPrincipal(new CanonicalUserPrincipal('cannonical-user-2'))); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Statement: [ { Effect: 'Allow', Principal: { CanonicalUser: 'cannonical-user-1' } }, { Effect: 'Allow', Principal: { CanonicalUser: 'cannonical-user-2' } } @@ -287,7 +296,7 @@ export = { }; const s = new PolicyStatement().addAccountRootPrincipal(stack) .addPrincipal(arrayPrincipal); - test.deepEqual(resolve(s), { + test.deepEqual(stack.node.resolve(s), { Effect: 'Allow', Principal: { AWS: [ @@ -301,13 +310,14 @@ export = { // https://github.com/awslabs/aws-cdk/issues/1201 'policy statements with multiple principal types can be created using multiple addPrincipal calls'(test: Test) { + const stack = new Stack(); const s = new PolicyStatement() .addAwsPrincipal('349494949494') .addServicePrincipal('ec2.amazonaws.com') .addResource('resource') .addAction('action'); - test.deepEqual(resolve(s), { + test.deepEqual(stack.node.resolve(s), { Action: 'action', Effect: 'Allow', Principal: { AWS: '349494949494', Service: 'ec2.amazonaws.com' }, @@ -320,9 +330,10 @@ export = { 'CompositePrincipal can be used to represent a principal that has multiple types': { 'with a single principal'(test: Test) { + const stack = new Stack(); const p = new CompositePrincipal(new ArnPrincipal('i:am:an:arn')); const statement = new PolicyStatement().addPrincipal(p); - test.deepEqual(resolve(statement), { Effect: 'Allow', Principal: { AWS: 'i:am:an:arn' } }); + test.deepEqual(stack.node.resolve(statement), { Effect: 'Allow', Principal: { AWS: 'i:am:an:arn' } }); test.done(); }, @@ -335,6 +346,7 @@ export = { }, 'principals and conditions are a big nice merge'(test: Test) { + const stack = new Stack(); // add via ctor const p = new CompositePrincipal( new ArnPrincipal('i:am:an:arn'), @@ -352,7 +364,7 @@ export = { statement.addAwsPrincipal('aws-principal-3'); statement.addCondition('cond2', { boom: 123 }); - test.deepEqual(resolve(statement), { + test.deepEqual(stack.node.resolve(statement), { Condition: { cond2: { boom: 123 } }, diff --git a/packages/@aws-cdk/aws-iam/test/test.role.ts b/packages/@aws-cdk/aws-iam/test/test.role.ts index b78deac2bb514..54ea02a6a3121 100644 --- a/packages/@aws-cdk/aws-iam/test/test.role.ts +++ b/packages/@aws-cdk/aws-iam/test/test.role.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { resolve, Resource, Stack } from '@aws-cdk/cdk'; +import { Resource, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { ArnPrincipal, CompositePrincipal, FederatedPrincipal, PolicyStatement, Role, ServicePrincipal } from '../lib'; @@ -249,13 +249,13 @@ export = { const importedRole = Role.import(stack, 'ImportedRole', exportedRole); // THEN - test.deepEqual(resolve(exportedRole), { + test.deepEqual(stack.node.resolve(exportedRole), { roleArn: { 'Fn::ImportValue': 'MyRoleRoleArn3388B7E2' }, roleId: { 'Fn::ImportValue': 'MyRoleRoleIdF7B258D8' } }); - test.deepEqual(resolve(importedRole.roleArn), { 'Fn::ImportValue': 'MyRoleRoleArn3388B7E2' }); - test.deepEqual(resolve(importedRole.roleId), { 'Fn::ImportValue': 'MyRoleRoleIdF7B258D8' }); + test.deepEqual(stack.node.resolve(importedRole.roleArn), { 'Fn::ImportValue': 'MyRoleRoleArn3388B7E2' }); + test.deepEqual(stack.node.resolve(importedRole.roleId), { 'Fn::ImportValue': 'MyRoleRoleIdF7B258D8' }); test.done(); } }; diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index e4f6cf6f47741..70bc73fd208aa 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -1,5 +1,5 @@ import { PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam'; -import { Construct, DeletionPolicy, IConstruct, Output, resolve } from '@aws-cdk/cdk'; +import { Construct, DeletionPolicy, IConstruct, Output } from '@aws-cdk/cdk'; import { EncryptionKeyAlias } from './alias'; import { CfnKey } from './kms.generated'; @@ -68,7 +68,7 @@ export abstract class EncryptionKeyBase extends Construct { public addToResourcePolicy(statement: PolicyStatement, allowNoOp = true) { if (!this.policy) { if (allowNoOp) { return; } - throw new Error(`Unable to add statement to IAM resource policy for KMS key: ${JSON.stringify(resolve(this.keyArn))}`); + throw new Error(`Unable to add statement to IAM resource policy for KMS key: ${JSON.stringify(this.node.resolve(this.keyArn))}`); } this.policy.addStatement(statement); diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index efdcf513947f4..480b6af15c6d9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -416,7 +416,7 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction { return (principal as iam.ServicePrincipal).service; } - throw new Error(`Invalid principal type for Lambda permission statement: ${JSON.stringify(cdk.resolve(principal))}. ` + + throw new Error(`Invalid principal type for Lambda permission statement: ${JSON.stringify(this.node.resolve(principal))}. ` + 'Supported: AccountPrincipal, ServicePrincipal'); } } diff --git a/packages/@aws-cdk/aws-lambda/test/test.alias.ts b/packages/@aws-cdk/aws-lambda/test/test.alias.ts index 25d91bc154b1c..f94762a77b27a 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.alias.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.alias.ts @@ -1,6 +1,6 @@ import { beASupersetOfTemplate, expect, haveResource } from '@aws-cdk/assert'; import { AccountPrincipal } from '@aws-cdk/aws-iam'; -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import lambda = require('../lib'); @@ -31,7 +31,7 @@ export = { Type: "AWS::Lambda::Alias", Properties: { FunctionName: { Ref: "MyLambdaCCE802FB" }, - FunctionVersion: resolve(version.functionVersion), + FunctionVersion: stack.node.resolve(version.functionVersion), Name: "prod" } } @@ -59,11 +59,11 @@ export = { }); expect(stack).to(haveResource('AWS::Lambda::Alias', { - FunctionVersion: resolve(version1.functionVersion), + FunctionVersion: stack.node.resolve(version1.functionVersion), RoutingConfig: { AdditionalVersionWeights: [ { - FunctionVersion: resolve(version2.functionVersion), + FunctionVersion: stack.node.resolve(version2.functionVersion), FunctionWeight: 0.1 } ] @@ -123,7 +123,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::Lambda::Permission', { - FunctionName: resolve(fn.functionName), + FunctionName: stack.node.resolve(fn.functionName), Principal: "123456" })); diff --git a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts index 881f6e0769da0..31efd84861ea6 100644 --- a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts +++ b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts @@ -95,6 +95,6 @@ export class CrossAccountDestination extends cdk.Construct implements ILogSubscr * Return a stringified JSON version of the PolicyDocument */ private stringifiedPolicyDocument() { - return this.policyDocument.isEmpty ? '' : cdk.CloudFormationJSON.stringify(cdk.resolve(this.policyDocument)); + return this.policyDocument.isEmpty ? '' : cdk.CloudFormationJSON.stringify(this.node.resolve(this.policyDocument), this); } } diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 841a6b2776d04..9ec072c6aa298 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -136,8 +136,8 @@ export = { const imported = ClusterParameterGroup.import(stack, 'ImportParams', exported); // THEN - test.deepEqual(cdk.resolve(exported), { parameterGroupName: { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' } }); - test.deepEqual(cdk.resolve(imported.parameterGroupName), { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' }); + test.deepEqual(stack.node.resolve(exported), { parameterGroupName: { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' } }); + test.deepEqual(stack.node.resolve(imported.parameterGroupName), { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' }); test.done(); } }; diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts index c22c49cbedd79..e7dd3d2d09b06 100644 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts @@ -33,7 +33,7 @@ export = { // WHEN const provider = new HostedZoneProvider(stack, filter); - const zoneProps = cdk.resolve(provider.findHostedZone()); + const zoneProps = stack.node.resolve(provider.findHostedZone()); const zoneRef = provider.findAndImport(stack, 'MyZoneProvider'); // THEN diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index d71f7fe980e3b..5045c2c318e1d 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -964,7 +964,7 @@ class ImportedBucket extends BucketBase { constructor(scope: cdk.Construct, id: string, private readonly props: BucketImportProps) { super(scope, id); - const bucketName = parseBucketName(props); + const bucketName = parseBucketName(this, props); if (!bucketName) { throw new Error('Bucket name is required'); } diff --git a/packages/@aws-cdk/aws-s3/lib/util.ts b/packages/@aws-cdk/aws-s3/lib/util.ts index a6c9861829a15..3f96219f0d70a 100644 --- a/packages/@aws-cdk/aws-s3/lib/util.ts +++ b/packages/@aws-cdk/aws-s3/lib/util.ts @@ -22,7 +22,7 @@ export function parseBucketArn(props: BucketImportProps): string { throw new Error('Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed'); } -export function parseBucketName(props: BucketImportProps): string | undefined { +export function parseBucketName(construct: cdk.IConstruct, props: BucketImportProps): string | undefined { // if we have an explicit bucket name, use it. if (props.bucketName) { @@ -32,7 +32,7 @@ export function parseBucketName(props: BucketImportProps): string | undefined { // if we have a string arn, we can extract the bucket name from it. if (props.bucketArn) { - const resolved = cdk.resolve(props.bucketArn); + const resolved = construct.node.resolve(props.bucketArn); if (typeof(resolved) === 'string') { const components = cdk.ArnUtils.parse(resolved); if (components.service !== 's3') { diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index 1c1acab595baf..cd368c0232005 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -246,7 +246,7 @@ export = { const x = new iam.PolicyStatement().addResource(bucket.bucketArn).addAction('s3:ListBucket'); - test.deepEqual(cdk.resolve(x), { + test.deepEqual(bucket.node.resolve(x), { Action: 's3:ListBucket', Effect: 'Allow', Resource: { 'Fn::GetAtt': [ 'MyBucketF68F3FF0', 'Arn' ] } @@ -262,7 +262,7 @@ export = { const p = new iam.PolicyStatement().addResource(bucket.arnForObjects('hello/world')).addAction('s3:GetObject'); - test.deepEqual(cdk.resolve(p), { + test.deepEqual(bucket.node.resolve(p), { Action: 's3:GetObject', Effect: 'Allow', Resource: { @@ -288,7 +288,7 @@ export = { const resource = bucket.arnForObjects('home/', team.groupName, '/', user.userName, '/*'); const p = new iam.PolicyStatement().addResource(resource).addAction('s3:GetObject'); - test.deepEqual(cdk.resolve(p), { + test.deepEqual(bucket.node.resolve(p), { Action: 's3:GetObject', Effect: 'Allow', Resource: { @@ -331,7 +331,7 @@ export = { const stack = new cdk.Stack(undefined, 'MyStack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const bucketRef = bucket.export(); - test.deepEqual(cdk.resolve(bucketRef), { + test.deepEqual(bucket.node.resolve(bucketRef), { bucketArn: { 'Fn::ImportValue': 'MyStack:MyBucketBucketArnE260558C' }, bucketName: { 'Fn::ImportValue': 'MyStack:MyBucketBucketName8A027014' }, bucketDomainName: { 'Fn::ImportValue': 'MyStack:MyBucketDomainNameF76B9A7A' } @@ -343,7 +343,7 @@ export = { const stack = new cdk.Stack(undefined, 'MyStack'); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.Kms }); const bucketRef = bucket.export(); - test.deepEqual(cdk.resolve(bucketRef), { + test.deepEqual(bucket.node.resolve(bucketRef), { bucketArn: { 'Fn::ImportValue': 'MyStack:MyBucketBucketArnE260558C' }, bucketName: { 'Fn::ImportValue': 'MyStack:MyBucketBucketName8A027014' }, bucketDomainName: { 'Fn::ImportValue': 'MyStack:MyBucketDomainNameF76B9A7A' } @@ -363,14 +363,14 @@ export = { const p = new iam.PolicyStatement().addResource(bucket.bucketArn).addAction('s3:ListBucket'); // it is possible to obtain a permission statement for a ref - test.deepEqual(cdk.resolve(p), { + test.deepEqual(bucket.node.resolve(p), { Action: 's3:ListBucket', Effect: 'Allow', Resource: 'arn:aws:s3:::my-bucket' }); test.deepEqual(bucket.bucketArn, bucketArn); - test.deepEqual(cdk.resolve(bucket.bucketName), 'my-bucket'); + test.deepEqual(bucket.node.resolve(bucket.bucketName), 'my-bucket'); test.deepEqual(stack.toCloudFormation(), {}, 'the ref is not a real resource'); test.done(); diff --git a/packages/@aws-cdk/aws-s3/test/test.util.ts b/packages/@aws-cdk/aws-s3/test/test.util.ts index 4116984d16250..bde65d934eb8b 100644 --- a/packages/@aws-cdk/aws-s3/test/test.util.ts +++ b/packages/@aws-cdk/aws-s3/test/test.util.ts @@ -1,5 +1,4 @@ import cdk = require('@aws-cdk/cdk'); -import { Token } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { parseBucketArn, parseBucketName } from '../lib/util'; @@ -12,8 +11,9 @@ export = { }, 'produce arn from bucket name'(test: Test) { + const stack = new cdk.Stack(); const bucketName = 'hello'; - test.deepEqual(cdk.resolve(parseBucketArn({ bucketName })), { 'Fn::Join': + test.deepEqual(stack.node.resolve(parseBucketArn({ bucketName })), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -30,38 +30,44 @@ export = { parseBucketName: { 'explicit name'(test: Test) { + const stack = new cdk.Stack(); const bucketName = 'foo'; - test.deepEqual(cdk.resolve(parseBucketName({ bucketName })), 'foo'); + test.deepEqual(stack.node.resolve(parseBucketName(stack, { bucketName })), 'foo'); test.done(); }, 'extract bucket name from string arn'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket'; - test.deepEqual(cdk.resolve(parseBucketName({ bucketArn })), 'my-bucket'); + test.deepEqual(stack.node.resolve(parseBucketName(stack, { bucketArn })), 'my-bucket'); test.done(); }, 'undefined if cannot extract name from a non-string arn'(test: Test) { - const bucketArn = `arn:aws:s3:::${new Token({ Ref: 'my-bucket' })}`; - test.deepEqual(cdk.resolve(parseBucketName({ bucketArn })), undefined); + const stack = new cdk.Stack(); + const bucketArn = `arn:aws:s3:::${new cdk.Token({ Ref: 'my-bucket' })}`; + test.deepEqual(stack.node.resolve(parseBucketName(stack, { bucketArn })), undefined); test.done(); }, 'fails if arn uses a non "s3" service'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'arn:aws:xx:::my-bucket'; - test.throws(() => parseBucketName({ bucketArn }), /Invalid ARN/); + test.throws(() => parseBucketName(stack, { bucketArn }), /Invalid ARN/); test.done(); }, 'fails if ARN has invalid format'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'invalid-arn'; - test.throws(() => parseBucketName({ bucketArn }), /ARNs must have at least 6 components/); + test.throws(() => parseBucketName(stack, { bucketArn }), /ARNs must have at least 6 components/); test.done(); }, 'fails if ARN has path'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket/path'; - test.throws(() => parseBucketName({ bucketArn }), /Bucket ARN must not contain a path/); + test.throws(() => parseBucketName(stack, { bucketArn }), /Bucket ARN must not contain a path/); test.done(); } }, diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts index 56883bd0f7837..2a0eb8f0afcfd 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts @@ -13,7 +13,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.value), '{{resolve:secretsmanager:SomeSecret:SecretString:::}}'); + test.equal(ref.node.resolve(ref.value), '{{resolve:secretsmanager:SomeSecret:SecretString:::}}'); test.done(); }, @@ -28,7 +28,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.jsonFieldValue('subkey')), '{{resolve:secretsmanager:SomeSecret:SecretString:subkey::}}'); + test.equal(ref.node.resolve(ref.jsonFieldValue('subkey')), '{{resolve:secretsmanager:SomeSecret:SecretString:subkey::}}'); test.done(); }, diff --git a/packages/@aws-cdk/aws-sns/test/test.sns.ts b/packages/@aws-cdk/aws-sns/test/test.sns.ts index ad7f6bdbc5a27..7d9b21872bd2a 100644 --- a/packages/@aws-cdk/aws-sns/test/test.sns.ts +++ b/packages/@aws-cdk/aws-sns/test/test.sns.ts @@ -5,7 +5,6 @@ import lambda = require('@aws-cdk/aws-lambda'); import s3n = require('@aws-cdk/aws-s3-notifications'); import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); -import { resolve } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sns = require('../lib'); @@ -552,7 +551,7 @@ export = { { "Action": "sns:Publish", "Effect": "Allow", - "Resource": cdk.resolve(topic.topicArn) + "Resource": stack.node.resolve(topic.topicArn) } ], } @@ -692,11 +691,11 @@ export = { "Action": "sqs:SendMessage", "Condition": { "ArnEquals": { - "aws:SourceArn": resolve(imported.topicArn) + "aws:SourceArn": stack2.node.resolve(imported.topicArn) } }, "Principal": { "Service": "sns.amazonaws.com" }, - "Resource": resolve(queue.queueArn), + "Resource": stack2.node.resolve(queue.queueArn), "Effect": "Allow", } ], @@ -715,7 +714,7 @@ export = { const bucketId = 'bucketId'; const dest1 = topic.asBucketNotificationDestination(bucketArn, bucketId); - test.deepEqual(resolve(dest1.arn), resolve(topic.topicArn)); + test.deepEqual(stack.node.resolve(dest1.arn), stack.node.resolve(topic.topicArn)); test.deepEqual(dest1.type, s3n.BucketNotificationDestinationType.Topic); const dep: cdk.Construct = dest1.dependencies![0] as any; @@ -723,12 +722,12 @@ export = { // calling again on the same bucket yields is idempotent const dest2 = topic.asBucketNotificationDestination(bucketArn, bucketId); - test.deepEqual(resolve(dest2.arn), resolve(topic.topicArn)); + test.deepEqual(stack.node.resolve(dest2.arn), stack.node.resolve(topic.topicArn)); test.deepEqual(dest2.type, s3n.BucketNotificationDestinationType.Topic); // another bucket will be added to the topic policy const dest3 = topic.asBucketNotificationDestination('bucket2', 'bucket2'); - test.deepEqual(resolve(dest3.arn), resolve(topic.topicArn)); + test.deepEqual(stack.node.resolve(dest3.arn), stack.node.resolve(topic.topicArn)); test.deepEqual(dest3.type, s3n.BucketNotificationDestinationType.Topic); expect(stack).toMatch({ diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index 37e9faff6a5ff..fb2bfcc28883a 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sqs = require('../lib'); import { Queue } from '../lib'; @@ -103,8 +103,8 @@ export = { // THEN // "import" returns an IQueue bound to `Fn::ImportValue`s. - test.deepEqual(resolve(imports.queueArn), { 'Fn::ImportValue': 'QueueQueueArn8CF496D5' }); - test.deepEqual(resolve(imports.queueUrl), { 'Fn::ImportValue': 'QueueQueueUrlC30FF916' }); + test.deepEqual(stack.node.resolve(imports.queueArn), { 'Fn::ImportValue': 'QueueQueueArn8CF496D5' }); + test.deepEqual(stack.node.resolve(imports.queueUrl), { 'Fn::ImportValue': 'QueueQueueUrlC30FF916' }); // the exporting stack has Outputs for QueueARN and QueueURL const outputs = stack.toCloudFormation().Outputs; @@ -246,7 +246,7 @@ export = { const exportCustom = customKey.export(); - test.deepEqual(resolve(exportCustom), { + test.deepEqual(stack.node.resolve(exportCustom), { queueArn: { 'Fn::ImportValue': 'QueueWithCustomKeyQueueArnD326BB9B' }, queueUrl: { 'Fn::ImportValue': 'QueueWithCustomKeyQueueUrlF07DDC70' }, keyArn: { 'Fn::ImportValue': 'QueueWithCustomKeyKeyArn537F6E42' } @@ -294,7 +294,7 @@ export = { const exportManaged = managedKey.export(); - test.deepEqual(resolve(exportManaged), { + test.deepEqual(stack.node.resolve(exportManaged), { queueArn: { 'Fn::ImportValue': 'QueueWithManagedKeyQueueArn8798A14E' }, queueUrl: { 'Fn::ImportValue': 'QueueWithManagedKeyQueueUrlD735C981' }, keyArn: { 'Fn::ImportValue': 'QueueWithManagedKeyKeyArn9C42A85D' } diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts index 897f8cfc96cfe..5eaa03e8974ce 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts @@ -14,7 +14,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.value), '{{resolve:ssm:/some/key:123}}'); + test.equal(ref.node.resolve(ref.value), '{{resolve:ssm:/some/key:123}}'); test.done(); }, @@ -30,7 +30,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.value), '{{resolve:ssm-secure:/some/key:123}}'); + test.equal(ref.node.resolve(ref.value), '{{resolve:ssm-secure:/some/key:123}}'); test.done(); }, diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index e7c2b335e8876..88d70427ae49e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -82,7 +82,7 @@ export class StateMachine extends cdk.Construct implements IStateMachine { const resource = new CfnStateMachine(this, 'Resource', { stateMachineName: props.stateMachineName, roleArn: this.role.roleArn, - definitionString: cdk.CloudFormationJSON.stringify(graph.toGraphJson()), + definitionString: cdk.CloudFormationJSON.stringify(graph.toGraphJson(), this), }); for (const statement of graph.policyStatements) { diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts index 96f1921d90d6c..d4d15c8dec872 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts @@ -60,13 +60,13 @@ export = { namespace: 'AWS/States', dimensions: { ActivityArn: { Ref: 'Activity04690B0A' }}, }; - test.deepEqual(cdk.resolve(activity.metricRunTime()), { + test.deepEqual(stack.node.resolve(activity.metricRunTime()), { ...sharedMetric, metricName: 'ActivityRunTime', statistic: 'Average' }); - test.deepEqual(cdk.resolve(activity.metricFailed()), { + test.deepEqual(stack.node.resolve(activity.metricFailed()), { ...sharedMetric, metricName: 'ActivitiesFailed', statistic: 'Sum' diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index 6484828c19ec9..43b232a101612 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -79,13 +79,13 @@ export = { namespace: 'AWS/States', dimensions: { ResourceArn: 'resource' }, }; - test.deepEqual(cdk.resolve(task.metricRunTime()), { + test.deepEqual(stack.node.resolve(task.metricRunTime()), { ...sharedMetric, metricName: 'FakeResourceRunTime', statistic: 'Average' }); - test.deepEqual(cdk.resolve(task.metricFailed()), { + test.deepEqual(stack.node.resolve(task.metricFailed()), { ...sharedMetric, metricName: 'FakeResourcesFailed', statistic: 'Sum' diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index fd30ca17a1d59..5b15006d36395 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -729,5 +729,5 @@ class FakeResource implements stepfunctions.IStepFunctionsTaskResource { } function render(sm: stepfunctions.IChainable) { - return cdk.resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); + return new cdk.Stack().node.resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); } diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index 7999b2944272a..0038e709e3551 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -3,7 +3,6 @@ import fs = require('fs'); import path = require('path'); import { Stack } from './cloudformation/stack'; import { IConstruct, MetadataEntry, PATH_SEP, Root } from './core/construct'; -import { resolve } from './core/tokens'; /** * Represents a CDK program. @@ -117,7 +116,7 @@ export class App extends Root { function visit(node: IConstruct) { if (node.node.metadata.length > 0) { // Make the path absolute - output[PATH_SEP + node.node.path] = node.node.metadata.map(md => resolve(md) as MetadataEntry); + output[PATH_SEP + node.node.path] = node.node.metadata.map(md => node.node.resolve(md) as MetadataEntry); } for (const child of node.node.children) { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts index 4ba8cd2f1e7c9..b65dd2daee18b 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts @@ -1,5 +1,7 @@ -import { resolve, Token } from "../core/tokens"; -import { isIntrinsic } from "./tokens"; +import { IConstruct } from "../core/construct"; +import { Token } from "../core/tokens"; +import { isIntrinsic } from "../core/tokens/cfn-tokens"; +import { resolve } from "../core/tokens/resolve"; /** * Class for JSON routines that are framework-aware @@ -14,8 +16,11 @@ export class CloudFormationJSON { * * All Tokens substituted in this way must return strings, or the evaluation * in CloudFormation will fail. + * + * @param obj The object to stringify + * @param context The Construct from which to resolve any Tokens found in the object */ - public static stringify(obj: any): Token { + public static stringify(obj: any, context: IConstruct): Token { return new Token(() => { // Resolve inner value first so that if they evaluate to literals, we // maintain the type (and discard 'undefined's). @@ -26,7 +31,10 @@ export class CloudFormationJSON { // deep-escapes any strings inside the intrinsic, so that if literal // strings are used in {Fn::Join} or something, they will end up // escaped in the final JSON output. - const resolved = resolve(obj); + const resolved = resolve(obj, { + construct: context, + prefix: [] + }); // We can just directly return this value, since resolve() will be called // on our return value anyway. diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index bc17c2c9127b7..42c3d050d9ddb 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -1,6 +1,7 @@ -import { resolve, Token, unresolved } from '../core/tokens'; +import { ResolveContext, Token, unresolved } from '../core/tokens'; +import { isIntrinsic } from '../core/tokens/cfn-tokens'; +import { resolve } from '../core/tokens/resolve'; import { FnCondition } from './condition'; -import { isIntrinsic } from './tokens'; // tslint:disable:max-line-length @@ -625,20 +626,19 @@ class FnJoin extends Token { if (listOfValues.length === 0) { throw new Error(`FnJoin requires at least one value to be provided`); } - // Passing the values as a token, optimization requires resolving stringified tokens, we should be deferred until - // this token is itself being resolved. - super({ 'Fn::Join': [ delimiter, new Token(() => this.resolveValues()) ] }); + super(); + this.delimiter = delimiter; this.listOfValues = listOfValues; this.canOptimize = true; } - public resolve(): any { - const resolved = this.resolveValues(); + public resolve(context: ResolveContext): any { + const resolved = this.resolveValues(context); if (this.canOptimize && resolved.length === 1) { return resolved[0]; } - return super.resolve(); + return { 'Fn::Join': [ this.delimiter, resolved ] }; } /** @@ -646,7 +646,7 @@ class FnJoin extends Token { * if two concatenated elements are literal strings (not tokens), then pre-concatenate them with the delimiter, to * generate shorter output. */ - private resolveValues() { + private resolveValues(context: ResolveContext) { if (this._resolvedValues) { return this._resolvedValues; } if (unresolved(this.listOfValues)) { @@ -655,7 +655,7 @@ class FnJoin extends Token { return this._resolvedValues = this.listOfValues; } - const resolvedValues = [...this.listOfValues.map(e => resolve(e))]; + const resolvedValues = [...this.listOfValues.map(e => resolve(e, context))]; let i = 0; while (i < resolvedValues.length) { const el = resolvedValues[i]; @@ -681,4 +681,4 @@ class FnJoin extends Token { return typeof obj === 'string' && !unresolved(obj); } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 5fd1a1a024310..10de9d1e80406 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { CfnReference } from './tokens'; +import { CfnReference } from '../core/tokens/cfn-tokens'; /** * Accessor for pseudo parameters diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts deleted file mode 100644 index 19f9b64c95efb..0000000000000 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resolve-concat.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Produce a CloudFormation expression to concat two arbitrary expressions when resolving - */ -export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { - if (left === undefined && right === undefined) { return ''; } - - const parts = new Array(); - if (left !== undefined) { parts.push(left); } - if (right !== undefined) { parts.push(right); } - - // Some case analysis to produce minimal expressions - if (parts.length === 1) { return parts[0]; } - if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { - return parts[0] + parts[1]; - } - - // Otherwise return a Join intrinsic (already in the target document language to avoid taking - // circular dependencies on FnJoin & friends) - return { 'Fn::Join': ['', parts] }; -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 1af6a1ba4a5fd..98e490325004f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,10 +1,10 @@ import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; +import { CfnReference } from '../core/tokens/cfn-tokens'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; import { IDependable, Referenceable, Stack, StackElement } from './stack'; -import { CfnReference } from './tokens'; export interface ResourceProps { /** @@ -187,12 +187,12 @@ export class Resource extends Referenceable { Resources: { [this.logicalId]: deepMerge({ Type: this.resourceType, - Properties: ignoreEmpty(properties), - DependsOn: ignoreEmpty(this.renderDependsOn()), - CreationPolicy: capitalizePropertyNames(this.options.creationPolicy), - UpdatePolicy: capitalizePropertyNames(this.options.updatePolicy), - DeletionPolicy: capitalizePropertyNames(this.options.deletionPolicy), - Metadata: ignoreEmpty(this.options.metadata), + Properties: ignoreEmpty(this, properties), + DependsOn: ignoreEmpty(this, this.renderDependsOn()), + CreationPolicy: capitalizePropertyNames(this, this.options.creationPolicy), + UpdatePolicy: capitalizePropertyNames(this, this.options.updatePolicy), + DeletionPolicy: capitalizePropertyNames(this, this.options.deletionPolicy), + Metadata: ignoreEmpty(this, this.options.metadata), Condition: this.options.condition && this.options.condition.logicalId }, this.rawOverrides) } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index 407a247a92e79..0ba9d751ead31 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -97,7 +97,7 @@ export class Rule extends Referenceable { Rules: { [this.logicalId]: { RuleCondition: this.ruleCondition, - Assertions: capitalizePropertyNames(this.assertions) + Assertions: capitalizePropertyNames(this, this.assertions) } } }; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 6b8789b386acd..632696f910023 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -1,11 +1,11 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; import { Construct, IConstruct, PATH_SEP } from '../core/construct'; -import { resolve, RESOLVE_OPTIONS, Token, unresolved } from '../core/tokens'; +import { Token } from '../core/tokens'; +import { CfnReference } from '../core/tokens/cfn-tokens'; import { Environment } from '../environment'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; -import { CfnReference } from './tokens'; export interface StackProps { /** @@ -177,7 +177,7 @@ export class Stack extends Construct { } // resolve all tokens and remove all empties - const ret = resolve(template) || {}; + const ret = this.node.resolve(template) || {}; this.logicalIds.assertAllRenamesApplied(); @@ -274,7 +274,7 @@ export class Stack extends Construct { } // Ensure a singleton Output for this value - const resolved = resolve(tokenValue); + const resolved = this.node.resolve(tokenValue); const id = 'Output' + JSON.stringify(resolved); if (this.crossStackExports === undefined) { this.crossStackExports = new Construct(this, 'Exports'); @@ -545,29 +545,7 @@ export abstract class StackElement extends Construct implements IDependable { public abstract substituteCrossStackReferences(): void; protected deepSubCrossStackReferences(sourceStack: Stack, x: any): any { - if (CfnReference.isInstance(x)) { - return x.substituteToken(sourceStack); - } - - if (unresolved(x)) { - const options = RESOLVE_OPTIONS.push({ recurse: (y: any) => this.deepSubCrossStackReferences(sourceStack, y) }); - try { - x = resolve(x); - } finally { - options.pop(); - } - } - - if (Array.isArray(x)) { - return x.map(e => this.deepSubCrossStackReferences(sourceStack, e)); - } - - if (typeof x === 'object' && x !== null) { - for (const [key, value] of Object.entries(x)) { - x[key] = this.deepSubCrossStackReferences(sourceStack, value); - } - return x; - } + Array.isArray(sourceStack); return x; } } @@ -648,4 +626,4 @@ export class Ref extends CfnReference { // These imports have to be at the end to prevent circular imports import { Output } from './output'; -import { Aws } from './pseudo'; \ No newline at end of file +import { Aws } from './pseudo'; diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 96b6453b35602..b4bf3b3762dee 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -1,6 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { makeUniqueId } from '../util/uniqueid'; -import { unresolved } from './tokens'; +import { resolve } from './tokens/resolve'; +import { unresolved } from './tokens/token'; export const PATH_SEP = '/'; /** @@ -350,15 +351,6 @@ export class ConstructNode { this._locked = false; } - /** - * Return the path of components up to but excluding the root - */ - private rootPath(): IConstruct[] { - const ancestors = this.ancestors(); - ancestors.shift(); - return ancestors; - } - /** * Returns true if this construct or the scopes in which it is defined are * locked. @@ -375,6 +367,25 @@ export class ConstructNode { return false; } + /** + * Resolve a tokenized value in the context of the current Construct + */ + public resolve(obj: any): any { + return resolve(obj, { + construct: this.host, + prefix: [] + }); + } + + /** + * Return the path of components up to but excluding the root + */ + private rootPath(): IConstruct[] { + const ancestors = this.ancestors(); + ancestors.shift(); + return ancestors; + } + /** * If the construct ID contains a path separator, it is replaced by double dash (`--`). */ diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts deleted file mode 100644 index 1e7a7e864d0bc..0000000000000 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { cloudFormationConcat } from "../cloudformation/resolve-concat"; - -/** - * If objects has a function property by this name, they will be considered tokens, and this - * function will be called to resolve the value for this object. - */ -export const RESOLVE_METHOD = 'resolve'; - -/** - * Represents a special or lazily-evaluated value. - * - * Can be used to delay evaluation of a certain value in case, for example, - * that it requires some context or late-bound data. Can also be used to - * mark values that need special processing at document rendering time. - * - * Tokens can be embedded into strings while retaining their original - * semantics. - */ -export class Token { - private tokenStringification?: string; - private tokenListification?: string[]; - - /** - * Creates a token that resolves to `value`. - * - * If value is a function, the function is evaluated upon resolution and - * the value it returns will be used as the token's value. - * - * displayName is used to represent the Token when it's embedded into a string; it - * will look something like this: - * - * "embedded in a larger string is ${Token[DISPLAY_NAME.123]}" - * - * This value is used as a hint to humans what the meaning of the Token is, - * and does not have any effect on the evaluation. - * - * Must contain only alphanumeric and simple separator characters (_.:-). - * - * @param valueOrFunction What this token will evaluate to, literal or function. - * @param displayName A human-readable display hint for this Token - */ - constructor(private readonly valueOrFunction?: any, private readonly displayName?: string) { - } - - /** - * @returns The resolved value for this token. - */ - public resolve(): any { - let value = this.valueOrFunction; - if (typeof(value) === 'function') { - value = value(); - } - - return value; - } - - /** - * Return a reversible string representation of this token - * - * If the Token is initialized with a literal, the stringified value of the - * literal is returned. Otherwise, a special quoted string representation - * of the Token is returned that can be embedded into other strings. - * - * Strings with quoted Tokens in them can be restored back into - * complex values with the Tokens restored by calling `resolve()` - * on the string. - */ - public toString(): string { - const valueType = typeof this.valueOrFunction; - // Optimization: if we can immediately resolve this, don't bother - // registering a Token. - if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { - return this.valueOrFunction.toString(); - } - - if (this.tokenStringification === undefined) { - this.tokenStringification = TOKEN_MAP.registerString(this, this.displayName); - } - return this.tokenStringification; - } - - /** - * Turn this Token into JSON - * - * This gets called by JSON.stringify(). We want to prohibit this, because - * it's not possible to do this properly, so we just throw an error here. - */ - public toJSON(): any { - // tslint:disable-next-line:max-line-length - throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use a document-specific stringification method instead.'); - } - - /** - * Return a string list representation of this token - * - * Call this if the Token intrinsically evaluates to a list of strings. - * If so, you can represent the Token in a similar way in the type - * system. - * - * Note that even though the Token is represented as a list of strings, you - * still cannot do any operations on it such as concatenation, indexing, - * or taking its length. The only useful operations you can do to these lists - * is constructing a `FnJoin` or a `FnSelect` on it. - */ - public toList(): string[] { - const valueType = typeof this.valueOrFunction; - if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { - throw new Error('Got a literal Token value; cannot be encoded as a list.'); - } - - if (this.tokenListification === undefined) { - this.tokenListification = TOKEN_MAP.registerList(this, this.displayName); - } - return this.tokenListification; - } -} - -/** - * Returns true if obj is a token (i.e. has the resolve() method or is a string - * that includes token markers), or it's a listifictaion of a Token string. - * - * @param obj The object to test. - */ -export function unresolved(obj: any): boolean { - if (typeof(obj) === 'string') { - return TOKEN_MAP.createStringTokenString(obj).test(); - } else if (Array.isArray(obj) && obj.length === 1) { - return isListToken(obj[0]); - } else { - return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; - } -} - -/** - * Resolves an object by evaluating all tokens and removing any undefined or empty objects or arrays. - * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. - * - * @param obj The object to resolve. - * @param prefix Prefix key path components for diagnostics. - */ -export function resolve(obj: any, prefix?: string[]): any { - prefix = prefix || [ ]; - const pathName = '/' + prefix.join('/'); - const recurse = RESOLVE_OPTIONS.recurse || resolve; - - // protect against cyclic references by limiting depth. - if (prefix.length > 200) { - throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); - } - - // - // undefined - // - - if (typeof(obj) === 'undefined') { - return undefined; - } - - // - // null - // - - if (obj === null) { - return null; - } - - // - // functions - not supported (only tokens are supported) - // - - if (typeof(obj) === 'function') { - throw new Error(`Trying to resolve a non-data object. Only token are supported for lazy evaluation. Path: ${pathName}. Object: ${obj}`); - } - - // - // string - potentially replace all stringified Tokens - // - if (typeof(obj) === 'string') { - return TOKEN_MAP.resolveStringTokens(obj, recurse); - } - - // - // primitives - as-is - // - - if (typeof(obj) !== 'object' || obj instanceof Date) { - return obj; - } - - // - // arrays - resolve all values, remove undefined and remove empty arrays - // - - if (Array.isArray(obj)) { - if (containsListToken(obj)) { - return TOKEN_MAP.resolveListTokens(obj); - } - - const arr = obj - .map((x, i) => recurse(x, prefix!.concat(i.toString()))) - .filter(x => typeof(x) !== 'undefined'); - - return arr; - } - - // - // tokens - invoke 'resolve' and continue to resolve recursively - // - - if (unresolved(obj)) { - const value = obj[RESOLVE_METHOD](); - return recurse(value, prefix); - } - - // - // objects - deep-resolve all values - // - - // Must not be a Construct at this point, otherwise you probably made a type - // mistake somewhere and resolve will get into an infinite loop recursing into - // child.parent <---> parent.children - if (isConstruct(obj)) { - throw new Error('Trying to resolve() a Construct at ' + pathName); - } - - const result: any = { }; - for (const key of Object.keys(obj)) { - const resolvedKey = recurse(key, prefix); - if (typeof(resolvedKey) !== 'string') { - throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); - } - - const value = recurse(obj[key], prefix.concat(key)); - - // skip undefined - if (typeof(value) === 'undefined') { - continue; - } - - result[resolvedKey] = value; - } - - return result; -} - -function isListToken(x: any) { - return typeof(x) === 'string' && TOKEN_MAP.createListTokenString(x).test(); -} - -function containsListToken(xs: any[]) { - return xs.some(isListToken); -} - -/** - * Central place where we keep a mapping from Tokens to their String representation - * - * The string representation is used to embed token into strings, - * and stored to be able to - * - * All instances of TokenStringMap share the same storage, so that this process - * works even when different copies of the library are loaded. - */ -class TokenMap { - private readonly tokenMap: {[key: string]: Token} = {}; - - /** - * Generate a unique string for this Token, returning a key - * - * Every call for the same Token will produce a new unique string, no - * attempt is made to deduplicate. Token objects should cache the - * value themselves, if required. - * - * The token can choose (part of) its own representation string with a - * hint. This may be used to produce aesthetically pleasing and - * recognizable token representations for humans. - */ - public registerString(token: Token, representationHint?: string): string { - const key = this.register(token, representationHint); - return `${BEGIN_STRING_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`; - } - - /** - * Generate a unique string for this Token, returning a key - */ - public registerList(token: Token, representationHint?: string): string[] { - const key = this.register(token, representationHint); - return [`${BEGIN_LIST_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`]; - } - - /** - * Returns a `TokenString` for this string. - */ - public createStringTokenString(s: string) { - return new TokenString(s, BEGIN_STRING_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); - } - - /** - * Returns a `TokenString` for this string. - */ - public createListTokenString(s: string) { - return new TokenString(s, BEGIN_LIST_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); - } - - /** - * Replace any Token markers in this string with their resolved values - */ - public resolveStringTokens(s: string, resolver: ResolveFunc): any { - const str = this.createStringTokenString(s); - const fragments = str.split(this.lookupToken.bind(this)); - const ret = fragments.mapUnresolved(resolver).join(cloudFormationConcat); - if (unresolved(ret)) { - return resolve(ret); - } - return ret; - } - - public resolveListTokens(xs: string[]): any { - // Must be a singleton list token, because concatenation is not allowed. - if (xs.length !== 1) { - throw new Error(`Cannot add elements to list token, got: ${xs}`); - } - - const str = this.createListTokenString(xs[0]); - const fragments = str.split(this.lookupToken.bind(this)); - if (fragments.length !== 1) { - throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); - } - return fragments.values()[0]; - } - - /** - * Find a Token by key - */ - public lookupToken(key: string): Token { - if (!(key in this.tokenMap)) { - throw new Error(`Unrecognized token key: ${key}`); - } - - return this.tokenMap[key]; - } - - private register(token: Token, representationHint?: string): string { - const counter = Object.keys(this.tokenMap).length; - const representation = representationHint || `TOKEN`; - - 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; - } -} - -const BEGIN_STRING_TOKEN_MARKER = '${Token['; -const BEGIN_LIST_TOKEN_MARKER = '#{Token['; -const END_TOKEN_MARKER = ']}'; -const VALID_KEY_CHARS = 'a-zA-Z0-9:._-'; - -/** - * Interface that Token joiners implement - */ -export interface ITokenJoiner { - /** - * The name of the joiner. - * - * Must be unique per joiner: this value will be used to assert that there - * is exactly only type of joiner in a join operation. - */ - id: string; - - /** - * Return the language intrinsic that will combine the strings in the given engine - */ - join(fragments: any[]): any; -} - -/** - * A string with markers in it that can be resolved to external values - */ -class TokenString { - private pattern: string; - - constructor( - private readonly str: string, - private readonly beginMarker: string, - private readonly idPattern: string, - private readonly endMarker: string) { - this.pattern = `${regexQuote(this.beginMarker)}(${this.idPattern})${regexQuote(this.endMarker)}`; - } - - /** - * Split string on markers, substituting markers with Tokens - */ - public split(lookup: (id: string) => Token): TokenizedStringFragments { - const re = new RegExp(this.pattern, 'g'); - const ret = new TokenizedStringFragments(); - - let rest = 0; - let m = re.exec(this.str); - while (m) { - if (m.index > rest) { - ret.addLiteral(this.str.substring(rest, m.index)); - } - - ret.addUnresolved(lookup(m[1])); - - rest = re.lastIndex; - m = re.exec(this.str); - } - - if (rest < this.str.length) { - ret.addLiteral(this.str.substring(rest)); - } - - return ret; - } - - /** - * Indicates if this string includes tokens. - */ - public test(): boolean { - const re = new RegExp(this.pattern, 'g'); - return re.test(this.str); - } -} - -/** - * Result of the split of a string with Tokens - * - * Either a literal part of the string, or an unresolved Token. - */ -type LiteralFragment = { type: 'literal'; lit: any; }; -type UnresolvedFragment = { type: 'unresolved'; token: any; }; -type Fragment = LiteralFragment | UnresolvedFragment; - -/** - * Fragments of a string with markers - */ -class TokenizedStringFragments { - private readonly fragments = new Array(); - - public get length() { - return this.fragments.length; - } - - public values(): any[] { - return this.fragments.map(f => f.type === 'unresolved' ? resolve(f.token) : f.lit); - } - - public addLiteral(lit: any) { - this.fragments.push({ type: 'literal', lit }); - } - - public addUnresolved(token: Token) { - this.fragments.push({ type: 'unresolved', token }); - } - - public mapUnresolved(fn: (t: any) => any): TokenizedStringFragments { - const ret = new TokenizedStringFragments(); - - for (const f of this.fragments) { - switch (f.type) { - case 'literal': - ret.addLiteral(f.lit); - break; - case 'unresolved': - const mappedToken = fn(f.token); - - if (unresolved(mappedToken)) { - ret.addUnresolved(mappedToken); - } else { - ret.addLiteral(mappedToken); - } - break; - } - } - - return ret; - } - - /** - * Combine the resolved string fragments using the Tokens to join. - * - * Resolves the result. - */ - public join(concat: ConcatFunc): any { - if (this.fragments.length === 0) { return concat(undefined, undefined); } - - const values = this.fragments.map(fragmentValue); - - while (values.length > 1) { - const prefix = values.splice(0, 2); - values.splice(0, 0, concat(prefix[0], prefix[1])); - } - - return values[0]; - } -} - -/** - * Resolve the value from a single fragment - */ -function fragmentValue(fragment: Fragment): any { - return fragment.type === 'literal' ? fragment.lit : fragment.token; -} - -/** - * Quote a string for use in a regex - */ -function regexQuote(s: string) { - return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); -} - -/** - * Global options for resolve() - * - * Because there are many independent calls to resolve(), some losing context, - * we cannot simply pass through options at each individual call. Instead, - * we configure global context at the stack synthesis level. - */ -class ResolveConfiguration { - private readonly options = new Array(); - - public push(options: ResolveOptions): IOptionsContext { - this.options.push(options); - - return { - pop: () => { - if (this.options.length === 0 || this.options[this.options.length - 1] !== options) { - throw new Error('ResolveConfiguration push/pop mismatch'); - } - this.options.pop(); - } - }; - } - - public get recurse(): ResolveFunc | undefined { - for (let i = this.options.length - 1; i >= 0; i--) { - if (this.options[i].recurse) { - return this.options[i].recurse; - } - } - return undefined; - } -} - -interface IOptionsContext { - pop(): void; -} - -interface ResolveOptions { - /** - * What function to use for recursing into deeper resolutions - */ - recurse?: ResolveFunc; -} - -/** - * Function used to resolve Tokens - */ -export type ResolveFunc = (obj: any) => any; - -/** - * Function used to concatenate symbols in the target document language - */ -export type ConcatFunc = (left: any | undefined, right: any | undefined) => any; - -const glob = global as any; - -/** - * Singleton instance of the token string map - */ -const TOKEN_MAP: TokenMap = glob.__cdkTokenMap = glob.__cdkTokenMap || new TokenMap(); - -/** - * Singleton instance of resolver options - */ -export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); - -/** - * Determine whether an object is a Construct - * - * Not in 'construct.ts' because that would lead to a dependency cycle via 'uniqueid.ts', - * and this is a best-effort protection against a common programming mistake anyway. - */ -function isConstruct(x: any): boolean { - return x._children !== undefined && x._metadata !== undefined; -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts similarity index 61% rename from packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts rename to packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts index 1dd6097fa2063..2f0230a66460f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts @@ -1,5 +1,5 @@ -import { Construct } from "../core/construct"; -import { Token } from "../core/tokens"; +import { Construct } from "../construct"; +import { Token } from "./token"; /** * A Token that represents a CloudFormation reference to another resource @@ -39,6 +39,27 @@ export class CfnReference extends Token { } } +/** + * Produce a CloudFormation expression to concat two arbitrary expressions when resolving + */ +export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { + if (left === undefined && right === undefined) { return ''; } + + const parts = new Array(); + if (left !== undefined) { parts.push(left); } + if (right !== undefined) { parts.push(right); } + + // Some case analysis to produce minimal expressions + if (parts.length === 1) { return parts[0]; } + if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { + return parts[0] + parts[1]; + } + + // Otherwise return a Join intrinsic (already in the target document language to avoid taking + // circular dependencies on FnJoin & friends) + return { 'Fn::Join': ['', parts] }; +} + /** * Return whether the given value represents a CloudFormation intrinsic */ @@ -51,4 +72,4 @@ export function isIntrinsic(x: any) { return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); } -import { Stack } from "./stack"; \ No newline at end of file +import { Stack } from "../../cloudformation/stack"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts new file mode 100644 index 0000000000000..1169c4ae26a28 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts @@ -0,0 +1,287 @@ +import { cloudFormationConcat } from "./cfn-tokens"; +import { resolve } from "./resolve"; +import { ResolveContext, Token, unresolved } from "./token"; + +// Encoding Tokens into native types; should not be exported + +/** + * Central place where we keep a mapping from Tokens to their String representation + * + * The string representation is used to embed token into strings, + * and stored to be able to + * + * All instances of TokenStringMap share the same storage, so that this process + * works even when different copies of the library are loaded. + */ +export class TokenMap { + private readonly tokenMap: {[key: string]: Token} = {}; + + /** + * Generate a unique string for this Token, returning a key + * + * Every call for the same Token will produce a new unique string, no + * attempt is made to deduplicate. Token objects should cache the + * value themselves, if required. + * + * The token can choose (part of) its own representation string with a + * hint. This may be used to produce aesthetically pleasing and + * recognizable token representations for humans. + */ + public registerString(token: Token, representationHint?: string): string { + const key = this.register(token, representationHint); + return `${BEGIN_STRING_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`; + } + + /** + * Generate a unique string for this Token, returning a key + */ + public registerList(token: Token, representationHint?: string): string[] { + const key = this.register(token, representationHint); + return [`${BEGIN_LIST_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`]; + } + + /** + * Returns a `TokenString` for this string. + */ + public createStringTokenString(s: string) { + return new TokenString(s, BEGIN_STRING_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); + } + + /** + * Returns a `TokenString` for this string. + */ + public createListTokenString(s: string) { + return new TokenString(s, BEGIN_LIST_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); + } + + /** + * Replace any Token markers in this string with their resolved values + */ + public resolveStringTokens(s: string, context: ResolveContext): any { + const str = this.createStringTokenString(s); + const fragments = str.split(this.lookupToken.bind(this)); + const ret = fragments.mapUnresolved(x => resolve(x, context)).join(cloudFormationConcat); + if (unresolved(ret)) { + return resolve(ret, context); + } + return ret; + } + + public resolveListTokens(xs: string[], context: ResolveContext): any { + // Must be a singleton list token, because concatenation is not allowed. + if (xs.length !== 1) { + throw new Error(`Cannot add elements to list token, got: ${xs}`); + } + + const str = this.createListTokenString(xs[0]); + const fragments = str.split(this.lookupToken.bind(this)); + if (fragments.length !== 1) { + throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); + } + return fragments.mapUnresolved(x => resolve(x, context)).join(cloudFormationConcat).values()[0]; + } + + /** + * Find a Token by key + */ + public lookupToken(key: string): Token { + if (!(key in this.tokenMap)) { + throw new Error(`Unrecognized token key: ${key}`); + } + + return this.tokenMap[key]; + } + + private register(token: Token, representationHint?: string): string { + const counter = Object.keys(this.tokenMap).length; + const representation = representationHint || `TOKEN`; + + 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; + } +} + +const BEGIN_STRING_TOKEN_MARKER = '${Token['; +const BEGIN_LIST_TOKEN_MARKER = '#{Token['; +const END_TOKEN_MARKER = ']}'; +const VALID_KEY_CHARS = 'a-zA-Z0-9:._-'; + +/** + * Interface that Token joiners implement + */ +export interface ITokenJoiner { + /** + * The name of the joiner. + * + * Must be unique per joiner: this value will be used to assert that there + * is exactly only type of joiner in a join operation. + */ + id: string; + + /** + * Return the language intrinsic that will combine the strings in the given engine + */ + join(fragments: any[]): any; +} + +/** + * A string with markers in it that can be resolved to external values + */ +class TokenString { + private pattern: string; + + constructor( + private readonly str: string, + private readonly beginMarker: string, + private readonly idPattern: string, + private readonly endMarker: string) { + this.pattern = `${regexQuote(this.beginMarker)}(${this.idPattern})${regexQuote(this.endMarker)}`; + } + + /** + * Split string on markers, substituting markers with Tokens + */ + public split(lookup: (id: string) => Token): TokenizedStringFragments { + const re = new RegExp(this.pattern, 'g'); + const ret = new TokenizedStringFragments(); + + let rest = 0; + let m = re.exec(this.str); + while (m) { + if (m.index > rest) { + ret.addLiteral(this.str.substring(rest, m.index)); + } + + ret.addUnresolved(lookup(m[1])); + + rest = re.lastIndex; + m = re.exec(this.str); + } + + if (rest < this.str.length) { + ret.addLiteral(this.str.substring(rest)); + } + + return ret; + } + + /** + * Indicates if this string includes tokens. + */ + public test(): boolean { + const re = new RegExp(this.pattern, 'g'); + return re.test(this.str); + } +} + +/** + * Result of the split of a string with Tokens + * + * Either a literal part of the string, or an unresolved Token. + */ +type LiteralFragment = { type: 'literal'; lit: any; }; +type UnresolvedFragment = { type: 'unresolved'; token: any; }; +type Fragment = LiteralFragment | UnresolvedFragment; + +/** + * Fragments of a string with markers + */ +class TokenizedStringFragments { + private readonly fragments = new Array(); + + public get length() { + return this.fragments.length; + } + + public values(): any[] { + return this.fragments.map(f => f.type === 'unresolved' ? f.token : f.lit); + } + + public addLiteral(lit: any) { + this.fragments.push({ type: 'literal', lit }); + } + + public addUnresolved(token: Token) { + this.fragments.push({ type: 'unresolved', token }); + } + + public mapUnresolved(fn: (t: any) => any): TokenizedStringFragments { + const ret = new TokenizedStringFragments(); + + for (const f of this.fragments) { + switch (f.type) { + case 'literal': + ret.addLiteral(f.lit); + break; + case 'unresolved': + const mappedToken = fn(f.token); + + if (unresolved(mappedToken)) { + ret.addUnresolved(mappedToken); + } else { + ret.addLiteral(mappedToken); + } + break; + } + } + + return ret; + } + + /** + * Combine the resolved string fragments using the Tokens to join. + * + * Resolves the result. + */ + public join(concat: ConcatFunc): any { + if (this.fragments.length === 0) { return concat(undefined, undefined); } + + const values = this.fragments.map(fragmentValue); + + while (values.length > 1) { + const prefix = values.splice(0, 2); + values.splice(0, 0, concat(prefix[0], prefix[1])); + } + + return values[0]; + } +} + +/** + * Resolve the value from a single fragment + */ +function fragmentValue(fragment: Fragment): any { + return fragment.type === 'literal' ? fragment.lit : fragment.token; +} + +/** + * Quote a string for use in a regex + */ +function regexQuote(s: string) { + return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); +} + +/** + * Function used to concatenate symbols in the target document language + */ +export type ConcatFunc = (left: any | undefined, right: any | undefined) => any; + +const glob = global as any; + +/** + * Singleton instance of the token string map + */ +export const TOKEN_MAP: TokenMap = glob.__cdkTokenMap = glob.__cdkTokenMap || new TokenMap(); + +export function isListToken(x: any) { + return typeof(x) === 'string' && TOKEN_MAP.createListTokenString(x).test(); +} + +export function containsListToken(xs: any[]) { + return xs.some(isListToken); +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/index.ts b/packages/@aws-cdk/cdk/lib/core/tokens/index.ts new file mode 100644 index 0000000000000..04e896145743c --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/index.ts @@ -0,0 +1,3 @@ +// This exports the modules that should be publicly available (not all of them) + +export * from './token'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts new file mode 100644 index 0000000000000..f4e316f3aae50 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts @@ -0,0 +1,55 @@ +/** + * Global options for resolve() + * + * Because there are many independent calls to resolve(), some losing context, + * we cannot simply pass through options at each individual call. Instead, + * we configure global context at the stack synthesis level. + */ +export class ResolveConfiguration { + private readonly options = new Array(); + + public push(options: ResolveOptions): IOptionsContext { + this.options.push(options); + + return { + pop: () => { + if (this.options.length === 0 || this.options[this.options.length - 1] !== options) { + throw new Error('ResolveConfiguration push/pop mismatch'); + } + this.options.pop(); + } + }; + } + + public get recurse(): ResolveFunc | undefined { + for (let i = this.options.length - 1; i >= 0; i--) { + if (this.options[i].recurse) { + return this.options[i].recurse; + } + } + return undefined; + } +} + +interface IOptionsContext { + pop(): void; +} + +interface ResolveOptions { + /** + * What function to use for recursing into deeper resolutions + */ + recurse?: ResolveFunc; +} + +const glob = global as any; + +/** + * Singleton instance of resolver options + */ +export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); + +/** + * Function used to resolve Tokens + */ +export type ResolveFunc = (obj: any, prefix?: string[]) => any; diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts new file mode 100644 index 0000000000000..723d0af045e47 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts @@ -0,0 +1,124 @@ +import { containsListToken, TOKEN_MAP } from "./encoding"; +import { RESOLVE_METHOD, ResolveContext, unresolved } from "./token"; + +// This file should not be exported to consumers, resolving should happen through Construct.resolve() + +/** + * Resolves an object by evaluating all tokens and removing any undefined or empty objects or arrays. + * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. + * + * @param obj The object to resolve. + * @param prefix Prefix key path components for diagnostics. + */ +export function resolve(obj: any, context: ResolveContext): any { + const pathName = '/' + context.prefix.join('/'); + + // protect against cyclic references by limiting depth. + if (context.prefix.length > 200) { + throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); + } + + // + // undefined + // + + if (typeof(obj) === 'undefined') { + return undefined; + } + + // + // null + // + + if (obj === null) { + return null; + } + + // + // functions - not supported (only tokens are supported) + // + + if (typeof(obj) === 'function') { + throw new Error(`Trying to resolve a non-data object. Only token are supported for lazy evaluation. Path: ${pathName}. Object: ${obj}`); + } + + // + // string - potentially replace all stringified Tokens + // + if (typeof(obj) === 'string') { + return TOKEN_MAP.resolveStringTokens(obj, context); + } + + // + // primitives - as-is + // + + if (typeof(obj) !== 'object' || obj instanceof Date) { + return obj; + } + + // + // arrays - resolve all values, remove undefined and remove empty arrays + // + + if (Array.isArray(obj)) { + if (containsListToken(obj)) { + return TOKEN_MAP.resolveListTokens(obj, context); + } + + const arr = obj + .map((x, i) => resolve(x, { ...context, prefix: context.prefix.concat(i.toString()) })) + .filter(x => typeof(x) !== 'undefined'); + + return arr; + } + + // + // tokens - invoke 'resolve' and continue to resolve recursively + // + + if (unresolved(obj)) { + const value = obj[RESOLVE_METHOD](); + return resolve(value, context); + } + + // + // objects - deep-resolve all values + // + + // Must not be a Construct at this point, otherwise you probably made a typo + // mistake somewhere and resolve will get into an infinite loop recursing into + // child.parent <---> parent.children + if (isConstruct(obj)) { + throw new Error('Trying to resolve() a Construct at ' + pathName); + } + + const result: any = { }; + for (const key of Object.keys(obj)) { + const resolvedKey = resolve(key, context); + if (typeof(resolvedKey) !== 'string') { + throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); + } + + const value = resolve(obj[key], {...context, prefix: context.prefix.concat(key) }); + + // skip undefined + if (typeof(value) === 'undefined') { + continue; + } + + result[resolvedKey] = value; + } + + return result; +} + +/** + * Determine whether an object is a Construct + * + * Not in 'construct.ts' because that would lead to a dependency cycle via 'uniqueid.ts', + * and this is a best-effort protection against a common programming mistake anyway. + */ +function isConstruct(x: any): boolean { + return x._children !== undefined && x._metadata !== undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts new file mode 100644 index 0000000000000..76d618872a0d8 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts @@ -0,0 +1,141 @@ +import { IConstruct } from "../construct"; +import { isListToken, TOKEN_MAP } from "./encoding"; + +/** + * If objects has a function property by this name, they will be considered tokens, and this + * function will be called to resolve the value for this object. + */ +export const RESOLVE_METHOD = 'resolve'; + +/** + * Represents a special or lazily-evaluated value. + * + * Can be used to delay evaluation of a certain value in case, for example, + * that it requires some context or late-bound data. Can also be used to + * mark values that need special processing at document rendering time. + * + * Tokens can be embedded into strings while retaining their original + * semantics. + */ +export class Token { + private tokenStringification?: string; + private tokenListification?: string[]; + + /** + * Creates a token that resolves to `value`. + * + * If value is a function, the function is evaluated upon resolution and + * the value it returns will be used as the token's value. + * + * displayName is used to represent the Token when it's embedded into a string; it + * will look something like this: + * + * "embedded in a larger string is ${Token[DISPLAY_NAME.123]}" + * + * This value is used as a hint to humans what the meaning of the Token is, + * and does not have any effect on the evaluation. + * + * Must contain only alphanumeric and simple separator characters (_.:-). + * + * @param valueOrFunction What this token will evaluate to, literal or function. + * @param displayName A human-readable display hint for this Token + */ + constructor(private readonly valueOrFunction?: any, private readonly displayName?: string) { + } + + /** + * @returns The resolved value for this token. + */ + public resolve(_context: ResolveContext): any { + let value = this.valueOrFunction; + if (typeof(value) === 'function') { + value = value(); + } + + return value; + } + + /** + * Return a reversible string representation of this token + * + * If the Token is initialized with a literal, the stringified value of the + * literal is returned. Otherwise, a special quoted string representation + * of the Token is returned that can be embedded into other strings. + * + * Strings with quoted Tokens in them can be restored back into + * complex values with the Tokens restored by calling `resolve()` + * on the string. + */ + public toString(): string { + const valueType = typeof this.valueOrFunction; + // Optimization: if we can immediately resolve this, don't bother + // registering a Token. + if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { + return this.valueOrFunction.toString(); + } + + if (this.tokenStringification === undefined) { + this.tokenStringification = TOKEN_MAP.registerString(this, this.displayName); + } + return this.tokenStringification; + } + + /** + * Turn this Token into JSON + * + * This gets called by JSON.stringify(). We want to prohibit this, because + * it's not possible to do this properly, so we just throw an error here. + */ + public toJSON(): any { + // tslint:disable-next-line:max-line-length + throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use a document-specific stringification method instead.'); + } + + /** + * Return a string list representation of this token + * + * Call this if the Token intrinsically evaluates to a list of strings. + * If so, you can represent the Token in a similar way in the type + * system. + * + * Note that even though the Token is represented as a list of strings, you + * still cannot do any operations on it such as concatenation, indexing, + * or taking its length. The only useful operations you can do to these lists + * is constructing a `FnJoin` or a `FnSelect` on it. + */ + public toList(): string[] { + const valueType = typeof this.valueOrFunction; + if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { + throw new Error('Got a literal Token value; cannot be encoded as a list.'); + } + + if (this.tokenListification === undefined) { + this.tokenListification = TOKEN_MAP.registerList(this, this.displayName); + } + return this.tokenListification; + } +} + +/** + * Current resolution context for tokens + */ +export interface ResolveContext { + construct: IConstruct; + prefix: string[]; +} + +/** + * Returns true if obj is a token (i.e. has the resolve() method or is a string + * that includes token markers), or it's a listifictaion of a Token string. + * + * @param obj The object to test. + */ +export function unresolved(obj: any): boolean { + if (typeof(obj) === 'string') { + return TOKEN_MAP.createStringTokenString(obj).test(); + } else if (Array.isArray(obj) && obj.length === 1) { + return isListToken(obj[0]); + } else { + return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/util.ts b/packages/@aws-cdk/cdk/lib/core/util.ts index d1be7700e7c26..e083ddc5fc271 100644 --- a/packages/@aws-cdk/cdk/lib/core/util.ts +++ b/packages/@aws-cdk/cdk/lib/core/util.ts @@ -1,18 +1,18 @@ -import { resolve } from './tokens'; +import { IConstruct } from "./construct"; /** * Given an object, converts all keys to PascalCase given they are currently in camel case. * @param obj The object. */ -export function capitalizePropertyNames(obj: any): any { - obj = resolve(obj); +export function capitalizePropertyNames(construct: IConstruct, obj: any): any { + obj = construct.node.resolve(obj); if (typeof(obj) !== 'object') { return obj; } if (Array.isArray(obj)) { - return obj.map(x => capitalizePropertyNames(x)); + return obj.map(x => capitalizePropertyNames(construct, x)); } const newObj: any = { }; @@ -21,7 +21,7 @@ export function capitalizePropertyNames(obj: any): any { const first = key.charAt(0).toUpperCase(); const newKey = first + key.slice(1); - newObj[newKey] = capitalizePropertyNames(value); + newObj[newKey] = capitalizePropertyNames(construct, value); } return newObj; @@ -30,8 +30,8 @@ export function capitalizePropertyNames(obj: any): any { /** * Turns empty arrays/objects to undefined (after evaluating tokens). */ -export function ignoreEmpty(o: any): any { - o = resolve(o); // first resolve tokens, in case they evaluate to 'undefined'. +export function ignoreEmpty(construct: IConstruct, o: any): any { + o = construct.node.resolve(o); // first resolve tokens, in case they evaluate to 'undefined'. // undefined/null if (o == null) { diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 8f02e9ab8a201..6f4b43675677d 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -3,7 +3,6 @@ export * from './core/tokens'; export * from './core/tag-manager'; export * from './cloudformation/cloudformation-json'; -export * from './cloudformation/tokens'; export * from './cloudformation/condition'; export * from './cloudformation/fn'; export * from './cloudformation/include'; diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index 1d5f50ba6f66b..2bacde3bc7dbf 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { ArnComponents, ArnUtils, Aws, resolve, Stack, Token } from '../../lib'; +import { ArnComponents, ArnUtils, Aws, Stack, Token } from '../../lib'; export = { 'create from components with defaults'(test: Test) { @@ -12,12 +12,14 @@ export = { const pseudo = new Aws(stack); - test.deepEqual(resolve(arn), - resolve(`arn:${pseudo.partition}:sqs:${pseudo.region}:${pseudo.accountId}:myqueuename`)); + test.deepEqual(stack.node.resolve(arn), + stack.node.resolve(`arn:${pseudo.partition}:sqs:${pseudo.region}:${pseudo.accountId}:myqueuename`)); test.done(); }, 'create from components with specific values for the various components'(test: Test) { + const stack = new Stack(); + const arn = ArnUtils.fromComponents({ service: 'dynamodb', resource: 'table', @@ -27,12 +29,14 @@ export = { resourceName: 'mytable/stream/label' }); - test.deepEqual(resolve(arn), + test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:dynamodb:us-east-1:123456789012:table/mytable/stream/label'); test.done(); }, 'allow empty string in components'(test: Test) { + const stack = new Stack(); + const arn = ArnUtils.fromComponents({ service: 's3', resource: 'my-bucket', @@ -41,7 +45,7 @@ export = { partition: 'aws-cn', }); - test.deepEqual(resolve(arn), + test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:s3:::my-bucket'); test.done(); @@ -59,8 +63,8 @@ export = { const pseudo = new Aws(stack); - test.deepEqual(resolve(arn), - resolve(`arn:${pseudo.partition}:codedeploy:${pseudo.region}:${pseudo.accountId}:application:WordPress_App`)); + test.deepEqual(stack.node.resolve(arn), + stack.node.resolve(`arn:${pseudo.partition}:codedeploy:${pseudo.region}:${pseudo.accountId}:application:WordPress_App`)); test.done(); }, @@ -145,30 +149,32 @@ export = { }, 'a Token with : separator'(test: Test) { + const stack = new Stack(); const theToken = { Ref: 'SomeParameter' }; const parsed = ArnUtils.parseToken(new Token(() => theToken).toString(), ':'); - test.deepEqual(resolve(parsed.partition), { 'Fn::Select': [ 1, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.service), { 'Fn::Select': [ 2, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.region), { 'Fn::Select': [ 3, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.account), { 'Fn::Select': [ 4, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.resource), { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.resourceName), { 'Fn::Select': [ 6, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.partition), { 'Fn::Select': [ 1, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.service), { 'Fn::Select': [ 2, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.region), { 'Fn::Select': [ 3, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.account), { 'Fn::Select': [ 4, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resource), { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resourceName), { 'Fn::Select': [ 6, { 'Fn::Split': [ ':', theToken ]} ]}); test.equal(parsed.sep, ':'); test.done(); }, 'a Token with / separator'(test: Test) { + const stack = new Stack(); const theToken = { Ref: 'SomeParameter' }; const parsed = ArnUtils.parseToken(new Token(() => theToken).toString()); test.equal(parsed.sep, '/'); // tslint:disable-next-line:max-line-length - test.deepEqual(resolve(parsed.resource), { 'Fn::Select': [ 0, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resource), { 'Fn::Select': [ 0, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); // tslint:disable-next-line:max-line-length - test.deepEqual(resolve(parsed.resourceName), { 'Fn::Select': [ 1, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resourceName), { 'Fn::Select': [ 1, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); test.done(); } diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts index 8366e7c4084d5..440e782af6ac6 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationJSON, Fn, resolve, Token } from '../../lib'; +import { CloudFormationJSON, Fn, Stack, Token } from '../../lib'; import { evaluateCFN } from './evaluate-cfn'; export = { @@ -16,12 +16,14 @@ export = { }, 'string tokens can be JSONified and JSONification can be reversed'(test: Test) { + const stack = new Stack(); + for (const token of tokensThatResolveTo('woof woof')) { // GIVEN const fido = { name: 'Fido', speaks: token }; // WHEN - const resolved = resolve(CloudFormationJSON.stringify(fido)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"woof woof"}'); @@ -31,12 +33,14 @@ export = { }, 'string tokens can be embedded while being JSONified'(test: Test) { + const stack = new Stack(); + for (const token of tokensThatResolveTo('woof woof')) { // GIVEN const fido = { name: 'Fido', speaks: `deep ${token}` }; // WHEN - const resolved = resolve(CloudFormationJSON.stringify(fido)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"deep woof woof"}'); @@ -47,25 +51,27 @@ export = { 'integer Tokens behave correctly in stringification and JSONification'(test: Test) { // GIVEN + const stack = new Stack(); const num = new Token(() => 1); const embedded = `the number is ${num}`; // WHEN - test.equal(evaluateCFN(resolve(embedded)), "the number is 1"); - test.equal(evaluateCFN(resolve(CloudFormationJSON.stringify({ embedded }))), "{\"embedded\":\"the number is 1\"}"); - test.equal(evaluateCFN(resolve(CloudFormationJSON.stringify({ num }))), "{\"num\":1}"); + test.equal(evaluateCFN(stack.node.resolve(embedded)), "the number is 1"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ embedded }, stack))), "{\"embedded\":\"the number is 1\"}"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ num }, stack))), "{\"num\":1}"); test.done(); }, 'tokens in strings survive additional TokenJSON.stringification()'(test: Test) { // GIVEN + const stack = new Stack(); for (const token of tokensThatResolveTo('pong!')) { // WHEN - const stringified = CloudFormationJSON.stringify(`ping? ${token}`); + const stringified = CloudFormationJSON.stringify(`ping? ${token}`, stack); // THEN - test.equal(evaluateCFN(resolve(stringified)), '"ping? pong!"'); + test.equal(evaluateCFN(stack.node.resolve(stringified)), '"ping? pong!"'); } test.done(); @@ -73,10 +79,11 @@ export = { 'intrinsic Tokens embed correctly in JSONification'(test: Test) { // GIVEN + const stack = new Stack(); const bucketName = new Token({ Ref: 'MyBucket' }); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ theBucket: bucketName })); + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: bucketName }, stack)); // THEN const context = {MyBucket: 'TheName'}; @@ -86,14 +93,15 @@ export = { }, 'embedded string literals in intrinsics are escaped when calling TokenJSON.stringify()'(test: Test) { - // WHEN + // GIVEN + const stack = new Stack(); const token = Fn.join('', [ 'Hello', 'This\nIs', 'Very "cool"' ]); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ literal: 'I can also "contain" quotes', token - })); + }, stack)); // THEN const expected = '{"literal":"I can also \\"contain\\" quotes","token":"HelloThis\\nIsVery \\"cool\\""}'; @@ -104,11 +112,12 @@ export = { 'Tokens in Tokens are handled correctly'(test: Test) { // GIVEN + const stack = new Stack(); const bucketName = new Token({ Ref: 'MyBucket' }); const combinedName = Fn.join('', [ 'The bucket name is ', bucketName.toString() ]); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ theBucket: combinedName })); + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: combinedName }, stack)); // THEN const context = {MyBucket: 'TheName'}; @@ -119,12 +128,13 @@ export = { 'Doubly nested strings evaluate correctly in JSON context'(test: Test) { // WHEN + const stack = new Stack(); const fidoSays = new Token(() => 'woof'); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - })); + }, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: woof"}'); @@ -133,13 +143,14 @@ export = { }, 'Doubly nested intrinsics evaluate correctly in JSON context'(test: Test) { - // WHEN + // GIVEN + const stack = new Stack(); const fidoSays = new Token(() => ({ Ref: 'Something' })); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - })); + }, stack)); // THEN const context = {Something: 'woof woof'}; @@ -149,13 +160,14 @@ export = { }, 'Quoted strings in embedded JSON context are escaped'(test: Test) { - // WHEN + // GIVEN + const stack = new Stack(); const fidoSays = new Token(() => '"woof"'); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - })); + }, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: \\"woof\\""}'); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts index 4dc2f2767f40d..37b3751b3c167 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { DynamicReference, DynamicReferenceService, resolve, Stack } from '../../lib'; +import { DynamicReference, DynamicReferenceService, Stack } from '../../lib'; export = { 'can create dynamic references with service and key with colons'(test: Test) { @@ -13,7 +13,7 @@ export = { }); // THEN - test.equal(resolve(ref.value), '{{resolve:ssm:a:b:c}}'); + test.equal(stack.node.resolve(ref.value), '{{resolve:ssm:a:b:c}}'); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts index a4dd6a3976f5f..ac4d94a22e7eb 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts @@ -1,8 +1,7 @@ import fc = require('fast-check'); import _ = require('lodash'); import nodeunit = require('nodeunit'); -import { Fn } from '../../lib/cloudformation/fn'; -import { resolve, Token } from '../../lib/core/tokens'; +import { Fn, Stack, Token } from '../../lib'; function asyncTest(cb: (test: nodeunit.Test) => Promise): (test: nodeunit.Test) => void { return async (test: nodeunit.Test) => { @@ -31,35 +30,39 @@ export = nodeunit.testCase({ test.done(); }, 'resolves to the value if only one value is joined': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), anyValue, - (delimiter, value) => _.isEqual(resolve(Fn.join(delimiter, [value])), value) + (delimiter, value) => _.isEqual(stack.node.resolve(Fn.join(delimiter, [value])), value) ), { verbose: true } ); }), 'pre-concatenates string literals': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.array(nonEmptyString, 1, 15), - (delimiter, values) => resolve(Fn.join(delimiter, values)) === values.join(delimiter) + (delimiter, values) => stack.node.resolve(Fn.join(delimiter, values)) === values.join(delimiter) ), { verbose: true } ); }), 'pre-concatenates around tokens': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.array(nonEmptyString, 1, 3), tokenish, fc.array(nonEmptyString, 1, 3), (delimiter, prefix, obj, suffix) => - _.isEqual(resolve(Fn.join(delimiter, [...prefix, stringToken(obj), ...suffix])), + _.isEqual(stack.node.resolve(Fn.join(delimiter, [...prefix, stringToken(obj), ...suffix])), { 'Fn::Join': [delimiter, [prefix.join(delimiter), obj, suffix.join(delimiter)]] }) ), { verbose: true, seed: 1539874645005, path: "0:0:0:0:0:0:0:0:0" } ); }), 'flattens joins nested under joins with same delimiter': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.array(anyValue), @@ -67,13 +70,14 @@ export = nodeunit.testCase({ fc.array(anyValue), (delimiter, prefix, nested, suffix) => // Gonna test - _.isEqual(resolve(Fn.join(delimiter, [...prefix, Fn.join(delimiter, nested), ...suffix])), - resolve(Fn.join(delimiter, [...prefix, ...nested, ...suffix]))) + _.isEqual(stack.node.resolve(Fn.join(delimiter, [...prefix, Fn.join(delimiter, nested), ...suffix])), + stack.node.resolve(Fn.join(delimiter, [...prefix, ...nested, ...suffix]))) ), { verbose: true } ); }), 'does not flatten joins nested under joins with different delimiter': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.string(), @@ -83,7 +87,7 @@ export = nodeunit.testCase({ (delimiter1, delimiter2, prefix, nested, suffix) => { fc.pre(delimiter1 !== delimiter2); const join = Fn.join(delimiter1, [...prefix, Fn.join(delimiter2, stringListToken(nested)), ...suffix]); - const resolved = resolve(join); + const resolved = stack.node.resolve(join); return resolved['Fn::Join'][1].find((e: any) => typeof e === 'object' && ('Fn::Join' in e) && e['Fn::Join'][0] === delimiter2) != null; diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts index 9121b85287169..b6ee26217d2d7 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { Construct, Output, Ref, resolve, Resource, Stack } from '../../lib'; +import { Construct, Output, Ref, Resource, Stack } from '../../lib'; export = { 'outputs can be added to the stack'(test: Test) { @@ -63,7 +63,7 @@ export = { 'makeImportValue can be used to create an Fn::ImportValue from an output'(test: Test) { const stack = new Stack(undefined, 'MyStack'); const output = new Output(stack, 'MyOutput'); - test.deepEqual(resolve(output.makeImportValue()), { 'Fn::ImportValue': 'MyStack:MyOutput' }); + test.deepEqual(stack.node.resolve(output.makeImportValue()), { 'Fn::ImportValue': 'MyStack:MyOutput' }); test.done(); } }; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts index 5bccaec77161a..2a5526c4255f2 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { Construct, Parameter, resolve, Resource, Stack } from '../../lib'; +import { Construct, Parameter, Resource, Stack } from '../../lib'; export = { 'parameters can be used and referenced using param.ref'(test: Test) { @@ -32,7 +32,7 @@ export = { const stack = new Stack(); const param = new Parameter(stack, 'MyParam', { type: 'String' }); - test.deepEqual(resolve(param), { Ref: 'MyParam' }); + test.deepEqual(stack.node.resolve(param), { Ref: 'MyParam' }); test.done(); } }; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 9debe742f70c5..3b0ee595aa35a 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -2,7 +2,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { applyRemovalPolicy, Condition, Construct, DeletionPolicy, Fn, HashedAddressingScheme, IDependable, - RemovalPolicy, resolve, Resource, Root, Stack } from '../../lib'; + RemovalPolicy, Resource, Root, Stack } from '../../lib'; export = { 'all resources derive from Resource, which derives from Entity'(test: Test) { @@ -359,7 +359,7 @@ export = { const stack = new Stack(); const r = new Resource(stack, 'MyResource', { type: 'R' }); - test.deepEqual(resolve(r.ref), { Ref: 'MyResource' }); + test.deepEqual(stack.node.resolve(r.ref), { Ref: 'MyResource' }); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts index 33422251adaa9..37b4dbd2bc805 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts @@ -1,13 +1,14 @@ import { Test } from 'nodeunit'; -import { resolve, Secret, SecretParameter, Stack } from '../../lib'; +import { Secret, SecretParameter, Stack } from '../../lib'; export = { 'Secret is merely a token'(test: Test) { + const stack = new Stack(); const foo = new Secret('Foo'); const bar = new Secret(() => 'Bar'); - test.deepEqual(resolve(foo), 'Foo'); - test.deepEqual(resolve(bar), 'Bar'); + test.deepEqual(stack.node.resolve(foo), 'Foo'); + test.deepEqual(stack.node.resolve(bar), 'Bar'); test.done(); }, @@ -43,7 +44,7 @@ export = { NoEcho: true } } }); // value resolves to a "Ref" - test.deepEqual(resolve(mySecret.value), { Ref: 'MySecretParameterBB81DE58' }); + test.deepEqual(stack.node.resolve(mySecret.value), { Ref: 'MySecretParameterBB81DE58' }); test.done(); } diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts index 781f5c9534753..71af1326f6b7b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -1,7 +1,5 @@ import { Test } from 'nodeunit'; -import { Construct, Root } from '../../lib/core/construct'; -import { ITaggable, TagManager } from '../../lib/core/tag-manager'; -import { resolve } from '../../lib/core/tokens'; +import { Construct, ITaggable, Root, TagManager } from '../../lib'; class ChildTagger extends Construct implements ITaggable { public readonly tags: TagManager; @@ -33,10 +31,10 @@ export = { const tagArray = [tag]; for (const construct of [ctagger, ctagger1]) { - test.deepEqual(resolve(construct.tags), tagArray); + test.deepEqual(root.node.resolve(construct.tags), tagArray); } - test.deepEqual(resolve(ctagger2.tags), undefined); + test.deepEqual(root.node.resolve(ctagger2.tags), undefined); test.done(); }, 'setTag with propagate false tags do not propagate'(test: Test) { @@ -52,10 +50,10 @@ export = { ctagger.tags.setTag(tag.key, tag.value, {propagate: false}); for (const construct of [ctagger1, ctagger2]) { - test.deepEqual(resolve(construct.tags), undefined); + test.deepEqual(root.node.resolve(construct.tags), undefined); } - test.deepEqual(resolve(ctagger.tags)[0].key, 'Name'); - test.deepEqual(resolve(ctagger.tags)[0].value, 'TheCakeIsALie'); + test.deepEqual(root.node.resolve(ctagger.tags)[0].key, 'Name'); + test.deepEqual(root.node.resolve(ctagger.tags)[0].value, 'TheCakeIsALie'); test.done(); }, 'setTag with overwrite false does not overwrite a tag'(test: Test) { @@ -63,7 +61,7 @@ export = { const ctagger = new ChildTagger(root, 'one'); ctagger.tags.setTag('Env', 'Dev'); ctagger.tags.setTag('Env', 'Prod', {overwrite: false}); - const result = resolve(ctagger.tags); + const result = root.node.resolve(ctagger.tags); test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); test.done(); }, @@ -73,8 +71,8 @@ export = { const ctagger1 = new ChildTagger(ctagger, 'two'); ctagger.tags.setTag('Parent', 'Is always right'); ctagger1.tags.setTag('Parent', 'Is wrong', {sticky: false}); - const parent = resolve(ctagger.tags); - const child = resolve(ctagger1.tags); + const parent = root.node.resolve(ctagger.tags); + const child = root.node.resolve(ctagger1.tags); test.deepEqual(parent, child); test.done(); @@ -87,7 +85,7 @@ export = { const ctagger2 = new ChildTagger(cNoTag, 'four'); const tag = {key: 'Name', value: 'TheCakeIsALie'}; ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(resolve(ctagger2.tags), [tag]); + test.deepEqual(root.node.resolve(ctagger2.tags), [tag]); test.done(); }, 'a tag can be removed and added back'(test: Test) { @@ -95,11 +93,11 @@ export = { const ctagger = new ChildTagger(root, 'one'); const tag = {key: 'Name', value: 'TheCakeIsALie'}; ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(resolve(ctagger.tags), [tag]); + test.deepEqual(root.node.resolve(ctagger.tags), [tag]); ctagger.tags.removeTag(tag.key); - test.deepEqual(resolve(ctagger.tags), undefined); + test.deepEqual(root.node.resolve(ctagger.tags), undefined); ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(resolve(ctagger.tags), [tag]); + test.deepEqual(root.node.resolve(ctagger.tags), [tag]); test.done(); }, 'removeTag removes a tag by key'(test: Test) { @@ -116,7 +114,7 @@ export = { ctagger.tags.removeTag('Name'); for (const construct of [ctagger, ctagger1, ctagger2]) { - test.deepEqual(resolve(construct.tags), undefined); + test.deepEqual(root.node.resolve(construct.tags), undefined); } test.done(); }, @@ -126,9 +124,9 @@ export = { const ctagger1 = new ChildTagger(ctagger, 'two'); ctagger.tags.setTag('Env', 'Dev'); ctagger1.tags.removeTag('Env', {blockPropagate: true}); - const result = resolve(ctagger.tags); + const result = root.node.resolve(ctagger.tags); test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); - test.deepEqual(resolve(ctagger1.tags), undefined); + test.deepEqual(root.node.resolve(ctagger1.tags), undefined); test.done(); }, 'children can override parent propagated tags'(test: Test) { @@ -140,8 +138,8 @@ export = { ctagger.tags.setTag(tag2.key, tag2.value); ctagger.tags.setTag(tag.key, tag.value); ctagChild.tags.setTag(tag2.key, tag2.value); - const parentTags = resolve(ctagger.tags); - const childTags = resolve(ctagChild.tags); + const parentTags = root.node.resolve(ctagger.tags); + const childTags = root.node.resolve(ctagChild.tags); test.deepEqual(parentTags, [tag]); test.deepEqual(childTags, [tag2]); test.done(); @@ -168,11 +166,11 @@ export = { const cAll = ctagger.tags; const cProp = ctagChild.tags; - for (const tag of resolve(cAll)) { + for (const tag of root.node.resolve(cAll)) { const expectedTag = allTags.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } - for (const tag of resolve(cProp)) { + for (const tag of root.node.resolve(cProp)) { const expectedTag = tagsProp.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index 50f57e12cf405..2fa0ed4e4f08d 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { Fn, resolve, Token, unresolved } from '../../lib'; +import { Fn, Root, Token, unresolved } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; export = { @@ -412,3 +412,12 @@ function cloudFormationTokensThatResolveTo(value: any): Token[] { function tokensThatResolveTo(value: string): Token[] { return literalTokensThatResolveTo(value).concat(cloudFormationTokensThatResolveTo(value)); } + +/** + * Wrapper for resolve that creates an throwaway Construct to call it on + * + * So I don't have to change all call sites in this file. + */ +function resolve(x: any) { + return new Root().node.resolve(x); +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/core/test.util.ts b/packages/@aws-cdk/cdk/test/core/test.util.ts index 2a7cb94ec87c5..694e3ae86f456 100644 --- a/packages/@aws-cdk/cdk/test/core/test.util.ts +++ b/packages/@aws-cdk/cdk/test/core/test.util.ts @@ -1,25 +1,27 @@ import { Test } from 'nodeunit'; +import { Root } from '../../lib'; import { capitalizePropertyNames, ignoreEmpty } from '../../lib/core/util'; export = { 'capitalizeResourceProperties capitalizes all keys of an object (recursively) from camelCase to PascalCase'(test: Test) { + const c = new Root(); - test.equal(capitalizePropertyNames(undefined), undefined); - test.equal(capitalizePropertyNames(12), 12); - test.equal(capitalizePropertyNames('hello'), 'hello'); - test.deepEqual(capitalizePropertyNames([ 'hello', 88 ]), [ 'hello', 88 ]); - test.deepEqual(capitalizePropertyNames( + test.equal(capitalizePropertyNames(c, undefined), undefined); + test.equal(capitalizePropertyNames(c, 12), 12); + test.equal(capitalizePropertyNames(c, 'hello'), 'hello'); + test.deepEqual(capitalizePropertyNames(c, [ 'hello', 88 ]), [ 'hello', 88 ]); + test.deepEqual(capitalizePropertyNames(c, { Hello: 'world', hey: 'dude' }), { Hello: 'world', Hey: 'dude' }); - test.deepEqual(capitalizePropertyNames( + test.deepEqual(capitalizePropertyNames(c, [ 1, 2, { three: 3 }]), [ 1, 2, { Three: 3 }]); - test.deepEqual(capitalizePropertyNames( + test.deepEqual(capitalizePropertyNames(c, { Hello: 'world', recursive: { foo: 123, there: { another: [ 'hello', { world: 123 } ]} } }), { Hello: 'world', Recursive: { Foo: 123, There: { Another: [ 'hello', { World: 123 } ]} } }); // make sure tokens are resolved and result is also capitalized - test.deepEqual(capitalizePropertyNames( + test.deepEqual(capitalizePropertyNames(c, { hello: { resolve: () => ({ foo: 'bar' }) }, world: new SomeToken() }), { Hello: { Foo: 'bar' }, World: 100 }); @@ -29,38 +31,44 @@ export = { 'ignoreEmpty': { '[]'(test: Test) { - test.strictEqual(ignoreEmpty([]), undefined); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, []), undefined); test.done(); }, '{}'(test: Test) { - test.strictEqual(ignoreEmpty({}), undefined); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, {}), undefined); test.done(); }, 'undefined/null'(test: Test) { - test.strictEqual(ignoreEmpty(undefined), undefined); - test.strictEqual(ignoreEmpty(null), null); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, undefined), undefined); + test.strictEqual(ignoreEmpty(c, null), null); test.done(); }, 'primitives'(test: Test) { - test.strictEqual(ignoreEmpty(12), 12); - test.strictEqual(ignoreEmpty("12"), "12"); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, 12), 12); + test.strictEqual(ignoreEmpty(c, "12"), "12"); test.done(); }, 'non-empty arrays/objects'(test: Test) { - test.deepEqual(ignoreEmpty([ 1, 2, 3, undefined ]), [ 1, 2, 3 ]); // undefined array values is cleaned up by "resolve" - test.deepEqual(ignoreEmpty({ o: 1, b: 2, j: 3 }), { o: 1, b: 2, j: 3 }); + const c = new Root(); + test.deepEqual(ignoreEmpty(c, [ 1, 2, 3, undefined ]), [ 1, 2, 3 ]); // undefined array values is cleaned up by "resolve" + test.deepEqual(ignoreEmpty(c, { o: 1, b: 2, j: 3 }), { o: 1, b: 2, j: 3 }); test.done(); }, 'resolve first'(test: Test) { - test.deepEqual(ignoreEmpty({ xoo: { resolve: () => 123 }}), { xoo: 123 }); - test.strictEqual(ignoreEmpty({ xoo: { resolve: () => undefined }}), undefined); - test.deepEqual(ignoreEmpty({ xoo: { resolve: () => [ ] }}), { xoo: [] }); - test.deepEqual(ignoreEmpty({ xoo: { resolve: () => [ undefined, undefined ] }}), { xoo: [] }); + const c = new Root(); + test.deepEqual(ignoreEmpty(c, { xoo: { resolve: () => 123 }}), { xoo: 123 }); + test.strictEqual(ignoreEmpty(c, { xoo: { resolve: () => undefined }}), undefined); + test.deepEqual(ignoreEmpty(c, { xoo: { resolve: () => [ ] }}), { xoo: [] }); + test.deepEqual(ignoreEmpty(c, { xoo: { resolve: () => [ undefined, undefined ] }}), { xoo: [] }); test.done(); } } diff --git a/packages/@aws-cdk/cdk/test/test.context.ts b/packages/@aws-cdk/cdk/test/test.context.ts index 373c5e0dfcea4..0752b7fa1c2f1 100644 --- a/packages/@aws-cdk/cdk/test/test.context.ts +++ b/packages/@aws-cdk/cdk/test/test.context.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { App, AvailabilityZoneProvider, Construct, ContextProvider, - MetadataEntry, resolve, SSMParameterProvider, Stack } from '../lib'; + MetadataEntry, SSMParameterProvider, Stack } from '../lib'; export = { 'AvailabilityZoneProvider returns a list with dummy values if the context is not available'(test: Test) { @@ -86,7 +86,7 @@ export = { stack.node.setContext(key, 'abc'); const ssmp = new SSMParameterProvider(stack, {parameterName: 'test'}); - const azs = resolve(ssmp.parameterValue()); + const azs = stack.node.resolve(ssmp.parameterValue()); test.deepEqual(azs, 'abc'); test.done(); diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 63a65507d666c..db6027dc69023 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -330,7 +330,7 @@ export default class CodeGenerator { this.code.closeBlock(); this.code.openBlock('protected renderProperties(properties: any): { [key: string]: any } '); - this.code.line(`return ${genspec.cfnMapperName(propsType).fqn}(${CORE}.resolve(properties));`); + this.code.line(`return ${genspec.cfnMapperName(propsType).fqn}(this.node.resolve(properties));`); this.code.closeBlock(); } From 6330ab50c74b9f8a01734664b97563da5d1aaa6f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 4 Jan 2019 15:59:13 +0100 Subject: [PATCH 19/39] Introduce concept of references and use it to implement cross-stack refs --- packages/@aws-cdk/cdk/lib/app.ts | 10 +- .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 3 +- .../cdk/lib/cloudformation/condition.ts | 6 +- .../cdk/lib/cloudformation/include.ts | 4 - .../cdk/lib/cloudformation/mapping.ts | 6 +- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 4 - .../cdk/lib/cloudformation/parameter.ts | 6 +- .../cdk/lib/cloudformation/resource.ts | 6 +- .../@aws-cdk/cdk/lib/cloudformation/rule.ts | 4 - .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 42 ++++---- packages/@aws-cdk/cdk/lib/core/construct.ts | 95 ++++++++++++++++++- .../cdk/lib/core/tokens/cfn-concat.ts | 20 ++++ .../cdk/lib/core/tokens/cfn-tokens.ts | 76 +++++++-------- .../@aws-cdk/cdk/lib/core/tokens/encoding.ts | 2 +- .../@aws-cdk/cdk/lib/core/tokens/options.ts | 28 +++--- .../@aws-cdk/cdk/lib/core/tokens/resolve.ts | 6 +- .../@aws-cdk/cdk/lib/core/tokens/token.ts | 5 + .../cdk/test/cloudformation/test.stack.ts | 12 +-- 18 files changed, 212 insertions(+), 123 deletions(-) create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index 0038e709e3551..1cb6355635936 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -41,8 +41,6 @@ export class App extends Root { return; } - this.applyCrossEnvironmentReferences(); - const result: cxapi.SynthesizeResponse = { version: cxapi.PROTO_RESPONSE_VERSION, stacks: this.synthesizeStacks(Object.keys(this.stacks)), @@ -60,6 +58,8 @@ export class App extends Root { public synthesizeStack(stackName: string): cxapi.SynthesizedStack { const stack = this.getStack(stackName); + this.prepareConstructTree(); + // first, validate this stack and stop if there are errors. const errors = stack.node.validateTree(); if (errors.length > 0) { @@ -125,12 +125,6 @@ export class App extends Root { } } - public applyCrossEnvironmentReferences() { - for (const stack of Object.values(this.stacks)) { - stack.applyCrossEnvironmentReferences(); - } - } - private collectRuntimeInformation(): cxapi.AppRuntime { const libraries: { [name: string]: string } = {}; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 7e6593485c3db..0d369ba06ce6c 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,6 +1,7 @@ -import { Construct, Stack } from '..'; import { Fn } from '../cloudformation/fn'; +import { Construct } from '../core/construct'; import { unresolved } from '../core/tokens'; +import { Stack } from './stack'; /** * An Amazon Resource Name (ARN). diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index eb5e4cd8985da..b61db4a745bde 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { Referenceable, Stack } from './stack'; +import { Referenceable } from './stack'; export interface ConditionProps { expression?: FnCondition; @@ -32,10 +32,6 @@ export class Condition extends Referenceable { } }; } - - public substituteCrossStackReferences(): void { - this.expression = this.deepSubCrossStackReferences(Stack.find(this), this.expression); - } } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index 477f6ac875090..ba307cd58b475 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -34,8 +34,4 @@ export class Include extends StackElement { public toCloudFormation() { return this.template; } - - public substituteCrossStackReferences(): void { - // Left empty on purpose - } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index 4de2aaaeeb8a0..65f4025bdd2a3 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Fn } from './fn'; -import { Referenceable, Stack } from './stack'; +import { Referenceable } from './stack'; export interface MappingProps { mapping?: { [k1: string]: { [k2: string]: any } }; @@ -50,8 +50,4 @@ export class Mapping extends Referenceable { } }; } - - public substituteCrossStackReferences(): void { - this.mapping = this.deepSubCrossStackReferences(Stack.find(this), this.mapping); - } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index ed3cf3418d4a6..3a2d4d2d0396d 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -123,10 +123,6 @@ export class Output extends StackElement { }; } - public substituteCrossStackReferences(): void { - this._value = this.deepSubCrossStackReferences(Stack.find(this), this._value); - } - public get ref(): string { throw new Error('Outputs cannot be referenced'); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index 5a842e95ed72b..0a22b6779cf74 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { Ref, Referenceable, Stack } from './stack'; +import { Ref, Referenceable } from './stack'; export interface ParameterProps { /** @@ -124,10 +124,6 @@ export class Parameter extends Referenceable { }; } - public substituteCrossStackReferences(): void { - this.properties = this.deepSubCrossStackReferences(Stack.find(this), this.properties); - } - /** * Allows using parameters as tokens without the need to dereference them. * This implicitly implements Token, until we make it an interface. diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 98e490325004f..3dcf258fee044 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -4,7 +4,7 @@ import { CfnReference } from '../core/tokens/cfn-tokens'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; -import { IDependable, Referenceable, Stack, StackElement } from './stack'; +import { IDependable, Referenceable, StackElement } from './stack'; export interface ResourceProps { /** @@ -209,10 +209,6 @@ export class Resource extends Referenceable { } } - public substituteCrossStackReferences(): void { - this.deepSubCrossStackReferences(Stack.find(this), this.properties); - } - protected renderProperties(properties: any): { [key: string]: any } { return properties; } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index 0ba9d751ead31..cca72b5744cfa 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -102,10 +102,6 @@ export class Rule extends Referenceable { } }; } - - public substituteCrossStackReferences(): void { - // Empty on purpose - } } /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 632696f910023..8024794b5f5a1 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -3,6 +3,7 @@ import { App } from '../app'; import { Construct, IConstruct, PATH_SEP } from '../core/construct'; import { Token } from '../core/tokens'; import { CfnReference } from '../core/tokens/cfn-tokens'; +import { RESOLVE_OPTIONS } from '../core/tokens/options'; import { Environment } from '../environment'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -33,7 +34,7 @@ export class Stack extends Construct { * @param node A construct in the tree * @returns The Stack object (throws if the node is not part of a Stack-rooted tree) */ - public static find(node: Construct): Stack { + public static find(node: IConstruct): Stack { let curr: IConstruct | undefined = node; while (curr != null && !Stack.isStack(curr)) { curr = curr.node.scope; @@ -354,16 +355,6 @@ export class Stack extends Construct { return new Aws(this).notificationArns; } - /** - * Find cross-stack references embedded in the stack's content and replace them - * - * Do not call this as an app author; this is automatically called as part of synthesis. - */ - public applyCrossEnvironmentReferences() { - const elements = stackElements(this); - elements.forEach(e => e.substituteCrossStackReferences()); - } - /** * Validate stack name * @@ -376,6 +367,17 @@ export class Stack extends Construct { } } + /** + * Prepare stack + * + * Find all CloudFormation references and tell them we're consuming them. + */ + protected prepare() { + for (const cfnRef of this.node.findReferences(CfnReference.ReferenceType)) { + (cfnRef as CfnReference).consumeFromStack(this); + } + } + /** * Applied defaults to environment attributes. */ @@ -542,11 +544,17 @@ export abstract class StackElement extends Construct implements IDependable { */ public abstract toCloudFormation(): object; - public abstract substituteCrossStackReferences(): void; - - protected deepSubCrossStackReferences(sourceStack: Stack, x: any): any { - Array.isArray(sourceStack); - return x; + /** + * Automatically detect references in this StackElement + */ + protected prepare() { + const options = RESOLVE_OPTIONS.push({ preProcess: (token, _) => { this.node.recordReference(token); return token; } }); + try { + // Execute for side effect of calling 'preProcess' + this.toCloudFormation(); + } finally { + options.pop(); + } } } @@ -626,4 +634,4 @@ export class Ref extends CfnReference { // These imports have to be at the end to prevent circular imports import { Output } from './output'; -import { Aws } from './pseudo'; +import { Aws } from './pseudo'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index b4bf3b3762dee..10baeba6ea190 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { makeUniqueId } from '../util/uniqueid'; import { resolve } from './tokens/resolve'; -import { unresolved } from './tokens/token'; +import { Token, unresolved } from './tokens/token'; export const PATH_SEP = '/'; /** @@ -36,6 +36,7 @@ export class ConstructNode { private readonly _children: { [name: string]: IConstruct } = { }; private readonly context: { [key: string]: any } = { }; private readonly _metadata = new Array(); + private readonly references = new Set(); /** * If this is set to 'true'. addChild() calls for this construct and any child @@ -377,6 +378,38 @@ export class ConstructNode { }); } + /** + * Record a reference originating from this construct node + */ + public recordReference(ref: Token) { + if (ref.referenceType !== undefined && ref.referenceType !== '') { + this.references.add(ref); + } + } + + /** + * Return all references of the given type originating from this node or any of its children + */ + public findReferences(type: string): Token[] { + const ret = new Set(); + + function recurse(node: ConstructNode) { + for (const ref of node.references) { + if (ref.referenceType === type) { + ret.add(ref); + } + } + + for (const child of node.children) { + recurse(child.node); + } + } + + recurse(this); + + return Array.from(ret); + } + /** * Return the path of components up to but excluding the root */ @@ -402,6 +435,18 @@ export class ConstructNode { * another construct. */ export class Construct implements IConstruct { + /** + * Run the prepare phase on the given construct + */ + public static doPrepare(construct: IConstruct) { + // Static method to make it possible to run 'prepare' from outside the + // object while not polluting the IDE autocomplete of instances with the + // presence of this method. + if (isConstruct(construct)) { + construct.prepare(); + } + } + /** * Construct node. */ @@ -428,6 +473,8 @@ export class Construct implements IConstruct { } /** + * Validate the current construct. + * * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * @@ -436,6 +483,17 @@ export class Construct implements IConstruct { public validate(): string[] { return []; } + + /** + * Perform final modifications before synthesis + * + * This method can be implemented by derived constructs in order to perform + * final changes before synthesis. Prepare() will be called on a construct's + * children first. + */ + protected prepare(): void { + // Empty on purpose + } } /** @@ -447,6 +505,16 @@ export class Root extends Construct { // Bypass type checks super(undefined as any, ''); } + + /** + * Run 'prepare()' on all constructs in the tree + */ + public prepareConstructTree() { + // Use .reverse() to achieve post-order traversal + for (const construct of allConstructs(this, CrawlStyle.BreadthFirst).reverse()) { + Construct.doPrepare(construct); + } + } } /** @@ -490,3 +558,28 @@ function createStackTrace(below: Function): string[] { } return object.stack.split('\n').slice(1).map(s => s.replace(/^\s*at\s+/, '')); } + +/** + * Return all constructs from the given root + */ +export function allConstructs(root: IConstruct, style: CrawlStyle): IConstruct[] { + const ret = new Array(); + const queue = [root]; + + while (queue.length > 0) { + const next = style === CrawlStyle.BreadthFirst ? queue.splice(0, 1)[0] : queue.pop()!; + ret.push(next); + queue.push(...next.node.children); + } + + return ret; +} + +export enum CrawlStyle { + BreadthFirst, + DepthFirst +} + +export function isConstruct(x: IConstruct): x is Construct { + return (x as any).prepare !== undefined && (x as any).validate !== undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts new file mode 100644 index 0000000000000..f41da52d25d97 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts @@ -0,0 +1,20 @@ +/** + * Produce a CloudFormation expression to concat two arbitrary expressions when resolving + */ +export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { + if (left === undefined && right === undefined) { return ''; } + + const parts = new Array(); + if (left !== undefined) { parts.push(left); } + if (right !== undefined) { parts.push(right); } + + // Some case analysis to produce minimal expressions + if (parts.length === 1) { return parts[0]; } + if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { + return parts[0] + parts[1]; + } + + // Otherwise return a Join intrinsic (already in the target document language to avoid taking + // circular dependencies on FnJoin & friends) + return { 'Fn::Join': ['', parts] }; +} diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts index 2f0230a66460f..1bf7b26993bbd 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts @@ -1,65 +1,53 @@ import { Construct } from "../construct"; -import { Token } from "./token"; +import { ResolveContext, Token } from "./token"; /** * A Token that represents a CloudFormation reference to another resource */ export class CfnReference extends Token { - public static isInstance(x: any): x is CfnReference { - return x && x._isCfnReference; - } - - protected readonly _isCfnReference: boolean; + /** + * The reference type for instances of this class + */ + public static ReferenceType = 'cfn-reference'; + public readonly referenceType?: string; private readonly tokenStack?: Stack; + private readonly replacementTokens: Map; constructor(value: any, displayName?: string, anchor?: Construct) { - if (typeof(value) === 'function') { - throw new Error('CfnReference can only contain eager values'); - } - super(value, displayName); - this._isCfnReference = true; + if (typeof(value) === 'function') { + throw new Error('CfnReference can only contain eager values'); + } + super(value, displayName); + this.referenceType = CfnReference.ReferenceType; + this.replacementTokens = new Map(); + + if (anchor !== undefined) { + this.tokenStack = Stack.find(anchor); + } + } - if (anchor !== undefined) { - this.tokenStack = Stack.find(anchor); - } + public resolve(context: ResolveContext): any { + // If we have a special token for this stack, resolve that instead, otherwise resolve the original + const token = this.replacementTokens.get(Stack.find(context.construct)); + if (token) { + return token.resolve(context); + } else { + return super.resolve(context); + } } /** * In a consuming context, potentially substitute this Token with a different one */ - public substituteToken(consumingStack: Stack): Token { - if (this.tokenStack && this.tokenStack !== consumingStack) { - // We're trying to resolve a cross-stack reference - consumingStack.addDependency(this.tokenStack); - return this.tokenStack.exportValue(this, consumingStack); - } - // In case of doubt, return same Token - return this; - } -} - -/** - * Produce a CloudFormation expression to concat two arbitrary expressions when resolving - */ -export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { - if (left === undefined && right === undefined) { return ''; } - - const parts = new Array(); - if (left !== undefined) { parts.push(left); } - if (right !== undefined) { parts.push(right); } - - // Some case analysis to produce minimal expressions - if (parts.length === 1) { return parts[0]; } - if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { - return parts[0] + parts[1]; + public consumeFromStack(consumingStack: Stack) { + if (this.tokenStack && this.tokenStack !== consumingStack && !this.replacementTokens.has(consumingStack)) { + // We're trying to resolve a cross-stack reference + consumingStack.addDependency(this.tokenStack); + this.replacementTokens.set(consumingStack, this.tokenStack.exportValue(this, consumingStack)); + } } - - // Otherwise return a Join intrinsic (already in the target document language to avoid taking - // circular dependencies on FnJoin & friends) - return { 'Fn::Join': ['', parts] }; } - /** * Return whether the given value represents a CloudFormation intrinsic */ diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts index 1169c4ae26a28..f98d746087e54 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts @@ -1,4 +1,4 @@ -import { cloudFormationConcat } from "./cfn-tokens"; +import { cloudFormationConcat } from "./cfn-concat"; import { resolve } from "./resolve"; import { ResolveContext, Token, unresolved } from "./token"; diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts index f4e316f3aae50..bf490916b5a0f 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts @@ -1,3 +1,10 @@ +import { ResolveContext, Token } from "./token"; + +/** + * Function used to preprocess Tokens before resolving + */ +export type PreProcessFunc = (token: Token, context: ResolveContext) => Token; + /** * Global options for resolve() * @@ -21,13 +28,12 @@ export class ResolveConfiguration { }; } - public get recurse(): ResolveFunc | undefined { + public get preProcess(): PreProcessFunc { for (let i = this.options.length - 1; i >= 0; i--) { - if (this.options[i].recurse) { - return this.options[i].recurse; - } + const ret = this.options[i].preProcess; + if (ret) { return ret; } } - return undefined; + return noPreprocessFunction; } } @@ -37,9 +43,9 @@ interface IOptionsContext { interface ResolveOptions { /** - * What function to use for recursing into deeper resolutions + * What function to use to preprocess Tokens before resolving them */ - recurse?: ResolveFunc; + preProcess?: PreProcessFunc; } const glob = global as any; @@ -49,7 +55,7 @@ const glob = global as any; */ export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); -/** - * Function used to resolve Tokens - */ -export type ResolveFunc = (obj: any, prefix?: string[]) => any; + +function noPreprocessFunction(x: Token, _: ResolveContext) { + return x; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts index 723d0af045e47..44c097db73fa0 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts @@ -1,5 +1,6 @@ import { containsListToken, TOKEN_MAP } from "./encoding"; -import { RESOLVE_METHOD, ResolveContext, unresolved } from "./token"; +import { RESOLVE_OPTIONS } from "./options"; +import { RESOLVE_METHOD, ResolveContext, Token, unresolved } from "./token"; // This file should not be exported to consumers, resolving should happen through Construct.resolve() @@ -78,7 +79,8 @@ export function resolve(obj: any, context: ResolveContext): any { // if (unresolved(obj)) { - const value = obj[RESOLVE_METHOD](); + const preProcess = RESOLVE_OPTIONS.preProcess; + const value = preProcess(obj as Token, context)[RESOLVE_METHOD](context); return resolve(value, context); } diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts index 76d618872a0d8..4be0b78567296 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts @@ -18,6 +18,11 @@ export const RESOLVE_METHOD = 'resolve'; * semantics. */ export class Token { + /** + * If this Token represents a reference, an identifier for the reference Type + */ + public readonly referenceType?: string; + private tokenStringification?: string; private tokenListification?: string[]; diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index f495ed1155057..aea10a9373ee4 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -185,7 +185,7 @@ export = { // THEN // Need to do this manually now, since we're in testing mode. In a normal CDK app, // this happens as part of app.run(). - app.applyCrossEnvironmentReferences(); + app.prepareConstructTree(); test.deepEqual(stack1.toCloudFormation(), { Outputs: { @@ -218,7 +218,7 @@ export = { // WHEN - used in another stack new Parameter(stack2, 'SomeParameter', { type: 'String', default: new Token(() => account1) }); - app.applyCrossEnvironmentReferences(); + app.prepareConstructTree(); // THEN test.deepEqual(stack1.toCloudFormation(), { @@ -252,7 +252,7 @@ export = { // WHEN - used in another stack new Parameter(stack2, 'SomeParameter', { type: 'String', default: `TheAccountIs${account1}` }); - app.applyCrossEnvironmentReferences(); + app.prepareConstructTree(); // THEN test.deepEqual(stack2.toCloudFormation(), { @@ -280,7 +280,7 @@ export = { new Parameter(stack1, 'SomeParameter', { type: 'String', default: account2 }); test.throws(() => { - app.applyCrossEnvironmentReferences(); + app.prepareConstructTree(); }, /Adding this dependency would create a cyclic reference/); test.done(); @@ -296,7 +296,7 @@ export = { // WHEN new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); - app.applyCrossEnvironmentReferences(); + app.prepareConstructTree(); // THEN test.deepEqual(stack2.dependencies().map(s => s.node.id), ['Stack1']); @@ -315,7 +315,7 @@ export = { new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); test.throws(() => { - app.applyCrossEnvironmentReferences(); + app.prepareConstructTree(); }, /Can only reference cross stacks in the same region and account/); test.done(); From 2c224c4c45f160a593e47d2b2a182ddbd37ef5f4 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 4 Jan 2019 16:27:05 +0100 Subject: [PATCH 20/39] Fix some bugs --- packages/@aws-cdk/cdk/lib/cloudformation/arn.ts | 6 +++--- packages/@aws-cdk/cdk/lib/cloudformation/stack.ts | 2 +- packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts | 4 ++-- packages/@aws-cdk/cdk/lib/core/tokens/options.ts | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 0d369ba06ce6c..2f4c1e01a8dd6 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -23,9 +23,9 @@ export class ArnUtils { * */ public static fromComponents(components: ArnComponents, anchor?: Construct): string { - const partition = components.partition || theStack('partition').partition; - const region = components.region || theStack('region').region; - const account = components.account || theStack('account').accountId; + const partition = components.partition !== undefined ? components.partition : theStack('partition').partition; + const region = components.region !== undefined ? components.region : theStack('region').region; + const account = components.account !== undefined ? components.account : theStack('account').accountId; const values = [ 'arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource ]; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 8024794b5f5a1..7cc049f96c2dd 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -551,7 +551,7 @@ export abstract class StackElement extends Construct implements IDependable { const options = RESOLVE_OPTIONS.push({ preProcess: (token, _) => { this.node.recordReference(token); return token; } }); try { // Execute for side effect of calling 'preProcess' - this.toCloudFormation(); + this.node.resolve(this.toCloudFormation()); } finally { options.pop(); } diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts index f98d746087e54..3d6f0d4d4fabc 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts @@ -78,7 +78,7 @@ export class TokenMap { if (fragments.length !== 1) { throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); } - return fragments.mapUnresolved(x => resolve(x, context)).join(cloudFormationConcat).values()[0]; + return fragments.mapUnresolved(x => resolve(x, context)).values[0]; } /** @@ -198,7 +198,7 @@ class TokenizedStringFragments { return this.fragments.length; } - public values(): any[] { + public get values(): any[] { return this.fragments.map(f => f.type === 'unresolved' ? f.token : f.lit); } diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts index bf490916b5a0f..529067f8f8bde 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts @@ -31,7 +31,7 @@ export class ResolveConfiguration { public get preProcess(): PreProcessFunc { for (let i = this.options.length - 1; i >= 0; i--) { const ret = this.options[i].preProcess; - if (ret) { return ret; } + if (ret !== undefined) { return ret; } } return noPreprocessFunction; } @@ -55,7 +55,6 @@ const glob = global as any; */ export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); - function noPreprocessFunction(x: Token, _: ResolveContext) { return x; } \ No newline at end of file From b17d811967b3765c46a0733bc5d1c00731e3aec5 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 4 Jan 2019 17:47:48 +0100 Subject: [PATCH 21/39] More context attachments trying to make the tests work Now stuck in dependency cycle hell --- .../assets-docker/lib/adopted-repository.ts | 4 +- .../aws-apigateway/lib/integrations/aws.ts | 11 +- .../@aws-cdk/aws-apigateway/lib/method.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 4 +- .../lib/pipeline-actions.ts | 12 +- .../test/test.pipeline-actions.ts | 14 +- .../@aws-cdk/aws-codebuild/lib/project.ts | 4 +- .../@aws-cdk/aws-codecommit/lib/repository.ts | 2 +- .../aws-codedeploy/lib/application.ts | 8 +- .../aws-codedeploy/lib/deployment-config.ts | 24 ++- .../aws-codedeploy/lib/deployment-group.ts | 12 +- .../aws-codedeploy/lib/pipeline-action.ts | 2 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 2 +- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 2 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 6 +- .../@aws-cdk/aws-ecr/test/test.repository.ts | 2 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../@aws-cdk/aws-iam/lib/managed-policy.ts | 4 +- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 4 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 2 +- .../notifications-resource-handler.ts | 2 +- packages/@aws-cdk/aws-s3/lib/util.ts | 4 +- packages/@aws-cdk/aws-s3/test/test.util.ts | 8 +- .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 7 +- .../lib/cloudformation/cloudformation-json.ts | 2 +- .../cdk/lib/cloudformation/condition.ts | 6 +- .../@aws-cdk/cdk/lib/cloudformation/fn.ts | 45 +---- .../cdk/lib/cloudformation/include.ts | 2 +- .../cdk/lib/cloudformation/instrinsics.ts | 43 +++++ .../cdk/lib/cloudformation/logical-id.ts | 2 +- .../cdk/lib/cloudformation/mapping.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 19 +- .../cdk/lib/cloudformation/parameter.ts | 2 +- .../cdk/lib/cloudformation/resource.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/rule.ts | 2 +- .../cdk/lib/cloudformation/stack-element.ts | 161 +++++++++++++++++ .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 162 +----------------- packages/@aws-cdk/cdk/lib/core/construct.ts | 2 +- .../cdk/lib/core/tokens/cfn-concat.ts | 4 +- .../cdk/lib/core/tokens/cfn-tokens.ts | 11 -- .../@aws-cdk/cdk/lib/core/tokens/encoding.ts | 3 +- .../@aws-cdk/cdk/lib/core/tokens/index.ts | 3 +- .../@aws-cdk/cdk/lib/core/tokens/resolve.ts | 5 +- .../@aws-cdk/cdk/lib/core/tokens/token.ts | 22 +-- .../cdk/lib/core/tokens/unresolved.ts | 18 ++ packages/@aws-cdk/cdk/lib/index.ts | 1 + .../cdk/test/cloudformation/test.arn.ts | 6 +- .../cdk/test/cloudformation/test.fn.ts | 19 ++ packages/@aws-cdk/runtime-values/lib/rtv.ts | 2 +- 49 files changed, 375 insertions(+), 315 deletions(-) create mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts create mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts diff --git a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts index e8948eb436c0a..c8f15a7fc8dbe 100644 --- a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts +++ b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts @@ -42,7 +42,7 @@ export class AdoptedRepository extends ecr.RepositoryBase { }); fn.addToRolePolicy(new iam.PolicyStatement() - .addResource(ecr.Repository.arnForLocalRepository(props.repositoryName)) + .addResource(ecr.Repository.arnForLocalRepository(props.repositoryName, this)) .addActions( 'ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy', @@ -67,7 +67,7 @@ export class AdoptedRepository extends ecr.RepositoryBase { // this this repository is "local" to the stack (in the same region/account) // we can render it's ARN from it's name. - this.repositoryArn = ecr.Repository.arnForLocalRepository(this.repositoryName); + this.repositoryArn = ecr.Repository.arnForLocalRepository(this.repositoryName, this); } /** diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index 70d13b641c2f0..375ba833ec33c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -1,5 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { Integration, IntegrationOptions, IntegrationType } from '../integration'; +import { Method } from '../method'; import { parseAwsApiCall } from '../util'; export interface AwsIntegrationProps { @@ -61,6 +62,8 @@ export interface AwsIntegrationProps { * technology. */ export class AwsIntegration extends Integration { + private _anchor?: cdk.IConstruct; + constructor(props: AwsIntegrationProps) { const backend = props.subdomain ? `${props.subdomain}.${props.service}` : props.service; const type = props.proxy ? IntegrationType.AwsProxy : IntegrationType.Aws; @@ -68,14 +71,18 @@ export class AwsIntegration extends Integration { super({ type, integrationHttpMethod: 'POST', - uri: cdk.ArnUtils.fromComponents({ + uri: new cdk.Token(() => cdk.ArnUtils.fromComponents({ service: 'apigateway', account: backend, resource: apiType, sep: '/', resourceName: apiValue, - }), + }, this._anchor)), options: props.options, }); } + + public bind(_method: Method) { + this._anchor = _method; + } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 08dc1841ee4ff..414a0bc279565 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -158,7 +158,7 @@ export class Method extends cdk.Construct { credentials = options.credentialsRole.roleArn; } else if (options.credentialsPassthrough) { // arn:aws:iam::*:user/* - credentials = cdk.ArnUtils.fromComponents({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }); + credentials = cdk.ArnUtils.fromComponents({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }, this); } return { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 3ce28aa6de68d..c86aa07e8439c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -306,7 +306,7 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi resource: this.restApiId, sep: '/', resourceName: `${stage}/${method}${path}` - }); + }, this); } /** @@ -365,7 +365,7 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi resource: 'policy', sep: '/', resourceName: 'service-role/AmazonAPIGatewayPushToCloudWatchLogs' - }) ] + }, this) ] }); const resource = new CfnAccount(this, 'Account', { diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index af813f657b5af..1f03509973f15 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -410,7 +410,7 @@ class SingletonPolicy extends cdk.Construct { this.statementFor({ actions: ['cloudformation:ExecuteChangeSet'], conditions: { StringEquals: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResource(stackArnFromProps(props)); + }).addResource(stackArnFromProps(props, this)); } public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void { @@ -422,7 +422,7 @@ class SingletonPolicy extends cdk.Construct { 'cloudformation:DescribeStacks', ], conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResource(stackArnFromProps(props)); + }).addResource(stackArnFromProps(props, this)); } public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean, region?: string }): void { @@ -438,7 +438,7 @@ class SingletonPolicy extends cdk.Construct { if (props.replaceOnFailure) { actions.push('cloudformation:DeleteStack'); } - this.statementFor({ actions }).addResource(stackArnFromProps(props)); + this.statementFor({ actions }).addResource(stackArnFromProps(props, this)); } public grantDeleteStack(props: { stackName: string, region?: string }): void { @@ -447,7 +447,7 @@ class SingletonPolicy extends cdk.Construct { 'cloudformation:DescribeStack*', 'cloudformation:DeleteStack', ] - }).addResource(stackArnFromProps(props)); + }).addResource(stackArnFromProps(props, this)); } public grantPassRole(role: iam.IRole): void { @@ -494,11 +494,11 @@ interface StatementTemplate { type StatementCondition = { [op: string]: { [attribute: string]: string } }; -function stackArnFromProps(props: { stackName: string, region?: string }): string { +function stackArnFromProps(props: { stackName: string, region?: string }, anchor: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ region: props.region, service: 'cloudformation', resource: 'stack', resourceName: `${props.stackName}/*` - }); + }, anchor); } diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 76536df8a26e8..a1f143c171af2 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -23,7 +23,7 @@ export = nodeunit.testCase({ _assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); const changeSetCondition = { StringEqualsIfExists: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }; _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn, changeSetCondition); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition); @@ -108,7 +108,7 @@ export = nodeunit.testCase({ stackName: 'MyStack', }); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn, { StringEquals: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }); @@ -168,7 +168,7 @@ export = nodeunit.testCase({ adminPermissions: false, replaceOnFailure: true, }); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:CreateStack', stackArn); @@ -188,7 +188,7 @@ export = nodeunit.testCase({ adminPermissions: false, stackName: 'MyStack', }); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); @@ -271,12 +271,12 @@ function _isOrContains(entity: string | string[], value: string): boolean { return false; } -function _stackArn(stackName: string): string { +function _stackArn(stackName: string, anchor: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'cloudformation', resource: 'stack', resourceName: `${stackName}/*`, - }); + }, anchor); } class PipelineDouble implements cpapi.IPipeline { @@ -290,7 +290,7 @@ class PipelineDouble implements cpapi.IPipeline { constructor({ pipelineName, role }: { pipelineName?: string, role: iam.Role }) { this.pipelineName = pipelineName || 'TestPipeline'; - this.pipelineArn = cdk.ArnUtils.fromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }); + this.pipelineArn = cdk.ArnUtils.fromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }, this); this.role = role; } diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 42defa04578b7..a10c717c8b9ab 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -440,7 +440,7 @@ class ImportedProject extends ProjectBase { service: 'codebuild', resource: 'project', resourceName: props.projectName, - }); + }, this); this.projectName = props.projectName; } @@ -776,7 +776,7 @@ export class Project extends ProjectBase { resource: 'log-group', sep: ':', resourceName: `/aws/codebuild/${this.projectName}`, - }); + }, this); const logGroupStarArn = `${logGroupArn}:*`; diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 357a275a266cc..b152c16e48c7f 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -247,7 +247,7 @@ class ImportedRepository extends RepositoryBase { this.repositoryArn = cdk.ArnUtils.fromComponents({ service: 'codecommit', resource: props.repositoryName, - }); + }, this); this.repositoryName = props.repositoryName; } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/application.ts index 3c8bfeee3e806..b0dad72e75056 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/application.ts @@ -41,7 +41,7 @@ class ImportedServerApplication extends cdk.Construct implements IServerApplicat super(scope, id); this.applicationName = props.applicationName; - this.applicationArn = applicationName2Arn(this.applicationName); + this.applicationArn = applicationNameToArn(this.applicationName, this); } public export() { @@ -90,7 +90,7 @@ export class ServerApplication extends cdk.Construct implements IServerApplicati }); this.applicationName = resource.ref; - this.applicationArn = applicationName2Arn(this.applicationName); + this.applicationArn = applicationNameToArn(this.applicationName, this); } public export(): ServerApplicationImportProps { @@ -100,11 +100,11 @@ export class ServerApplication extends cdk.Construct implements IServerApplicati } } -function applicationName2Arn(applicationName: string): string { +function applicationNameToArn(applicationName: string, anchor: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'codedeploy', resource: 'application', resourceName: applicationName, sep: ':', - }); + }, anchor); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts index 6783ecb4114f8..c10bacbaf5a46 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts @@ -10,7 +10,7 @@ import { CfnDeploymentConfig } from './codedeploy.generated'; */ export interface IServerDeploymentConfig { readonly deploymentConfigName: string; - readonly deploymentConfigArn: string; + deploymentConfigArn(anchor: cdk.IConstruct): string; export(): ServerDeploymentConfigImportProps; } @@ -30,13 +30,15 @@ export interface ServerDeploymentConfigImportProps { class ImportedServerDeploymentConfig extends cdk.Construct implements IServerDeploymentConfig { public readonly deploymentConfigName: string; - public readonly deploymentConfigArn: string; constructor(scope: cdk.Construct, id: string, private readonly props: ServerDeploymentConfigImportProps) { super(scope, id); this.deploymentConfigName = props.deploymentConfigName; - this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } + + public deploymentConfigArn(anchor: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, anchor); } public export() { @@ -46,11 +48,13 @@ class ImportedServerDeploymentConfig extends cdk.Construct implements IServerDep class DefaultServerDeploymentConfig implements IServerDeploymentConfig { public readonly deploymentConfigName: string; - public readonly deploymentConfigArn: string; constructor(deploymentConfigName: string) { this.deploymentConfigName = deploymentConfigName; - this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } + + public deploymentConfigArn(anchor: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, anchor); } public export(): ServerDeploymentConfigImportProps { @@ -110,7 +114,6 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl } public readonly deploymentConfigName: string; - public readonly deploymentConfigArn: string; constructor(scope: cdk.Construct, id: string, props: ServerDeploymentConfigProps) { super(scope, id); @@ -121,7 +124,10 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl }); this.deploymentConfigName = resource.ref.toString(); - this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } + + public deploymentConfigArn(anchor: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, anchor); } public export(): ServerDeploymentConfigImportProps { @@ -150,11 +156,11 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl } } -function arnForDeploymentConfigName(name: string): string { +function arnForDeploymentConfigName(name: string, anchor: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'codedeploy', resource: 'deploymentconfig', resourceName: name, sep: ':', - }); + }, anchor); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index 11273f7afb1de..8e963ae2423dd 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -104,8 +104,8 @@ class ImportedServerDeploymentGroup extends ServerDeploymentGroupBase { this.application = props.application; this.deploymentGroupName = props.deploymentGroupName; - this.deploymentGroupArn = deploymentGroupName2Arn(props.application.applicationName, - props.deploymentGroupName); + this.deploymentGroupArn = deploymentGroupNameToArn(props.application.applicationName, + props.deploymentGroupName, this); } public export() { @@ -343,8 +343,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { }); this.deploymentGroupName = resource.deploymentGroupName; - this.deploymentGroupArn = deploymentGroupName2Arn(this.application.applicationName, - this.deploymentGroupName); + this.deploymentGroupArn = deploymentGroupNameToArn(this.application.applicationName, + this.deploymentGroupName, this); } public export(): ServerDeploymentGroupImportProps { @@ -560,11 +560,11 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { } } -function deploymentGroupName2Arn(applicationName: string, deploymentGroupName: string): string { +function deploymentGroupNameToArn(applicationName: string, deploymentGroupName: string, anchor: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'codedeploy', resource: 'deploymentgroup', resourceName: `${applicationName}/${deploymentGroupName}`, sep: ':', - }); + }, anchor); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts b/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts index 8461eb85a6734..27bd3485d3ba0 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts @@ -60,7 +60,7 @@ export class PipelineDeployAction extends codepipeline.DeployAction { )); props.stage.pipeline.role.addToPolicy(new iam.PolicyStatement() - .addResource(props.deploymentGroup.deploymentConfig.deploymentConfigArn) + .addResource(props.deploymentGroup.deploymentConfig.deploymentConfigArn(this)) .addActions( 'codedeploy:GetDeploymentConfig', )); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 81437ec3eeb0a..bb42c26606c16 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -130,7 +130,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { this.pipelineArn = cdk.ArnUtils.fromComponents({ service: 'codepipeline', resource: this.pipelineName - }); + }, this); } /** diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 7f3d8e1685344..6d68a389ae4ee 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -619,7 +619,7 @@ export class Table extends Construct { service: 'iam', resource: 'role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com', resourceName: 'AWSServiceRoleForApplicationAutoScaling_DynamoDBTable' - }) + }, this) }); } } diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index f85be5d0db917..03b586b6cf55a 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -122,12 +122,12 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor * Returns an ECR ARN for a repository that resides in the same account/region * as the current stack. */ - public static arnForLocalRepository(repositoryName: string): string { + public static arnForLocalRepository(repositoryName: string, anchor: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'ecr', resource: 'repository', resourceName: repositoryName - }); + }, anchor); } /** @@ -265,7 +265,7 @@ class ImportedRepository extends RepositoryBase { 'which also implies that the repository resides in the same region/account as this stack'); } - this.repositoryArn = RepositoryBase.arnForLocalRepository(props.repositoryName); + this.repositoryArn = RepositoryBase.arnForLocalRepository(props.repositoryName, this); } if (props.repositoryName) { diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index d27f5724721a5..0869fbb59fe0e 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -249,7 +249,7 @@ export = { // WHEN const repo = ecr.Repository.import(stack, 'Repo', { - repositoryArn: ecr.Repository.arnForLocalRepository(repoName), + repositoryArn: ecr.Repository.arnForLocalRepository(repoName, stack), repositoryName: repoName }); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 9ccfe00335f60..d5b40fef614b1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -232,7 +232,7 @@ export abstract class BaseService extends cdk.Construct service: 'iam', resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', - }) + }, this) }); } } diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index 64d78f4c2fe2e..604d0156030e3 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -16,7 +16,7 @@ export class AwsManagedPolicy { /** * The Arn of this managed policy */ - public get policyArn(): string { + public policyArn(anchor: cdk.IConstruct): string { // the arn is in the form of - arn:aws:iam::aws:policy/ return cdk.ArnUtils.fromComponents({ service: "iam", @@ -24,6 +24,6 @@ export class AwsManagedPolicy { account: "aws", // the account for a managed policy is 'aws' resource: "policy", resourceName: this.managedPolicyName - }); + }, anchor); } } diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 22536616043b9..280b42102c47c 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -311,11 +311,11 @@ export class Function extends FunctionBase { const managedPolicyArns = new Array(); // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaBasicExecutionRole").policyArn); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaBasicExecutionRole").policyArn(this)); if (props.vpc) { // Policy that will have ENI creation permissions - managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn(this)); } this.role = props.role || new iam.Role(this, 'ServiceRole', { diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 5045c2c318e1d..f8f4f2cf1952d 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -969,7 +969,7 @@ class ImportedBucket extends BucketBase { throw new Error('Bucket name is required'); } - this.bucketArn = parseBucketArn(props); + this.bucketArn = parseBucketArn(this, props); this.bucketName = bucketName; this.domainName = props.bucketDomainName || this.generateDomainName(); this.autoCreatePolicy = false; diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index a3c04c426b26a..c19cf1d974ce8 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -56,7 +56,7 @@ export class NotificationsResourceHandler extends cdk.Construct { account: 'aws', // the account for a managed policy is 'aws' resource: 'policy', resourceName: 'service-role/AWSLambdaBasicExecutionRole', - }) + }, this) ] }); diff --git a/packages/@aws-cdk/aws-s3/lib/util.ts b/packages/@aws-cdk/aws-s3/lib/util.ts index 3f96219f0d70a..74b32411debf5 100644 --- a/packages/@aws-cdk/aws-s3/lib/util.ts +++ b/packages/@aws-cdk/aws-s3/lib/util.ts @@ -1,7 +1,7 @@ import cdk = require('@aws-cdk/cdk'); import { BucketImportProps } from './bucket'; -export function parseBucketArn(props: BucketImportProps): string { +export function parseBucketArn(construct: cdk.IConstruct, props: BucketImportProps): string { // if we have an explicit bucket ARN, use it. if (props.bucketArn) { @@ -16,7 +16,7 @@ export function parseBucketArn(props: BucketImportProps): string { account: '', service: 's3', resource: props.bucketName - }); + }, construct); } throw new Error('Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed'); diff --git a/packages/@aws-cdk/aws-s3/test/test.util.ts b/packages/@aws-cdk/aws-s3/test/test.util.ts index bde65d934eb8b..11b87854e3551 100644 --- a/packages/@aws-cdk/aws-s3/test/test.util.ts +++ b/packages/@aws-cdk/aws-s3/test/test.util.ts @@ -5,15 +5,16 @@ import { parseBucketArn, parseBucketName } from '../lib/util'; export = { parseBucketArn: { 'explicit arn'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'my:bucket:arn'; - test.deepEqual(parseBucketArn({ bucketArn }), bucketArn); + test.deepEqual(parseBucketArn(stack, { bucketArn }), bucketArn); test.done(); }, 'produce arn from bucket name'(test: Test) { const stack = new cdk.Stack(); const bucketName = 'hello'; - test.deepEqual(stack.node.resolve(parseBucketArn({ bucketName })), { 'Fn::Join': + test.deepEqual(stack.node.resolve(parseBucketArn(stack, { bucketName })), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -22,7 +23,8 @@ export = { }, 'fails if neither arn nor name are provided'(test: Test) { - test.throws(() => parseBucketArn({}), /Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); + const stack = new cdk.Stack(); + test.throws(() => parseBucketArn(stack, {}), /Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); test.done(); } }, diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 2f4c1e01a8dd6..fa1c8e3843e17 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,5 +1,5 @@ import { Fn } from '../cloudformation/fn'; -import { Construct } from '../core/construct'; +import { IConstruct } from '../core/construct'; import { unresolved } from '../core/tokens'; import { Stack } from './stack'; @@ -21,8 +21,11 @@ export class ArnUtils { * * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} * + * The required ARN pieces that are omitted will be taken from the stack that + * the 'anchor' is attached to. If all ARN pieces are supplied, the supplied anchor + * can be 'undefined'. */ - public static fromComponents(components: ArnComponents, anchor?: Construct): string { + public static fromComponents(components: ArnComponents, anchor: IConstruct | undefined): string { const partition = components.partition !== undefined ? components.partition : theStack('partition').partition; const region = components.region !== undefined ? components.region : theStack('region').region; const account = components.account !== undefined ? components.account : theStack('account').accountId; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts index b65dd2daee18b..d3640bb6782b3 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts @@ -1,7 +1,7 @@ import { IConstruct } from "../core/construct"; import { Token } from "../core/tokens"; -import { isIntrinsic } from "../core/tokens/cfn-tokens"; import { resolve } from "../core/tokens/resolve"; +import { isIntrinsic } from "./instrinsics"; /** * Class for JSON routines that are framework-aware diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index b61db4a745bde..5313140d6cc6b 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; -import { Token } from '../core/tokens'; -import { Referenceable } from './stack'; +import { Token } from '../core/tokens/token'; +import { Referenceable } from './stack-element'; export interface ConditionProps { expression?: FnCondition; @@ -57,4 +57,4 @@ export class FnCondition extends Token { constructor(type: string, value: any) { super({ [type]: value }); } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index 42c3d050d9ddb..324ebfcc77b3a 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -1,7 +1,7 @@ import { ResolveContext, Token, unresolved } from '../core/tokens'; -import { isIntrinsic } from '../core/tokens/cfn-tokens'; import { resolve } from '../core/tokens/resolve'; import { FnCondition } from './condition'; +import { minimalCloudFormationJoin } from './instrinsics'; // tslint:disable:max-line-length @@ -614,7 +614,6 @@ class FnJoin extends Token { private readonly listOfValues: any[]; // Cache for the result of resolveValues() - since it otherwise would be computed several times private _resolvedValues?: any[]; - private canOptimize: boolean; /** * Creates an ``Fn::Join`` function. @@ -630,12 +629,15 @@ class FnJoin extends Token { this.delimiter = delimiter; this.listOfValues = listOfValues; - this.canOptimize = true; } public resolve(context: ResolveContext): any { + if (unresolved(this.listOfValues)) { + // This is a list token, don't try to do smart things with it. + return this.listOfValues; + } const resolved = this.resolveValues(context); - if (this.canOptimize && resolved.length === 1) { + if (resolved.length === 1) { return resolved[0]; } return { 'Fn::Join': [ this.delimiter, resolved ] }; @@ -649,36 +651,7 @@ class FnJoin extends Token { private resolveValues(context: ResolveContext) { if (this._resolvedValues) { return this._resolvedValues; } - if (unresolved(this.listOfValues)) { - // This is a list token, don't resolve and also don't optimize. - this.canOptimize = false; - return this._resolvedValues = this.listOfValues; - } - - const resolvedValues = [...this.listOfValues.map(e => resolve(e, context))]; - let i = 0; - while (i < resolvedValues.length) { - const el = resolvedValues[i]; - if (isFnJoinIntrinsicWithSameDelimiter.call(this, el)) { - resolvedValues.splice(i, 1, ...el['Fn::Join'][1]); - } else if (i > 0 && isPlainString(resolvedValues[i - 1]) && isPlainString(resolvedValues[i])) { - resolvedValues[i - 1] += this.delimiter + resolvedValues[i]; - resolvedValues.splice(i, 1); - } else { - i += 1; - } - } - - return this._resolvedValues = resolvedValues; - - function isFnJoinIntrinsicWithSameDelimiter(this: FnJoin, obj: any): boolean { - return isIntrinsic(obj) - && Object.keys(obj)[0] === 'Fn::Join' - && obj['Fn::Join'][0] === this.delimiter; - } - - function isPlainString(obj: any): boolean { - return typeof obj === 'string' && !unresolved(obj); - } + const resolvedValues = this.listOfValues.map(e => resolve(e, context)); + return this._resolvedValues = minimalCloudFormationJoin(this.delimiter, resolvedValues); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index ba307cd58b475..a938fe87a58ba 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { StackElement } from './stack'; +import { StackElement } from './stack-element'; export interface IncludeProps { /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts b/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts new file mode 100644 index 0000000000000..dc39c892375b1 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts @@ -0,0 +1,43 @@ +/** + * Do an intelligent CloudFormation join on the given values, producing a minimal expression + */ +export function minimalCloudFormationJoin(delimiter: string, values: any[]): any[] { + let i = 0; + while (i < values.length) { + const el = values[i]; + if (isSplicableFnJoinInstrinsic(el)) { + values.splice(i, 1, ...el['Fn::Join'][1]); + } else if (i > 0 && isPlainString(values[i - 1]) && isPlainString(values[i])) { + values[i - 1] += delimiter + values[i]; + values.splice(i, 1); + } else { + i += 1; + } + } + + return values; + + function isPlainString(obj: any): boolean { + return typeof obj === 'string' && !unresolved(obj); + } + + function isSplicableFnJoinInstrinsic(obj: any): boolean { + return isIntrinsic(obj) + && Object.keys(obj)[0] === 'Fn::Join' + && obj['Fn::Join'][0] === delimiter; + } +} + +/** + * Return whether the given value represents a CloudFormation intrinsic + */ +export function isIntrinsic(x: any) { + if (Array.isArray(x) || x === null || typeof x !== 'object') { return false; } + + const keys = Object.keys(x); + if (keys.length !== 1) { return false; } + + return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); +} + +import { unresolved } from "../core/tokens/unresolved"; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts b/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts index 54d3465cd2dba..5f858f774efd8 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts @@ -1,5 +1,5 @@ import { makeUniqueId } from '../util/uniqueid'; -import { StackElement } from './stack'; +import { StackElement } from './stack-element'; const PATH_SEP = '/'; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index 65f4025bdd2a3..5446b6977f49b 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Fn } from './fn'; -import { Referenceable } from './stack'; +import { Referenceable } from './stack-element'; export interface MappingProps { mapping?: { [k1: string]: { [k2: string]: any } }; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index 3a2d4d2d0396d..3ac13d52a34c4 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -1,7 +1,5 @@ import { Construct } from '../core/construct'; -import { Condition } from './condition'; -import { Fn } from './fn'; -import { Stack, StackElement } from './stack'; +import { StackElement } from './stack-element'; export interface OutputProps { /** @@ -85,7 +83,7 @@ export class Output extends StackElement { this.export = props.export; } else if (!props.disableExport) { // prefix export name with stack name since exports are global within account + region. - const stackName = Stack.find(this).node.id; + const stackName = require('./stack').Stack.find(this).node.id; this.export = stackName ? stackName + ':' : ''; this.export += this.logicalId; } @@ -107,7 +105,7 @@ export class Output extends StackElement { if (!this.export) { throw new Error('Cannot create an ImportValue without an export name'); } - return Fn.importValue(this.export); + return fn().importValue(this.export); } public toCloudFormation(): object { @@ -210,7 +208,7 @@ export class StringListOutput extends Construct { condition: props.condition, disableExport: props.disableExport, export: props.export, - value: Fn.join(this.separator, props.values) + value: fn().join(this.separator, props.values) }); } @@ -222,9 +220,16 @@ export class StringListOutput extends Construct { const ret = []; for (let i = 0; i < this.length; i++) { - ret.push(Fn.select(i, Fn.split(this.separator, combined))); + ret.push(fn().select(i, fn().split(this.separator, combined))); } return ret; } } + +function fn() { + // Lazy loading of "Fn" module to break dependency cycles on startup + return require('./fn').Fn; +} + +import { Condition } from './condition'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index 0a22b6779cf74..6931baa629760 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { Ref, Referenceable } from './stack'; +import { Ref, Referenceable } from './stack-element'; export interface ParameterProps { /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 3dcf258fee044..f10239e43c99f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -4,7 +4,7 @@ import { CfnReference } from '../core/tokens/cfn-tokens'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; -import { IDependable, Referenceable, StackElement } from './stack'; +import { IDependable, Referenceable, StackElement } from './stack-element'; export interface ResourceProps { /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index cca72b5744cfa..b15a15068bf2a 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -1,7 +1,7 @@ import { Construct } from '../core/construct'; import { capitalizePropertyNames } from '../core/util'; import { FnCondition } from './condition'; -import { Referenceable } from './stack'; +import { Referenceable } from './stack-element'; /** * A rule can include a RuleCondition property and must include an Assertions property. diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts new file mode 100644 index 0000000000000..1f0f2646abb2e --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts @@ -0,0 +1,161 @@ +import { Construct, IConstruct, PATH_SEP } from "../core/construct"; +import { CfnReference } from "../core/tokens/cfn-tokens"; +import { RESOLVE_OPTIONS } from "../core/tokens/options"; + +const LOGICAL_ID_MD = 'aws:cdk:logicalId'; + +/** + * Represents a construct that can be "depended on" via `addDependency`. + */ +export interface IDependable { + /** + * Returns the set of all stack elements (resources, parameters, conditions) + * that should be added when a resource "depends on" this construct. + */ + readonly dependencyElements: IDependable[]; +} + +/** + * An element of a CloudFormation stack. + */ +export abstract class StackElement extends Construct implements IDependable { + /** + * Returns `true` if a construct is a stack element (i.e. part of the + * synthesized cloudformation template). + * + * Uses duck-typing instead of `instanceof` to allow stack elements from different + * versions of this library to be included in the same stack. + * + * @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; + } + } + + /** + * 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; + + /** + * 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). + * + * @param parent The parent construct + * @param props Construct properties + */ + constructor(scope: Construct, id: string) { + super(scope, id); + const s = Stack.find(this); + if (!s) { + throw new Error('The tree root must be derived from "Stack"'); + } + this.stack = s; + + this.node.addMetadata(LOGICAL_ID_MD, new (require("../core/tokens/token").Token)(() => this.logicalId), this.constructor); + + this.logicalId = this.stack.logicalIds.getLogicalId(this); + } + + /** + * @returns the stack trace of the point where this Resource was created from, sourced + * from the +metadata+ entry typed +aws:cdk:logicalId+, and with the bottom-most + * node +internal+ entries filtered. + */ + public get creationStackTrace(): string[] { + return filterStackTrace(this.node.metadata.find(md => md.type === LOGICAL_ID_MD)!.trace); + + function filterStackTrace(stack: string[]): string[] { + const result = Array.of(...stack); + while (result.length > 0 && shouldFilter(result[result.length - 1])) { + result.pop(); + } + // It's weird if we filtered everything, so return the whole stack... + return result.length === 0 ? stack : result; + } + + function shouldFilter(str: string): boolean { + return str.match(/[^(]+\(internal\/.*/) !== null; + } + } + + /** + * Return the path with respect to the stack + */ + public get stackPath(): string { + return this.node.ancestors(this.stack).map(c => c.node.id).join(PATH_SEP); + } + + public get dependencyElements(): IDependable[] { + return [ this ]; + } + + /** + * Returns the CloudFormation 'snippet' for this entity. The snippet will only be merged + * at the root level to ensure there are no identity conflicts. + * + * For example, a Resource class will return something like: + * { + * Resources: { + * [this.logicalId]: { + * Type: this.resourceType, + * Properties: this.props, + * Condition: this.condition + * } + * } + * } + */ + public abstract toCloudFormation(): object; + + /** + * Automatically detect references in this StackElement + */ + protected prepare() { + const options = RESOLVE_OPTIONS.push({ preProcess: (token, _) => { this.node.recordReference(token); return token; } }); + try { + // Execute for side effect of calling 'preProcess' + this.node.resolve(this.toCloudFormation()); + } finally { + options.pop(); + } + } +} + +/** + * A generic, untyped reference to a Stack Element + */ +export class Ref extends CfnReference { + constructor(element: StackElement) { + super({ Ref: element.logicalId }, `${element.logicalId}.Ref`, element); + } +} + +import { Stack } from "./stack"; + +/** + * Base class for referenceable CloudFormation constructs which are not Resources + * + * These constructs are things like Conditions and Parameters, can be + * referenced by taking the `.ref` attribute. + * + * Resource constructs do not inherit from Referenceable because they have their + * own, more specific types returned from the .ref attribute. Also, some + * resources aren't referenceable at all (such as BucketPolicies or GatewayAttachments). + */ +export abstract class Referenceable extends StackElement { + /** + * Returns a token to a CloudFormation { Ref } that references this entity based on it's logical ID. + */ + public get ref(): string { + return new Ref(this).toString(); + } +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 7cc049f96c2dd..d6deb085bd273 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -1,9 +1,8 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; -import { Construct, IConstruct, PATH_SEP } from '../core/construct'; +import { Construct, IConstruct } from '../core/construct'; import { Token } from '../core/tokens'; import { CfnReference } from '../core/tokens/cfn-tokens'; -import { RESOLVE_OPTIONS } from '../core/tokens/options'; import { Environment } from '../environment'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -430,134 +429,6 @@ function merge(template: any, part: any) { } } -const LOGICAL_ID_MD = 'aws:cdk:logicalId'; - -/** - * Represents a construct that can be "depended on" via `addDependency`. - */ -export interface IDependable { - /** - * Returns the set of all stack elements (resources, parameters, conditions) - * that should be added when a resource "depends on" this construct. - */ - readonly dependencyElements: IDependable[]; -} - -/** - * An element of a CloudFormation stack. - */ -export abstract class StackElement extends Construct implements IDependable { - /** - * Returns `true` if a construct is a stack element (i.e. part of the - * synthesized cloudformation template). - * - * Uses duck-typing instead of `instanceof` to allow stack elements from different - * versions of this library to be included in the same stack. - * - * @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; - } - } - - /** - * 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; - - /** - * 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). - * - * @param parent The parent construct - * @param props Construct properties - */ - constructor(scope: Construct, id: string) { - super(scope, id); - const s = Stack.find(this); - if (!s) { - throw new Error('The tree root must be derived from "Stack"'); - } - this.stack = s; - - this.node.addMetadata(LOGICAL_ID_MD, new Token(() => this.logicalId), this.constructor); - - this.logicalId = this.stack.logicalIds.getLogicalId(this); - } - - /** - * @returns the stack trace of the point where this Resource was created from, sourced - * from the +metadata+ entry typed +aws:cdk:logicalId+, and with the bottom-most - * node +internal+ entries filtered. - */ - public get creationStackTrace(): string[] { - return filterStackTrace(this.node.metadata.find(md => md.type === LOGICAL_ID_MD)!.trace); - - function filterStackTrace(stack: string[]): string[] { - const result = Array.of(...stack); - while (result.length > 0 && shouldFilter(result[result.length - 1])) { - result.pop(); - } - // It's weird if we filtered everything, so return the whole stack... - return result.length === 0 ? stack : result; - } - - function shouldFilter(str: string): boolean { - return str.match(/[^(]+\(internal\/.*/) !== null; - } - } - - /** - * Return the path with respect to the stack - */ - public get stackPath(): string { - return this.node.ancestors(this.stack).map(c => c.node.id).join(PATH_SEP); - } - - public get dependencyElements(): IDependable[] { - return [ this ]; - } - - /** - * Returns the CloudFormation 'snippet' for this entity. The snippet will only be merged - * at the root level to ensure there are no identity conflicts. - * - * For example, a Resource class will return something like: - * { - * Resources: { - * [this.logicalId]: { - * Type: this.resourceType, - * Properties: this.props, - * Condition: this.condition - * } - * } - * } - */ - public abstract toCloudFormation(): object; - - /** - * Automatically detect references in this StackElement - */ - protected prepare() { - const options = RESOLVE_OPTIONS.push({ preProcess: (token, _) => { this.node.recordReference(token); return token; } }); - try { - // Execute for side effect of calling 'preProcess' - this.node.resolve(this.toCloudFormation()); - } finally { - options.pop(); - } - } -} - /** * CloudFormation template options for a stack. */ @@ -584,25 +455,6 @@ export interface TemplateOptions { metadata?: { [key: string]: any }; } -/** - * Base class for referenceable CloudFormation constructs which are not Resources - * - * These constructs are things like Conditions and Parameters, can be - * referenced by taking the `.ref` attribute. - * - * Resource constructs do not inherit from Referenceable because they have their - * own, more specific types returned from the .ref attribute. Also, some - * resources aren't referenceable at all (such as BucketPolicies or GatewayAttachments). - */ -export abstract class Referenceable extends StackElement { - /** - * Returns a token to a CloudFormation { Ref } that references this entity based on it's logical ID. - */ - public get ref(): string { - return new Ref(this).toString(); - } -} - /** * Collect all StackElements from a construct * @@ -623,15 +475,7 @@ function stackElements(node: IConstruct, into: StackElement[] = []): StackElemen return into; } -/** - * A generic, untyped reference to a Stack Element - */ -export class Ref extends CfnReference { - constructor(element: StackElement) { - super({ Ref: element.logicalId }, `${element.logicalId}.Ref`, element); - } -} - // These imports have to be at the end to prevent circular imports import { Output } from './output'; -import { Aws } from './pseudo'; \ No newline at end of file +import { Aws } from './pseudo'; +import { StackElement } from './stack-element'; diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 10baeba6ea190..09c7c14f28d32 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { makeUniqueId } from '../util/uniqueid'; +import { Token, unresolved } from './tokens'; import { resolve } from './tokens/resolve'; -import { Token, unresolved } from './tokens/token'; export const PATH_SEP = '/'; /** diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts index f41da52d25d97..f454521bce255 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts @@ -16,5 +16,7 @@ export function cloudFormationConcat(left: any | undefined, right: any | undefin // Otherwise return a Join intrinsic (already in the target document language to avoid taking // circular dependencies on FnJoin & friends) - return { 'Fn::Join': ['', parts] }; + return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; } + +import { minimalCloudFormationJoin } from "../../cloudformation/instrinsics"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts index 1bf7b26993bbd..484e9ef3b8cc3 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts @@ -48,16 +48,5 @@ export class CfnReference extends Token { } } } -/** - * Return whether the given value represents a CloudFormation intrinsic - */ -export function isIntrinsic(x: any) { - if (Array.isArray(x) || x === null || typeof x !== 'object') { return false; } - - const keys = Object.keys(x); - if (keys.length !== 1) { return false; } - - return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); -} import { Stack } from "../../cloudformation/stack"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts index 3d6f0d4d4fabc..5c47f0f57c008 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts @@ -1,6 +1,7 @@ import { cloudFormationConcat } from "./cfn-concat"; import { resolve } from "./resolve"; -import { ResolveContext, Token, unresolved } from "./token"; +import { ResolveContext, Token } from "./token"; +import { unresolved } from "./unresolved"; // Encoding Tokens into native types; should not be exported diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/index.ts b/packages/@aws-cdk/cdk/lib/core/tokens/index.ts index 04e896145743c..af82e0f1c238c 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/index.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/index.ts @@ -1,3 +1,4 @@ // This exports the modules that should be publicly available (not all of them) -export * from './token'; \ No newline at end of file +export * from './token'; +export * from './unresolved'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts index 44c097db73fa0..b9555e997d223 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts @@ -1,6 +1,7 @@ import { containsListToken, TOKEN_MAP } from "./encoding"; import { RESOLVE_OPTIONS } from "./options"; -import { RESOLVE_METHOD, ResolveContext, Token, unresolved } from "./token"; +import { RESOLVE_METHOD, ResolveContext } from "./token"; +import { unresolved } from "./unresolved"; // This file should not be exported to consumers, resolving should happen through Construct.resolve() @@ -80,7 +81,7 @@ export function resolve(obj: any, context: ResolveContext): any { if (unresolved(obj)) { const preProcess = RESOLVE_OPTIONS.preProcess; - const value = preProcess(obj as Token, context)[RESOLVE_METHOD](context); + const value = preProcess(obj, context)[RESOLVE_METHOD](context); return resolve(value, context); } diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts index 4be0b78567296..3bbd19a645873 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts @@ -1,5 +1,5 @@ import { IConstruct } from "../construct"; -import { isListToken, TOKEN_MAP } from "./encoding"; +import { TOKEN_MAP } from "./encoding"; /** * If objects has a function property by this name, they will be considered tokens, and this @@ -93,7 +93,7 @@ export class Token { */ public toJSON(): any { // tslint:disable-next-line:max-line-length - throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use a document-specific stringification method instead.'); + throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use CloudFormationJSON.stringify() instead.'); } /** @@ -111,7 +111,7 @@ export class Token { public toList(): string[] { const valueType = typeof this.valueOrFunction; if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { - throw new Error('Got a literal Token value; cannot be encoded as a list.'); + throw new Error('Got a literal Token value; only intrinsics can ever evaluate to lists.'); } if (this.tokenListification === undefined) { @@ -127,20 +127,4 @@ export class Token { export interface ResolveContext { construct: IConstruct; prefix: string[]; -} - -/** - * Returns true if obj is a token (i.e. has the resolve() method or is a string - * that includes token markers), or it's a listifictaion of a Token string. - * - * @param obj The object to test. - */ -export function unresolved(obj: any): boolean { - if (typeof(obj) === 'string') { - return TOKEN_MAP.createStringTokenString(obj).test(); - } else if (Array.isArray(obj) && obj.length === 1) { - return isListToken(obj[0]); - } else { - return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; - } } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts b/packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts new file mode 100644 index 0000000000000..1262f6f86c0fd --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts @@ -0,0 +1,18 @@ +import { isListToken, TOKEN_MAP } from "./encoding"; +import { RESOLVE_METHOD } from "./token"; + +/** + * Returns true if obj is a token (i.e. has the resolve() method or is a string + * that includes token markers), or it's a listifictaion of a Token string. + * + * @param obj The object to test. + */ +export function unresolved(obj: any): boolean { + if (typeof(obj) === 'string') { + return TOKEN_MAP.createStringTokenString(obj).test(); + } else if (Array.isArray(obj) && obj.length === 1) { + return isListToken(obj[0]); + } else { + return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; + } +} diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 6f4b43675677d..695e59c7b56bc 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -15,6 +15,7 @@ export * from './cloudformation/resource'; export * from './cloudformation/resource-policy'; export * from './cloudformation/rule'; export * from './cloudformation/stack'; +export * from './cloudformation/stack-element'; export * from './cloudformation/dynamic-reference'; export * from './cloudformation/tag'; export * from './cloudformation/removal-policy'; diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index 2bacde3bc7dbf..dd815293fe8a0 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -27,7 +27,7 @@ export = { region: 'us-east-1', partition: 'aws-cn', resourceName: 'mytable/stream/label' - }); + }, undefined); test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:dynamodb:us-east-1:123456789012:table/mytable/stream/label'); @@ -43,7 +43,7 @@ export = { account: '', region: '', partition: 'aws-cn', - }); + }, undefined); test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:s3:::my-bucket'); @@ -72,7 +72,7 @@ export = { test.throws(() => ArnUtils.fromComponents({ service: 'foo', resource: 'bar', - sep: 'x' })); + sep: 'x' }, undefined)); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts index ac4d94a22e7eb..adb82677c5248 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts @@ -29,6 +29,25 @@ export = nodeunit.testCase({ test.throws(() => Fn.join('.', [])); test.done(); }, + 'collapse nested FnJoins even if they contain tokens'(test: nodeunit.Test) { + const stack = new Stack(); + + const obj = Fn.join('', [ + 'a', + Fn.join('', [Fn.getAtt('a', 'bc').toString(), 'c']), + 'd' + ]); + + test.deepEqual(stack.node.resolve(obj), { 'Fn::Join': [ "", + [ + "a", + { 'Fn::GetAtt': ['a', 'bc'] }, + 'cd', + ] + ]}); + + test.done(); + }, 'resolves to the value if only one value is joined': asyncTest(async () => { const stack = new Stack(); await fc.assert( diff --git a/packages/@aws-cdk/runtime-values/lib/rtv.ts b/packages/@aws-cdk/runtime-values/lib/rtv.ts index 7d4a7c74f958a..9cddd96e96d7a 100644 --- a/packages/@aws-cdk/runtime-values/lib/rtv.ts +++ b/packages/@aws-cdk/runtime-values/lib/rtv.ts @@ -69,7 +69,7 @@ export class RuntimeValue extends cdk.Construct { service: 'ssm', resource: 'parameter', resourceName: this.parameterName - }); + }, this); } /** From a8e00d9a919052574630f405b413dadde7baf60c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 7 Jan 2019 16:36:35 +0100 Subject: [PATCH 22/39] More work to get tests to pass --- packages/@aws-cdk/assert/lib/expect.ts | 5 ++- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 11 ++++-- .../aws-apigateway/lib/integrations/aws.ts | 17 +++++---- .../aws-apigateway/lib/integrations/lambda.ts | 1 + .../test/integ.restapi.books.expected.json | 4 +- .../test/integ.restapi.defaults.expected.json | 4 +- .../test/integ.restapi.expected.json | 4 +- .../aws-apigateway/test/test.deployment.ts | 2 +- .../aws-apigateway/test/test.restapi.ts | 14 +++++-- .../test/test.pipeline-actions.ts | 23 +++++------- .../aws-cloudtrail/test/test.cloudtrail.ts | 6 +-- ...eg.pipeline-cfn-cross-region.expected.json | 6 +-- .../@aws-cdk/aws-iam/lib/policy-document.ts | 4 +- .../aws-iam/test/test.managed-policy.ts | 2 +- .../aws-iam/test/test.policy-document.ts | 12 ------ .../@aws-cdk/aws-lambda/test/test.lambda.ts | 18 +++------ .../@aws-cdk/aws-route53/test/test.route53.ts | 6 +-- packages/@aws-cdk/cdk/lib/app.ts | 8 +++- .../@aws-cdk/cdk/lib/cloudformation/fn.ts | 2 +- .../cdk/lib/cloudformation/instrinsics.ts | 7 ++++ .../cdk/lib/cloudformation/stack-element.ts | 10 ++++- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 20 +++++----- packages/@aws-cdk/cdk/lib/core/construct.ts | 28 +++++++------- .../cdk/lib/core/tokens/cfn-tokens.ts | 10 ++--- packages/@aws-cdk/cdk/lib/runtime.ts | 6 ++- .../cdk/test/cloudformation/test.stack.ts | 37 ++++++++++++++++--- .../@aws-cdk/cdk/test/core/test.tokens.ts | 15 ++++++++ packages/@aws-cdk/cdk/test/test.app.ts | 2 - tools/cdk-integ-tools/bin/cdk-integ.ts | 3 +- 29 files changed, 173 insertions(+), 114 deletions(-) diff --git a/packages/@aws-cdk/assert/lib/expect.ts b/packages/@aws-cdk/assert/lib/expect.ts index a98a18abdc389..13112fbf4b6cd 100644 --- a/packages/@aws-cdk/assert/lib/expect.ts +++ b/packages/@aws-cdk/assert/lib/expect.ts @@ -9,6 +9,9 @@ export function expect(stack: api.SynthesizedStack | cdk.Stack, skipValidation = if (isStackClassInstance(stack)) { if (!skipValidation) { + // Do a prepare-and-validate run over the given stack + stack.node.prepareTree(); + const errors = stack.node.validateTree(); if (errors.length > 0) { throw new Error(`Stack validation failed:\n${errors.map(e => `${e.message} at: ${e.source.node.scope}`).join('\n')}`); @@ -34,4 +37,4 @@ export function expect(stack: api.SynthesizedStack | cdk.Stack, skipValidation = function isStackClassInstance(x: api.SynthesizedStack | cdk.Stack): x is cdk.Stack { return 'toCloudFormation' in x; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index addf085cf134a..e6dfe7efc5ac8 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -107,6 +107,7 @@ class LatestDeploymentResource extends CfnDeployment { private originalLogicalId?: string; private lazyLogicalIdRequired: boolean; private lazyLogicalId?: string; + private logicalIdToken: cdk.Token; private hashComponents = new Array(); constructor(scope: cdk.Construct, id: string, props: CfnDeploymentProps) { @@ -114,6 +115,8 @@ class LatestDeploymentResource extends CfnDeployment { // from this point, don't allow accessing logical ID before synthesis this.lazyLogicalIdRequired = true; + + this.logicalIdToken = new cdk.Token(() => this.lazyLogicalId); } /** @@ -124,11 +127,15 @@ class LatestDeploymentResource extends CfnDeployment { return this.originalLogicalId!; } + return this.logicalIdToken.toString(); + + /* if (!this.lazyLogicalId) { throw new Error('This resource has a lazy logical ID which is calculated just before synthesis. Use a cdk.Token to evaluate'); } return this.lazyLogicalId; + */ } /** @@ -170,7 +177,7 @@ class LatestDeploymentResource extends CfnDeployment { * Hooks into synthesis to calculate a logical ID that hashes all the components * add via `addToLogicalId`. */ - public validate() { + protected prepare() { // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. if (this.hashComponents.length === 0) { @@ -183,7 +190,5 @@ class LatestDeploymentResource extends CfnDeployment { this.lazyLogicalId = this.originalLogicalId + md5.digest("hex"); } - - return []; } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index 375ba833ec33c..b7911de68c6b5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -71,13 +71,16 @@ export class AwsIntegration extends Integration { super({ type, integrationHttpMethod: 'POST', - uri: new cdk.Token(() => cdk.ArnUtils.fromComponents({ - service: 'apigateway', - account: backend, - resource: apiType, - sep: '/', - resourceName: apiValue, - }, this._anchor)), + uri: new cdk.Token(() => { + if (!this._anchor) { throw new Error('AwsIntegration must be used in API'); } + return cdk.ArnUtils.fromComponents({ + service: 'apigateway', + account: backend, + resource: apiType, + sep: '/', + resourceName: apiValue, + }, this._anchor); + }), options: props.options, }); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts index 1970b71923455..10ba40547523d 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -52,6 +52,7 @@ export class LambdaIntegration extends AwsIntegration { } public bind(method: Method) { + super.bind(method); const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); const desc = `${method.httpMethod}.${method.resource.resourcePath.replace(/\//g, '.')}`; diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index e76050bfba5a0..acdcd09664d43 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -836,7 +836,9 @@ { "Ref": "AWS::Region" }, - ".amazonaws.com/", + ".", + { "Ref": "AWS::URLSuffix" }, + "/", { "Ref": "booksapiDeploymentStageprod55D8E03E" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index aac6799e40c09..888fa29a306be 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -109,7 +109,9 @@ { "Ref": "AWS::Region" }, - ".amazonaws.com/", + ".", + { "Ref": "AWS::URLSuffix" }, + "/", { "Ref": "myapiDeploymentStageprod298F01AF" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index 5d15ff5c7b253..3af25751a5649 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -601,7 +601,9 @@ { "Ref": "AWS::Region" }, - ".amazonaws.com/", + ".", + { "Ref": "AWS::URLSuffix" }, + "/", { "Ref": "myapiDeploymentStagebeta96434BEB" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index f9b927af1bd2a..aaec885005146 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -152,7 +152,7 @@ export = { test.done(); function synthesize() { - stack.node.validateTree(); + stack.node.prepareTree(); return stack.toCloudFormation(); } }, diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 0eb64c31291e2..0810e8938c67b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -125,7 +125,9 @@ export = { { Ref: "AWS::Region" }, - ".amazonaws.com/", + ".", + { Ref: "AWS::URLSuffix" }, + "/", { Ref: "myapiDeploymentStageprod298F01AF" }, @@ -386,7 +388,7 @@ export = { const exported = api.export(); // THEN - stack.node.validateTree(); + stack.node.prepareTree(); test.deepEqual(stack.toCloudFormation().Outputs.MyRestApiRestApiIdB93C5C2D, { Value: { Ref: 'MyRestApi2D1F47A9' }, Export: { Name: 'MyRestApiRestApiIdB93C5C2D' } @@ -409,7 +411,9 @@ export = { { Ref: 'apiC8550315' }, '.execute-api.', { Ref: 'AWS::Region' }, - '.amazonaws.com/', + ".", + { Ref: "AWS::URLSuffix" }, + "/", { Ref: 'apiDeploymentStageprod896C8101' }, '/' ] ] }); test.deepEqual(api.node.resolve(api.urlForPath('/foo/bar')), { 'Fn::Join': @@ -418,7 +422,9 @@ export = { { Ref: 'apiC8550315' }, '.execute-api.', { Ref: 'AWS::Region' }, - '.amazonaws.com/', + ".", + { Ref: "AWS::URLSuffix" }, + "/", { Ref: 'apiDeploymentStageprod896C8101' }, '/foo/bar' ] ] }); test.done(); diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index a1f143c171af2..789f6009ee7c0 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -11,7 +11,7 @@ export = nodeunit.testCase({ 'works'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); const artifact = new cpapi.Artifact(stack as any, 'TestArtifact'); const action = new cloudformation.PipelineCreateReplaceChangeSetAction(stack, 'Action', { stage, @@ -45,7 +45,7 @@ export = nodeunit.testCase({ 'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); const artifact = new cpapi.Artifact(stack as any, 'TestArtifact'); new cloudformation.PipelineCreateReplaceChangeSetAction(stack, 'ActionA', { stage, @@ -101,7 +101,7 @@ export = nodeunit.testCase({ 'works'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); new cloudformation.PipelineExecuteChangeSetAction(stack, 'Action', { stage, changeSetName: 'MyChangeSet', @@ -124,7 +124,7 @@ export = nodeunit.testCase({ 'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); new cloudformation.PipelineExecuteChangeSetAction(stack, 'ActionA', { stage, changeSetName: 'MyChangeSet', @@ -162,7 +162,7 @@ export = nodeunit.testCase({ const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cloudformation.PipelineCreateUpdateStackAction(stack, 'Action', { - stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }), + stage: new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }), templatePath: new cpapi.Artifact(stack as any, 'TestArtifact').atPath('some/file'), stackName: 'MyStack', adminPermissions: false, @@ -184,7 +184,7 @@ export = nodeunit.testCase({ const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cloudformation.PipelineDeleteStackAction(stack, 'Action', { - stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }), + stage: new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }), adminPermissions: false, stackName: 'MyStack', }); @@ -279,16 +279,13 @@ function _stackArn(stackName: string, anchor: cdk.IConstruct): string { }, anchor); } -class PipelineDouble implements cpapi.IPipeline { +class PipelineDouble extends cdk.Construct implements cpapi.IPipeline { public readonly pipelineName: string; public readonly pipelineArn: string; public readonly role: iam.Role; - public get node(): cdk.ConstructNode { - throw new Error('this is not a real construct'); - } - - constructor({ pipelineName, role }: { pipelineName?: string, role: iam.Role }) { + constructor(scope: cdk.Construct, id: string, { pipelineName, role }: { pipelineName?: string, role: iam.Role }) { + super(scope, id); this.pipelineName = pipelineName || 'TestPipeline'; this.pipelineArn = cdk.ArnUtils.fromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }, this); this.role = role; @@ -354,5 +351,5 @@ class RoleDouble extends iam.Role { } function resolve(x: any): any { - return new cdk.Root().node.resolve(x); + return new cdk.Stack().node.resolve(x); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index 59783aee5764f..ae6066ec43ff4 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -40,11 +40,7 @@ const ExpectedBucketPolicyProperties = { "Arn" ] }, - "/AWSLogs/", - { - Ref: "AWS::AccountId" - }, - "/*" + "/AWSLogs/123456789012/*", ] ] } diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json index f2f25bf9fcd04..99f7e7886620d 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json @@ -122,11 +122,7 @@ { "Ref": "AWS::Partition" }, - ":cloudformation:us-west-2:", - { - "Ref": "AWS::AccountId" - }, - ":stack/aws-cdk-codepipeline-cross-region-deploy-stack/*" + ":cloudformation:us-west-2:12345678:stack/aws-cdk-codepipeline-cross-region-deploy-stack/*" ] ] } diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index fbfe83e343a9c..be8578eb7bb5a 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -12,7 +12,7 @@ export class PolicyDocument extends cdk.Token { super(); } - public resolve(): any { + public resolve(_context: cdk.ResolveContext): any { if (this.isEmpty) { return undefined; } @@ -372,7 +372,7 @@ export class PolicyStatement extends cdk.Token { // Serialization // - public resolve(): any { + public resolve(_context: cdk.ResolveContext): any { return this.toJson(); } diff --git a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts index 23a0d83d815c0..4ac13bcfc270f 100644 --- a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts @@ -7,7 +7,7 @@ export = { const stack = new cdk.Stack(); const mp = new AwsManagedPolicy("service-role/SomePolicy"); - test.deepEqual(stack.node.resolve(mp.policyArn), { + test.deepEqual(stack.node.resolve(mp.policyArn(stack)), { "Fn::Join": ['', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts index 2694df9538380..9db0774aa6321 100644 --- a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts +++ b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts @@ -258,18 +258,6 @@ export = { test.done(); }, - 'addPrincipal prohibits mixing principal types'(test: Test) { - const stack = new Stack(); - - const s = new PolicyStatement().addAccountRootPrincipal(stack); - test.throws(() => { s.addServicePrincipal('rds.amazonaws.com'); }, - /Attempted to add principal key Service/); - test.throws(() => { s.addFederatedPrincipal('federation', { ConditionOp: { ConditionKey: 'ConditionValue' } }); }, - /Attempted to add principal key Federated/); - - test.done(); - }, - 'addCanonicalUserPrincipal can be used to add cannonical user principals'(test: Test) { const stack = new Stack(); const p = new PolicyDocument(); diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 63c1109a555cd..bd16167997a51 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, MatchStyle, ResourcePart } from '@aws-cdk/assert'; import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import sqs = require('@aws-cdk/aws-sqs'); @@ -621,9 +621,7 @@ export = { 'default function with SQS DLQ when client provides Queue to be used as DLQ'(test: Test) { const stack = new cdk.Stack(); - const dlqStack = new cdk.Stack(); - - const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { queueName: 'MyLambda_DLQ', retentionPeriodSec: 1209600 }); @@ -725,16 +723,14 @@ export = { } } } - ); + , MatchStyle.SUPERSET); test.done(); }, 'default function with SQS DLQ when client provides Queue to be used as DLQ and deadLetterQueueEnabled set to true'(test: Test) { const stack = new cdk.Stack(); - const dlqStack = new cdk.Stack(); - - const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { queueName: 'MyLambda_DLQ', retentionPeriodSec: 1209600 }); @@ -837,16 +833,14 @@ export = { } } } - ); + , MatchStyle.SUPERSET); test.done(); }, 'error when default function with SQS DLQ when client provides Queue to be used as DLQ and deadLetterQueueEnabled set to false'(test: Test) { const stack = new cdk.Stack(); - const dlqStack = new cdk.Stack(); - - const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { queueName: 'MyLambda_DLQ', retentionPeriodSec: 1209600 }); diff --git a/packages/@aws-cdk/aws-route53/test/test.route53.ts b/packages/@aws-cdk/aws-route53/test/test.route53.ts index 3a18465c1ff03..0298700adb28e 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/test.route53.ts @@ -33,7 +33,7 @@ export = { Name: "test.private.", VPCs: [{ VPCId: { Ref: 'VPCB9E5F0B4' }, - VPCRegion: { Ref: 'AWS::Region' } + VPCRegion: 'bermuda-triangle' }] } } @@ -55,11 +55,11 @@ export = { Name: "test.private.", VPCs: [{ VPCId: { Ref: 'VPC17DE2CF87' }, - VPCRegion: { Ref: 'AWS::Region' } + VPCRegion: 'bermuda-triangle' }, { VPCId: { Ref: 'VPC2C1F0E711' }, - VPCRegion: { Ref: 'AWS::Region' } + VPCRegion: 'bermuda-triangle' }] } } diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index 1cb6355635936..dc38817aafba8 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -58,7 +58,7 @@ export class App extends Root { public synthesizeStack(stackName: string): cxapi.SynthesizedStack { const stack = this.getStack(stackName); - this.prepareConstructTree(); + this.node.prepareTree(); // first, validate this stack and stop if there are errors. const errors = stack.node.validateTree(); @@ -83,7 +83,7 @@ export class App extends Root { missing, template: stack.toCloudFormation(), metadata: this.collectMetadata(stack), - dependsOn: stack.dependencies().map(s => s.node.id), + dependsOn: noEmptyArray(stack.dependencies().map(s => s.node.id)), }; } @@ -228,4 +228,8 @@ function getJsiiAgentVersion() { } return jsiiAgent; +} + +function noEmptyArray(xs: T[]): T[] | undefined { + return xs.length > 0 ? xs : undefined; } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index 324ebfcc77b3a..f398d13de38a1 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -634,7 +634,7 @@ class FnJoin extends Token { public resolve(context: ResolveContext): any { if (unresolved(this.listOfValues)) { // This is a list token, don't try to do smart things with it. - return this.listOfValues; + return { 'Fn::Join': [ this.delimiter, this.listOfValues ] }; } const resolved = this.resolveValues(context); if (resolved.length === 1) { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts b/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts index dc39c892375b1..af2094cc5894d 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts @@ -40,4 +40,11 @@ export function isIntrinsic(x: any) { return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); } +/** + * Return whether this is an intrinsic that could potentially (or definitely) evaluate to a list + */ +export function canEvaluateToList(x: any) { + return isIntrinsic(x) && ['Ref', 'Fn::GetAtt', 'Fn::GetAZs', 'Fn::Split', 'Fn::FindInMap', 'Fn::ImportValue'].includes(Object.keys(x)[0]); +} + import { unresolved } from "../core/tokens/unresolved"; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts index 1f0f2646abb2e..a4280e54b0375 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts @@ -1,5 +1,4 @@ import { Construct, IConstruct, PATH_SEP } from "../core/construct"; -import { CfnReference } from "../core/tokens/cfn-tokens"; import { RESOLVE_OPTIONS } from "../core/tokens/options"; const LOGICAL_ID_MD = 'aws:cdk:logicalId'; @@ -122,14 +121,21 @@ export abstract class StackElement extends Construct implements IDependable { protected prepare() { const options = RESOLVE_OPTIONS.push({ preProcess: (token, _) => { this.node.recordReference(token); return token; } }); try { - // Execute for side effect of calling 'preProcess' + // Execute for side effect of calling 'preProcess'. + // Note: it might be that the properties of the CFN object aren't valid. This will usually be preventatively + // caught in a construct's validate() and turned into a nicely descriptive error, but we're running prepare() + // before validate(). Swallow errors that occur because the CFN layer doesn't validate completely. this.node.resolve(this.toCloudFormation()); + } catch (e) { + if (e.type !== 'CloudFormationSynthesisError') { throw e; } } finally { options.pop(); } } } +import { CfnReference } from "../core/tokens/cfn-tokens"; + /** * A generic, untyped reference to a Stack Element */ diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index d6deb085bd273..fbd2cd222dd3a 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -121,7 +121,7 @@ export class Stack extends Construct { * @param name The name of the CloudFormation stack. Defaults to "Stack". * @param props Stack properties. */ - public constructor(scope?: App, name?: string, props?: StackProps) { + public constructor(scope?: App, name?: string, private readonly props?: StackProps) { // For unit test convenience parents are optional, so bypass the type check when calling the parent. super(scope!, name!); @@ -292,12 +292,13 @@ export class Stack extends Construct { /** * The account in which this stack is defined * - * Either returns the literal account for this stack, or a symbolic value - * that will evaluate to the correct account at deployment time. + * Either returns the literal account for this stack if it was specified + * literally upon Stack construction, or a symbolic value that will evaluate + * to the correct account at deployment time. */ public get accountId(): string { - if (this.env.account) { - return this.env.account; + if (this.props && this.props.env && this.props.env.account) { + return this.props.env.account; } return new Aws(this).accountId; } @@ -305,12 +306,13 @@ export class Stack extends Construct { /** * The region in which this stack is defined * - * Either returns the literal region for this stack, or a symbolic value - * that will evaluate to the correct region at deployment time. + * Either returns the literal region for this stack if it was specified + * literally upon Stack construction, or a symbolic value that will evaluate + * to the correct region at deployment time. */ public get region(): string { - if (this.env.region) { - return this.env.region; + if (this.props && this.props.env && this.props.env.region) { + return this.props.env.region; } return new Aws(this).region; } diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 09c7c14f28d32..f16c5ba6b46a1 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -269,6 +269,17 @@ export class ConstructNode { return errors.concat(localErrors.map(msg => new ValidationError(this.host, msg))); } + /** + * Run 'prepare()' on all constructs in the tree + */ + public prepareTree() { + // Use .reverse() to achieve post-order traversal + const constructs = allConstructs(this.host, CrawlStyle.BreadthFirst); + for (const construct of constructs.reverse()) { + Construct.doPrepare(construct); + } + } + /** * Return the ancestors (including self) of this Construct up until and excluding the indicated component * @@ -488,11 +499,12 @@ export class Construct implements IConstruct { * Perform final modifications before synthesis * * This method can be implemented by derived constructs in order to perform - * final changes before synthesis. Prepare() will be called on a construct's - * children first. + * final changes before synthesis. prepare() will be called after child + * constructs have been prepared. + * */ protected prepare(): void { - // Empty on purpose + // Intentionally left blank } } @@ -505,16 +517,6 @@ export class Root extends Construct { // Bypass type checks super(undefined as any, ''); } - - /** - * Run 'prepare()' on all constructs in the tree - */ - public prepareConstructTree() { - // Use .reverse() to achieve post-order traversal - for (const construct of allConstructs(this, CrawlStyle.BreadthFirst).reverse()) { - Construct.doPrepare(construct); - } - } } /** diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts index 484e9ef3b8cc3..183a438a4eb2f 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts @@ -1,4 +1,3 @@ -import { Construct } from "../construct"; import { ResolveContext, Token } from "./token"; /** @@ -42,11 +41,12 @@ export class CfnReference extends Token { */ public consumeFromStack(consumingStack: Stack) { if (this.tokenStack && this.tokenStack !== consumingStack && !this.replacementTokens.has(consumingStack)) { - // We're trying to resolve a cross-stack reference - consumingStack.addDependency(this.tokenStack); - this.replacementTokens.set(consumingStack, this.tokenStack.exportValue(this, consumingStack)); + // We're trying to resolve a cross-stack reference + consumingStack.addDependency(this.tokenStack); + this.replacementTokens.set(consumingStack, this.tokenStack.exportValue(this, consumingStack)); } } } -import { Stack } from "../../cloudformation/stack"; \ No newline at end of file +import { Stack } from "../../cloudformation/stack"; +import { Construct } from "../construct"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/runtime.ts b/packages/@aws-cdk/cdk/lib/runtime.ts index 7087a5cee19ad..7ec4c167c1f2f 100644 --- a/packages/@aws-cdk/cdk/lib/runtime.ts +++ b/packages/@aws-cdk/cdk/lib/runtime.ts @@ -138,7 +138,7 @@ export class ValidationResult { let message = this.errorTree(); // The first letter will be lowercase, so uppercase it for a nicer error message message = message.substr(0, 1).toUpperCase() + message.substr(1); - throw new TypeError(message); + throw new CloudFormationSynthesisError(message); } } @@ -384,3 +384,7 @@ function isCloudFormationIntrinsic(x: any) { return keys[0] === 'Ref' || keys[0].substr(0, 4) === 'Fn::'; } + +export class CloudFormationSynthesisError extends Error { + public readonly type = 'CloudFormationSynthesisError'; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index aea10a9373ee4..880068e92d589 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -1,3 +1,4 @@ +import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { App, Aws, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack, Token } from '../../lib'; @@ -185,7 +186,7 @@ export = { // THEN // Need to do this manually now, since we're in testing mode. In a normal CDK app, // this happens as part of app.run(). - app.prepareConstructTree(); + app.node.prepareTree(); test.deepEqual(stack1.toCloudFormation(), { Outputs: { @@ -218,7 +219,7 @@ export = { // WHEN - used in another stack new Parameter(stack2, 'SomeParameter', { type: 'String', default: new Token(() => account1) }); - app.prepareConstructTree(); + app.node.prepareTree(); // THEN test.deepEqual(stack1.toCloudFormation(), { @@ -252,7 +253,7 @@ export = { // WHEN - used in another stack new Parameter(stack2, 'SomeParameter', { type: 'String', default: `TheAccountIs${account1}` }); - app.prepareConstructTree(); + app.node.prepareTree(); // THEN test.deepEqual(stack2.toCloudFormation(), { @@ -280,7 +281,7 @@ export = { new Parameter(stack1, 'SomeParameter', { type: 'String', default: account2 }); test.throws(() => { - app.prepareConstructTree(); + app.node.prepareTree(); }, /Adding this dependency would create a cyclic reference/); test.done(); @@ -296,7 +297,7 @@ export = { // WHEN new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); - app.prepareConstructTree(); + app.node.prepareTree(); // THEN test.deepEqual(stack2.dependencies().map(s => s.node.id), ['Stack1']); @@ -315,11 +316,35 @@ export = { new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); test.throws(() => { - app.prepareConstructTree(); + app.node.prepareTree(); }, /Can only reference cross stacks in the same region and account/); test.done(); }, + + 'stack with region supplied via props returns literal value'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'Stack1', { env: { account: '123456789012', region: 'es-norst-1' }}); + + // THEN + test.equal(stack.node.resolve(stack.region), 'es-norst-1'); + + test.done(); + }, + + 'stack with region supplied via context returns symbolic value'(test: Test) { + // GIVEN + const app = new App(); + + app.node.setContext(cxapi.DEFAULT_REGION_CONTEXT_KEY, 'es-norst-1'); + const stack = new Stack(app, 'Stack1'); + + // THEN + test.deepEqual(stack.node.resolve(stack.region), { Ref: 'AWS::Region' }); + + test.done(); + }, }; class StackWithPostProcessor extends Stack { diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index 2fa0ed4e4f08d..dfe4a54498b0b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -348,6 +348,21 @@ export = { 'Fn::Join': ['/', { Ref: 'Other'}] }); + test.done(); + }, + + 'can pass encoded lists to FnJoin, even if join is stringified'(test: Test) { + // GIVEN + const encoded: string[] = new Token({ Ref: 'Other' }).toList(); + + // WHEN + const struct = Fn.join('/', encoded).toString(); + + // THEN + test.deepEqual(resolve(struct), { + 'Fn::Join': ['/', { Ref: 'Other'}] + }); + test.done(); } } diff --git a/packages/@aws-cdk/cdk/test/test.app.ts b/packages/@aws-cdk/cdk/test/test.app.ts index 272b589583feb..8c55bc8cc4d45 100644 --- a/packages/@aws-cdk/cdk/test/test.app.ts +++ b/packages/@aws-cdk/cdk/test/test.app.ts @@ -78,7 +78,6 @@ export = { { name: '12345/us-east-1', account: '12345', region: 'us-east-1' }, - dependsOn: [], template: { Resources: { s1c1: { Type: 'DummyResource', Properties: { Prop1: 'Prop1' } }, @@ -88,7 +87,6 @@ export = { { name: 'unknown-account/unknown-region', account: 'unknown-account', region: 'unknown-region' }, - dependsOn: [], template: { Resources: { s2c1: { Type: 'DummyResource', Properties: { Prog2: 'Prog2' } }, diff --git a/tools/cdk-integ-tools/bin/cdk-integ.ts b/tools/cdk-integ-tools/bin/cdk-integ.ts index 898f86996423f..96c39794d5c0f 100644 --- a/tools/cdk-integ-tools/bin/cdk-integ.ts +++ b/tools/cdk-integ-tools/bin/cdk-integ.ts @@ -35,7 +35,8 @@ async function main() { } try { - await test.invoke([ ...args, 'deploy', '--prompt', 'never' ], { verbose: argv.verbose }); // Note: no context, so use default user settings! + // tslint:disable-next-line:max-line-length + await test.invoke([ ...args, 'deploy', '--require-approval', 'never' ], { verbose: argv.verbose }); // Note: no context, so use default user settings! console.error(`Success! Writing out reference synth.`); From eef74a5cb4923282f8fea84edfe249f7b794b2e5 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 7 Jan 2019 16:58:29 +0100 Subject: [PATCH 23/39] Also generate private (tools) packages in global tsconfig --- scripts/generate-aggregate-tsconfig.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/generate-aggregate-tsconfig.sh b/scripts/generate-aggregate-tsconfig.sh index 9b6b4649aae86..724186a28663a 100755 --- a/scripts/generate-aggregate-tsconfig.sh +++ b/scripts/generate-aggregate-tsconfig.sh @@ -8,10 +8,12 @@ echo ' "__comment__": "This file is necessary to make transitive Project Refe echo ' "files": [],' echo ' "references": [' comma=' ' -for package in $(node_modules/.bin/lerna ls -p); do - relpath=${package#"$prefix"} - echo ' '"$comma"'{ "path": "'"$relpath"'" }' - comma=', ' +for package in $(node_modules/.bin/lerna ls -ap); do + if [[ -f ${package}/tsconfig.json ]]; then + relpath=${package#"$prefix"} + echo ' '"$comma"'{ "path": "'"$relpath"'" }' + comma=', ' + fi done echo ' ]' echo '}' From 5978c6b037ccdb20187914759cec8a88281f0ee7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 7 Jan 2019 17:09:03 +0100 Subject: [PATCH 24/39] Fix examples --- .../advanced-usage/index.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/cdk-examples-typescript/advanced-usage/index.ts b/examples/cdk-examples-typescript/advanced-usage/index.ts index 20fb650ebbe2c..0bd33a422e47e 100644 --- a/examples/cdk-examples-typescript/advanced-usage/index.ts +++ b/examples/cdk-examples-typescript/advanced-usage/index.ts @@ -157,7 +157,7 @@ class CloudFormationExample extends cdk.Stack { // outputs are constructs the synthesize into the template's "Outputs" section new cdk.Output(this, 'Output', { description: 'This is an output of the template', - value: `${new cdk.AwsAccountId()}/${param.ref}` + value: `${this.accountId}/${param.ref}` }); // stack.templateOptions can be used to specify template-level options @@ -166,14 +166,13 @@ class CloudFormationExample extends cdk.Stack { // all CloudFormation's pseudo-parameters are supported via the `cdk.AwsXxx` classes PseudoParameters: [ - new cdk.AwsAccountId(), - new cdk.AwsDomainSuffix(), - new cdk.AwsNotificationARNs(), - new cdk.AwsNoValue(), - new cdk.AwsPartition(), - new cdk.AwsRegion(), - new cdk.AwsStackId(), - new cdk.AwsStackName(), + this.accountId, + this.urlSuffix, + this.notificationArns, + this.partition, + this.region, + this.stackId, + this.stackName, ], // all CloudFormation's intrinsic functions are supported via the `cdk.Fn.xxx` static methods. From e0ec5216de5767b08cd66f373ed0b1be06484728 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 10:41:17 +0100 Subject: [PATCH 25/39] Review comments --- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 8 --- .../aws-apigateway/lib/integrations/aws.ts | 10 ++-- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 2 - .../lib/pipeline-actions.ts | 28 +++++----- .../test/test.pipeline-actions.ts | 4 +- .../aws-codedeploy/lib/application.ts | 4 +- .../aws-codedeploy/lib/deployment-config.ts | 18 +++---- .../aws-codedeploy/lib/deployment-group.ts | 4 +- .../lib/cross-region-scaffold-stack.ts | 2 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 4 +- .../@aws-cdk/aws-codepipeline/lib/stage.ts | 2 +- .../aws-codepipeline/test/test.action.ts | 2 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 4 +- .../@aws-cdk/aws-iam/lib/managed-policy.ts | 4 +- .../@aws-cdk/aws-iam/lib/policy-document.ts | 20 +++---- .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 12 ++--- .../lib/cloudformation/cloudformation-json.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 52 +++++++++---------- .../cdk/lib/cloudformation/stack-element.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 8 +-- packages/@aws-cdk/cdk/lib/core/construct.ts | 4 +- .../cdk/lib/core/tokens/cfn-tokens.ts | 10 ++-- .../@aws-cdk/cdk/lib/core/tokens/token.ts | 2 +- packages/@aws-cdk/cdk/lib/runtime.ts | 6 +-- 24 files changed, 103 insertions(+), 111 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index e6dfe7efc5ac8..4aef0dd03306a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -128,14 +128,6 @@ class LatestDeploymentResource extends CfnDeployment { } return this.logicalIdToken.toString(); - - /* - if (!this.lazyLogicalId) { - throw new Error('This resource has a lazy logical ID which is calculated just before synthesis. Use a cdk.Token to evaluate'); - } - - return this.lazyLogicalId; - */ } /** diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index b7911de68c6b5..4f37f6333b2c3 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -62,7 +62,7 @@ export interface AwsIntegrationProps { * technology. */ export class AwsIntegration extends Integration { - private _anchor?: cdk.IConstruct; + private scope?: cdk.IConstruct; constructor(props: AwsIntegrationProps) { const backend = props.subdomain ? `${props.subdomain}.${props.service}` : props.service; @@ -72,20 +72,20 @@ export class AwsIntegration extends Integration { type, integrationHttpMethod: 'POST', uri: new cdk.Token(() => { - if (!this._anchor) { throw new Error('AwsIntegration must be used in API'); } + if (!this.scope) { throw new Error('AwsIntegration must be used in API'); } return cdk.ArnUtils.fromComponents({ service: 'apigateway', account: backend, resource: apiType, sep: '/', resourceName: apiValue, - }, this._anchor); + }, this.scope); }), options: props.options, }); } - public bind(_method: Method) { - this._anchor = _method; + public bind(method: Method) { + this.scope = method; } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index c86aa07e8439c..84ba238833c20 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -405,8 +405,6 @@ export enum EndpointType { Private = 'PRIVATE' } -export class RestApiUrl extends cdk.Token { } - class ImportedRestApi extends cdk.Construct implements IRestApi { public restApiId: string; diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index 1f03509973f15..a5ea5c4cb714f 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -410,7 +410,7 @@ class SingletonPolicy extends cdk.Construct { this.statementFor({ actions: ['cloudformation:ExecuteChangeSet'], conditions: { StringEquals: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResource(stackArnFromProps(props, this)); + }).addResource(this.stackArnFromProps(props)); } public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void { @@ -422,7 +422,7 @@ class SingletonPolicy extends cdk.Construct { 'cloudformation:DescribeStacks', ], conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResource(stackArnFromProps(props, this)); + }).addResource(this.stackArnFromProps(props)); } public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean, region?: string }): void { @@ -438,7 +438,7 @@ class SingletonPolicy extends cdk.Construct { if (props.replaceOnFailure) { actions.push('cloudformation:DeleteStack'); } - this.statementFor({ actions }).addResource(stackArnFromProps(props, this)); + this.statementFor({ actions }).addResource(this.stackArnFromProps(props)); } public grantDeleteStack(props: { stackName: string, region?: string }): void { @@ -447,7 +447,7 @@ class SingletonPolicy extends cdk.Construct { 'cloudformation:DescribeStack*', 'cloudformation:DeleteStack', ] - }).addResource(stackArnFromProps(props, this)); + }).addResource(this.stackArnFromProps(props)); } public grantPassRole(role: iam.IRole): void { @@ -485,6 +485,15 @@ class SingletonPolicy extends cdk.Construct { } } } + + private stackArnFromProps(props: { stackName: string, region?: string }): string { + return cdk.ArnUtils.fromComponents({ + region: props.region, + service: 'cloudformation', + resource: 'stack', + resourceName: `${props.stackName}/*` + }, this); + } } interface StatementTemplate { @@ -492,13 +501,4 @@ interface StatementTemplate { conditions?: StatementCondition; } -type StatementCondition = { [op: string]: { [attribute: string]: string } }; - -function stackArnFromProps(props: { stackName: string, region?: string }, anchor: cdk.IConstruct): string { - return cdk.ArnUtils.fromComponents({ - region: props.region, - service: 'cloudformation', - resource: 'stack', - resourceName: `${props.stackName}/*` - }, anchor); -} +type StatementCondition = { [op: string]: { [attribute: string]: string } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 789f6009ee7c0..7f4251f75260c 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -271,12 +271,12 @@ function _isOrContains(entity: string | string[], value: string): boolean { return false; } -function _stackArn(stackName: string, anchor: cdk.IConstruct): string { +function _stackArn(stackName: string, scope: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'cloudformation', resource: 'stack', resourceName: `${stackName}/*`, - }, anchor); + }, scope); } class PipelineDouble extends cdk.Construct implements cpapi.IPipeline { diff --git a/packages/@aws-cdk/aws-codedeploy/lib/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/application.ts index b0dad72e75056..31327c70c200e 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/application.ts @@ -100,11 +100,11 @@ export class ServerApplication extends cdk.Construct implements IServerApplicati } } -function applicationNameToArn(applicationName: string, anchor: cdk.IConstruct): string { +function applicationNameToArn(applicationName: string, scope: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'codedeploy', resource: 'application', resourceName: applicationName, sep: ':', - }, anchor); + }, scope); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts index c10bacbaf5a46..241db739f0a26 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts @@ -10,7 +10,7 @@ import { CfnDeploymentConfig } from './codedeploy.generated'; */ export interface IServerDeploymentConfig { readonly deploymentConfigName: string; - deploymentConfigArn(anchor: cdk.IConstruct): string; + deploymentConfigArn(scope: cdk.IConstruct): string; export(): ServerDeploymentConfigImportProps; } @@ -37,8 +37,8 @@ class ImportedServerDeploymentConfig extends cdk.Construct implements IServerDep this.deploymentConfigName = props.deploymentConfigName; } - public deploymentConfigArn(anchor: cdk.IConstruct): string { - return arnForDeploymentConfigName(this.deploymentConfigName, anchor); + public deploymentConfigArn(scope: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, scope); } public export() { @@ -53,8 +53,8 @@ class DefaultServerDeploymentConfig implements IServerDeploymentConfig { this.deploymentConfigName = deploymentConfigName; } - public deploymentConfigArn(anchor: cdk.IConstruct): string { - return arnForDeploymentConfigName(this.deploymentConfigName, anchor); + public deploymentConfigArn(scope: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, scope); } public export(): ServerDeploymentConfigImportProps { @@ -126,8 +126,8 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl this.deploymentConfigName = resource.ref.toString(); } - public deploymentConfigArn(anchor: cdk.IConstruct): string { - return arnForDeploymentConfigName(this.deploymentConfigName, anchor); + public deploymentConfigArn(scope: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, scope); } public export(): ServerDeploymentConfigImportProps { @@ -156,11 +156,11 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl } } -function arnForDeploymentConfigName(name: string, anchor: cdk.IConstruct): string { +function arnForDeploymentConfigName(name: string, scope: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'codedeploy', resource: 'deploymentconfig', resourceName: name, sep: ':', - }, anchor); + }, scope); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index 8e963ae2423dd..f01555f95f58e 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -560,11 +560,11 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { } } -function deploymentGroupNameToArn(applicationName: string, deploymentGroupName: string, anchor: cdk.IConstruct): string { +function deploymentGroupNameToArn(applicationName: string, deploymentGroupName: string, scope: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'codedeploy', resource: 'deploymentgroup', resourceName: `${applicationName}/${deploymentGroupName}`, sep: ':', - }, anchor); + }, scope); } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts index 43d37c1f388a6..428ba07df430e 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts @@ -51,7 +51,7 @@ function generateStackName(props: CrossRegionScaffoldStackProps): string { } function generateUniqueName(baseName: string, region: string, account: string, - toUpperCase: boolean, hashPartLen: number = 8): string { + toUpperCase: boolean, hashPartLen: e = 8): string { const sha256 = crypto.createHash('sha256') .update(baseName) .update(region) diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index bb42c26606c16..08c9a5f63e0a6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -230,7 +230,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { /** * Get the number of Stages in this Pipeline. */ - public get stageCount(): number { + public get stageCount(): e { return this.stages.length; } @@ -369,7 +369,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { 'Please provide it explicitly with the inputArtifact property.'); } - private calculateInsertIndexFromPlacement(placement: StagePlacement): number { + private calculateInsertIndexFromPlacement(placement: StagePlacement): e { // check if at most one placement property was provided const providedPlacementProps = ['rightBefore', 'justAfter', 'atIndex'] .filter((prop) => (placement as any)[prop] !== undefined); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index ee51f5c5fa33b..e5e0c3936b485 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -34,7 +34,7 @@ export interface StagePlacement { * The maximum allowed value is {@link Pipeline#stageCount}, * which will insert the new Stage at the end of the Pipeline. */ - readonly atIndex?: number; + readonly atIndex?: e; } /** diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts index 33e2709e700e2..5510db37d6543 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts @@ -119,7 +119,7 @@ export = { }, }; -function boundsValidationResult(numberOfArtifacts: number, min: number, max: number): string[] { +function boundsValidationResult(numberOfArtifacts: e, min: e, max: e): string[] { const stack = new cdk.Stack(); const pipeline = new codepipeline.Pipeline(stack, 'pipeline'); const stage = new codepipeline.Stage(stack, 'stage', { pipeline }); diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 03b586b6cf55a..c90f07f0f53f2 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -122,12 +122,12 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor * Returns an ECR ARN for a repository that resides in the same account/region * as the current stack. */ - public static arnForLocalRepository(repositoryName: string, anchor: cdk.IConstruct): string { + public static arnForLocalRepository(repositoryName: string, scope: cdk.IConstruct): string { return cdk.ArnUtils.fromComponents({ service: 'ecr', resource: 'repository', resourceName: repositoryName - }, anchor); + }, scope); } /** diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index 604d0156030e3..46646db8284fc 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -16,7 +16,7 @@ export class AwsManagedPolicy { /** * The Arn of this managed policy */ - public policyArn(anchor: cdk.IConstruct): string { + public policyArn(scope: cdk.IConstruct): string { // the arn is in the form of - arn:aws:iam::aws:policy/ return cdk.ArnUtils.fromComponents({ service: "iam", @@ -24,6 +24,6 @@ export class AwsManagedPolicy { account: "aws", // the account for a managed policy is 'aws' resource: "policy", resourceName: this.managedPolicyName - }, anchor); + }, scope); } } diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index be8578eb7bb5a..f2f190294f738 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -12,7 +12,7 @@ export class PolicyDocument extends cdk.Token { super(); } - public resolve(_context: cdk.ResolveContext): any { + public scope(_context: cdk.ResolveContext): any { if (this.isEmpty) { return undefined; } @@ -81,8 +81,8 @@ export class ArnPrincipal extends PolicyPrincipal { } export class AccountPrincipal extends ArnPrincipal { - constructor(public readonly anchor: cdk.Construct, public readonly accountId: any) { - super(`arn:${new cdk.Aws(anchor).partition}:iam::${accountId}:root`); + constructor(public readonly scope: cdk.Construct, public readonly accountId: any) { + super(`arn:${new cdk.Aws(scope).partition}:iam::${accountId}:root`); } } @@ -136,8 +136,8 @@ export class FederatedPrincipal extends PolicyPrincipal { } export class AccountRootPrincipal extends AccountPrincipal { - constructor(anchor: cdk.Construct) { - super(anchor, new cdk.Aws(anchor).accountId); + constructor(scope: cdk.Construct) { + super(scope, new cdk.Aws(scope).scope); } } @@ -250,8 +250,8 @@ export class PolicyStatement extends cdk.Token { return this.addPrincipal(new ArnPrincipal(arn)); } - public addAwsAccountPrincipal(anchor: cdk.Construct, accountId: string): this { - return this.addPrincipal(new AccountPrincipal(anchor, accountId)); + public addAwsAccountPrincipal(scope: cdk.Construct, accountId: string): this { + return this.addPrincipal(new AccountPrincipal(scope, accountId)); } public addArnPrincipal(arn: string): this { @@ -266,8 +266,8 @@ export class PolicyStatement extends cdk.Token { return this.addPrincipal(new FederatedPrincipal(federated, conditions)); } - public addAccountRootPrincipal(anchor: cdk.Construct): this { - return this.addPrincipal(new AccountRootPrincipal(anchor)); + public addAccountRootPrincipal(scope: cdk.Construct): this { + return this.addPrincipal(new AccountRootPrincipal(scope)); } public addCanonicalUserPrincipal(canonicalUserId: string): this { @@ -372,7 +372,7 @@ export class PolicyStatement extends cdk.Token { // Serialization // - public resolve(_context: cdk.ResolveContext): any { + public scope(_context: cdk.ResolveContext): any { return this.toJson(); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index fa1c8e3843e17..f874141b7eb78 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -22,10 +22,10 @@ export class ArnUtils { * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} * * The required ARN pieces that are omitted will be taken from the stack that - * the 'anchor' is attached to. If all ARN pieces are supplied, the supplied anchor + * the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope * can be 'undefined'. */ - public static fromComponents(components: ArnComponents, anchor: IConstruct | undefined): string { + public static fromComponents(components: ArnComponents, scope: IConstruct | undefined): string { const partition = components.partition !== undefined ? components.partition : theStack('partition').partition; const region = components.region !== undefined ? components.region : theStack('region').region; const account = components.account !== undefined ? components.account : theStack('account').accountId; @@ -45,13 +45,13 @@ export class ArnUtils { return values.join(''); /** - * Return the anchored stack (so the caller can get an attribute from it), throw a descriptive error if we don't have an anchor + * Return the stack we're scoped to (so the caller can get an attribute from it), throw a descriptive error if we don't have a scope */ function theStack(attribute: string) { - if (!anchor) { - throw new Error(`Must provide anchor when using implicit ${attribute}`); + if (!scope) { + throw new Error(`Must provide scope when using implicit ${attribute}`); } - return Stack.find(anchor); + return Stack.find(scope); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts index d3640bb6782b3..3b9ef94bda4c2 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts @@ -32,7 +32,7 @@ export class CloudFormationJSON { // strings are used in {Fn::Join} or something, they will end up // escaped in the final JSON output. const resolved = resolve(obj, { - construct: context, + scope: context, prefix: [] }); diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 10de9d1e80406..847bf828f6aca 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -6,63 +6,63 @@ import { CfnReference } from '../core/tokens/cfn-tokens'; * Accessor for pseudo parameters * * Since pseudo parameters need to be anchored to a stack somewhere in the - * construct tree, this class takes an anchor parameter; the pseudo parameter - * values can be obtained as properties from an anchored object. + * construct tree, this class takes an scope parameter; the pseudo parameter + * values can be obtained as properties from an scoped object. */ export class Aws { - constructor(private readonly anchor: Construct) { + constructor(private readonly scope: Construct) { } public get accountId(): string { - return new AwsAccountId(this.anchor).toString(); + return new AwsAccountId(this.scope).toString(); } public get urlSuffix(): string { - return new AwsURLSuffix(this.anchor).toString(); + return new AwsURLSuffix(this.scope).toString(); } public get notificationArns(): string[] { - return new AwsNotificationARNs(this.anchor).toList(); + return new AwsNotificationARNs(this.scope).toList(); } public get partition(): string { - return new AwsPartition(this.anchor).toString(); + return new AwsPartition(this.scope).toString(); } public get region(): string { - return new AwsRegion(this.anchor).toString(); + return new AwsRegion(this.scope).toString(); } public get stackId(): string { - return new AwsStackId(this.anchor).toString(); + return new AwsStackId(this.scope).toString(); } public get stackName(): string { - return new AwsStackName(this.anchor).toString(); + return new AwsStackName(this.scope).toString(); } } class PseudoParameter extends CfnReference { - constructor(name: string, anchor: Construct) { - super({ Ref: name }, name, anchor); + constructor(name: string, scope: Construct) { + super({ Ref: name }, name, scope); } } class AwsAccountId extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::AccountId', anchor); + constructor(scope: Construct) { + super('AWS::AccountId', scope); } } class AwsURLSuffix extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::URLSuffix', anchor); + constructor(scope: Construct) { + super('AWS::URLSuffix', scope); } } class AwsNotificationARNs extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::NotificationARNs', anchor); + constructor(scope: Construct) { + super('AWS::NotificationARNs', scope); } } @@ -73,25 +73,25 @@ export class AwsNoValue extends Token { } class AwsPartition extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::Partition', anchor); + constructor(scope: Construct) { + super('AWS::Partition', scope); } } class AwsRegion extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::Region', anchor); + constructor(scope: Construct) { + super('AWS::Region', scope); } } class AwsStackId extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::StackId', anchor); + constructor(scope: Construct) { + super('AWS::StackId', scope); } } class AwsStackName extends PseudoParameter { - constructor(anchor: Construct) { - super('AWS::StackName', anchor); + constructor(scope: Construct) { + super('AWS::StackName', scope); } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts index a4280e54b0375..d80fd2ed658de 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts @@ -127,7 +127,7 @@ export abstract class StackElement extends Construct implements IDependable { // before validate(). Swallow errors that occur because the CFN layer doesn't validate completely. this.node.resolve(this.toCloudFormation()); } catch (e) { - if (e.type !== 'CloudFormationSynthesisError') { throw e; } + if (e.type !== 'CfnSynthesisError') { throw e; } } finally { options.pop(); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index fbd2cd222dd3a..a7d0ca34149d5 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -30,17 +30,17 @@ export interface StackProps { export class Stack extends Construct { /** * Traverses the tree and looks up for the Stack root. - * @param node A construct in the tree + * @param scope A construct in the tree * @returns The Stack object (throws if the node is not part of a Stack-rooted tree) */ - public static find(node: IConstruct): Stack { - let curr: IConstruct | undefined = node; + public static find(scope: IConstruct): Stack { + let curr: IConstruct | undefined = scope; while (curr != null && !Stack.isStack(curr)) { curr = curr.node.scope; } if (curr == null) { - throw new Error(`Cannot find a Stack parent for '${node.toString()}'`); + throw new Error(`Cannot find a Stack parent for '${scope.toString()}'`); } return curr; } diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index f16c5ba6b46a1..ab3ca3f97cea5 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -384,7 +384,7 @@ export class ConstructNode { */ public resolve(obj: any): any { return resolve(obj, { - construct: this.host, + scope: this.host, prefix: [] }); } @@ -502,6 +502,8 @@ export class Construct implements IConstruct { * final changes before synthesis. prepare() will be called after child * constructs have been prepared. * + * This is an advanced framework feature. Only use this if you + * understand the implications. */ protected prepare(): void { // Intentionally left blank diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts index 183a438a4eb2f..23890c8d24905 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts @@ -13,22 +13,22 @@ export class CfnReference extends Token { private readonly tokenStack?: Stack; private readonly replacementTokens: Map; - constructor(value: any, displayName?: string, anchor?: Construct) { + constructor(value: any, displayName?: string, scope?: Construct) { if (typeof(value) === 'function') { - throw new Error('CfnReference can only contain eager values'); + throw new Error('CfnReference can only hold CloudFormation intrinsics (not a function)'); } super(value, displayName); this.referenceType = CfnReference.ReferenceType; this.replacementTokens = new Map(); - if (anchor !== undefined) { - this.tokenStack = Stack.find(anchor); + if (scope !== undefined) { + this.tokenStack = Stack.find(scope); } } public resolve(context: ResolveContext): any { // If we have a special token for this stack, resolve that instead, otherwise resolve the original - const token = this.replacementTokens.get(Stack.find(context.construct)); + const token = this.replacementTokens.get(Stack.find(context.scope)); if (token) { return token.resolve(context); } else { diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts index 3bbd19a645873..2776ee0030a1c 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts @@ -125,6 +125,6 @@ export class Token { * Current resolution context for tokens */ export interface ResolveContext { - construct: IConstruct; + scope: IConstruct; prefix: string[]; } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/runtime.ts b/packages/@aws-cdk/cdk/lib/runtime.ts index 7ec4c167c1f2f..1524a5443a051 100644 --- a/packages/@aws-cdk/cdk/lib/runtime.ts +++ b/packages/@aws-cdk/cdk/lib/runtime.ts @@ -138,7 +138,7 @@ export class ValidationResult { let message = this.errorTree(); // The first letter will be lowercase, so uppercase it for a nicer error message message = message.substr(0, 1).toUpperCase() + message.substr(1); - throw new CloudFormationSynthesisError(message); + throw new CfnSynthesisError(message); } } @@ -385,6 +385,6 @@ function isCloudFormationIntrinsic(x: any) { return keys[0] === 'Ref' || keys[0].substr(0, 4) === 'Fn::'; } -export class CloudFormationSynthesisError extends Error { - public readonly type = 'CloudFormationSynthesisError'; +export class CfnSynthesisError extends Error { + public readonly type = 'CfnSynthesisError'; } \ No newline at end of file From 3a0d5b5cf98e3cbcbd6139ef1e07135b4bece5f7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 13:06:49 +0100 Subject: [PATCH 26/39] Fix compilation --- .../aws-codepipeline/lib/cross-region-scaffold-stack.ts | 2 +- packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts | 4 ++-- packages/@aws-cdk/aws-codepipeline/lib/stage.ts | 2 +- packages/@aws-cdk/aws-codepipeline/test/test.action.ts | 2 +- packages/@aws-cdk/aws-iam/lib/policy-document.ts | 2 +- .../cdk/lib/{core/tokens => cloudformation}/cfn-tokens.ts | 8 ++++---- packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 2 +- packages/@aws-cdk/cdk/lib/cloudformation/resource.ts | 2 +- packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts | 2 +- packages/@aws-cdk/cdk/lib/cloudformation/stack.ts | 2 +- packages/@aws-cdk/cdk/lib/core/tag-manager.ts | 4 ++-- packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts | 4 ++-- packages/@aws-cdk/cdk/lib/index.ts | 1 + packages/@aws-cdk/cdk/lib/runtime.ts | 3 ++- 14 files changed, 21 insertions(+), 19 deletions(-) rename packages/@aws-cdk/cdk/lib/{core/tokens => cloudformation}/cfn-tokens.ts (89%) diff --git a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts index 428ba07df430e..43d37c1f388a6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts @@ -51,7 +51,7 @@ function generateStackName(props: CrossRegionScaffoldStackProps): string { } function generateUniqueName(baseName: string, region: string, account: string, - toUpperCase: boolean, hashPartLen: e = 8): string { + toUpperCase: boolean, hashPartLen: number = 8): string { const sha256 = crypto.createHash('sha256') .update(baseName) .update(region) diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 08c9a5f63e0a6..bb42c26606c16 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -230,7 +230,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { /** * Get the number of Stages in this Pipeline. */ - public get stageCount(): e { + public get stageCount(): number { return this.stages.length; } @@ -369,7 +369,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { 'Please provide it explicitly with the inputArtifact property.'); } - private calculateInsertIndexFromPlacement(placement: StagePlacement): e { + private calculateInsertIndexFromPlacement(placement: StagePlacement): number { // check if at most one placement property was provided const providedPlacementProps = ['rightBefore', 'justAfter', 'atIndex'] .filter((prop) => (placement as any)[prop] !== undefined); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index e5e0c3936b485..ee51f5c5fa33b 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -34,7 +34,7 @@ export interface StagePlacement { * The maximum allowed value is {@link Pipeline#stageCount}, * which will insert the new Stage at the end of the Pipeline. */ - readonly atIndex?: e; + readonly atIndex?: number; } /** diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts index 5510db37d6543..33e2709e700e2 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts @@ -119,7 +119,7 @@ export = { }, }; -function boundsValidationResult(numberOfArtifacts: e, min: e, max: e): string[] { +function boundsValidationResult(numberOfArtifacts: number, min: number, max: number): string[] { const stack = new cdk.Stack(); const pipeline = new codepipeline.Pipeline(stack, 'pipeline'); const stage = new codepipeline.Stage(stack, 'stage', { pipeline }); diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index f2f190294f738..47c33799f60cf 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -137,7 +137,7 @@ export class FederatedPrincipal extends PolicyPrincipal { export class AccountRootPrincipal extends AccountPrincipal { constructor(scope: cdk.Construct) { - super(scope, new cdk.Aws(scope).scope); + super(scope, new cdk.Aws(scope).accountId); } } diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts similarity index 89% rename from packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts rename to packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts index 23890c8d24905..84a6818d462b9 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-tokens.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts @@ -1,4 +1,4 @@ -import { ResolveContext, Token } from "./token"; +import { ResolveContext, Token } from "../core/tokens"; /** * A Token that represents a CloudFormation reference to another resource @@ -7,7 +7,7 @@ export class CfnReference extends Token { /** * The reference type for instances of this class */ - public static ReferenceType = 'cfn-reference'; + public static readonly ReferenceType = 'cfn-reference'; public readonly referenceType?: string; private readonly tokenStack?: Stack; @@ -48,5 +48,5 @@ export class CfnReference extends Token { } } -import { Stack } from "../../cloudformation/stack"; -import { Construct } from "../construct"; \ No newline at end of file +import { Construct } from "../core/construct"; +import { Stack } from "./stack"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 847bf828f6aca..7e1a312ae49ab 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { CfnReference } from '../core/tokens/cfn-tokens'; +import { CfnReference } from './cfn-tokens'; /** * Accessor for pseudo parameters diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index f10239e43c99f..a2a4243dde2ac 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; -import { CfnReference } from '../core/tokens/cfn-tokens'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; +import { CfnReference } from './cfn-tokens'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; import { IDependable, Referenceable, StackElement } from './stack-element'; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts index d80fd2ed658de..8d5721a43a782 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts @@ -134,7 +134,7 @@ export abstract class StackElement extends Construct implements IDependable { } } -import { CfnReference } from "../core/tokens/cfn-tokens"; +import { CfnReference } from "./cfn-tokens"; /** * A generic, untyped reference to a Stack Element diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index a7d0ca34149d5..504233566aa79 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -2,8 +2,8 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; import { Construct, IConstruct } from '../core/construct'; import { Token } from '../core/tokens'; -import { CfnReference } from '../core/tokens/cfn-tokens'; import { Environment } from '../environment'; +import { CfnReference } from './cfn-tokens'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts index 8ee0ab75b07e3..7f5f6c57d3814 100644 --- a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -1,5 +1,5 @@ import { Construct, IConstruct } from './construct'; -import { Token } from './tokens'; +import { ResolveContext, Token } from './tokens'; /** * ITaggable indicates a entity manages tags via the `tags` property @@ -171,7 +171,7 @@ export class TagManager extends Token { /** * Converts the `tags` to a Token for use in lazy evaluation */ - public resolve(): any { + public resolve(_context: ResolveContext): any { // need this for scoping const blockedTags = this.blockedTags; function filterTags(_tags: FullTags, filter: TagProps = {}): Tags { diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts index 5c47f0f57c008..301698214dbb9 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts @@ -1,4 +1,3 @@ -import { cloudFormationConcat } from "./cfn-concat"; import { resolve } from "./resolve"; import { ResolveContext, Token } from "./token"; import { unresolved } from "./unresolved"; @@ -61,7 +60,8 @@ export class TokenMap { public resolveStringTokens(s: string, context: ResolveContext): any { const str = this.createStringTokenString(s); const fragments = str.split(this.lookupToken.bind(this)); - const ret = fragments.mapUnresolved(x => resolve(x, context)).join(cloudFormationConcat); + // require() here to break cyclic dependencies + const ret = fragments.mapUnresolved(x => resolve(x, context)).join(require('./cfn-concat').cloudFormationConcat); if (unresolved(ret)) { return resolve(ret, context); } diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 695e59c7b56bc..72e4e00592b2d 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -3,6 +3,7 @@ export * from './core/tokens'; export * from './core/tag-manager'; export * from './cloudformation/cloudformation-json'; +export * from './cloudformation/cfn-tokens'; export * from './cloudformation/condition'; export * from './cloudformation/fn'; export * from './cloudformation/include'; diff --git a/packages/@aws-cdk/cdk/lib/runtime.ts b/packages/@aws-cdk/cdk/lib/runtime.ts index 1524a5443a051..e8fb8ba893064 100644 --- a/packages/@aws-cdk/cdk/lib/runtime.ts +++ b/packages/@aws-cdk/cdk/lib/runtime.ts @@ -385,6 +385,7 @@ function isCloudFormationIntrinsic(x: any) { return keys[0] === 'Ref' || keys[0].substr(0, 4) === 'Fn::'; } -export class CfnSynthesisError extends Error { +// Cannot be public because JSII gets confused about es5.d.ts +class CfnSynthesisError extends Error { public readonly type = 'CfnSynthesisError'; } \ No newline at end of file From 1d5388dfdd10ab9796315ea2cb1476fa504f2c2f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 14:12:46 +0100 Subject: [PATCH 27/39] Review comments --- packages/@aws-cdk/aws-cloudwatch/lib/graph.ts | 6 +- .../@aws-cdk/aws-iam/lib/managed-policy.ts | 6 +- .../aws-iam/test/test.managed-policy.ts | 4 +- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 4 +- .../cdk/lib/cloudformation/cfn-tokens.ts | 84 ++++++++++++++++--- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 24 +++--- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 40 +-------- packages/@aws-cdk/cdk/lib/core/construct.ts | 75 +++++++++-------- .../@aws-cdk/cdk/lib/core/tokens/token.ts | 7 +- 9 files changed, 143 insertions(+), 107 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts index 89ee6bd233994..55b4808b25bd1 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts @@ -73,7 +73,7 @@ export class AlarmWidget extends ConcreteWidget { properties: { view: 'timeSeries', title: this.props.title, - region: this.props.region || new cdk.Token({ Ref: 'AWS::Region' }), + region: this.props.region || new cdk.Aws().region, annotations: { alarms: [this.props.alarm.alarmArn] }, @@ -150,7 +150,7 @@ export class GraphWidget extends ConcreteWidget { properties: { view: 'timeSeries', title: this.props.title, - region: this.props.region || new cdk.Token({ Ref: 'AWS::Region' }), + region: this.props.region || new cdk.Aws().region, metrics: (this.props.left || []).map(m => metricJson(m, 'left')).concat( (this.props.right || []).map(m => metricJson(m, 'right'))), annotations: { @@ -197,7 +197,7 @@ export class SingleValueWidget extends ConcreteWidget { properties: { view: 'singleValue', title: this.props.title, - region: this.props.region || new cdk.Token({ Ref: 'AWS::Region' }), + region: this.props.region || new cdk.Aws().region, metrics: this.props.metrics.map(m => metricJson(m, 'left')) } }]; diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index 46646db8284fc..d60c98824a1c4 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -10,13 +10,13 @@ import cdk = require('@aws-cdk/cdk'); * prefix when constructing this object. */ export class AwsManagedPolicy { - constructor(private readonly managedPolicyName: string) { + constructor(private readonly managedPolicyName: string, private readonly scope: cdk.IConstruct) { } /** * The Arn of this managed policy */ - public policyArn(scope: cdk.IConstruct): string { + public get policyArn(): string { // the arn is in the form of - arn:aws:iam::aws:policy/ return cdk.ArnUtils.fromComponents({ service: "iam", @@ -24,6 +24,6 @@ export class AwsManagedPolicy { account: "aws", // the account for a managed policy is 'aws' resource: "policy", resourceName: this.managedPolicyName - }, scope); + }, this.scope); } } diff --git a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts index 4ac13bcfc270f..142a239febaee 100644 --- a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts @@ -5,9 +5,9 @@ import { AwsManagedPolicy } from '../lib'; export = { 'simple managed policy'(test: Test) { const stack = new cdk.Stack(); - const mp = new AwsManagedPolicy("service-role/SomePolicy"); + const mp = new AwsManagedPolicy("service-role/SomePolicy", stack); - test.deepEqual(stack.node.resolve(mp.policyArn(stack)), { + test.deepEqual(stack.node.resolve(mp.policyArn), { "Fn::Join": ['', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 280b42102c47c..a1eaaa906fa8c 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -311,11 +311,11 @@ export class Function extends FunctionBase { const managedPolicyArns = new Array(); // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaBasicExecutionRole").policyArn(this)); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaBasicExecutionRole", this).policyArn); if (props.vpc) { // Policy that will have ENI creation permissions - managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn(this)); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole", this).policyArn); } this.role = props.role || new iam.Role(this, 'ServiceRole', { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts index 84a6818d462b9..44e184bbbc8ee 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts @@ -2,15 +2,36 @@ import { ResolveContext, Token } from "../core/tokens"; /** * A Token that represents a CloudFormation reference to another resource + * + * If these references are used in a different stack from where they are + * defined, appropriate CloudFormation `Export`s and `Fn::ImportValue`s will be + * synthesized automatically instead of the regular CloudFormation references. + * + * Additionally, the dependency between the stacks will be recorded, and the toolkit + * will make sure to deploy producing stack before the consuming stack. + * + * This magic happens in the prepare() phase, where consuming stacks will call + * `consumeFromStack` on these Tokens and if they happen to be exported by a different + * Stack, we'll register the dependency. */ export class CfnReference extends Token { /** - * The reference type for instances of this class + * Check whether this is actually a CfnReference */ - public static readonly ReferenceType = 'cfn-reference'; + public static isInstance(x: Token): x is CfnReference { + return (x as any).consumeFromStack !== undefined; + } + + public readonly isReference?: boolean; + + /** + * What stack this Token is pointing to + */ + private readonly producingStack?: Stack; - public readonly referenceType?: string; - private readonly tokenStack?: Stack; + /** + * The Tokens that should be returned for each consuming stack (as decided by the producing Stack) + */ private readonly replacementTokens: Map; constructor(value: any, displayName?: string, scope?: Construct) { @@ -18,16 +39,17 @@ export class CfnReference extends Token { throw new Error('CfnReference can only hold CloudFormation intrinsics (not a function)'); } super(value, displayName); - this.referenceType = CfnReference.ReferenceType; this.replacementTokens = new Map(); + this.isReference = true; if (scope !== undefined) { - this.tokenStack = Stack.find(scope); + this.producingStack = Stack.find(scope); } } public resolve(context: ResolveContext): any { - // If we have a special token for this stack, resolve that instead, otherwise resolve the original + // If we have a special token for this consuming stack, resolve that. Otherwise resolve as if + // we are in the same stack. const token = this.replacementTokens.get(Stack.find(context.scope)); if (token) { return token.resolve(context); @@ -37,16 +59,54 @@ export class CfnReference extends Token { } /** - * In a consuming context, potentially substitute this Token with a different one + * Register a stack this references is being consumed from. */ public consumeFromStack(consumingStack: Stack) { - if (this.tokenStack && this.tokenStack !== consumingStack && !this.replacementTokens.has(consumingStack)) { + if (this.producingStack && this.producingStack !== consumingStack && !this.replacementTokens.has(consumingStack)) { // We're trying to resolve a cross-stack reference - consumingStack.addDependency(this.tokenStack); - this.replacementTokens.set(consumingStack, this.tokenStack.exportValue(this, consumingStack)); + consumingStack.addDependency(this.producingStack); + this.replacementTokens.set(consumingStack, this.exportValue(this, consumingStack)); + } + } + + /** + * Export a Token value for use in another stack + * + * Works by mutating the producing stack in-place. + */ + private exportValue(tokenValue: Token, consumingStack: Stack): Token { + const producingStack = this.producingStack!; + + if (producingStack.env.account !== consumingStack.env.account || producingStack.env.region !== consumingStack.env.region) { + throw new Error('Can only reference cross stacks in the same region and account.'); + } + + // Ensure a singleton "Exports" scoping Construct + // This mostly exists to trigger LogicalID munging, which would be + // disabled if we parented constructs directly under Stack. + // Also it nicely prevents likely construct name clashes + + const exportsName = 'Exports'; + let stackExports = producingStack.node.tryFindChild(exportsName) as Construct; + if (stackExports === undefined) { + stackExports = new Construct(producingStack, exportsName); + } + + // Ensure a singleton Output for this value + const resolved = producingStack.node.resolve(tokenValue); + const id = 'Output' + JSON.stringify(resolved); + let output = stackExports.node.tryFindChild(id) as Output; + if (!output) { + output = new Output(stackExports, id, { value: tokenValue }); } + + // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', + // so construct one in-place. + return new Token({ 'Fn::ImportValue': output.export }); } + } import { Construct } from "../core/construct"; -import { Stack } from "./stack"; \ No newline at end of file +import { Output } from "./output"; +import { Stack } from "./stack"; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index 7e1a312ae49ab..e85ac027ad082 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -10,7 +10,7 @@ import { CfnReference } from './cfn-tokens'; * values can be obtained as properties from an scoped object. */ export class Aws { - constructor(private readonly scope: Construct) { + constructor(private readonly scope?: Construct) { } public get accountId(): string { @@ -40,28 +40,32 @@ export class Aws { public get stackName(): string { return new AwsStackName(this.scope).toString(); } + + public get noValue(): string { + return new AwsNoValue().toString(); + } } class PseudoParameter extends CfnReference { - constructor(name: string, scope: Construct) { + constructor(name: string, scope: Construct | undefined) { super({ Ref: name }, name, scope); } } class AwsAccountId extends PseudoParameter { - constructor(scope: Construct) { + constructor(scope: Construct | undefined) { super('AWS::AccountId', scope); } } class AwsURLSuffix extends PseudoParameter { - constructor(scope: Construct) { + constructor(scope: Construct | undefined) { super('AWS::URLSuffix', scope); } } class AwsNotificationARNs extends PseudoParameter { - constructor(scope: Construct) { + constructor(scope: Construct | undefined) { super('AWS::NotificationARNs', scope); } } @@ -73,25 +77,25 @@ export class AwsNoValue extends Token { } class AwsPartition extends PseudoParameter { - constructor(scope: Construct) { + constructor(scope: Construct | undefined) { super('AWS::Partition', scope); } } class AwsRegion extends PseudoParameter { - constructor(scope: Construct) { + constructor(scope: Construct | undefined) { super('AWS::Region', scope); } } class AwsStackId extends PseudoParameter { - constructor(scope: Construct) { + constructor(scope: Construct | undefined) { super('AWS::StackId', scope); } } class AwsStackName extends PseudoParameter { - constructor(scope: Construct) { + constructor(scope: Construct | undefined) { super('AWS::StackName', scope); } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index 504233566aa79..b200f2a15dc12 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -1,7 +1,6 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; import { Construct, IConstruct } from '../core/construct'; -import { Token } from '../core/tokens'; import { Environment } from '../environment'; import { CfnReference } from './cfn-tokens'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; @@ -106,14 +105,6 @@ export class Stack extends Construct { */ private readonly stackDependencies = new Set(); - /** - * A construct to hold cross-stack exports - * - * This mostly exists to trigger LogicalID munging, which would be - * disabled if we parented constructs directly under Stack. - */ - private crossStackExports?: Construct; - /** * Creates a new stack. * @@ -265,30 +256,6 @@ export class Stack extends Construct { return Array.from(this.stackDependencies.values()); } - /** - * Export a Token value for use in another stack - */ - public exportValue(tokenValue: Token, consumingStack: Stack): Token { - if (this.env.account !== consumingStack.env.account || this.env.region !== consumingStack.env.region) { - throw new Error('Can only reference cross stacks in the same region and account.'); - } - - // Ensure a singleton Output for this value - const resolved = this.node.resolve(tokenValue); - const id = 'Output' + JSON.stringify(resolved); - if (this.crossStackExports === undefined) { - this.crossStackExports = new Construct(this, 'Exports'); - } - let output = this.crossStackExports.node.tryFindChild(id) as Output; - if (!output) { - output = new Output(this.crossStackExports, id, { value: tokenValue }); - } - - // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', - // so construct one in-place. - return new Token({ 'Fn::ImportValue': output.export }); - } - /** * The account in which this stack is defined * @@ -374,8 +341,10 @@ export class Stack extends Construct { * Find all CloudFormation references and tell them we're consuming them. */ protected prepare() { - for (const cfnRef of this.node.findReferences(CfnReference.ReferenceType)) { - (cfnRef as CfnReference).consumeFromStack(this); + for (const ref of this.node.findReferences()) { + if (CfnReference.isInstance(ref)) { + ref.consumeFromStack(this); + } } } @@ -478,6 +447,5 @@ function stackElements(node: IConstruct, into: StackElement[] = []): StackElemen } // These imports have to be at the end to prevent circular imports -import { Output } from './output'; import { Aws } from './pseudo'; import { StackElement } from './stack-element'; diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index ab3ca3f97cea5..60d549728f2b5 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -152,7 +152,23 @@ export class ConstructNode { * All direct children of this construct. */ public get children() { - return Object.keys(this._children).map(k => this._children[k]); + return Object.values(this._children); + } + + /** + * Return this construct and all of its children in the given order + */ + public findAll(order: ConstructOrder = ConstructOrder.DepthFirst): IConstruct[] { + const ret = new Array(); + const queue: IConstruct[] = [this.host]; + + while (queue.length > 0) { + const next = order === ConstructOrder.BreadthFirst ? queue.splice(0, 1)[0] : queue.pop()!; + ret.push(next); + queue.push(...next.node.children); + } + + return ret; } /** @@ -265,7 +281,7 @@ export class ConstructNode { errors = errors.concat(child.node.validateTree()); } - const localErrors: string[] = this.host.validate(); + const localErrors: string[] = (this.host as any).validate(); return errors.concat(localErrors.map(msg => new ValidationError(this.host, msg))); } @@ -273,10 +289,12 @@ export class ConstructNode { * Run 'prepare()' on all constructs in the tree */ public prepareTree() { + const constructs = this.host.node.findAll(ConstructOrder.BreadthFirst); // Use .reverse() to achieve post-order traversal - const constructs = allConstructs(this.host, CrawlStyle.BreadthFirst); for (const construct of constructs.reverse()) { - Construct.doPrepare(construct); + if (Construct.isInstance(construct)) { + (construct as any).prepare(); + } } } @@ -393,7 +411,7 @@ export class ConstructNode { * Record a reference originating from this construct node */ public recordReference(ref: Token) { - if (ref.referenceType !== undefined && ref.referenceType !== '') { + if (ref.isReference) { this.references.add(ref); } } @@ -401,14 +419,12 @@ export class ConstructNode { /** * Return all references of the given type originating from this node or any of its children */ - public findReferences(type: string): Token[] { + public findReferences(): Token[] { const ret = new Set(); function recurse(node: ConstructNode) { for (const ref of node.references) { - if (ref.referenceType === type) { - ret.add(ref); - } + ret.add(ref); } for (const child of node.children) { @@ -447,15 +463,10 @@ export class ConstructNode { */ export class Construct implements IConstruct { /** - * Run the prepare phase on the given construct + * Return whether the given object is a Construct */ - public static doPrepare(construct: IConstruct) { - // Static method to make it possible to run 'prepare' from outside the - // object while not polluting the IDE autocomplete of instances with the - // presence of this method. - if (isConstruct(construct)) { - construct.prepare(); - } + public static isInstance(x: IConstruct): x is Construct { + return (x as any).prepare !== undefined && (x as any).validate !== undefined; } /** @@ -491,7 +502,7 @@ export class Construct implements IConstruct { * * @returns An array of validation error messages, or an empty array if there the construct is valid. */ - public validate(): string[] { + protected validate(): string[] { return []; } @@ -564,26 +575,16 @@ function createStackTrace(below: Function): string[] { } /** - * Return all constructs from the given root + * In what order to return constructs */ -export function allConstructs(root: IConstruct, style: CrawlStyle): IConstruct[] { - const ret = new Array(); - const queue = [root]; - - while (queue.length > 0) { - const next = style === CrawlStyle.BreadthFirst ? queue.splice(0, 1)[0] : queue.pop()!; - ret.push(next); - queue.push(...next.node.children); - } - - return ret; -} - -export enum CrawlStyle { +export enum ConstructOrder { + /** + * Breadth first + */ BreadthFirst, + + /** + * Depth first + */ DepthFirst } - -export function isConstruct(x: IConstruct): x is Construct { - return (x as any).prepare !== undefined && (x as any).validate !== undefined; -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts index 2776ee0030a1c..3a488c4a81e92 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts @@ -19,9 +19,12 @@ export const RESOLVE_METHOD = 'resolve'; */ export class Token { /** - * If this Token represents a reference, an identifier for the reference Type + * Indicate whether this Token represent a "reference" + * + * The Construct tree can be queried for the Reference Tokens that + * are used in it. */ - public readonly referenceType?: string; + public readonly isReference?: boolean; private tokenStringification?: string; private tokenListification?: string[]; From 3ee48f75986a284dea26dfa61c4751caf2e30ba7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 14:26:52 +0100 Subject: [PATCH 28/39] Undo more refactoring breakage --- packages/@aws-cdk/aws-iam/lib/policy-document.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index 47c33799f60cf..1e7e4ec447b57 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -12,7 +12,7 @@ export class PolicyDocument extends cdk.Token { super(); } - public scope(_context: cdk.ResolveContext): any { + public resolve(_context: cdk.ResolveContext): any { if (this.isEmpty) { return undefined; } @@ -371,8 +371,7 @@ export class PolicyStatement extends cdk.Token { // // Serialization // - - public scope(_context: cdk.ResolveContext): any { + public resolve(_context: cdk.ResolveContext): any { return this.toJson(); } From d1b3c228cb18eeaba09f1bb9312c9deddc1bb824 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 15:00:48 +0100 Subject: [PATCH 29/39] Fold ArnUtils.fromComponents() and ArnUtils.parse() into Stack --- design/aws-guidelines.md | 2 +- .../aws-apigateway/lib/integrations/aws.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/method.ts | 3 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 8 +- .../lib/pipeline-actions.ts | 4 +- .../test/test.pipeline-actions.ts | 6 +- .../@aws-cdk/aws-codebuild/lib/project.ts | 8 +- .../aws-codedeploy/lib/application.ts | 4 +- .../aws-codedeploy/lib/deployment-config.ts | 4 +- .../aws-codedeploy/lib/deployment-group.ts | 4 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 4 +- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 4 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 6 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 4 +- .../@aws-cdk/aws-iam/lib/managed-policy.ts | 4 +- packages/@aws-cdk/aws-kinesis/lib/stream.ts | 2 +- packages/@aws-cdk/aws-logs/lib/log-group.ts | 2 +- .../notifications-resource-handler.ts | 4 +- packages/@aws-cdk/aws-s3/lib/util.ts | 6 +- .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 378 ++++++++---------- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 65 ++- packages/@aws-cdk/cdk/lib/core/construct.ts | 8 + .../cdk/test/cloudformation/test.arn.ts | 43 +- packages/@aws-cdk/runtime-values/lib/rtv.ts | 4 +- 24 files changed, 314 insertions(+), 267 deletions(-) diff --git a/design/aws-guidelines.md b/design/aws-guidelines.md index e03a689de5a38..879dcee75cf20 100644 --- a/design/aws-guidelines.md +++ b/design/aws-guidelines.md @@ -204,7 +204,7 @@ properties that allow the user to specify an external resource identity, usually by providing one or more resource attributes such as ARN, physical name, etc. The import interface should have the minimum required properties, that is: if it -is possible to parse the resource name from the ARN (using `cdk.ArnUtils.parse`), +is possible to parse the resource name from the ARN (using `cdk.Stack.parseArn`), then only the ARN should be required. In cases where it is not possible to parse the ARN (e.g. if it is a token and the resource name might have use "/" characters), both the ARN and the name should be optional and diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index 4f37f6333b2c3..4053534e1383c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -73,13 +73,13 @@ export class AwsIntegration extends Integration { integrationHttpMethod: 'POST', uri: new cdk.Token(() => { if (!this.scope) { throw new Error('AwsIntegration must be used in API'); } - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(this.scope).arnFromComponents({ service: 'apigateway', account: backend, resource: apiType, sep: '/', resourceName: apiValue, - }, this.scope); + }); }), options: props.options, }); diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 414a0bc279565..bde742b97acfa 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -158,7 +158,8 @@ export class Method extends cdk.Construct { credentials = options.credentialsRole.roleArn; } else if (options.credentialsPassthrough) { // arn:aws:iam::*:user/* - credentials = cdk.ArnUtils.fromComponents({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }, this); + // tslint:disable-next-line:max-line-length + credentials = cdk.Stack.find(this).arnFromComponents({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }); } return { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 84ba238833c20..6c86b43f83279 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -301,12 +301,12 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi method = '*'; } - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(this).arnFromComponents({ service: 'execute-api', resource: this.restApiId, sep: '/', resourceName: `${stage}/${method}${path}` - }, this); + }); } /** @@ -358,14 +358,14 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi private configureCloudWatchRole(apiResource: CfnRestApi) { const role = new iam.Role(this, 'CloudWatchRole', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), - managedPolicyArns: [ cdk.ArnUtils.fromComponents({ + managedPolicyArns: [ cdk.Stack.find(this).arnFromComponents({ service: 'iam', region: '', account: 'aws', resource: 'policy', sep: '/', resourceName: 'service-role/AmazonAPIGatewayPushToCloudWatchLogs' - }, this) ] + }) ] }); const resource = new CfnAccount(this, 'Account', { diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index a5ea5c4cb714f..b7f20c7ca9a47 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -487,12 +487,12 @@ class SingletonPolicy extends cdk.Construct { } private stackArnFromProps(props: { stackName: string, region?: string }): string { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(this).arnFromComponents({ region: props.region, service: 'cloudformation', resource: 'stack', resourceName: `${props.stackName}/*` - }, this); + }); } } diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 7f4251f75260c..9d62f40b51088 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -272,11 +272,11 @@ function _isOrContains(entity: string | string[], value: string): boolean { } function _stackArn(stackName: string, scope: cdk.IConstruct): string { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(scope).arnFromComponents({ service: 'cloudformation', resource: 'stack', resourceName: `${stackName}/*`, - }, scope); + }); } class PipelineDouble extends cdk.Construct implements cpapi.IPipeline { @@ -287,7 +287,7 @@ class PipelineDouble extends cdk.Construct implements cpapi.IPipeline { constructor(scope: cdk.Construct, id: string, { pipelineName, role }: { pipelineName?: string, role: iam.Role }) { super(scope, id); this.pipelineName = pipelineName || 'TestPipeline'; - this.pipelineArn = cdk.ArnUtils.fromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }, this); + this.pipelineArn = cdk.Stack.find(this).arnFromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }); this.role = role; } diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index a10c717c8b9ab..371f7a2f45e84 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -436,11 +436,11 @@ class ImportedProject extends ProjectBase { constructor(scope: cdk.Construct, id: string, private readonly props: ProjectImportProps) { super(scope, id); - this.projectArn = cdk.ArnUtils.fromComponents({ + this.projectArn = cdk.Stack.find(this).arnFromComponents({ service: 'codebuild', resource: 'project', resourceName: props.projectName, - }, this); + }); this.projectName = props.projectName; } @@ -771,12 +771,12 @@ export class Project extends ProjectBase { } private createLoggingPermission() { - const logGroupArn = cdk.ArnUtils.fromComponents({ + const logGroupArn = cdk.Stack.find(this).arnFromComponents({ service: 'logs', resource: 'log-group', sep: ':', resourceName: `/aws/codebuild/${this.projectName}`, - }, this); + }); const logGroupStarArn = `${logGroupArn}:*`; diff --git a/packages/@aws-cdk/aws-codedeploy/lib/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/application.ts index 31327c70c200e..19803d66a4d6a 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/application.ts @@ -101,10 +101,10 @@ export class ServerApplication extends cdk.Construct implements IServerApplicati } function applicationNameToArn(applicationName: string, scope: cdk.IConstruct): string { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(scope).arnFromComponents({ service: 'codedeploy', resource: 'application', resourceName: applicationName, sep: ':', - }, scope); + }); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts index 241db739f0a26..c4516eefd2dc9 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts @@ -157,10 +157,10 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl } function arnForDeploymentConfigName(name: string, scope: cdk.IConstruct): string { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(scope).arnFromComponents({ service: 'codedeploy', resource: 'deploymentconfig', resourceName: name, sep: ':', - }, scope); + }); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index f01555f95f58e..afe8405de5504 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -561,10 +561,10 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { } function deploymentGroupNameToArn(applicationName: string, deploymentGroupName: string, scope: cdk.IConstruct): string { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(scope).arnFromComponents({ service: 'codedeploy', resource: 'deploymentgroup', resourceName: `${applicationName}/${deploymentGroupName}`, sep: ':', - }, scope); + }); } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index bb42c26606c16..14fd719d48c7e 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -127,10 +127,10 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { this.artifactStores = {}; // Does not expose a Fn::GetAtt for the ARN so we'll have to make it ourselves - this.pipelineArn = cdk.ArnUtils.fromComponents({ + this.pipelineArn = cdk.Stack.find(this).arnFromComponents({ service: 'codepipeline', resource: this.pipelineName - }, this); + }); } /** diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 6d68a389ae4ee..00f54cfdd04ad 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -614,12 +614,12 @@ export class Table extends Construct { private makeScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { - roleArn: cdk.ArnUtils.fromComponents({ + roleArn: cdk.Stack.find(this).arnFromComponents({ // https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html service: 'iam', resource: 'role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com', resourceName: 'AWSServiceRoleForApplicationAutoScaling_DynamoDBTable' - }, this) + }) }); } } diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index c90f07f0f53f2..c676f966ae9b0 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -123,11 +123,11 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor * as the current stack. */ public static arnForLocalRepository(repositoryName: string, scope: cdk.IConstruct): string { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(scope).arnFromComponents({ service: 'ecr', resource: 'repository', resourceName: repositoryName - }, scope); + }); } /** @@ -164,7 +164,7 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor */ public repositoryUriForTag(tag?: string): string { const tagSuffix = tag ? `:${tag}` : ''; - const parts = cdk.ArnUtils.parse(this.repositoryArn); + const parts = cdk.Stack.find(this).parseArn(this.repositoryArn); return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${this.repositoryName}${tagSuffix}`; } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index d5b40fef614b1..b93d227fb9e09 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -228,11 +228,11 @@ export abstract class BaseService extends cdk.Construct private makeAutoScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { - roleArn: cdk.ArnUtils.fromComponents({ + roleArn: cdk.Stack.find(this).arnFromComponents({ service: 'iam', resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', - }, this) + }) }); } } diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index d60c98824a1c4..016cb61219e12 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -18,12 +18,12 @@ export class AwsManagedPolicy { */ public get policyArn(): string { // the arn is in the form of - arn:aws:iam::aws:policy/ - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(this.scope).arnFromComponents({ service: "iam", region: "", // no region for managed policy account: "aws", // the account for a managed policy is 'aws' resource: "policy", resourceName: this.managedPolicyName - }, this.scope); + }); } } diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index d8acbebe208ef..b26862874afe7 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -429,7 +429,7 @@ class ImportedStream extends StreamBase { this.streamArn = props.streamArn; // Get the name from the ARN - this.streamName = cdk.ArnUtils.parse(props.streamArn).resourceName!; + this.streamName = cdk.Stack.find(this).parseArn(props.streamArn).resourceName!; if (props.encryptionKey) { // TODO: import "scope" should be changed to "this" diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index dec077a84d1b3..4f548fd295bdc 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -295,7 +295,7 @@ class ImportedLogGroup extends LogGroupBase { super(scope, id); this.logGroupArn = props.logGroupArn; - this.logGroupName = cdk.ArnUtils.resourceNameComponent(props.logGroupArn, ':'); + this.logGroupName = cdk.Stack.find(this).parseArn(props.logGroupArn, ':').resourceName!; } /** diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index c19cf1d974ce8..4404784164259 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -50,13 +50,13 @@ export class NotificationsResourceHandler extends cdk.Construct { const role = new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicyArns: [ - cdk.ArnUtils.fromComponents({ + cdk.Stack.find(this).arnFromComponents({ service: 'iam', region: '', // no region for managed policy account: 'aws', // the account for a managed policy is 'aws' resource: 'policy', resourceName: 'service-role/AWSLambdaBasicExecutionRole', - }, this) + }) ] }); diff --git a/packages/@aws-cdk/aws-s3/lib/util.ts b/packages/@aws-cdk/aws-s3/lib/util.ts index 74b32411debf5..577bc481fd6e6 100644 --- a/packages/@aws-cdk/aws-s3/lib/util.ts +++ b/packages/@aws-cdk/aws-s3/lib/util.ts @@ -9,14 +9,14 @@ export function parseBucketArn(construct: cdk.IConstruct, props: BucketImportPro } if (props.bucketName) { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(construct).arnFromComponents({ // S3 Bucket names are globally unique in a partition, // and so their ARNs have empty region and account components region: '', account: '', service: 's3', resource: props.bucketName - }, construct); + }); } throw new Error('Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed'); @@ -34,7 +34,7 @@ export function parseBucketName(construct: cdk.IConstruct, props: BucketImportPr const resolved = construct.node.resolve(props.bucketArn); if (typeof(resolved) === 'string') { - const components = cdk.ArnUtils.parse(resolved); + const components = cdk.Stack.find(construct).parseArn(resolved); if (components.service !== 's3') { throw new Error('Invalid ARN. Expecting "s3" service:' + resolved); } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index f874141b7eb78..e6b86166c3a01 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,248 +1,216 @@ import { Fn } from '../cloudformation/fn'; -import { IConstruct } from '../core/construct'; import { unresolved } from '../core/tokens'; import { Stack } from './stack'; /** - * An Amazon Resource Name (ARN). - * http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + * Creates an ARN from components. + * + * If `partition`, `region` or `account` are not specified, the stack's + * partition, region and account will be used. + * + * If any component is the empty string, an empty string will be inserted + * into the generated ARN at the location that component corresponds to. + * + * The ARN will be formatted as follows: + * + * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} + * + * The required ARN pieces that are omitted will be taken from the stack that + * the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope + * can be 'undefined'. */ -export class ArnUtils { - /** - * Creates an ARN from components. - * - * If `partition`, `region` or `account` are not specified, the stack's - * partition, region and account will be used. - * - * If any component is the empty string, an empty string will be inserted - * into the generated ARN at the location that component corresponds to. - * - * The ARN will be formatted as follows: - * - * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} - * - * The required ARN pieces that are omitted will be taken from the stack that - * the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope - * can be 'undefined'. - */ - public static fromComponents(components: ArnComponents, scope: IConstruct | undefined): string { - const partition = components.partition !== undefined ? components.partition : theStack('partition').partition; - const region = components.region !== undefined ? components.region : theStack('region').region; - const account = components.account !== undefined ? components.account : theStack('account').accountId; +export function arnFromComponents(components: ArnComponents, stack: Stack): string { + const partition = components.partition !== undefined ? components.partition : stack.partition; + const region = components.region !== undefined ? components.region : stack.region; + const account = components.account !== undefined ? components.account : stack.accountId; - const values = [ 'arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource ]; - - const sep = components.sep || '/'; - if (sep !== '/' && sep !== ':') { - throw new Error('resourcePathSep may only be ":" or "/"'); - } + const values = [ 'arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource ]; - if (components.resourceName != null) { - values.push(sep); - values.push(components.resourceName); - } + const sep = components.sep || '/'; + if (sep !== '/' && sep !== ':') { + throw new Error('resourcePathSep may only be ":" or "/"'); + } - return values.join(''); + if (components.resourceName != null) { + values.push(sep); + values.push(components.resourceName); + } - /** - * Return the stack we're scoped to (so the caller can get an attribute from it), throw a descriptive error if we don't have a scope - */ - function theStack(attribute: string) { - if (!scope) { - throw new Error(`Must provide scope when using implicit ${attribute}`); - } - return Stack.find(scope); + return values.join(''); +} - } +/** + * Given an ARN, parses it and returns components. + * + * If the ARN is a concrete string, it will be parsed and validated. The + * separator (`sep`) will be set to '/' if the 6th component includes a '/', + * in which case, `resource` will be set to the value before the '/' and + * `resourceName` will be the rest. In case there is no '/', `resource` will + * be set to the 6th components and `resourceName` will be set to the rest + * of the string. + * + * If the ARN includes tokens (or is a token), the ARN cannot be validated, + * since we don't have the actual value yet at the time of this function + * call. You will have to know the separator and the type of ARN. The + * resulting `ArnComponents` object will contain tokens for the + * subexpressions of the ARN, not string literals. In this case this + * function cannot properly parse the complete final resourceName (path) out + * of ARNs that use '/' to both separate the 'resource' from the + * 'resourceName' AND to subdivide the resourceName further. For example, in + * S3 ARNs: + * + * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png + * + * After parsing the resourceName will not contain + * 'path/to/exampleobject.png' but simply 'path'. This is a limitation + * because there is no slicing functionality in CloudFormation templates. + * + * @param sep The separator used to separate resource from resourceName + * @param hasName Whether there is a name component in the ARN at all. For + * example, SNS Topics ARNs have the 'resource' component contain the topic + * name, and no 'resourceName' component. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + */ +export function parseArn(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents { + if (unresolved(arn)) { + return parseToken(arn, sepIfToken, hasName); } - /** - * Given an ARN, parses it and returns components. - * - * If the ARN is a concrete string, it will be parsed and validated. The - * separator (`sep`) will be set to '/' if the 6th component includes a '/', - * in which case, `resource` will be set to the value before the '/' and - * `resourceName` will be the rest. In case there is no '/', `resource` will - * be set to the 6th components and `resourceName` will be set to the rest - * of the string. - * - * If the ARN includes tokens (or is a token), the ARN cannot be validated, - * since we don't have the actual value yet at the time of this function - * call. You will have to know the separator and the type of ARN. The - * resulting `ArnComponents` object will contain tokens for the - * subexpressions of the ARN, not string literals. In this case this - * function cannot properly parse the complete final resourceName (path) out - * of ARNs that use '/' to both separate the 'resource' from the - * 'resourceName' AND to subdivide the resourceName further. For example, in - * S3 ARNs: - * - * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png - * - * After parsing the resourceName will not contain - * 'path/to/exampleobject.png' but simply 'path'. This is a limitation - * because there is no slicing functionality in CloudFormation templates. - * - * @param sep The separator used to separate resource from resourceName - * @param hasName Whether there is a name component in the ARN at all. For - * example, SNS Topics ARNs have the 'resource' component contain the topic - * name, and no 'resourceName' component. - * - * @returns an ArnComponents object which allows access to the various - * components of the ARN. - * - * @returns an ArnComponents object which allows access to the various - * components of the ARN. - */ - public static parse(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents { - if (unresolved(arn)) { - return ArnUtils.parseToken(arn, sepIfToken, hasName); - } + const components = arn.split(':') as Array; - const components = arn.split(':') as Array; + if (components.length < 6) { + throw new Error('ARNs must have at least 6 components: ' + arn); + } - if (components.length < 6) { - throw new Error('ARNs must have at least 6 components: ' + arn); - } + const [ arnPrefix, partition, service, region, account, sixth, ...rest ] = components; - const [ arnPrefix, partition, service, region, account, sixth, ...rest ] = components; + if (arnPrefix !== 'arn') { + throw new Error('ARNs must start with "arn:": ' + arn); + } - if (arnPrefix !== 'arn') { - throw new Error('ARNs must start with "arn:": ' + arn); - } + if (!service) { + throw new Error('The `service` component (3rd component) is required: ' + arn); + } - if (!service) { - throw new Error('The `service` component (3rd component) is required: ' + arn); - } + if (!sixth) { + throw new Error('The `resource` component (6th component) is required: ' + arn); + } - if (!sixth) { - throw new Error('The `resource` component (6th component) is required: ' + arn); - } + let resource: string; + let resourceName: string | undefined; + let sep: string | undefined; - let resource: string; - let resourceName: string | undefined; - let sep: string | undefined; + let sepIndex = sixth.indexOf('/'); + if (sepIndex !== -1) { + sep = '/'; + } else if (rest.length > 0) { + sep = ':'; + sepIndex = -1; + } - let sepIndex = sixth.indexOf('/'); - if (sepIndex !== -1) { - sep = '/'; - } else if (rest.length > 0) { - sep = ':'; - sepIndex = -1; - } + if (sepIndex !== -1) { + resource = sixth.substr(0, sepIndex); + resourceName = sixth.substr(sepIndex + 1); + } else { + resource = sixth; + } - if (sepIndex !== -1) { - resource = sixth.substr(0, sepIndex); - resourceName = sixth.substr(sepIndex + 1); + if (rest.length > 0) { + if (!resourceName) { + resourceName = ''; } else { - resource = sixth; - } - - if (rest.length > 0) { - if (!resourceName) { - resourceName = ''; - } else { - resourceName += ':'; - } - - resourceName += rest.join(':'); + resourceName += ':'; } - const result: ArnComponents = { service, resource }; - if (partition) { - result.partition = partition; - } - - if (region) { - result.region = region; - } + resourceName += rest.join(':'); + } - if (account) { - result.account = account; - } + const result: ArnComponents = { service, resource }; + if (partition) { + result.partition = partition; + } - if (resourceName) { - result.resourceName = resourceName; - } + if (region) { + result.region = region; + } - if (sep) { - result.sep = sep; - } + if (account) { + result.account = account; + } - return result; + if (resourceName) { + result.resourceName = resourceName; } - /** - * Given a Token evaluating to ARN, parses it and returns components. - * - * The ARN cannot be validated, since we don't have the actual value yet - * at the time of this function call. You will have to know the separator - * and the type of ARN. - * - * The resulting `ArnComponents` object will contain tokens for the - * subexpressions of the ARN, not string literals. - * - * WARNING: this function cannot properly parse the complete final - * resourceName (path) out of ARNs that use '/' to both separate the - * 'resource' from the 'resourceName' AND to subdivide the resourceName - * further. For example, in S3 ARNs: - * - * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png - * - * After parsing the resourceName will not contain 'path/to/exampleobject.png' - * but simply 'path'. This is a limitation because there is no slicing - * functionality in CloudFormation templates. - * - * @param arnToken The input token that contains an ARN - * @param sep The separator used to separate resource from resourceName - * @param hasName Whether there is a name component in the ARN at all. - * For example, SNS Topics ARNs have the 'resource' component contain the - * topic name, and no 'resourceName' component. - * @returns an ArnComponents object which allows access to the various - * components of the ARN. - */ - public static parseToken(arnToken: string, sep: string = '/', hasName: boolean = true): ArnComponents { - // Arn ARN looks like: - // arn:partition:service:region:account-id:resource - // arn:partition:service:region:account-id:resourcetype/resource - // arn:partition:service:region:account-id:resourcetype:resource + if (sep) { + result.sep = sep; + } - // We need the 'hasName' argument because {Fn::Select}ing a nonexistent field - // throws an error. + return result; +} - const components = Fn.split(':', arnToken); +/** + * Given a Token evaluating to ARN, parses it and returns components. + * + * The ARN cannot be validated, since we don't have the actual value yet + * at the time of this function call. You will have to know the separator + * and the type of ARN. + * + * The resulting `ArnComponents` object will contain tokens for the + * subexpressions of the ARN, not string literals. + * + * WARNING: this function cannot properly parse the complete final + * resourceName (path) out of ARNs that use '/' to both separate the + * 'resource' from the 'resourceName' AND to subdivide the resourceName + * further. For example, in S3 ARNs: + * + * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png + * + * After parsing the resourceName will not contain 'path/to/exampleobject.png' + * but simply 'path'. This is a limitation because there is no slicing + * functionality in CloudFormation templates. + * + * @param arnToken The input token that contains an ARN + * @param sep The separator used to separate resource from resourceName + * @param hasName Whether there is a name component in the ARN at all. + * For example, SNS Topics ARNs have the 'resource' component contain the + * topic name, and no 'resourceName' component. + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + */ +function parseToken(arnToken: string, sep: string = '/', hasName: boolean = true): ArnComponents { + // Arn ARN looks like: + // arn:partition:service:region:account-id:resource + // arn:partition:service:region:account-id:resourcetype/resource + // arn:partition:service:region:account-id:resourcetype:resource - const partition = Fn.select(1, components).toString(); - const service = Fn.select(2, components).toString(); - const region = Fn.select(3, components).toString(); - const account = Fn.select(4, components).toString(); + // We need the 'hasName' argument because {Fn::Select}ing a nonexistent field + // throws an error. - if (sep === ':') { - const resource = Fn.select(5, components).toString(); - const resourceName = hasName ? Fn.select(6, components).toString() : undefined; + const components = Fn.split(':', arnToken); - return { partition, service, region, account, resource, resourceName, sep }; - } else { - const lastComponents = Fn.split(sep, Fn.select(5, components)); + const partition = Fn.select(1, components).toString(); + const service = Fn.select(2, components).toString(); + const region = Fn.select(3, components).toString(); + const account = Fn.select(4, components).toString(); - const resource = Fn.select(0, lastComponents).toString(); - const resourceName = hasName ? Fn.select(1, lastComponents).toString() : undefined; + if (sep === ':') { + const resource = Fn.select(5, components).toString(); + const resourceName = hasName ? Fn.select(6, components).toString() : undefined; - return { partition, service, region, account, resource, resourceName, sep }; - } - } + return { partition, service, region, account, resource, resourceName, sep }; + } else { + const lastComponents = Fn.split(sep, Fn.select(5, components)); - /** - * Return a Token that represents the resource component of the ARN - */ - public static resourceComponent(arn: string, sep: string = '/'): string { - return ArnUtils.parseToken(arn, sep).resource; - } + const resource = Fn.select(0, lastComponents).toString(); + const resourceName = hasName ? Fn.select(1, lastComponents).toString() : undefined; - /** - * Return a Token that represents the resource Name component of the ARN - */ - public static resourceNameComponent(arn: string, sep: string = '/'): string { - return ArnUtils.parseToken(arn, sep, true).resourceName!; + return { partition, service, region, account, resource, resourceName, sep }; } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index b200f2a15dc12..fa77b8d8b5329 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -2,6 +2,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; import { Construct, IConstruct } from '../core/construct'; import { Environment } from '../environment'; +import { ArnComponents, arnFromComponents, parseArn } from './arn'; import { CfnReference } from './cfn-tokens'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -323,6 +324,68 @@ export class Stack extends Construct { return new Aws(this).notificationArns; } + /** + * Creates an ARN from components. + * + * If `partition`, `region` or `account` are not specified, the stack's + * partition, region and account will be used. + * + * If any component is the empty string, an empty string will be inserted + * into the generated ARN at the location that component corresponds to. + * + * The ARN will be formatted as follows: + * + * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} + * + * The required ARN pieces that are omitted will be taken from the stack that + * the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope + * can be 'undefined'. + */ + public arnFromComponents(components: ArnComponents): string { + return arnFromComponents(components, this); + } + + /** + * Given an ARN, parses it and returns components. + * + * If the ARN is a concrete string, it will be parsed and validated. The + * separator (`sep`) will be set to '/' if the 6th component includes a '/', + * in which case, `resource` will be set to the value before the '/' and + * `resourceName` will be the rest. In case there is no '/', `resource` will + * be set to the 6th components and `resourceName` will be set to the rest + * of the string. + * + * If the ARN includes tokens (or is a token), the ARN cannot be validated, + * since we don't have the actual value yet at the time of this function + * call. You will have to know the separator and the type of ARN. The + * resulting `ArnComponents` object will contain tokens for the + * subexpressions of the ARN, not string literals. In this case this + * function cannot properly parse the complete final resourceName (path) out + * of ARNs that use '/' to both separate the 'resource' from the + * 'resourceName' AND to subdivide the resourceName further. For example, in + * S3 ARNs: + * + * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png + * + * After parsing the resourceName will not contain + * 'path/to/exampleobject.png' but simply 'path'. This is a limitation + * because there is no slicing functionality in CloudFormation templates. + * + * @param sep The separator used to separate resource from resourceName + * @param hasName Whether there is a name component in the ARN at all. For + * example, SNS Topics ARNs have the 'resource' component contain the topic + * name, and no 'resourceName' component. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + */ + public parseArn(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents { + return parseArn(arn, sepIfToken, hasName); + } + /** * Validate stack name * @@ -448,4 +511,4 @@ function stackElements(node: IConstruct, into: StackElement[] = []): StackElemen // These imports have to be at the end to prevent circular imports import { Aws } from './pseudo'; -import { StackElement } from './stack-element'; +import { StackElement } from './stack-element'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 60d549728f2b5..675392530cbd2 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -1,4 +1,5 @@ import cxapi = require('@aws-cdk/cx-api'); +import { CloudFormationJSON } from '../cloudformation/cloudformation-json'; import { makeUniqueId } from '../util/uniqueid'; import { Token, unresolved } from './tokens'; import { resolve } from './tokens/resolve'; @@ -407,6 +408,13 @@ export class ConstructNode { }); } + /** + * Convert an object, potentially containing tokens, to a JSON string + */ + public stringifyJson(obj: any): string { + return CloudFormationJSON.stringify(obj, this.host).toString(); + } + /** * Record a reference originating from this construct node */ diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index dd815293fe8a0..7971cbcf6355e 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,14 +1,14 @@ import { Test } from 'nodeunit'; -import { ArnComponents, ArnUtils, Aws, Stack, Token } from '../../lib'; +import { ArnComponents, Aws, Stack, Token } from '../../lib'; export = { 'create from components with defaults'(test: Test) { const stack = new Stack(); - const arn = ArnUtils.fromComponents({ + const arn = stack.arnFromComponents({ service: 'sqs', resource: 'myqueuename' - }, stack); + }); const pseudo = new Aws(stack); @@ -20,14 +20,14 @@ export = { 'create from components with specific values for the various components'(test: Test) { const stack = new Stack(); - const arn = ArnUtils.fromComponents({ + const arn = stack.arnFromComponents({ service: 'dynamodb', resource: 'table', account: '123456789012', region: 'us-east-1', partition: 'aws-cn', resourceName: 'mytable/stream/label' - }, undefined); + }); test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:dynamodb:us-east-1:123456789012:table/mytable/stream/label'); @@ -37,13 +37,13 @@ export = { 'allow empty string in components'(test: Test) { const stack = new Stack(); - const arn = ArnUtils.fromComponents({ + const arn = stack.arnFromComponents({ service: 's3', resource: 'my-bucket', account: '', region: '', partition: 'aws-cn', - }, undefined); + }); test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:s3:::my-bucket'); @@ -54,12 +54,12 @@ export = { 'resourcePathSep can be set to ":" instead of the default "/"'(test: Test) { const stack = new Stack(); - const arn = ArnUtils.fromComponents({ + const arn = stack.arnFromComponents({ service: 'codedeploy', resource: 'application', sep: ':', resourceName: 'WordPress_App' - }, stack); + }); const pseudo = new Aws(stack); @@ -69,10 +69,12 @@ export = { }, 'fails if resourcePathSep is neither ":" nor "/"'(test: Test) { - test.throws(() => ArnUtils.fromComponents({ + const stack = new Stack(); + + test.throws(() => stack.arnFromComponents({ service: 'foo', resource: 'bar', - sep: 'x' }, undefined)); + sep: 'x' })); test.done(); }, @@ -80,27 +82,32 @@ export = { 'fails': { 'if doesn\'t start with "arn:"'(test: Test) { - test.throws(() => ArnUtils.parse("barn:foo:x:a:1:2"), /ARNs must start with "arn:": barn:foo/); + const stack = new Stack(); + test.throws(() => stack.parseArn("barn:foo:x:a:1:2"), /ARNs must start with "arn:": barn:foo/); test.done(); }, 'if the ARN doesnt have enough components'(test: Test) { - test.throws(() => ArnUtils.parse('arn:is:too:short'), /ARNs must have at least 6 components: arn:is:too:short/); + const stack = new Stack(); + test.throws(() => stack.parseArn('arn:is:too:short'), /ARNs must have at least 6 components: arn:is:too:short/); test.done(); }, 'if "service" is not specified'(test: Test) { - test.throws(() => ArnUtils.parse('arn:aws::4:5:6'), /The `service` component \(3rd component\) is required/); + const stack = new Stack(); + test.throws(() => stack.parseArn('arn:aws::4:5:6'), /The `service` component \(3rd component\) is required/); test.done(); }, 'if "resource" is not specified'(test: Test) { - test.throws(() => ArnUtils.parse('arn:aws:service:::'), /The `resource` component \(6th component\) is required/); + const stack = new Stack(); + test.throws(() => stack.parseArn('arn:aws:service:::'), /The `resource` component \(6th component\) is required/); test.done(); } }, 'various successful parses'(test: Test) { + const stack = new Stack(); const tests: { [arn: string]: ArnComponents } = { 'arn:aws:a4b:region:accountid:resourcetype/resource': { partition: 'aws', @@ -142,7 +149,7 @@ export = { Object.keys(tests).forEach(arn => { const expected = tests[arn]; - test.deepEqual(ArnUtils.parse(arn), expected, arn); + test.deepEqual(stack.parseArn(arn), expected, arn); }); test.done(); @@ -151,7 +158,7 @@ export = { 'a Token with : separator'(test: Test) { const stack = new Stack(); const theToken = { Ref: 'SomeParameter' }; - const parsed = ArnUtils.parseToken(new Token(() => theToken).toString(), ':'); + const parsed = stack.parseArn(new Token(() => theToken).toString(), ':'); test.deepEqual(stack.node.resolve(parsed.partition), { 'Fn::Select': [ 1, { 'Fn::Split': [ ':', theToken ]} ]}); test.deepEqual(stack.node.resolve(parsed.service), { 'Fn::Select': [ 2, { 'Fn::Split': [ ':', theToken ]} ]}); @@ -167,7 +174,7 @@ export = { 'a Token with / separator'(test: Test) { const stack = new Stack(); const theToken = { Ref: 'SomeParameter' }; - const parsed = ArnUtils.parseToken(new Token(() => theToken).toString()); + const parsed = stack.parseArn(new Token(() => theToken).toString()); test.equal(parsed.sep, '/'); diff --git a/packages/@aws-cdk/runtime-values/lib/rtv.ts b/packages/@aws-cdk/runtime-values/lib/rtv.ts index 9cddd96e96d7a..1c3570e473434 100644 --- a/packages/@aws-cdk/runtime-values/lib/rtv.ts +++ b/packages/@aws-cdk/runtime-values/lib/rtv.ts @@ -65,11 +65,11 @@ export class RuntimeValue extends cdk.Construct { value: props.value, }); - this.parameterArn = cdk.ArnUtils.fromComponents({ + this.parameterArn = cdk.Stack.find(this).arnFromComponents({ service: 'ssm', resource: 'parameter', resourceName: this.parameterName - }, this); + }); } /** From 85cc4d295c0d00361f17b4a42207cc7f57aa330a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 15:01:35 +0100 Subject: [PATCH 30/39] Missing commit --- packages/@aws-cdk/aws-codecommit/lib/repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index b152c16e48c7f..5fe78b798866e 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -244,10 +244,10 @@ class ImportedRepository extends RepositoryBase { constructor(scope: cdk.Construct, id: string, private readonly props: RepositoryImportProps) { super(scope, id); - this.repositoryArn = cdk.ArnUtils.fromComponents({ + this.repositoryArn = cdk.Stack.find(this).arnFromComponents({ service: 'codecommit', resource: props.repositoryName, - }, this); + }); this.repositoryName = props.repositoryName; } From 8e64bc225f450d55e8b4623fb2dc00e274b29802 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 15:03:58 +0100 Subject: [PATCH 31/39] Replace construct library's use of CloudFormationJSON with stringifyJson() --- packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts | 2 +- packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts | 4 ++-- packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts | 2 +- packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts | 2 +- packages/@aws-cdk/cdk/lib/core/tokens/token.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index b7f20c7ca9a47..2a0ba24086ca2 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -203,7 +203,7 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo // None evaluates to empty string which is falsey and results in undefined Capabilities: (capabilities && capabilities.toString()) || undefined, RoleArn: new cdk.Token(() => this.role.roleArn), - ParameterOverrides: new cdk.Token(() => cdk.CloudFormationJSON.stringify(props.parameterOverrides, this)), + ParameterOverrides: new cdk.Token(() => this.node.stringifyJson(props.parameterOverrides)), TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined, StackName: props.stackName, }); diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts index a06046f9260cf..10b4abed24a69 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts @@ -1,4 +1,4 @@ -import { CloudFormationJSON, Construct, Stack, Token } from "@aws-cdk/cdk"; +import { Construct, Stack, Token } from "@aws-cdk/cdk"; import { CfnDashboard } from './cloudwatch.generated'; import { Column, Row } from "./layout"; import { IWidget } from "./widget"; @@ -33,7 +33,7 @@ export class Dashboard extends Construct { dashboardBody: new Token(() => { const column = new Column(...this.rows); column.position(0, 0); - return CloudFormationJSON.stringify({ widgets: column.toJson() }, this); + return this.node.stringifyJson({ widgets: column.toJson() }); }) }); } diff --git a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts index 31efd84861ea6..3d45e386e2db6 100644 --- a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts +++ b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts @@ -95,6 +95,6 @@ export class CrossAccountDestination extends cdk.Construct implements ILogSubscr * Return a stringified JSON version of the PolicyDocument */ private stringifiedPolicyDocument() { - return this.policyDocument.isEmpty ? '' : cdk.CloudFormationJSON.stringify(this.node.resolve(this.policyDocument), this); + return this.policyDocument.isEmpty ? '' : this.node.stringifyJson(this.policyDocument); } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 88d70427ae49e..cba9fdbd0fb35 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -82,7 +82,7 @@ export class StateMachine extends cdk.Construct implements IStateMachine { const resource = new CfnStateMachine(this, 'Resource', { stateMachineName: props.stateMachineName, roleArn: this.role.roleArn, - definitionString: cdk.CloudFormationJSON.stringify(graph.toGraphJson(), this), + definitionString: this.node.stringifyJson(graph.toGraphJson()), }); for (const statement of graph.policyStatements) { diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts index 3a488c4a81e92..35924a457f717 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts @@ -96,7 +96,7 @@ export class Token { */ public toJSON(): any { // tslint:disable-next-line:max-line-length - throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use CloudFormationJSON.stringify() instead.'); + throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use this.node.stringifyJson() instead.'); } /** From 5694b9cd27d0f47cd84b6dd343b6bf034a5b45fa Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 15:15:31 +0100 Subject: [PATCH 32/39] Remove additional scope argument for IAM policies --- .../lib/alb/application-load-balancer.ts | 2 +- .../@aws-cdk/aws-iam/lib/policy-document.ts | 30 ++++++++++++++----- .../aws-iam/test/example.external-id.lit.ts | 2 +- packages/@aws-cdk/aws-iam/test/integ.role.ts | 2 +- .../aws-iam/test/test.policy-document.ts | 10 +++---- packages/@aws-cdk/aws-kinesis/lib/stream.ts | 2 +- packages/@aws-cdk/aws-kms/lib/key.ts | 2 +- .../@aws-cdk/aws-lambda/test/test.alias.ts | 2 +- .../@aws-cdk/aws-lambda/test/test.lambda.ts | 4 +-- .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 2 +- 10 files changed, 36 insertions(+), 22 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 10a6c6da8cb4d..bddec5bac2bae 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -92,7 +92,7 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic // FIXME: can't use grantPut() here because that only takes IAM objects, not arbitrary principals bucket.addToResourcePolicy(new iam.PolicyStatement() - .addPrincipal(new iam.AccountPrincipal(this, account)) + .addPrincipal(new iam.AccountPrincipal(account)) .addAction('s3:PutObject') .addResource(bucket.arnForObjects(prefix || '', '*'))); } diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index 1e7e4ec447b57..3a30676b18a59 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -81,8 +81,8 @@ export class ArnPrincipal extends PolicyPrincipal { } export class AccountPrincipal extends ArnPrincipal { - constructor(public readonly scope: cdk.Construct, public readonly accountId: any) { - super(`arn:${new cdk.Aws(scope).partition}:iam::${accountId}:root`); + constructor(public readonly accountId: any) { + super(new StackDependentToken(stack => `arn:${stack.partition}:iam::${accountId}:root`).toString()); } } @@ -136,8 +136,8 @@ export class FederatedPrincipal extends PolicyPrincipal { } export class AccountRootPrincipal extends AccountPrincipal { - constructor(scope: cdk.Construct) { - super(scope, new cdk.Aws(scope).accountId); + constructor() { + super(new StackDependentToken(stack => stack.accountId).toString()); } } @@ -250,8 +250,8 @@ export class PolicyStatement extends cdk.Token { return this.addPrincipal(new ArnPrincipal(arn)); } - public addAwsAccountPrincipal(scope: cdk.Construct, accountId: string): this { - return this.addPrincipal(new AccountPrincipal(scope, accountId)); + public addAwsAccountPrincipal(accountId: string): this { + return this.addPrincipal(new AccountPrincipal(accountId)); } public addArnPrincipal(arn: string): this { @@ -266,8 +266,8 @@ export class PolicyStatement extends cdk.Token { return this.addPrincipal(new FederatedPrincipal(federated, conditions)); } - public addAccountRootPrincipal(scope: cdk.Construct): this { - return this.addPrincipal(new AccountRootPrincipal(scope)); + public addAccountRootPrincipal(): this { + return this.addPrincipal(new AccountRootPrincipal()); } public addCanonicalUserPrincipal(canonicalUserId: string): this { @@ -449,3 +449,17 @@ function mergePrincipal(target: { [key: string]: string[] }, source: { [key: str return target; } + +/** + * A lazy token that requires an instance of Stack to evaluate + */ +class StackDependentToken extends cdk.Token { + constructor(private readonly fn: (stack: cdk.Stack) => any) { + super(); + } + + public resolve(context: cdk.ResolveContext) { + const stack = cdk.Stack.find(context.scope); + return this.fn(stack); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/example.external-id.lit.ts b/packages/@aws-cdk/aws-iam/test/example.external-id.lit.ts index d3bc7167d4ec2..11f313559707b 100644 --- a/packages/@aws-cdk/aws-iam/test/example.external-id.lit.ts +++ b/packages/@aws-cdk/aws-iam/test/example.external-id.lit.ts @@ -7,7 +7,7 @@ export class ExampleConstruct extends cdk.Construct { /// !show const role = new iam.Role(this, 'MyRole', { - assumedBy: new iam.AccountPrincipal(this, '123456789012'), + assumedBy: new iam.AccountPrincipal('123456789012'), externalId: 'SUPPLY-ME', }); /// !hide diff --git a/packages/@aws-cdk/aws-iam/test/integ.role.ts b/packages/@aws-cdk/aws-iam/test/integ.role.ts index 826ef417091fa..f3074a389aeac 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.role.ts +++ b/packages/@aws-cdk/aws-iam/test/integ.role.ts @@ -17,7 +17,7 @@ policy.attachToRole(role); // Role with an external ID new Role(stack, 'TestRole2', { - assumedBy: new AccountRootPrincipal(stack), + assumedBy: new AccountRootPrincipal(), externalId: 'supply-me', }); diff --git a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts index 9db0774aa6321..97eeef42f95aa 100644 --- a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts +++ b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts @@ -14,7 +14,7 @@ export = { p.addResource('yourQueue'); p.addAllResources(); - p.addAwsAccountPrincipal(stack, `my${new Token({ account: 'account' })}name`); + p.addAwsAccountPrincipal(`my${new Token({ account: 'account' })}name`); p.limitToAccount('12221121221'); test.deepEqual(stack.node.resolve(p), { Action: @@ -117,7 +117,7 @@ export = { const stack = new Stack(); const p = new PolicyStatement(); - p.addAccountRootPrincipal(stack); + p.addAccountRootPrincipal(); test.deepEqual(stack.node.resolve(p), { Effect: "Allow", Principal: { @@ -158,8 +158,8 @@ export = { const stack = new Stack(); const p = new PolicyStatement(); - p.addAwsAccountPrincipal(stack, '1234'); - p.addAwsAccountPrincipal(stack, '5678'); + p.addAwsAccountPrincipal('1234'); + p.addAwsAccountPrincipal('5678'); test.deepEqual(stack.node.resolve(p), { Effect: 'Allow', Principal: { @@ -282,7 +282,7 @@ export = { assumeRoleAction: 'sts:AssumeRole', policyFragment: () => new PrincipalPolicyFragment({ AWS: ['foo', 'bar'] }), }; - const s = new PolicyStatement().addAccountRootPrincipal(stack) + const s = new PolicyStatement().addAccountRootPrincipal() .addPrincipal(arrayPrincipal); test.deepEqual(stack.node.resolve(s), { Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index b26862874afe7..0b569ccf07227 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -249,7 +249,7 @@ export abstract class StreamBase extends cdk.Construct implements IStream { dest.addToPolicy(new iam.PolicyStatement() .addAction('logs:PutSubscriptionFilter') - .addAwsAccountPrincipal(this, sourceStack.env.account) + .addAwsAccountPrincipal(sourceStack.env.account) .addAllResources()); return dest.logSubscriptionDestination(sourceLogGroup); diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index 3dbb2ec309b34..7b259bfb8fc25 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -204,7 +204,7 @@ export class EncryptionKey extends EncryptionKeyBase { this.addToResourcePolicy(new PolicyStatement() .addAllResources() .addActions(...actions) - .addAccountRootPrincipal(this)); + .addAccountRootPrincipal()); } } diff --git a/packages/@aws-cdk/aws-lambda/test/test.alias.ts b/packages/@aws-cdk/aws-lambda/test/test.alias.ts index f94762a77b27a..cd2b30baeeaa9 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.alias.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.alias.ts @@ -118,7 +118,7 @@ export = { // WHEN alias.addPermission('Perm', { - principal: new AccountPrincipal(stack, '123456') + principal: new AccountPrincipal('123456') }); // THEN diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index bd16167997a51..c74dde7129541 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -191,7 +191,7 @@ export = { /Invalid principal type for Lambda permission statement/); fn.addPermission('S1', { principal: new iam.ServicePrincipal('my-service') }); - fn.addPermission('S2', { principal: new iam.AccountPrincipal(stack, 'account') }); + fn.addPermission('S2', { principal: new iam.AccountPrincipal('account') }); test.done(); }, @@ -1030,7 +1030,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const role = new iam.Role(stack, 'Role', { - assumedBy: new iam.AccountPrincipal(stack, '1234'), + assumedBy: new iam.AccountPrincipal('1234'), }); const fn = new lambda.Function(stack, 'Function', { code: lambda.Code.inline('xxx'), diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index fa77b8d8b5329..c8df077b2201d 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -2,7 +2,6 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; import { Construct, IConstruct } from '../core/construct'; import { Environment } from '../environment'; -import { ArnComponents, arnFromComponents, parseArn } from './arn'; import { CfnReference } from './cfn-tokens'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -510,5 +509,6 @@ function stackElements(node: IConstruct, into: StackElement[] = []): StackElemen } // These imports have to be at the end to prevent circular imports +import { ArnComponents, arnFromComponents, parseArn } from './arn'; import { Aws } from './pseudo'; import { StackElement } from './stack-element'; \ No newline at end of file From 12854e6eb8b9f91412579032f60875570194bad6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 15:52:25 +0100 Subject: [PATCH 33/39] arnFromComponents() -> formatArn() --- .../@aws-cdk/aws-apigateway/lib/integrations/aws.ts | 2 +- packages/@aws-cdk/aws-apigateway/lib/method.ts | 2 +- packages/@aws-cdk/aws-apigateway/lib/restapi.ts | 4 ++-- .../aws-cloudformation/lib/pipeline-actions.ts | 2 +- .../aws-cloudformation/test/test.pipeline-actions.ts | 4 ++-- packages/@aws-cdk/aws-codebuild/lib/project.ts | 4 ++-- packages/@aws-cdk/aws-codecommit/lib/repository.ts | 2 +- packages/@aws-cdk/aws-codedeploy/lib/application.ts | 2 +- .../@aws-cdk/aws-codedeploy/lib/deployment-config.ts | 2 +- .../@aws-cdk/aws-codedeploy/lib/deployment-group.ts | 2 +- packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts | 2 +- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 2 +- packages/@aws-cdk/aws-ecr/lib/repository-ref.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- packages/@aws-cdk/aws-iam/lib/managed-policy.ts | 2 +- .../notifications-resource-handler.ts | 2 +- packages/@aws-cdk/aws-s3/lib/util.ts | 2 +- packages/@aws-cdk/cdk/lib/cloudformation/stack.ts | 2 +- packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts | 10 +++++----- packages/@aws-cdk/runtime-values/lib/rtv.ts | 2 +- 20 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index 4053534e1383c..341bbc88b40fb 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -73,7 +73,7 @@ export class AwsIntegration extends Integration { integrationHttpMethod: 'POST', uri: new cdk.Token(() => { if (!this.scope) { throw new Error('AwsIntegration must be used in API'); } - return cdk.Stack.find(this.scope).arnFromComponents({ + return cdk.Stack.find(this.scope).formatArn({ service: 'apigateway', account: backend, resource: apiType, diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index bde742b97acfa..3822c56ee85e3 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -159,7 +159,7 @@ export class Method extends cdk.Construct { } else if (options.credentialsPassthrough) { // arn:aws:iam::*:user/* // tslint:disable-next-line:max-line-length - credentials = cdk.Stack.find(this).arnFromComponents({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }); + credentials = cdk.Stack.find(this).formatArn({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }); } return { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 6c86b43f83279..36d8b114bea2e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -301,7 +301,7 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi method = '*'; } - return cdk.Stack.find(this).arnFromComponents({ + return cdk.Stack.find(this).formatArn({ service: 'execute-api', resource: this.restApiId, sep: '/', @@ -358,7 +358,7 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi private configureCloudWatchRole(apiResource: CfnRestApi) { const role = new iam.Role(this, 'CloudWatchRole', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), - managedPolicyArns: [ cdk.Stack.find(this).arnFromComponents({ + managedPolicyArns: [ cdk.Stack.find(this).formatArn({ service: 'iam', region: '', account: 'aws', diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index 2a0ba24086ca2..1c3ee7be6767f 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -487,7 +487,7 @@ class SingletonPolicy extends cdk.Construct { } private stackArnFromProps(props: { stackName: string, region?: string }): string { - return cdk.Stack.find(this).arnFromComponents({ + return cdk.Stack.find(this).formatArn({ region: props.region, service: 'cloudformation', resource: 'stack', diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 9d62f40b51088..2e512336c0ab5 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -272,7 +272,7 @@ function _isOrContains(entity: string | string[], value: string): boolean { } function _stackArn(stackName: string, scope: cdk.IConstruct): string { - return cdk.Stack.find(scope).arnFromComponents({ + return cdk.Stack.find(scope).formatArn({ service: 'cloudformation', resource: 'stack', resourceName: `${stackName}/*`, @@ -287,7 +287,7 @@ class PipelineDouble extends cdk.Construct implements cpapi.IPipeline { constructor(scope: cdk.Construct, id: string, { pipelineName, role }: { pipelineName?: string, role: iam.Role }) { super(scope, id); this.pipelineName = pipelineName || 'TestPipeline'; - this.pipelineArn = cdk.Stack.find(this).arnFromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }); + this.pipelineArn = cdk.Stack.find(this).formatArn({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }); this.role = role; } diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 371f7a2f45e84..adaa2bec62d0f 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -436,7 +436,7 @@ class ImportedProject extends ProjectBase { constructor(scope: cdk.Construct, id: string, private readonly props: ProjectImportProps) { super(scope, id); - this.projectArn = cdk.Stack.find(this).arnFromComponents({ + this.projectArn = cdk.Stack.find(this).formatArn({ service: 'codebuild', resource: 'project', resourceName: props.projectName, @@ -771,7 +771,7 @@ export class Project extends ProjectBase { } private createLoggingPermission() { - const logGroupArn = cdk.Stack.find(this).arnFromComponents({ + const logGroupArn = cdk.Stack.find(this).formatArn({ service: 'logs', resource: 'log-group', sep: ':', diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 5fe78b798866e..80bb733212c59 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -244,7 +244,7 @@ class ImportedRepository extends RepositoryBase { constructor(scope: cdk.Construct, id: string, private readonly props: RepositoryImportProps) { super(scope, id); - this.repositoryArn = cdk.Stack.find(this).arnFromComponents({ + this.repositoryArn = cdk.Stack.find(this).formatArn({ service: 'codecommit', resource: props.repositoryName, }); diff --git a/packages/@aws-cdk/aws-codedeploy/lib/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/application.ts index 19803d66a4d6a..e2aeb6ce4087b 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/application.ts @@ -101,7 +101,7 @@ export class ServerApplication extends cdk.Construct implements IServerApplicati } function applicationNameToArn(applicationName: string, scope: cdk.IConstruct): string { - return cdk.Stack.find(scope).arnFromComponents({ + return cdk.Stack.find(scope).formatArn({ service: 'codedeploy', resource: 'application', resourceName: applicationName, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts index c4516eefd2dc9..dfca704574f25 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts @@ -157,7 +157,7 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl } function arnForDeploymentConfigName(name: string, scope: cdk.IConstruct): string { - return cdk.Stack.find(scope).arnFromComponents({ + return cdk.Stack.find(scope).formatArn({ service: 'codedeploy', resource: 'deploymentconfig', resourceName: name, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index afe8405de5504..083cb853653ad 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -561,7 +561,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { } function deploymentGroupNameToArn(applicationName: string, deploymentGroupName: string, scope: cdk.IConstruct): string { - return cdk.Stack.find(scope).arnFromComponents({ + return cdk.Stack.find(scope).formatArn({ service: 'codedeploy', resource: 'deploymentgroup', resourceName: `${applicationName}/${deploymentGroupName}`, diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 14fd719d48c7e..8a99849447de5 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -127,7 +127,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { this.artifactStores = {}; // Does not expose a Fn::GetAtt for the ARN so we'll have to make it ourselves - this.pipelineArn = cdk.Stack.find(this).arnFromComponents({ + this.pipelineArn = cdk.Stack.find(this).formatArn({ service: 'codepipeline', resource: this.pipelineName }); diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 00f54cfdd04ad..d54facfe611d9 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -614,7 +614,7 @@ export class Table extends Construct { private makeScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { - roleArn: cdk.Stack.find(this).arnFromComponents({ + roleArn: cdk.Stack.find(this).formatArn({ // https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html service: 'iam', resource: 'role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com', diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index c676f966ae9b0..ad7e8870f7ac1 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -123,7 +123,7 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor * as the current stack. */ public static arnForLocalRepository(repositoryName: string, scope: cdk.IConstruct): string { - return cdk.Stack.find(scope).arnFromComponents({ + return cdk.Stack.find(scope).formatArn({ service: 'ecr', resource: 'repository', resourceName: repositoryName diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index b93d227fb9e09..cc3e5cefd8e48 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -228,7 +228,7 @@ export abstract class BaseService extends cdk.Construct private makeAutoScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { - roleArn: cdk.Stack.find(this).arnFromComponents({ + roleArn: cdk.Stack.find(this).formatArn({ service: 'iam', resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index 016cb61219e12..531080b80ac4b 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -18,7 +18,7 @@ export class AwsManagedPolicy { */ public get policyArn(): string { // the arn is in the form of - arn:aws:iam::aws:policy/ - return cdk.Stack.find(this.scope).arnFromComponents({ + return cdk.Stack.find(this.scope).formatArn({ service: "iam", region: "", // no region for managed policy account: "aws", // the account for a managed policy is 'aws' diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index 4404784164259..29ac33224c938 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -50,7 +50,7 @@ export class NotificationsResourceHandler extends cdk.Construct { const role = new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicyArns: [ - cdk.Stack.find(this).arnFromComponents({ + cdk.Stack.find(this).formatArn({ service: 'iam', region: '', // no region for managed policy account: 'aws', // the account for a managed policy is 'aws' diff --git a/packages/@aws-cdk/aws-s3/lib/util.ts b/packages/@aws-cdk/aws-s3/lib/util.ts index 577bc481fd6e6..e241db77cd5a0 100644 --- a/packages/@aws-cdk/aws-s3/lib/util.ts +++ b/packages/@aws-cdk/aws-s3/lib/util.ts @@ -9,7 +9,7 @@ export function parseBucketArn(construct: cdk.IConstruct, props: BucketImportPro } if (props.bucketName) { - return cdk.Stack.find(construct).arnFromComponents({ + return cdk.Stack.find(construct).formatArn({ // S3 Bucket names are globally unique in a partition, // and so their ARNs have empty region and account components region: '', diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index c8df077b2201d..d096a75c4c446 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -340,7 +340,7 @@ export class Stack extends Construct { * the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope * can be 'undefined'. */ - public arnFromComponents(components: ArnComponents): string { + public formatArn(components: ArnComponents): string { return arnFromComponents(components, this); } diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index 7971cbcf6355e..cbb3c3caa96de 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -5,7 +5,7 @@ export = { 'create from components with defaults'(test: Test) { const stack = new Stack(); - const arn = stack.arnFromComponents({ + const arn = stack.formatArn({ service: 'sqs', resource: 'myqueuename' }); @@ -20,7 +20,7 @@ export = { 'create from components with specific values for the various components'(test: Test) { const stack = new Stack(); - const arn = stack.arnFromComponents({ + const arn = stack.formatArn({ service: 'dynamodb', resource: 'table', account: '123456789012', @@ -37,7 +37,7 @@ export = { 'allow empty string in components'(test: Test) { const stack = new Stack(); - const arn = stack.arnFromComponents({ + const arn = stack.formatArn({ service: 's3', resource: 'my-bucket', account: '', @@ -54,7 +54,7 @@ export = { 'resourcePathSep can be set to ":" instead of the default "/"'(test: Test) { const stack = new Stack(); - const arn = stack.arnFromComponents({ + const arn = stack.formatArn({ service: 'codedeploy', resource: 'application', sep: ':', @@ -71,7 +71,7 @@ export = { 'fails if resourcePathSep is neither ":" nor "/"'(test: Test) { const stack = new Stack(); - test.throws(() => stack.arnFromComponents({ + test.throws(() => stack.formatArn({ service: 'foo', resource: 'bar', sep: 'x' })); diff --git a/packages/@aws-cdk/runtime-values/lib/rtv.ts b/packages/@aws-cdk/runtime-values/lib/rtv.ts index 1c3570e473434..3cf7222e78c0b 100644 --- a/packages/@aws-cdk/runtime-values/lib/rtv.ts +++ b/packages/@aws-cdk/runtime-values/lib/rtv.ts @@ -65,7 +65,7 @@ export class RuntimeValue extends cdk.Construct { value: props.value, }); - this.parameterArn = cdk.Stack.find(this).arnFromComponents({ + this.parameterArn = cdk.Stack.find(this).formatArn({ service: 'ssm', resource: 'parameter', resourceName: this.parameterName From c7d4314e6bfbc0226fee3e0168162f29b1e935da Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 16:09:24 +0100 Subject: [PATCH 34/39] Move logic for collecting tokens to token layer --- .../cdk/lib/cloudformation/stack-element.ts | 23 +++++++++++------- packages/@aws-cdk/cdk/lib/core/construct.ts | 8 ++++--- .../@aws-cdk/cdk/lib/core/tokens/options.ts | 16 +++++-------- .../@aws-cdk/cdk/lib/core/tokens/resolve.ts | 24 ++++++++++++++++--- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts index 8d5721a43a782..e07cdae7a0825 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts @@ -1,5 +1,4 @@ import { Construct, IConstruct, PATH_SEP } from "../core/construct"; -import { RESOLVE_OPTIONS } from "../core/tokens/options"; const LOGICAL_ID_MD = 'aws:cdk:logicalId'; @@ -119,17 +118,22 @@ export abstract class StackElement extends Construct implements IDependable { * Automatically detect references in this StackElement */ protected prepare() { - const options = RESOLVE_OPTIONS.push({ preProcess: (token, _) => { this.node.recordReference(token); return token; } }); try { - // Execute for side effect of calling 'preProcess'. - // Note: it might be that the properties of the CFN object aren't valid. This will usually be preventatively - // caught in a construct's validate() and turned into a nicely descriptive error, but we're running prepare() - // before validate(). Swallow errors that occur because the CFN layer doesn't validate completely. - this.node.resolve(this.toCloudFormation()); + // Note: it might be that the properties of the CFN object aren't valid. + // This will usually be preventatively caught in a construct's validate() + // and turned into a nicely descriptive error, but we're running prepare() + // before validate(). Swallow errors that occur because the CFN layer + // doesn't validate completely. + // + // This does make the assumption that the error will not be rectified, + // but the error will be thrown later on anyway. If the error doesn't + // get thrown down the line, we may miss references. + this.node.recordReference(...findTokens(this.toCloudFormation(), { + scope: this, + prefix: [] + })); } catch (e) { if (e.type !== 'CfnSynthesisError') { throw e; } - } finally { - options.pop(); } } } @@ -145,6 +149,7 @@ export class Ref extends CfnReference { } } +import { findTokens } from "../core/tokens/resolve"; import { Stack } from "./stack"; /** diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 675392530cbd2..9551618c24b46 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -418,9 +418,11 @@ export class ConstructNode { /** * Record a reference originating from this construct node */ - public recordReference(ref: Token) { - if (ref.isReference) { - this.references.add(ref); + public recordReference(...refs: Token[]) { + for (const ref of refs) { + if (ref.isReference) { + this.references.add(ref); + } } } diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts index 529067f8f8bde..8fb5bc90eee16 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts @@ -1,9 +1,9 @@ -import { ResolveContext, Token } from "./token"; +import { Token } from "./token"; /** * Function used to preprocess Tokens before resolving */ -export type PreProcessFunc = (token: Token, context: ResolveContext) => Token; +export type CollectFunc = (token: Token) => void; /** * Global options for resolve() @@ -28,12 +28,12 @@ export class ResolveConfiguration { }; } - public get preProcess(): PreProcessFunc { + public get collect(): CollectFunc | undefined { for (let i = this.options.length - 1; i >= 0; i--) { - const ret = this.options[i].preProcess; + const ret = this.options[i].collect; if (ret !== undefined) { return ret; } } - return noPreprocessFunction; + return undefined; } } @@ -45,7 +45,7 @@ interface ResolveOptions { /** * What function to use to preprocess Tokens before resolving them */ - preProcess?: PreProcessFunc; + collect?: CollectFunc; } const glob = global as any; @@ -54,7 +54,3 @@ const glob = global as any; * Singleton instance of resolver options */ export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); - -function noPreprocessFunction(x: Token, _: ResolveContext) { - return x; -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts index b9555e997d223..9fec0fc3c12ac 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts @@ -1,6 +1,6 @@ import { containsListToken, TOKEN_MAP } from "./encoding"; import { RESOLVE_OPTIONS } from "./options"; -import { RESOLVE_METHOD, ResolveContext } from "./token"; +import { RESOLVE_METHOD, ResolveContext, Token } from "./token"; import { unresolved } from "./unresolved"; // This file should not be exported to consumers, resolving should happen through Construct.resolve() @@ -80,8 +80,9 @@ export function resolve(obj: any, context: ResolveContext): any { // if (unresolved(obj)) { - const preProcess = RESOLVE_OPTIONS.preProcess; - const value = preProcess(obj, context)[RESOLVE_METHOD](context); + const collect = RESOLVE_OPTIONS.collect; + if (collect) { collect(obj); } + const value = obj[RESOLVE_METHOD](context); return resolve(value, context); } @@ -116,6 +117,23 @@ export function resolve(obj: any, context: ResolveContext): any { return result; } +/** + * Find all Tokens that are used in the given structure + */ +export function findTokens(obj: any, context: ResolveContext): Token[] { + const ret = new Array(); + + const options = RESOLVE_OPTIONS.push({ collect: ret.push.bind(ret) }); + try { + // resolve() for side effect of calling 'preProcess', which adds to the + resolve(obj, context); + } finally { + options.pop(); + } + + return ret; +} + /** * Determine whether an object is a Construct * From d2a656f36d06e3f42737b7917d8f6cef412603a0 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 16:55:16 +0100 Subject: [PATCH 35/39] validate() is now protected --- .../app-delivery/lib/pipeline-deploy-stack-action.ts | 2 +- packages/@aws-cdk/aws-apigateway/lib/restapi.ts | 2 +- packages/@aws-cdk/aws-codebuild/lib/project.ts | 2 +- packages/@aws-cdk/aws-codepipeline-api/lib/action.ts | 2 +- packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts | 2 +- packages/@aws-cdk/aws-codepipeline/lib/stage.ts | 2 +- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts | 4 ++-- packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 2 +- .../lib/alb/application-listener-rule.ts | 4 ++-- .../lib/alb/application-listener.ts | 2 +- .../lib/shared/base-listener.ts | 2 +- packages/@aws-cdk/aws-events/lib/rule.ts | 2 +- packages/@aws-cdk/aws-iam/lib/policy.ts | 2 +- packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts | 2 +- .../@aws-cdk/aws-stepfunctions/lib/states/parallel.ts | 2 +- packages/@aws-cdk/cdk/test/core/test.construct.ts | 8 ++++---- packages/@aws-cdk/cdk/test/test.app.ts | 2 +- 18 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index e784850965d7a..d6b939196582e 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -140,7 +140,7 @@ export class PipelineDeployStackAction extends cdk.Construct { }); } - public validate(): string[] { + protected validate(): string[] { const result = super.validate(); const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA); if (assets.length > 0) { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 36d8b114bea2e..743077d422bc7 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -312,7 +312,7 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi /** * Performs validation of the REST API. */ - public validate() { + protected validate() { if (this.methods.length === 0) { return [ `The REST API doesn't contain any methods` ]; } diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index adaa2bec62d0f..b598da915b22e 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -708,7 +708,7 @@ export class Project extends ProjectBase { /** * @override */ - public validate(): string[] { + protected validate(): string[] { const ret = new Array(); if (this.source.type === SourceType.CodePipeline) { if (this._secondarySources.length > 0) { diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts index 5d6fdacf9fba8..f444635ba288b 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -239,7 +239,7 @@ export abstract class Action extends cdk.Construct { this.stage._internal._attachAction(this); } - public validate(): string[] { + protected validate(): string[] { return validation.validateArtifactBounds('input', this._actionInputArtifacts, this.artifactBounds.minInputs, this.artifactBounds.maxInputs, this.category, this.provider) .concat(validation.validateArtifactBounds('output', this._actionOutputArtifacts, this.artifactBounds.minOutputs, diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 8a99849447de5..00c0d6fb93ad1 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -220,7 +220,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { * https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements * @override */ - public validate(): string[] { + protected validate(): string[] { return [ ...this.validateHasStages(), ...this.validateSourceActionLocations() diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index ee51f5c5fa33b..8b947a47c96bd 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -105,7 +105,7 @@ export class Stage extends cdk.Construct implements cpapi.IStage, cpapi.IInterna return this._actions.slice(); } - public validate(): string[] { + protected validate(): string[] { return this.validateHasActions(); } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index d54facfe611d9..cd8ebef7be464 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -474,7 +474,7 @@ export class Table extends Construct { * * @returns an array of validation error message */ - public validate(): string[] { + protected validate(): string[] { const errors = new Array(); if (!this.tablePartitionKey) { diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 4c197604b76d2..42497d3e1ec3c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -245,7 +245,7 @@ export class TaskDefinition extends cdk.Construct { /** * Validate this task definition */ - public validate(): string[] { + protected validate(): string[] { const ret = super.validate(); if (isEc2Compatible(this.compatibility)) { @@ -423,4 +423,4 @@ export interface ITaskDefinitionExtension { * Apply the extension to the given TaskDefinition */ extend(taskDefinition: TaskDefinition): void; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 80e85bf387778..80628ad98ccb6 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -231,7 +231,7 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { /** * Validate this Ec2Service */ - public validate(): string[] { + protected validate(): string[] { const ret = super.validate(); if (!this.cluster.hasEc2Capacity) { ret.push('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index cbe634e20b6c6..df53ed3f16daa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -112,7 +112,7 @@ export class ApplicationListenerRule extends cdk.Construct implements cdk.IDepen /** * Validate the rule */ - public validate() { + protected validate() { if (this.actions.length === 0) { return ['Listener rule needs at least one action']; } @@ -142,4 +142,4 @@ export class ApplicationListenerRule extends cdk.Construct implements cdk.IDepen } return ret; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 60c2033f1e153..45fe5083c841e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -227,7 +227,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis /** * Validate this listener. */ - public validate(): string[] { + protected validate(): string[] { const errors = super.validate(); if (this.protocol === ApplicationProtocol.Https && this.certificateArns.length === 0) { errors.push('HTTPS Listener needs at least one certificate (call addCertificateArns)'); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index a4043438529b8..44b8299d5cb9b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -25,7 +25,7 @@ export abstract class BaseListener extends cdk.Construct implements cdk.IDependa /** * Validate this listener */ - public validate(): string[] { + protected validate(): string[] { if (this.defaultActions.length === 0) { return ['Listener needs at least one default target group (call addTargetGroups)']; } diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index e7ff424d4f582..56b58e73c96c5 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -199,7 +199,7 @@ export class EventRule extends Construct implements IEventRule { mergeEventPattern(this.eventPattern, eventPattern); } - public validate() { + protected validate() { if (Object.keys(this.eventPattern).length === 0 && !this.scheduleExpression) { return [ `Either 'eventPattern' or 'scheduleExpression' must be defined` ]; } diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index d3a5104a02b9e..6c5208a7b9dd9 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -171,7 +171,7 @@ export class Policy extends Construct implements IDependable { group.attachInlinePolicy(this); } - public validate(): string[] { + protected validate(): string[] { const result = new Array(); // validate that the policy document is not empty diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts b/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts index 59d0c1afb2441..10a8e89c06524 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts @@ -105,7 +105,7 @@ export class ClusterParameterGroup extends cdk.Construct implements IClusterPara /** * Validate this construct */ - public validate(): string[] { + protected validate(): string[] { if (Object.keys(this.parameters).length === 0) { return ['At least one parameter required, call setParameter().']; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index 1833dbf5988b5..d54a683547558 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -107,7 +107,7 @@ export class Parallel extends State implements INextable { /** * Validate this state */ - public validate(): string[] { + protected validate(): string[] { if (this.branches.length === 0) { return ['Parallel must have at least one branch']; } diff --git a/packages/@aws-cdk/cdk/test/core/test.construct.ts b/packages/@aws-cdk/cdk/test/core/test.construct.ts index aa70c643d8e51..dc786b63b1288 100644 --- a/packages/@aws-cdk/cdk/test/core/test.construct.ts +++ b/packages/@aws-cdk/cdk/test/core/test.construct.ts @@ -307,13 +307,13 @@ export = { 'construct.validate() can be implemented to perform validation, construct.validateTree() will return all errors from the subtree (DFS)'(test: Test) { class MyConstruct extends Construct { - public validate() { + protected validate() { return [ 'my-error1', 'my-error2' ]; } } class YourConstruct extends Construct { - public validate() { + protected validate() { return [ 'your-error1' ]; } } @@ -325,7 +325,7 @@ export = { new YourConstruct(this, 'YourConstruct'); } - public validate() { + protected validate() { return [ 'their-error' ]; } } @@ -338,7 +338,7 @@ export = { new TheirConstruct(this, 'TheirConstruct'); } - public validate() { + protected validate() { return [ 'stack-error' ]; } } diff --git a/packages/@aws-cdk/cdk/test/test.app.ts b/packages/@aws-cdk/cdk/test/test.app.ts index 8c55bc8cc4d45..b0e5edff50db5 100644 --- a/packages/@aws-cdk/cdk/test/test.app.ts +++ b/packages/@aws-cdk/cdk/test/test.app.ts @@ -195,7 +195,7 @@ export = { 'app.synthesizeStack(stack) performs validation first (app.validateAll()) and if there are errors, it returns the errors'(test: Test) { class Child extends Construct { - public validate() { + protected validate() { return [ `Error from ${this.node.id}` ]; } } From fdeaa5521ea264ca551a8b02ad4ab8e278dbec15 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 17:26:49 +0100 Subject: [PATCH 36/39] Move protected() methods down in source --- .../lib/pipeline-deploy-stack-action.ts | 20 +++++----- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 16 ++++---- .../@aws-cdk/aws-codebuild/lib/project.ts | 36 +++++++++--------- .../aws-codepipeline-api/lib/action.ts | 16 ++++---- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 30 +++++++-------- .../@aws-cdk/aws-codepipeline/lib/stage.ts | 8 ++-- .../aws-dynamodb/test/test.dynamodb.ts | 2 +- .../aws-ecs/lib/base/task-definition.ts | 38 +++++++++---------- .../lib/alb/application-listener-rule.ts | 20 +++++----- .../lib/alb/application-listener.ts | 22 +++++------ .../aws-stepfunctions/lib/states/parallel.ts | 20 +++++----- 11 files changed, 114 insertions(+), 114 deletions(-) diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index d6b939196582e..4b9873f6b48b2 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -140,16 +140,6 @@ export class PipelineDeployStackAction extends cdk.Construct { }); } - protected validate(): string[] { - const result = super.validate(); - const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA); - if (assets.length > 0) { - // FIXME: Implement the necessary actions to publish assets - result.push(`Cannot deploy the stack ${this.stack.name} because it references ${assets.length} asset(s)`); - } - return result; - } - /** * Add policy statements to the role deploying the stack. * @@ -162,6 +152,16 @@ export class PipelineDeployStackAction extends cdk.Construct { public addToRolePolicy(statement: iam.PolicyStatement) { this.role.addToPolicy(statement); } + + protected validate(): string[] { + const result = super.validate(); + const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA); + if (assets.length > 0) { + // FIXME: Implement the necessary actions to publish assets + result.push(`Cannot deploy the stack ${this.stack.name} because it references ${assets.length} asset(s)`); + } + return result; + } } function cfnCapabilities(adminPermissions: boolean, capabilities?: cfn.CloudFormationCapabilities): cfn.CloudFormationCapabilities { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 743077d422bc7..ea6ed72213356 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -309,6 +309,14 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi }); } + /** + * Internal API used by `Method` to keep an inventory of methods at the API + * level for validation purposes. + */ + public _attachMethod(method: Method) { + this.methods.push(method); + } + /** * Performs validation of the REST API. */ @@ -320,14 +328,6 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi return []; } - /** - * Internal API used by `Method` to keep an inventory of methods at the API - * level for validation purposes. - */ - public _attachMethod(method: Method) { - this.methods.push(method); - } - private configureDeployment(props: RestApiProps) { const deploy = props.deploy === undefined ? true : props.deploy; if (deploy) { diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index b598da915b22e..2ea98fa5e0c15 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -705,24 +705,6 @@ export class Project extends ProjectBase { this.addToRolePolicy(this.createLoggingPermission()); } - /** - * @override - */ - protected validate(): string[] { - const ret = new Array(); - if (this.source.type === SourceType.CodePipeline) { - if (this._secondarySources.length > 0) { - ret.push('A Project with a CodePipeline Source cannot have secondary sources. ' + - "Use the CodeBuild Pipeline Actions' `additionalInputArtifacts` property instead"); - } - if (this._secondaryArtifacts.length > 0) { - ret.push('A Project with a CodePipeline Source cannot have secondary artifacts. ' + - "Use the CodeBuild Pipeline Actions' `additionalOutputArtifactNames` property instead"); - } - } - return ret; - } - /** * Export this Project. Allows referencing this Project in a different CDK Stack. */ @@ -770,6 +752,24 @@ export class Project extends ProjectBase { this._secondaryArtifacts.push(secondaryArtifact); } + /** + * @override + */ + protected validate(): string[] { + const ret = new Array(); + if (this.source.type === SourceType.CodePipeline) { + if (this._secondarySources.length > 0) { + ret.push('A Project with a CodePipeline Source cannot have secondary sources. ' + + "Use the CodeBuild Pipeline Actions' `additionalInputArtifacts` property instead"); + } + if (this._secondaryArtifacts.length > 0) { + ret.push('A Project with a CodePipeline Source cannot have secondary artifacts. ' + + "Use the CodeBuild Pipeline Actions' `additionalOutputArtifactNames` property instead"); + } + } + return ret; + } + private createLoggingPermission() { const logGroupArn = cdk.Stack.find(this).formatArn({ service: 'logs', diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts index f444635ba288b..0e1bbf2344590 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -239,14 +239,6 @@ export abstract class Action extends cdk.Construct { this.stage._internal._attachAction(this); } - protected validate(): string[] { - return validation.validateArtifactBounds('input', this._actionInputArtifacts, this.artifactBounds.minInputs, - this.artifactBounds.maxInputs, this.category, this.provider) - .concat(validation.validateArtifactBounds('output', this._actionOutputArtifacts, this.artifactBounds.minOutputs, - this.artifactBounds.maxOutputs, this.category, this.provider) - ); - } - public onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { const rule = new events.EventRule(this, name, options); rule.addTarget(target); @@ -270,6 +262,14 @@ export abstract class Action extends cdk.Construct { return this._actionOutputArtifacts.slice(); } + protected validate(): string[] { + return validation.validateArtifactBounds('input', this._actionInputArtifacts, this.artifactBounds.minInputs, + this.artifactBounds.maxInputs, this.category, this.provider) + .concat(validation.validateArtifactBounds('output', this._actionOutputArtifacts, this.artifactBounds.minOutputs, + this.artifactBounds.maxOutputs, this.category, this.provider) + ); + } + protected addOutputArtifact(name: string = this.stage._internal._generateOutputArtifactName(this)): Artifact { const artifact = new Artifact(this, name); this._actionOutputArtifacts.push(artifact); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 00c0d6fb93ad1..7c29de60f231a 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -212,21 +212,6 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { return rule; } - /** - * Validate the pipeline structure - * - * Validation happens according to the rules documented at - * - * https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements - * @override - */ - protected validate(): string[] { - return [ - ...this.validateHasStages(), - ...this.validateSourceActionLocations() - ]; - } - /** * Get the number of Stages in this Pipeline. */ @@ -254,6 +239,21 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { return ret; } + /** + * Validate the pipeline structure + * + * Validation happens according to the rules documented at + * + * https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements + * @override + */ + protected validate(): string[] { + return [ + ...this.validateHasStages(), + ...this.validateSourceActionLocations() + ]; + } + /** * Adds a Stage to this Pipeline. * This is an internal operation - diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 8b947a47c96bd..1a1eb46e6e4ea 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -105,10 +105,6 @@ export class Stage extends cdk.Construct implements cpapi.IStage, cpapi.IInterna return this._actions.slice(); } - protected validate(): string[] { - return this.validateHasActions(); - } - public render(): CfnPipeline.StageDeclarationProperty { return { name: this.node.id, @@ -149,6 +145,10 @@ export class Stage extends cdk.Construct implements cpapi.IStage, cpapi.IInterna return (this.pipeline as any)._findInputArtifact(this, action); } + protected validate(): string[] { + return this.validateHasActions(); + } + private renderAction(action: cpapi.Action): CfnPipeline.ActionDeclarationProperty { return { name: action.node.id, diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index b5e5ab4c32e4b..122904699d9cb 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1175,7 +1175,7 @@ export = { sortKey: LSI_SORT_KEY }); - const errors = table.validate(); + const errors = table.node.validateTree(); test.strictEqual(1, errors.length); test.strictEqual('a sort key of the table must be specified to add local secondary indexes', errors[0]); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 42497d3e1ec3c..f63fe555c3def 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -242,25 +242,6 @@ export class TaskDefinition extends cdk.Construct { this.volumes.push(volume); } - /** - * Validate this task definition - */ - protected validate(): string[] { - const ret = super.validate(); - - if (isEc2Compatible(this.compatibility)) { - // EC2 mode validations - - // Container sizes - for (const container of this.containers) { - if (!container.memoryLimitSpecified) { - ret.push(`ECS Container ${container.node.id} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); - } - } - } - return ret; - } - /** * Constrain where tasks can be placed */ @@ -294,6 +275,25 @@ export class TaskDefinition extends cdk.Construct { return this.executionRole; } + /** + * Validate this task definition + */ + protected validate(): string[] { + const ret = super.validate(); + + if (isEc2Compatible(this.compatibility)) { + // EC2 mode validations + + // Container sizes + for (const container of this.containers) { + if (!container.memoryLimitSpecified) { + ret.push(`ECS Container ${container.node.id} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); + } + } + } + return ret; + } + /** * Render the placement constraints */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index df53ed3f16daa..b6857f0fdbfa0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -109,16 +109,6 @@ export class ApplicationListenerRule extends cdk.Construct implements cdk.IDepen this.conditions[field] = values; } - /** - * Validate the rule - */ - protected validate() { - if (this.actions.length === 0) { - return ['Listener rule needs at least one action']; - } - return []; - } - /** * Add a TargetGroup to load balance to */ @@ -130,6 +120,16 @@ export class ApplicationListenerRule extends cdk.Construct implements cdk.IDepen targetGroup.registerListener(this.listener, this); } + /** + * Validate the rule + */ + protected validate() { + if (this.actions.length === 0) { + return ['Listener rule needs at least one action']; + } + return []; + } + /** * Render the conditions for this rule */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 45fe5083c841e..eb32e0264f6b1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -224,17 +224,6 @@ export class ApplicationListener extends BaseListener implements IApplicationLis this.connections.allowTo(connectable, portRange, 'Load balancer to target'); } - /** - * Validate this listener. - */ - protected validate(): string[] { - const errors = super.validate(); - if (this.protocol === ApplicationProtocol.Https && this.certificateArns.length === 0) { - errors.push('HTTPS Listener needs at least one certificate (call addCertificateArns)'); - } - return errors; - } - /** * Export this listener */ @@ -246,6 +235,17 @@ export class ApplicationListener extends BaseListener implements IApplicationLis }; } + /** + * Validate this listener. + */ + protected validate(): string[] { + const errors = super.validate(); + if (this.protocol === ApplicationProtocol.Https && this.certificateArns.length === 0) { + errors.push('HTTPS Listener needs at least one certificate (call addCertificateArns)'); + } + return errors; + } + /** * Add a default TargetGroup */ diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index d54a683547558..22bd2fe03eca4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -104,16 +104,6 @@ export class Parallel extends State implements INextable { return this; } - /** - * Validate this state - */ - protected validate(): string[] { - if (this.branches.length === 0) { - return ['Parallel must have at least one branch']; - } - return []; - } - /** * Return the Amazon States Language object for this state */ @@ -128,4 +118,14 @@ export class Parallel extends State implements INextable { ...this.renderBranches(), }; } + + /** + * Validate this state + */ + protected validate(): string[] { + if (this.branches.length === 0) { + return ['Parallel must have at least one branch']; + } + return []; + } } From 00cbe073a5722bf9c4f145861d0fcc5a493801de Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 17:41:50 +0100 Subject: [PATCH 37/39] Call validateTree() instead of protected validate() in tests --- .../app-delivery/test/test.pipeline-deploy-stack-action.ts | 2 +- .../aws-codepipeline/test/test.general-validation.ts | 6 +++--- packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index 3a73933d135e9..7e158a11df1ef 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -274,7 +274,7 @@ export = nodeunit.testCase({ for (let i = 0 ; i < assetCount ; i++) { deployedStack.node.addMetadata(cxapi.ASSET_METADATA, {}); } - test.deepEqual(action.validate(), + test.deepEqual(action.node.validateTree(), [`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]); } ) diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts b/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts index f10c842b09e7e..a152b39a76579 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts @@ -37,7 +37,7 @@ export = { 'should fail if Stage has no Actions'(test: Test) { const stage = stageForTesting(); - test.deepEqual(stage.validate().length, 1); + test.deepEqual(stage.node.validateTree().length, 1); test.done(); } @@ -48,7 +48,7 @@ export = { const stack = new cdk.Stack(); const pipeline = new Pipeline(stack, 'Pipeline'); - test.deepEqual(pipeline.validate().length, 1); + test.deepEqual(pipeline.node.validateTree().length, 1); test.done(); }, @@ -73,7 +73,7 @@ export = { bucketKey: 'key', }); - test.deepEqual(pipeline.validate().length, 1); + test.deepEqual(pipeline.node.validateTree().length, 1); test.done(); } diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 5a29a47764a2f..de1b69b01c5be 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -38,7 +38,7 @@ export = { }); test.notDeepEqual(stack.toCloudFormation(), {}); - test.deepEqual([], pipeline.validate()); + test.deepEqual([], pipeline.node.validateTree()); test.done(); }, @@ -127,7 +127,7 @@ export = { ] })); - test.deepEqual([], p.validate()); + test.deepEqual([], p.node.validateTree()); test.done(); }, @@ -211,7 +211,7 @@ export = { ] })); - test.deepEqual([], pipeline.validate()); + test.deepEqual([], pipeline.node.validateTree()); test.done(); }, From 0346946d28a91f89c796834e5f4849660996cbfb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 18:05:43 +0100 Subject: [PATCH 38/39] Fix test --- packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index 122904699d9cb..c19eaeaf6521b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1178,7 +1178,7 @@ export = { const errors = table.node.validateTree(); test.strictEqual(1, errors.length); - test.strictEqual('a sort key of the table must be specified to add local secondary indexes', errors[0]); + test.strictEqual('a sort key of the table must be specified to add local secondary indexes', errors[0].message); test.done(); }, From 34bfb341f303fd6a0d4d109e46bcc8ebe8728c96 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 8 Jan 2019 18:27:36 +0100 Subject: [PATCH 39/39] Fix another test --- .../app-delivery/test/test.pipeline-deploy-stack-action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index 7e158a11df1ef..25f41261ec849 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -274,7 +274,7 @@ export = nodeunit.testCase({ for (let i = 0 ; i < assetCount ; i++) { deployedStack.node.addMetadata(cxapi.ASSET_METADATA, {}); } - test.deepEqual(action.node.validateTree(), + test.deepEqual(action.node.validateTree().map(x => x.message), [`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]); } )