From 6314e965c7c297d0427011ddbd2b1fc27ed60608 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sat, 11 Apr 2020 00:02:21 +0300 Subject: [PATCH 1/8] Squash --- .../aws-cloudformation/lib/nested-stack.ts | 95 +++--- .../test/integ.nested-stack.expected.json | 30 +- ...teg.nested-stacks-multi-refs.expected.json | 116 ++++++++ .../test/integ.nested-stacks-multi-refs.ts | 31 ++ ...cks-nested-export-to-sibling.expected.json | 106 +++++++ ....nested-stacks-nested-export-to-sibling.ts | 20 ++ .../integ.nested-stacks-refs3.expected.json | 24 +- .../test/test.nested-stack.ts | 59 +++- .../test/integ.global.expected.json | 18 +- .../test/integ.eks-cluster.expected.json | 40 ++- packages/@aws-cdk/core/lib/app.ts | 6 + packages/@aws-cdk/core/lib/deps.ts | 2 +- .../core/lib/private/cfn-reference.ts | 10 + .../@aws-cdk/core/lib/private/prepare-app.ts | 67 +++++ packages/@aws-cdk/core/lib/private/refs.ts | 276 ++++++++++++++++++ packages/@aws-cdk/core/lib/stack.ts | 175 +---------- packages/@aws-cdk/core/lib/util.ts | 19 -- 17 files changed, 802 insertions(+), 292 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json create mode 100644 packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts create mode 100644 packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.expected.json create mode 100644 packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.ts create mode 100644 packages/@aws-cdk/core/lib/private/prepare-app.ts create mode 100644 packages/@aws-cdk/core/lib/private/refs.ts diff --git a/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts index 58c5b346ebe6e..8b6ec7102efb4 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts @@ -1,5 +1,7 @@ import * as sns from '@aws-cdk/aws-sns'; -import { Aws, CfnOutput, CfnParameter, CfnResource, Construct, Duration, Fn, IResolvable, IResolveContext, Lazy, Reference, Stack, Token } from '@aws-cdk/core'; +import { Aws, CfnResource, Construct, Duration, FileAssetPackaging, Fn, IResolveContext, Stack, Token } from '@aws-cdk/core'; +import { Lazy } from 'constructs'; +import * as crypto from 'crypto'; import { CfnStack } from './cloudformation.generated'; const NESTED_STACK_SYMBOL = Symbol.for('@aws-cdk/aws-cloudformation.NestedStack'); @@ -10,7 +12,6 @@ const NESTED_STACK_SYMBOL = Symbol.for('@aws-cdk/aws-cloudformation.NestedStack' * @experimental */ export interface NestedStackProps { - /** * The set value pairs that represent the parameters passed to CloudFormation * when this nested stack is created. Each parameter has a name corresponding @@ -82,12 +83,16 @@ export class NestedStack extends Stack { private readonly resource: CfnStack; private readonly _contextualStackId: string; private readonly _contextualStackName: string; + private _templateUrl?: string; + private _parentStack: Stack; constructor(scope: Construct, id: string, props: NestedStackProps = { }) { const parentStack = findParentStack(scope); super(scope, id, { env: { account: parentStack.account, region: parentStack.region } }); + this._parentStack = parentStack; + // @deprecate: remove this in v2.0 (redundent) const parentScope = new Construct(scope, id + '.NestedStack'); @@ -99,7 +104,7 @@ export class NestedStack extends Stack { this.parameters = props.parameters || {}; this.resource = new CfnStack(parentScope, `${id}.NestedStackResource`, { - templateUrl: this.templateUrl, + templateUrl: Lazy.stringValue({ produce: () => this._templateUrl || '' }), parameters: Lazy.anyValue({ produce: () => Object.keys(this.parameters).length > 0 ? this.parameters : undefined }), notificationArns: props.notifications ? props.notifications.map(n => n.topicArn) : undefined, timeoutInMinutes: props.timeout ? props.timeout.toMinutes() : undefined, @@ -144,62 +149,46 @@ export class NestedStack extends Stack { } /** - * Called by the base "prepare" method when a reference is found. + * Assign a value to one of the nested stack parameters. + * @param name The parameter name (ID) + * @param value The value to assign */ - protected prepareCrossReference(sourceStack: Stack, reference: Reference): IResolvable { - const targetStack = Stack.of(reference.target); - - // the nested stack references a resource from the parent stack: - // we pass it through a as a cloudformation parameter - if (targetStack === sourceStack.nestedStackParent) { - // we call "this.resolve" to ensure that tokens do not creep in (for example, if the reference display name includes tokens) - const paramId = this.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); - let param = this.node.tryFindChild(paramId) as CfnParameter; - if (!param) { - param = new CfnParameter(this, paramId, { type: 'String' }); - this.parameters[param.logicalId] = Token.asString(reference); - } - - return param.value; - } - - // parent stack references a resource from the nested stack: - // we output it from the nested stack and use "Fn::GetAtt" as the reference value - if (targetStack === this && targetStack.nestedStackParent === sourceStack) { - return this.getCreateOutputForReference(reference); - } - - // sibling nested stacks (same parent): - // output from one and pass as parameter to the other - if (targetStack.nestedStackParent && targetStack.nestedStackParent === sourceStack.nestedStackParent) { - const outputValue = this.getCreateOutputForReference(reference); - return (sourceStack as NestedStack).prepareCrossReference(sourceStack, outputValue); - } - - // nested stack references a value from some other non-nested stack: - // normal export/import, with dependency between the parents - if (sourceStack.nestedStackParent && sourceStack.nestedStackParent !== targetStack) { - return super.prepareCrossReference(sourceStack, reference); - } + public setParameter(name: string, value: string) { + this.parameters[name] = value; + } - // some non-nested stack (that is not the parent) references a resource inside the nested stack: - // we output the value and let our parent export it - if (!sourceStack.nestedStackParent && targetStack.nestedStackParent && targetStack.nestedStackParent !== sourceStack) { - const outputValue = this.getCreateOutputForReference(reference); - return (targetStack.nestedStackParent as NestedStack).prepareCrossReference(sourceStack, outputValue); + /** + * Defines an asset at the parent stack which represents the template of this + * nested stack. + * + * This private API is used by `App.prepare()` within a loop that rectifies + * references every time an asset is added. This is because (at the moment) + * assets are addressed using CloudFormation parameters. + * + * @returns `true` if a new asset was added or `false` if an asset was + * previously added. When this returns `true`, App will do another reference + * rectification cycle. + * + * @internal + */ + public _prepareTemplateAsset() { + if (this._templateUrl) { + return false; } - throw new Error('unexpected nested stack cross reference'); - } + const cfn = JSON.stringify((this as any)._toCloudFormation()); + const templateHash = crypto.createHash('sha256').update(cfn).digest('hex'); - private getCreateOutputForReference(reference: Reference) { - const outputId = `${reference.target.node.uniqueId}${reference.displayName}`; - let output = this.node.tryFindChild(outputId) as CfnOutput; - if (!output) { - output = new CfnOutput(this, outputId, { value: Token.asString(reference) }); - } + const templateLocation = this._parentStack.addFileAsset({ + packaging: FileAssetPackaging.FILE, + sourceHash: templateHash, + fileName: this.templateFile + }); - return this.resource.getAtt(`Outputs.${output.logicalId}`); + // if bucketName/objectKey are cfn parameters from a stack other than the parent stack, they will + // be resolved as cross-stack references like any other (see "multi" tests). + this._templateUrl = `https://s3.${this._parentStack.region}.${this._parentStack.urlSuffix}/${templateLocation.bucketName}/${templateLocation.objectKey}`; + return true; } private contextualAttribute(innerValue: string, outerValue: string) { diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json index b765d6db35ab6..2e467682966ba 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json @@ -158,7 +158,7 @@ }, "/", { - "Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3BucketE3660F43" + "Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3Bucket1DDC9C52" }, "/", { @@ -168,7 +168,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470" + "Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1" } ] } @@ -181,7 +181,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470" + "Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1" } ] } @@ -254,29 +254,29 @@ } }, "Parameters": { - "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3BucketB322F951": { + "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3Bucket1DDC9C52": { "Type": "String", - "Description": "S3 bucket for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" + "Description": "S3 bucket for asset \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\"" }, - "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3VersionKeyAA9C5AF4": { + "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1": { "Type": "String", - "Description": "S3 key for asset version \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" + "Description": "S3 key for asset version \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\"" }, - "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfArtifactHash5D335705": { + "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aArtifactHash3AA59378": { "Type": "String", - "Description": "Artifact hash for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" + "Description": "Artifact hash for asset \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\"" }, - "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3BucketE3660F43": { + "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3BucketB322F951": { "Type": "String", - "Description": "S3 bucket for asset \"dddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccaf\"" + "Description": "S3 bucket for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" }, - "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470": { + "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3VersionKeyAA9C5AF4": { "Type": "String", - "Description": "S3 key for asset version \"dddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccaf\"" + "Description": "S3 key for asset version \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" }, - "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafArtifactHashEECD8E35": { + "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfArtifactHash5D335705": { "Type": "String", - "Description": "Artifact hash for asset \"dddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccaf\"" + "Description": "Artifact hash for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json new file mode 100644 index 0000000000000..a448e7119e235 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json @@ -0,0 +1,116 @@ +{ + "Resources": { + "Level1ABBD39B3": { + "Type": "AWS::SNS::Topic" + }, + "Nested1NestedStackNested1NestedStackResourceCD0AD36B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3Bucket13C1C23E" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3VersionKey95DF5102" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3VersionKey95DF5102" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetonestedstacksmultirefsLevel19FB2466DTopicName": { + "Fn::GetAtt": [ + "Level1ABBD39B3", + "TopicName" + ] + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket8F1E17B9Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket9A14AA6D" + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKey9EEEF950Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKeyF124C0D9" + }, + "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket03F0C3B1Ref": { + "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket58724FCA" + }, + "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey5F9CF809Ref": { + "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey2CCE0573" + } + } + } + } + }, + "Parameters": { + "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket58724FCA": { + "Type": "String", + "Description": "S3 bucket for asset \"495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3a\"" + }, + "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey2CCE0573": { + "Type": "String", + "Description": "S3 key for asset version \"495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3a\"" + }, + "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aArtifactHashAE1436B7": { + "Type": "String", + "Description": "Artifact hash for asset \"495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3a\"" + }, + "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket9A14AA6D": { + "Type": "String", + "Description": "S3 bucket for asset \"cc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847\"" + }, + "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKeyF124C0D9": { + "Type": "String", + "Description": "S3 key for asset version \"cc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847\"" + }, + "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847ArtifactHashAF64C405": { + "Type": "String", + "Description": "Artifact hash for asset \"cc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847\"" + }, + "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3Bucket13C1C23E": { + "Type": "String", + "Description": "S3 bucket for asset \"8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394f\"" + }, + "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3VersionKey95DF5102": { + "Type": "String", + "Description": "S3 key for asset version \"8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394f\"" + }, + "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fArtifactHashF74D4A90": { + "Type": "String", + "Description": "Artifact hash for asset \"8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394f\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts new file mode 100644 index 0000000000000..321e70fbb486a --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts @@ -0,0 +1,31 @@ +import * as sns from '@aws-cdk/aws-sns'; +import { App, Fn, Stack } from '@aws-cdk/core'; +import { NestedStack } from '../lib'; + +const app = new App(); +const top = new Stack(app, 'nested-stacks-multi-refs'); +const level1 = new sns.Topic(top, 'Level1'); +const nested1 = new NestedStack(top, 'Nested1'); +const nested2 = new NestedStack(nested1, 'Nested2'); +const nested3 = new NestedStack(nested2, 'Nested3'); + +// WHEN +const level2 = new sns.Topic(nested2, 'Level2ReferencesLevel1', { + displayName: shortName(level1.topicName) +}); + +new sns.Topic(nested3, 'Level3ReferencesLevel1', { + displayName: shortName(level1.topicName) +}); + +new sns.Topic(nested3, 'Level3ReferencesLevel2', { + displayName: shortName(level2.topicName) +}); + +app.synth(); + +// topicName is too long for displayName, so just take the second part: +// Stack1-NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B-EM64TEGA04J9-TopicInNestedUnderStack115E329C4-HEO7NLYC1AFL +function shortName(topicName: string) { + return Fn.select(1, Fn.split('-', topicName)); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.expected.json new file mode 100644 index 0000000000000..3cac1b07eb420 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.expected.json @@ -0,0 +1,106 @@ +[ + { + "Resources": { + "NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3BucketF628ECFB" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3VersionKey0E649F42" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3VersionKey0E649F42" + } + ] + } + ] + } + ] + ] + } + } + } + }, + "Outputs": { + "ExportsOutputFnGetAttNestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305BOutputsStack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicNameD753D416": { + "Value": { + "Fn::GetAtt": [ + "NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B", + "Outputs.Stack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicName" + ] + }, + "Export": { + "Name": "Stack1:ExportsOutputFnGetAttNestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305BOutputsStack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicNameD753D416" + } + } + }, + "Parameters": { + "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3BucketF628ECFB": { + "Type": "String", + "Description": "S3 bucket for asset \"5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219\"" + }, + "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3VersionKey0E649F42": { + "Type": "String", + "Description": "S3 key for asset version \"5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219\"" + }, + "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219ArtifactHash37AA0C4D": { + "Type": "String", + "Description": "Artifact hash for asset \"5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219\"" + } + } + }, + { + "Resources": { + "TopicInStack27FD9238C": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "-", + { + "Fn::ImportValue": "Stack1:ExportsOutputFnGetAttNestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305BOutputsStack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicNameD753D416" + } + ] + } + ] + } + } + } + } + } +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.ts new file mode 100644 index 0000000000000..2363b5bb790b4 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.ts @@ -0,0 +1,20 @@ +/// !cdk-integ Stack2 + +import * as sns from '@aws-cdk/aws-sns'; +import { App, Fn, Stack } from '@aws-cdk/core'; +import * as cfn from '../lib'; + +const app = new App(); +const stack1 = new Stack(app, 'Stack1'); +const stack2 = new Stack(app, 'Stack2'); + +const nestedUnderStack1 = new cfn.NestedStack(stack1, 'NestedUnderStack1'); +const topicInNestedUnderStack1 = new sns.Topic(nestedUnderStack1, 'TopicInNestedUnderStack1'); + +new sns.Topic(stack2, 'TopicInStack2', { + // topicName is too long for displayName, so just take the second part: + // Stack1-NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B-EM64TEGA04J9-TopicInNestedUnderStack115E329C4-HEO7NLYC1AFL + displayName: Fn.select(1, Fn.split('-', topicInNestedUnderStack1.topicName)) +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json index 25bb05cdf6ab9..80a2f7712026c 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json @@ -112,18 +112,6 @@ } }, "Parameters": { - "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3Bucket72E4418F": { - "Type": "String", - "Description": "S3 bucket for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" - }, - "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3VersionKeyC46A55B6": { - "Type": "String", - "Description": "S3 key for asset version \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" - }, - "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583ArtifactHashDF52341B": { - "Type": "String", - "Description": "Artifact hash for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" - }, "AssetParameters008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9aS3Bucket3AC5D089": { "Type": "String", "Description": "S3 bucket for asset \"008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9a\"" @@ -135,6 +123,18 @@ "AssetParameters008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9aArtifactHashEF790DCB": { "Type": "String", "Description": "Artifact hash for asset \"008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9a\"" + }, + "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3Bucket72E4418F": { + "Type": "String", + "Description": "S3 bucket for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" + }, + "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3VersionKeyC46A55B6": { + "Type": "String", + "Description": "S3 key for asset version \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" + }, + "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583ArtifactHashDF52341B": { + "Type": "String", + "Description": "Artifact hash for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts index e427ab45df8bb..542b1bffd3b1e 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; +import { expect, haveResource, matchTemplate, SynthUtils } from '@aws-cdk/assert'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import * as sns from '@aws-cdk/aws-sns'; import { App, CfnParameter, CfnResource, Construct, ContextProvider, Stack } from '@aws-cdk/core'; @@ -909,4 +909,61 @@ export = { test.done(); }, + + 'references to a resource from a deeply nested stack'(test: Test) { + // GIVEN + const app = new App(); + const top = new Stack(app, 'stack'); + const topLevel = new CfnResource(top, 'toplevel', { type: 'TopLevel' }); + const nested1 = new NestedStack(top, 'nested1'); + const nested2 = new NestedStack(nested1, 'nested2'); + + // WHEN + new CfnResource(nested2, 'refToTopLevel', { + type: 'BottomLevel', + properties: { RefToTopLevel: topLevel.ref } + }); + + // THEN + expect(top).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + referencetostackAssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3Bucket5DA5D2E7Ref: { + Ref: 'AssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3BucketDD4D96B5' + }, + referencetostackAssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3VersionKey8FBE5C12Ref: { + Ref: 'AssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3VersionKey83E381F3' + }, + referencetostacktoplevelBB16BF13Ref: { + Ref: 'toplevel' + } + } + })); + + expect(nested1).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + referencetostacktoplevelBB16BF13Ref: { + Ref: 'referencetostacktoplevelBB16BF13Ref' + } + } + })); + + expect(nested2).to(matchTemplate({ + Resources: { + refToTopLevel: { + Type: 'BottomLevel', + Properties: { + RefToTopLevel: { + Ref: 'referencetostacktoplevelBB16BF13Ref' + } + } + } + }, + Parameters: { + referencetostacktoplevelBB16BF13Ref: { + Type: 'String' + }, + }, + })); + test.done(); + } }; diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index 62b67091b4886..d659f402e5372 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -91,7 +91,7 @@ }, "/", { - "Ref": "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3BucketEE609B7A" + "Ref": "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3Bucket1375170E" }, "/", { @@ -101,7 +101,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3VersionKeyAE59C268" + "Ref": "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3VersionKeyB1F590C9" } ] } @@ -114,7 +114,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3VersionKeyAE59C268" + "Ref": "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3VersionKeyB1F590C9" } ] } @@ -174,17 +174,17 @@ "Type": "String", "Description": "Artifact hash for asset \"17429b8bbbb0484d167711d8d3baf4abc55be3663b0f19233952a7fa9d9db8d4\"" }, - "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3BucketEE609B7A": { + "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3Bucket1375170E": { "Type": "String", - "Description": "S3 bucket for asset \"8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bf\"" + "Description": "S3 bucket for asset \"ceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295b\"" }, - "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3VersionKeyAE59C268": { + "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3VersionKeyB1F590C9": { "Type": "String", - "Description": "S3 key for asset version \"8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bf\"" + "Description": "S3 key for asset version \"ceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295b\"" }, - "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfArtifactHash97214390": { + "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bArtifactHash392EC608": { "Type": "String", - "Description": "Artifact hash for asset \"8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bf\"" + "Description": "Artifact hash for asset \"ceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295b\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index d18ed1c8c32d1..38583c9d6d4a4 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -784,9 +784,7 @@ "eks:DeleteCluster", "eks:UpdateClusterVersion", "eks:UpdateClusterConfig", - "eks:CreateFargateProfile", - "eks:TagResource", - "eks:UntagResource" + "eks:CreateFargateProfile" ], "Effect": "Allow", "Resource": [ @@ -1895,7 +1893,7 @@ }, "/", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3Bucket41E299BC" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3Bucket522186DC" }, "/", { @@ -1905,7 +1903,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498" } ] } @@ -1918,7 +1916,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498" } ] } @@ -1956,7 +1954,7 @@ }, "/", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3Bucket186FBEF7" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3Bucket9B10D651" }, "/", { @@ -1966,7 +1964,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143" } ] } @@ -1979,7 +1977,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143" } ] } @@ -2113,29 +2111,29 @@ "Type": "String", "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3Bucket186FBEF7": { + "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3Bucket522186DC": { "Type": "String", - "Description": "S3 bucket for asset \"2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7\"" + "Description": "S3 bucket for asset \"0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420\"" }, - "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477": { + "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498": { "Type": "String", - "Description": "S3 key for asset version \"2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7\"" + "Description": "S3 key for asset version \"0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420\"" }, - "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7ArtifactHashD1316A92": { + "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420ArtifactHash8E0FEA21": { "Type": "String", - "Description": "Artifact hash for asset \"2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7\"" + "Description": "Artifact hash for asset \"0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420\"" }, - "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3Bucket41E299BC": { + "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3Bucket9B10D651": { "Type": "String", - "Description": "S3 bucket for asset \"c6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6\"" + "Description": "S3 bucket for asset \"4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83\"" }, - "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771": { + "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143": { "Type": "String", - "Description": "S3 key for asset version \"c6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6\"" + "Description": "S3 key for asset version \"4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83\"" }, - "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6ArtifactHashC32772E8": { + "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83ArtifactHash2E06714F": { "Type": "String", - "Description": "Artifact hash for asset \"c6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6\"" + "Description": "Artifact hash for asset \"4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83\"" }, "SsmParameterValueawsserviceeksoptimizedami114amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/core/lib/app.ts b/packages/@aws-cdk/core/lib/app.ts index 619ecc19a20da..cea43492a2e76 100644 --- a/packages/@aws-cdk/core/lib/app.ts +++ b/packages/@aws-cdk/core/lib/app.ts @@ -1,5 +1,6 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct, ConstructNode } from './construct-compat'; +import { prepareApp } from './private/prepare-app'; import { collectRuntimeInformation } from './private/runtime-info'; import { TreeMetadata } from './private/tree-metadata'; @@ -147,6 +148,11 @@ export class App extends Construct { return assembly; } + protected prepare() { + super.prepare(); + prepareApp(this); + } + private loadContext(defaults: { [key: string]: string } = { }) { // prime with defaults passed through constructor for (const [ k, v ] of Object.entries(defaults)) { diff --git a/packages/@aws-cdk/core/lib/deps.ts b/packages/@aws-cdk/core/lib/deps.ts index fdfa4bee677f4..b26fa28cb187b 100644 --- a/packages/@aws-cdk/core/lib/deps.ts +++ b/packages/@aws-cdk/core/lib/deps.ts @@ -62,7 +62,7 @@ export function addDependency(source: T, target: T, reason?: // `source` is a direct or indirect nested stack of `target`, and this is not // possible (nested stacks cannot depend on their parents). if (commonStack === target) { - throw new Error(`Nested stack '${sourceStack.node.path}' cannot depend on a parent stack '${targetStack.node.path}'`); + throw new Error(`Nested stack '${sourceStack.node.path}' cannot depend on a parent stack '${targetStack.node.path}': ${reason}`); } // we have a common stack from which we can reach both `source` and `target` diff --git a/packages/@aws-cdk/core/lib/private/cfn-reference.ts b/packages/@aws-cdk/core/lib/private/cfn-reference.ts index 09f6b51311ec0..f563853f50982 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-reference.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-reference.ts @@ -77,12 +77,14 @@ export class CfnReference extends Reference { * The Tokens that should be returned for each consuming stack (as decided by the producing Stack) */ private readonly replacementTokens: Map; + private readonly targetStack: Stack; protected constructor(value: any, displayName: string, target: IConstruct) { // prepend scope path to display name super(value, target, displayName); this.replacementTokens = new Map(); + this.targetStack = Stack.of(target); Object.defineProperty(this, CFN_REFERENCE_SYMBOL, { value: true }); } @@ -106,10 +108,18 @@ export class CfnReference extends Reference { } public hasValueForStack(stack: Stack) { + if (stack === this.targetStack) { + return true; + } + return this.replacementTokens.has(stack); } public assignValueForStack(stack: Stack, value: IResolvable) { + if (stack === this.targetStack) { + throw new Error('cannot assign a value for the same stack'); + } + if (this.hasValueForStack(stack)) { throw new Error('Cannot assign a reference value twice to the same stack. Use hasValueForStack to check first'); } diff --git a/packages/@aws-cdk/core/lib/private/prepare-app.ts b/packages/@aws-cdk/core/lib/private/prepare-app.ts new file mode 100644 index 0000000000000..bec9b66b0757b --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -0,0 +1,67 @@ +import { ConstructOrder } from 'constructs'; +import { Construct } from '../construct-compat'; +import { Stack } from '../stack'; +import { resolveReferences } from './refs'; + +/** + * Prepares the app for synthesis. This function is called by the root `prepare` + * (normally this the App, but if a Stack is a root, it is called by the stack), + * which means it's the last 'prepare' that executes. + * + * It takes care of reifying cross-references between stacks (or nested stacks), + * and of creating assets for nested stack templates. + * + * @param root The root of the construct tree. + */ +export function prepareApp(root: Construct) { + if (root.node.scope) { + throw new Error('prepareApp must be called on the root node'); + } + + const nestedStacks = findAllNestedStacks(root); + + let more = true; + while (more) { + resolveReferences(root); + more = defineNestedStackAssets(nestedStacks); + } +} + +/** + * Prepares the assets for nested stacks in this app. + * @returns `true` if assets were added to the parent of a nested stack, which + * implies that another round of reference resolution is in order. If this + * function returns `false`, we know we are done. + */ +function defineNestedStackAssets(nestedStacks: Stack[]): boolean { + for (const stack of nestedStacks) { + // this is needed temporarily until we move NestedStack to '@aws-cdk/core'. + const nested: INestedStackPrivateApi = stack as any; + + // '_prepareTemplateAsset' returns `true` if an asset was added to the nested stack's parent + if (nested._prepareTemplateAsset()) { + return true; + } + } + + // we are done, no more cycles + return false; +} + +function findAllNestedStacks(root: Construct) { + const result = new Array(); + + // create a list of all nested stacks in depth-first post order this means + // that we first prepare the leaves and then work our way up. + for (const stack of root.node.findAll(ConstructOrder.POSTORDER /* <== important */)) { + if (Stack.isStack(stack) && stack.nested) { + result.push(stack); + } + } + + return result; +} + +interface INestedStackPrivateApi { + _prepareTemplateAsset(): boolean; +} diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts new file mode 100644 index 0000000000000..218b99d544987 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -0,0 +1,276 @@ +// ---------------------------------------------------- +// CROSS REFERENCES +// ---------------------------------------------------- +import { CfnElement } from '../cfn-element'; +import { CfnOutput } from '../cfn-output'; +import { CfnParameter } from '../cfn-parameter'; +import { Construct } from '../construct-compat'; +import { Reference } from '../reference'; +import { IResolvable } from '../resolvable'; +import { Stack } from '../stack'; +import { Token } from '../token'; +import { CfnReference } from './cfn-reference'; +import { Intrinsic } from './intrinsic'; +import { findTokens } from './resolve'; +import { makeUniqueId } from './uniqueid'; + +/** + * This is called from the App level to resolve all references defined. Each + * reference is resolved based on it's consumption context. + */ +export function resolveReferences(scope: Construct) { + const edges = findAllReferences(scope); + + for (const { source, value } of edges) { + const consumer = Stack.of(source); + + // skip if we already have a value for this consumer + if (value.hasValueForStack(consumer)) { + continue; + } + + // resolve the value in the context of the consumer. + const resolved = resolveValue(consumer, value); + value.assignValueForStack(consumer, resolved); + } +} + +/** + * Resolves the value for `reference` in the context of `consumer`. + */ +function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { + const producer = Stack.of(reference.target); + + // produce and consumer stacks are the same, we can just return the value itself. + if (producer === consumer) { + return reference; + } + + // unsupported: stacks from different apps + if (producer.node.root !== consumer.node.root) { + throw new Error('Cannot reference across apps. Consuming and producing stacks must be defined within the same CDK app.'); + } + + // unsupported: stacks are not in the same environment + if (producer.environment !== consumer.environment) { + throw new Error( + `Stack "${consumer.node.path}" cannot consume a cross reference from stack "${producer.node.path}". ` + + 'Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack'); + } + + // ---------------------------------------------------------------------- + // consumer is nested in the producer (directly or indirectly) + // ---------------------------------------------------------------------- + + // if the consumer is a child of the producer, wire the reference through a + // CloudFormation parameter on the consumer and resolve recursively. + if (isParent(producer, consumer)) { + const parameterValue = createNestedStackParameter(consumer, reference); + return resolveValue(consumer, parameterValue); + } + + // ---------------------------------------------------------------------- + // producer is a nested stack + // ---------------------------------------------------------------------- + + // if the producer is nested, always publish the value through a + // cloudformation output and resolve recursively with the Fn::GetAtt + // of the output in the parent stack. + + // one might ask, if the consumer is not a parent of the producer, + // why not just use export/import? the reason is that we cannot + // generate an "export name" from a nested stack because the export + // name must contain the stack name to ensure uniqueness, and we + // don't know the stack name of a nested stack before we deploy it. + // therefore, we can only export from a top-level stack. + if (producer.nested) { + const outputValue = createNestedStackOutput(producer, reference); + return resolveValue(consumer, outputValue); + } + + // ---------------------------------------------------------------------- + // export/import + // ---------------------------------------------------------------------- + + // export the value through a cloudformation "export name" and use an + // Fn::ImportValue in the consumption site. + + // delcare a dependency between the two top-level (non-nested) stacks to make + // sure the producer is deployed before the consumer. + const producerDep = producer.nestedStackParent ?? producer; + const consumerDep = consumer.nestedStackParent ?? consumer; + consumerDep.addDependency(producerDep, + `${consumer.node.path} -> ${reference.target.node.path}.${reference.displayName}`); + + return createImportValue(reference); +} + +/** + * Finds all the CloudFormation references in a construct tree. + */ +function findAllReferences(root: Construct) { + const result = new Array<{ source: CfnElement, value: CfnReference }>(); + for (const consumer of root.node.findAll()) { + + // include only CfnElements (i.e. resources) + if (!CfnElement.isCfnElement(consumer)) { + continue; + } + + try { + const tokens = findTokens(consumer, () => consumer._toCloudFormation()); + + // iterate over all the tokens (e.g. intrinsic functions, lazies, etc) that + // were found in the cloudformation representation of this resource. + for (const token of tokens) { + + // include only CfnReferences (i.e. "Ref" and "Fn::GetAtt") + if (!CfnReference.isCfnReference(token)) { + continue; + } + + result.push({ + source: consumer, + value: token + }); + } + } catch (e) { + // 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. + if (e.type === 'CfnSynthesisError') { + continue; + } + + throw e; + } + } + + return result; +} + +// ------------------------------------------------------------------------------------------------ +// export/import +// ------------------------------------------------------------------------------------------------ + +/** + * Imports a value from another stack by creating an "Output" with an "ExportName" + * and returning an "Fn::ImportValue" token. + */ +function createImportValue(reference: Reference): IResolvable { + const exportingStack = Stack.of(reference.target); + + // 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 exportsScope = getCreateExportsScope(exportingStack); + + // Ensure a singleton CfnOutput for this value + const resolved = exportingStack.resolve(reference); + const id = 'Output' + JSON.stringify(resolved); + const exportName = generateExportName(exportsScope, id); + + if (Token.isUnresolved(exportName)) { + throw new Error(`unresolved token in generated export name: ${JSON.stringify(exportingStack.resolve(exportName))}`); + } + + const output = exportsScope.node.tryFindChild(id) as CfnOutput; + if (!output) { + new CfnOutput(exportsScope, id, { value: Token.asString(reference), exportName }); + } + + // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', + // so construct one in-place. + return new Intrinsic({ 'Fn::ImportValue': exportName }); +} + +function getCreateExportsScope(stack: Stack) { + const exportsName = 'Exports'; + let stackExports = stack.node.tryFindChild(exportsName) as Construct; + if (stackExports === undefined) { + stackExports = new Construct(stack, exportsName); + } + + return stackExports; +} + +function generateExportName(stackExports: Construct, id: string) { + const stack = Stack.of(stackExports); + const components = [...stackExports.node.scopes.slice(2).map(c => c.node.id), id]; + const prefix = stack.stackName ? stack.stackName + ':' : ''; + const exportName = prefix + makeUniqueId(components); + return exportName; +} + +// ------------------------------------------------------------------------------------------------ +// nested stacks +// ------------------------------------------------------------------------------------------------ + +/** + * Adds a CloudFormation parameter to a nested stack and assigns it with the + * value of the reference. + */ +function createNestedStackParameter(consumer: Stack, reference: Reference) { + // we call "this.resolve" to ensure that tokens do not creep in (for example, if the reference display name includes tokens) + const paramId = consumer.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); + let param = consumer.node.tryFindChild(paramId) as CfnParameter; + if (!param) { + param = new CfnParameter(consumer, paramId, { type: 'String' }); + + // Ugly little hack until we move NestedStack to this module. + if (!('setParameter' in consumer)) { + throw new Error('assertion failed: nested stack should have a "setParameter" method'); + } + + (consumer as any).setParameter(param.logicalId, Token.asString(reference)); + } + + return param.value as CfnReference; +} + +/** + * Adds a CloudFormation output to a nested stack and returns an "Fn::GetAtt" + * intrinsic that can be used to reference this output in the parent stack. + */ +function createNestedStackOutput(producer: Stack, reference: Reference): CfnReference { + const outputId = `${reference.target.node.uniqueId}${reference.displayName}`; + let output = producer.node.tryFindChild(outputId) as CfnOutput; + if (!output) { + output = new CfnOutput(producer, outputId, { value: Token.asString(reference) }); + } + + if (!producer.nestedStackResource) { + throw new Error('assertion failed'); + } + + return producer.nestedStackResource.getAtt(`Outputs.${output.logicalId}`) as CfnReference; +} + +/** + * @returns true if this stack is a direct or indirect parent of the nested + * stack `nested`. + * + * If `child` is not a nested stack, always returns `false` because it can't + * have a parent, dah. + */ +export function isParent(parent: Stack, child: Stack): boolean { + // if "nested" is not a nested stack, then by definition we cannot be its parent + if (!child.nestedStackParent) { + return false; + } + + // if this is the direct parent, then we found it + if (parent === child.nestedStackParent) { + return true; + } + + // traverse up + return isParent(parent, child.nestedStackParent); +} diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 104dd362b4196..162632d3a9ad4 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -1,16 +1,15 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; -import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; -import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation , FileAssetPackaging, FileAssetSource } from './assets'; +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from './assets'; import { Construct, ConstructNode, IConstruct, ISynthesisSession } from './construct-compat'; import { ContextProvider } from './context-provider'; import { Environment } from './environment'; import { FileAssetParameters } from './private/asset-parameters'; import { CLOUDFORMATION_TOKEN_RESOLVER, CloudFormationLang } from './private/cloudformation-lang'; import { LogicalIDs } from './private/logical-id'; -import { findTokens , resolve } from './private/resolve'; +import { resolve } from './private/resolve'; import { makeUniqueId } from './private/uniqueid'; const STACK_SYMBOL = Symbol.for('@aws-cdk/core.Stack'); @@ -190,14 +189,6 @@ export class Stack extends Construct implements ITaggable { */ public readonly nestedStackResource?: CfnResource; - /** - * An attribute (late-bound) that represents the URL of the template file - * in the deployment bucket. - * - * @experimental - */ - public readonly templateUrl: string; - /** * The name of the CloudFormation template file emitted to the output * directory during synthesis. @@ -233,7 +224,6 @@ export class Stack extends Construct implements ITaggable { */ private _assetParameters?: Construct; - private _templateUrl?: string; private readonly _stackName: string; /** @@ -291,7 +281,6 @@ export class Stack extends Construct implements ITaggable { : this.stackName; this.templateFile = `${this.artifactId}.template.json`; - this.templateUrl = Lazy.stringValue({ produce: () => this._templateUrl || '' }); } /** @@ -745,36 +734,6 @@ export class Stack extends Construct implements ITaggable { * Find all dependencies as well and add the appropriate DependsOn fields. */ protected prepare() { - const tokens = this.findTokens(); - - // References (originating from this stack) - for (const reference of tokens) { - - // skip if this is not a CfnReference - if (!CfnReference.isCfnReference(reference)) { - continue; - } - - const targetStack = Stack.of(reference.target); - - // skip if this is not a cross-stack reference - if (targetStack === this) { - continue; - } - - // determine which stack should create the cross reference - const factory = this.determineCrossReferenceFactory(targetStack); - - // if one side is a nested stack (has "parentStack"), we let it create the reference - // since it has more knowledge about the world. - const consumedValue = factory.prepareCrossReference(this, reference); - - // if the reference has already been assigned a value for the consuming stack, carry on. - if (!reference.hasValueForStack(this)) { - reference.assignValueForStack(this, consumedValue); - } - } - // Resource dependencies for (const dependency of this.node.dependencies) { for (const target of findCfnResources([ dependency.target ])) { @@ -788,20 +747,10 @@ export class Stack extends Construct implements ITaggable { this.node.addMetadata(cxschema.ArtifactMetadataEntryType.STACK_TAGS, this.tags.renderTags()); } - if (this.nestedStackParent) { - // add the nested stack template as an asset - const cfn = JSON.stringify(this._toCloudFormation()); - const templateHash = crypto.createHash('sha256').update(cfn).digest('hex'); - const parent = this.nestedStackParent; - const templateLocation = parent.addFileAsset({ - packaging: FileAssetPackaging.FILE, - sourceHash: templateHash, - fileName: this.templateFile - }); - - // if bucketName/objectKey are cfn parameters from a stack other than the parent stack, they will - // be resolved as cross-stack references like any other (see "multi" tests). - this._templateUrl = `https://s3.${parent.region}.${parent.urlSuffix}/${templateLocation.bucketName}/${templateLocation.objectKey}`; + // if this stack is a roort (e.g. in unit tests), call `prepareApp` so that + // we resolve cross-references and nested stack assets. + if (!this.node.scope) { + prepareApp(this); } } @@ -898,47 +847,14 @@ export class Stack extends Construct implements ITaggable { } /** - * Exports a resolvable value for use in another stack. + * Deprecated. * - * @returns a token that can be used to reference the value from the producing stack. + * @see https://github.com/aws/aws-cdk/pull/7187 + * @returns reference itself without any change + * @deprecated cross reference handling has been moved to `App.prepare()`. */ - protected prepareCrossReference(sourceStack: Stack, reference: Reference): IResolvable { - const targetStack = Stack.of(reference.target); - - // 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 exportsScope = targetStack.getCreateExportsScope(); - - // Ensure a singleton CfnOutput for this value - const resolved = targetStack.resolve(reference); - const id = 'Output' + JSON.stringify(resolved); - const exportName = targetStack.generateExportName(exportsScope, id); - const output = exportsScope.node.tryFindChild(id) as CfnOutput; - if (!output) { - new CfnOutput(exportsScope, id, { value: Token.asString(reference), exportName }); - } - - // add a dependency on the producing stack - it has to be deployed before this stack can consume the exported value - // if the producing stack is a nested stack (i.e. has a parent), the dependency is taken on the parent. - const producerDependency = targetStack.nestedStackParent ? targetStack.nestedStackParent : targetStack; - const consumerDependency = sourceStack.nestedStackParent ? sourceStack.nestedStackParent : sourceStack; - consumerDependency.addDependency(producerDependency, `${sourceStack.node.path} -> ${reference.target.node.path}.${reference.displayName}`); - - // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', - // so construct one in-place. - return new Intrinsic({ 'Fn::ImportValue': exportName }); - } - - private getCreateExportsScope() { - const exportsName = 'Exports'; - let stackExports = this.node.tryFindChild(exportsName) as Construct; - if (stackExports === undefined) { - stackExports = new Construct(this, exportsName); - } - - return stackExports; + protected prepareCrossReference(_sourceStack: Stack, reference: Reference): IResolvable { + return reference; } /** @@ -1042,72 +958,12 @@ export class Stack extends Construct implements ITaggable { return makeUniqueId(ids); } - private generateExportName(stackExports: Construct, id: string) { - const stack = Stack.of(stackExports); - const components = [...stackExports.node.scopes.slice(2).map(c => c.node.id), id]; - const prefix = stack.stackName ? stack.stackName + ':' : ''; - const exportName = prefix + makeUniqueId(components); - return exportName; - } - private get assetParameters() { if (!this._assetParameters) { this._assetParameters = new Construct(this, 'AssetParameters'); } return this._assetParameters; } - - private determineCrossReferenceFactory(target: Stack) { - // unsupported: stacks from different apps - if (target.node.root !== this.node.root) { - throw new Error( - 'Cannot reference across apps. ' + - 'Consuming and producing stacks must be defined within the same CDK app.'); - } - - // unsupported: stacks are not in the same environment - if (target.environment !== this.environment) { - throw new Error( - `Stack "${this.node.path}" cannot consume a cross reference from stack "${target.node.path}". ` + - 'Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack'); - } - - // if one of the stacks is a nested stack, go ahead and give it the right to make the cross reference - if (target.nested) { return target; } - if (this.nested) { return this; } - - // both stacks are top-level (non-nested), the taret (producing stack) gets to make the reference - return target; - } - - /** - * Returns all the tokens used within the scope of the current stack. - */ - private findTokens() { - const tokens = new Array(); - - for (const element of cfnElements(this)) { - try { - tokens.push(...findTokens(element, () => element._toCloudFormation())); - } catch (e) { - // 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. - if (e.type === 'CfnSynthesisError') { - continue; - } - - throw e; - } - } - return tokens; - } } function merge(template: any, part: any) { @@ -1164,7 +1020,7 @@ export interface ITemplateOptions { } /** - * Collect all CfnElements from a Stack + * Collect all CfnElements from a Stack. * * @param node Root node to collect all CfnElements from * @param into Array to append CfnElements to @@ -1189,13 +1045,10 @@ function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] { import { Arn, ArnComponents } from './arn'; import { CfnElement } from './cfn-element'; import { Fn } from './cfn-fn'; -import { CfnOutput } from './cfn-output'; import { Aws, ScopedAws } from './cfn-pseudo'; import { CfnResource, TagType } from './cfn-resource'; import { addDependency } from './deps'; -import { Lazy } from './lazy'; -import { CfnReference } from './private/cfn-reference'; -import { Intrinsic } from './private/intrinsic'; +import { prepareApp } from './private/prepare-app'; import { Reference } from './reference'; import { IResolvable } from './resolvable'; import { ITaggable, TagManager } from './tag-manager'; diff --git a/packages/@aws-cdk/core/lib/util.ts b/packages/@aws-cdk/core/lib/util.ts index 89204560bb919..0855c5f16b7ac 100644 --- a/packages/@aws-cdk/core/lib/util.ts +++ b/packages/@aws-cdk/core/lib/util.ts @@ -104,25 +104,6 @@ export function pathToTopLevelStack(s: Stack): Stack[] { } } -/** - * @returns true if this stack is a direct or indirect parent of the nested - * stack `nested`. If `nested` is a top-level stack, returns false. - */ -export function isParentOfNestedStack(parent: Stack, child: Stack): boolean { - // if "nested" is not a nested stack, then by definition we cannot be its parent - if (!child.nestedStackParent) { - return false; - } - - // if this is the direct parent, then we found it - if (parent === child.nestedStackParent) { - return true; - } - - // traverse up - return isParentOfNestedStack(parent, child.nestedStackParent); -} - /** * Given two arrays, returns the last common element or `undefined` if there * isn't (arrays are foriegn). From 88b8ac5b231ed04664611cb67bbe63407d1057ca Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sat, 11 Apr 2020 00:27:42 +0300 Subject: [PATCH 2/8] update eks test exp --- .../@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 38583c9d6d4a4..111f3bfe13e89 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -784,7 +784,9 @@ "eks:DeleteCluster", "eks:UpdateClusterVersion", "eks:UpdateClusterConfig", - "eks:CreateFargateProfile" + "eks:CreateFargateProfile", + "eks:TagResource", + "eks:UntagResource" ], "Effect": "Allow", "Resource": [ From e5cf4a6fce1c592ff7ef378a53aa1a34ea3983bc Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 13 Apr 2020 10:28:17 +0300 Subject: [PATCH 3/8] improve code readability by changing `isParent` to `isNested` --- packages/@aws-cdk/core/lib/private/refs.ts | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 218b99d544987..2bd8d694cf909 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -62,9 +62,10 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { // consumer is nested in the producer (directly or indirectly) // ---------------------------------------------------------------------- - // if the consumer is a child of the producer, wire the reference through a - // CloudFormation parameter on the consumer and resolve recursively. - if (isParent(producer, consumer)) { + // if the consumer is nested within the producer (directly or indirectly), + // wire through a CloudFormation parameter on the consumer and resolve + // recursively. + if (isNested(consumer, producer)) { const parameterValue = createNestedStackParameter(consumer, reference); return resolveValue(consumer, parameterValue); } @@ -260,17 +261,17 @@ function createNestedStackOutput(producer: Stack, reference: Reference): CfnRefe * If `child` is not a nested stack, always returns `false` because it can't * have a parent, dah. */ -export function isParent(parent: Stack, child: Stack): boolean { - // if "nested" is not a nested stack, then by definition we cannot be its parent - if (!child.nestedStackParent) { - return false; +export function isNested(nested: Stack, parent: Stack): boolean { + // if the parent is a direct parent + if (nested.nestedStackParent === parent) { + return true; } - // if this is the direct parent, then we found it - if (parent === child.nestedStackParent) { - return true; + // we reached a top-level (non-nested) stack without finding the parent + if (!nested.nestedStackParent) { + return false; } - // traverse up - return isParent(parent, child.nestedStackParent); + // recurse with the child's direct parent + return isNested(nested.nestedStackParent, parent); } From b5367ceaf789addf67473e9ad13ed93c018422c8 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 14 Apr 2020 10:19:42 +0300 Subject: [PATCH 4/8] simplify dependency definition for export/import since `stack.addDependency` has logic that handles nested stacks through LCA we can just declare that the consumer depends on the producer and that logic will kick in. add a test to that affect. --- .../test/test.nested-stack.ts | 26 ++++++++++++++++++- packages/@aws-cdk/core/lib/private/refs.ts | 11 ++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts index 542b1bffd3b1e..1172255710ea6 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts @@ -426,6 +426,30 @@ export = { test.done(); }, + 'nested stack within a nested stack references a resource in a sibling top-level stack'(test: Test) { + // GIVEN + const app = new App(); + const consumerTopLevel = new Stack(app, 'ConsumerTopLevel'); + const consumerNested1 = new NestedStack(consumerTopLevel, 'ConsumerNested1'); + const consumerNested2 = new NestedStack(consumerNested1, 'ConsumerNested2'); + const producerTopLevel = new Stack(app, 'ProducerTopLevel'); + const producer = new CfnResource(producerTopLevel, 'Producer', { type: 'Producer' }); + + // WHEN + new CfnResource(consumerNested2, 'Consumer', { + type: 'Consumer', + properties: { + Ref: producer.ref + } + }); + + // THEN + const manifest = app.synth(); + const consumerDeps = manifest.getStackArtifact(consumerTopLevel.artifactId).dependencies.map(d => d.id); + test.deepEqual(consumerDeps, [ 'ProducerTopLevel' ]); + test.done(); + }, + 'another non-nested stack takes a reference on a resource within the nested stack (the parent exports)'(test: Test) { // GIVEN const app = new App(); @@ -965,5 +989,5 @@ export = { }, })); test.done(); - } + }, }; diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 2bd8d694cf909..61d00c4938de2 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -96,11 +96,10 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { // export the value through a cloudformation "export name" and use an // Fn::ImportValue in the consumption site. - // delcare a dependency between the two top-level (non-nested) stacks to make - // sure the producer is deployed before the consumer. - const producerDep = producer.nestedStackParent ?? producer; - const consumerDep = consumer.nestedStackParent ?? consumer; - consumerDep.addDependency(producerDep, + // add a dependency between the producer and the consumer. dependency logic + // will take care of applying the dependency at the right level (e.g. the + // top-level stacks). + consumer.addDependency(producer, `${consumer.node.path} -> ${reference.target.node.path}.${reference.displayName}`); return createImportValue(reference); @@ -261,7 +260,7 @@ function createNestedStackOutput(producer: Stack, reference: Reference): CfnRefe * If `child` is not a nested stack, always returns `false` because it can't * have a parent, dah. */ -export function isNested(nested: Stack, parent: Stack): boolean { +function isNested(nested: Stack, parent: Stack): boolean { // if the parent is a direct parent if (nested.nestedStackParent === parent) { return true; From 253174a72b32d176480f367aea6f61249033b726 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 14 Apr 2020 10:20:52 +0300 Subject: [PATCH 5/8] no need to recurse after adding a parameter since the parameter is added to the consumer stack, no need to recurse, we can just terminate the recursion with the parameterValue itself. --- packages/@aws-cdk/core/lib/private/refs.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 61d00c4938de2..38efcdd9f0f1a 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -63,11 +63,9 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { // ---------------------------------------------------------------------- // if the consumer is nested within the producer (directly or indirectly), - // wire through a CloudFormation parameter on the consumer and resolve - // recursively. + // wire through a CloudFormation parameter. if (isNested(consumer, producer)) { - const parameterValue = createNestedStackParameter(consumer, reference); - return resolveValue(consumer, parameterValue); + return createNestedStackParameter(consumer, reference); } // ---------------------------------------------------------------------- @@ -217,19 +215,19 @@ function generateExportName(stackExports: Construct, id: string) { * Adds a CloudFormation parameter to a nested stack and assigns it with the * value of the reference. */ -function createNestedStackParameter(consumer: Stack, reference: Reference) { +function createNestedStackParameter(nested: Stack, reference: Reference) { // we call "this.resolve" to ensure that tokens do not creep in (for example, if the reference display name includes tokens) - const paramId = consumer.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); - let param = consumer.node.tryFindChild(paramId) as CfnParameter; + const paramId = nested.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); + let param = nested.node.tryFindChild(paramId) as CfnParameter; if (!param) { - param = new CfnParameter(consumer, paramId, { type: 'String' }); + param = new CfnParameter(nested, paramId, { type: 'String' }); // Ugly little hack until we move NestedStack to this module. - if (!('setParameter' in consumer)) { + if (!('setParameter' in nested)) { throw new Error('assertion failed: nested stack should have a "setParameter" method'); } - (consumer as any).setParameter(param.logicalId, Token.asString(reference)); + (nested as any).setParameter(param.logicalId, Token.asString(reference)); } return param.value as CfnReference; From e306a5e571636a36699fd0b9f2c9bd7406628189 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 15 Apr 2020 16:31:11 +0300 Subject: [PATCH 6/8] add a reconciliation loop in refs.ts After a reference is resolved (e.g. by adding a nested stack parameter or output), we loop back and look up all references again to make sure we resolve any new references that might be added. Loop stops when all references have values assigned to them. Also, simplify prepare-app to work on nested stacks as a queue. --- .../test/test.nested-stack.ts | 62 +++++++++++++++++++ .../@aws-cdk/core/lib/private/prepare-app.ts | 32 +++++----- packages/@aws-cdk/core/lib/private/refs.ts | 34 ++++++---- 3 files changed, 98 insertions(+), 30 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts index 1172255710ea6..62027ba6a37e2 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts @@ -990,4 +990,66 @@ export = { })); test.done(); }, + + 'bottom nested stack consumes value from a top-level stack through a parameter in a middle nested stack'(test: Test) { + // GIVEN + const app = new App(); + const top = new Stack(app, 'Grandparent'); + const middle = new NestedStack(top, 'Parent'); + const bottom = new NestedStack(middle, 'Child'); + const resourceInGrandparent = new CfnResource(top, 'ResourceInGrandparent', { type: 'ResourceInGrandparent' }); + + // WHEN + new CfnResource(bottom, 'ResourceInChild', { + type: 'ResourceInChild', + properties: { + RefToGrandparent: resourceInGrandparent.ref + } + }); + + // THEN + + // this is the name allocated for the parameter that's propagated through + // the hierarchy. + const paramName = 'referencetoGrandparentResourceInGrandparent010E997ARef'; + + // child (bottom) references through a parameter. + expect(bottom).toMatch({ + Resources: { + ResourceInChild: { + Type: 'ResourceInChild', + Properties: { + RefToGrandparent: { Ref: paramName } + } + } + }, + Parameters: { + [paramName]: { Type: 'String' } + } + }); + + // the parent (middle) sets the value of this parameter to be a reference to another parameter + expect(middle).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + [paramName]: { Ref: paramName } + } + })); + + // grandparent (top) assigns the actual value to the parameter + expect(top).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + [paramName]: { Ref: 'ResourceInGrandparent' }, + + // these are for the asset of the bottom nested stack + referencetoGrandparentAssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3Bucket06EEE58DRef: { + Ref: 'AssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3Bucket01877C2E' + }, + referencetoGrandparentAssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3VersionKeyD3B04909Ref: { + Ref: 'AssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3VersionKey5765F084' + } + } + })); + + test.done(); + } }; diff --git a/packages/@aws-cdk/core/lib/private/prepare-app.ts b/packages/@aws-cdk/core/lib/private/prepare-app.ts index bec9b66b0757b..cb268a88b4069 100644 --- a/packages/@aws-cdk/core/lib/private/prepare-app.ts +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -18,12 +18,19 @@ export function prepareApp(root: Construct) { throw new Error('prepareApp must be called on the root node'); } - const nestedStacks = findAllNestedStacks(root); + // depth-first (children first) queue of nested stacks. We will pop a stack + // from the head of this queue to prepare it's template asset. + const queue = findAllNestedStacks(root); - let more = true; - while (more) { + while (true) { resolveReferences(root); - more = defineNestedStackAssets(nestedStacks); + + const nested = queue.shift(); + if (!nested) { + break; + } + + defineNestedStackAsset(nested); } } @@ -33,19 +40,10 @@ export function prepareApp(root: Construct) { * implies that another round of reference resolution is in order. If this * function returns `false`, we know we are done. */ -function defineNestedStackAssets(nestedStacks: Stack[]): boolean { - for (const stack of nestedStacks) { - // this is needed temporarily until we move NestedStack to '@aws-cdk/core'. - const nested: INestedStackPrivateApi = stack as any; - - // '_prepareTemplateAsset' returns `true` if an asset was added to the nested stack's parent - if (nested._prepareTemplateAsset()) { - return true; - } - } - - // we are done, no more cycles - return false; +function defineNestedStackAsset(nestedStack: Stack) { + // this is needed temporarily until we move NestedStack to '@aws-cdk/core'. + const nested: INestedStackPrivateApi = nestedStack as any; + nested._prepareTemplateAsset(); } function findAllNestedStacks(root: Construct) { diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 38efcdd9f0f1a..4d79696df6450 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -18,20 +18,28 @@ import { makeUniqueId } from './uniqueid'; * This is called from the App level to resolve all references defined. Each * reference is resolved based on it's consumption context. */ -export function resolveReferences(scope: Construct) { - const edges = findAllReferences(scope); - - for (const { source, value } of edges) { - const consumer = Stack.of(source); - - // skip if we already have a value for this consumer - if (value.hasValueForStack(consumer)) { - continue; +export function resolveReferences(scope: Construct): void { + let modified = true; + + while (modified) { + const edges = findAllReferences(scope); + + // start by assume all edges have been processed + modified = false; + + for (const { source, value } of edges) { + const consumer = Stack.of(source); + + // if the edge was not processed yet, resolve it's reference in the + // context of the consumer and signal that we need to repeat the process + // because resolution likely involved mutation of the parent/child to + // propagate the reference, so we need to locate it and resolve it again. + if (!value.hasValueForStack(consumer)) { + const resolved = resolveValue(consumer, value); + value.assignValueForStack(consumer, resolved); + modified = true; + } } - - // resolve the value in the context of the consumer. - const resolved = resolveValue(consumer, value); - value.assignValueForStack(consumer, resolved); } } From 1d027332d781804249f8aef9a7e0bd62feeeb4ca Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 15 Apr 2020 16:53:42 +0300 Subject: [PATCH 7/8] update snapshot --- ...teg.nested-stacks-multi-refs.expected.json | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json index a448e7119e235..bc546994b8e41 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json @@ -20,7 +20,7 @@ }, "/", { - "Ref": "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3Bucket13C1C23E" + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3BucketDB605F9E" }, "/", { @@ -30,7 +30,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3VersionKey95DF5102" + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906" } ] } @@ -43,7 +43,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3VersionKey95DF5102" + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906" } ] } @@ -59,17 +59,17 @@ "TopicName" ] }, - "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket8F1E17B9Ref": { - "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket9A14AA6D" - }, - "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKey9EEEF950Ref": { - "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKeyF124C0D9" - }, "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket03F0C3B1Ref": { "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket58724FCA" }, "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey5F9CF809Ref": { "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey2CCE0573" + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket8F1E17B9Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket9A14AA6D" + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKey9EEEF950Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKeyF124C0D9" } } } @@ -100,17 +100,17 @@ "Type": "String", "Description": "Artifact hash for asset \"cc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847\"" }, - "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3Bucket13C1C23E": { + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3BucketDB605F9E": { "Type": "String", - "Description": "S3 bucket for asset \"8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394f\"" + "Description": "S3 bucket for asset \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" }, - "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fS3VersionKey95DF5102": { + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906": { "Type": "String", - "Description": "S3 key for asset version \"8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394f\"" + "Description": "S3 key for asset version \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" }, - "AssetParameters8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394fArtifactHashF74D4A90": { + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95ArtifactHashAF8D54FC": { "Type": "String", - "Description": "Artifact hash for asset \"8bcd5f713b8f15ec71cc7b367be4314290f49b13c6d99c2e21f482918fba394f\"" + "Description": "Artifact hash for asset \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" } } } \ No newline at end of file From 8fb40cc1071df223c4bf3b58a8fa02a936f4265d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 16 Apr 2020 13:04:31 +0300 Subject: [PATCH 8/8] replace iteration with recursion --- packages/@aws-cdk/core/lib/private/refs.ts | 41 +++++++++------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 4d79696df6450..51a2fc8902a30 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -1,12 +1,12 @@ // ---------------------------------------------------- // CROSS REFERENCES // ---------------------------------------------------- +import { IResolvable } from 'constructs'; import { CfnElement } from '../cfn-element'; import { CfnOutput } from '../cfn-output'; import { CfnParameter } from '../cfn-parameter'; import { Construct } from '../construct-compat'; import { Reference } from '../reference'; -import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; import { Token } from '../token'; import { CfnReference } from './cfn-reference'; @@ -19,26 +19,15 @@ import { makeUniqueId } from './uniqueid'; * reference is resolved based on it's consumption context. */ export function resolveReferences(scope: Construct): void { - let modified = true; + const edges = findAllReferences(scope); - while (modified) { - const edges = findAllReferences(scope); + for (const { source, value } of edges) { + const consumer = Stack.of(source); - // start by assume all edges have been processed - modified = false; - - for (const { source, value } of edges) { - const consumer = Stack.of(source); - - // if the edge was not processed yet, resolve it's reference in the - // context of the consumer and signal that we need to repeat the process - // because resolution likely involved mutation of the parent/child to - // propagate the reference, so we need to locate it and resolve it again. - if (!value.hasValueForStack(consumer)) { - const resolved = resolveValue(consumer, value); - value.assignValueForStack(consumer, resolved); - modified = true; - } + // resolve the value in the context of the consumer + if (!value.hasValueForStack(consumer)) { + const resolved = resolveValue(consumer, value); + value.assignValueForStack(consumer, resolved); } } } @@ -71,9 +60,11 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { // ---------------------------------------------------------------------- // if the consumer is nested within the producer (directly or indirectly), - // wire through a CloudFormation parameter. - if (isNested(consumer, producer)) { - return createNestedStackParameter(consumer, reference); + // wire through a CloudFormation parameter and then resolve the reference with + // the parent stack as the consumer. + if (consumer.nestedStackParent && isNested(consumer, producer)) { + const parameterValue = resolveValue(consumer.nestedStackParent, reference); + return createNestedStackParameter(consumer, reference, parameterValue); } // ---------------------------------------------------------------------- @@ -169,7 +160,7 @@ function findAllReferences(root: Construct) { * Imports a value from another stack by creating an "Output" with an "ExportName" * and returning an "Fn::ImportValue" token. */ -function createImportValue(reference: Reference): IResolvable { +function createImportValue(reference: Reference): Intrinsic { const exportingStack = Stack.of(reference.target); // Ensure a singleton "Exports" scoping Construct @@ -223,7 +214,7 @@ function generateExportName(stackExports: Construct, id: string) { * Adds a CloudFormation parameter to a nested stack and assigns it with the * value of the reference. */ -function createNestedStackParameter(nested: Stack, reference: Reference) { +function createNestedStackParameter(nested: Stack, reference: CfnReference, value: IResolvable) { // we call "this.resolve" to ensure that tokens do not creep in (for example, if the reference display name includes tokens) const paramId = nested.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); let param = nested.node.tryFindChild(paramId) as CfnParameter; @@ -235,7 +226,7 @@ function createNestedStackParameter(nested: Stack, reference: Reference) { throw new Error('assertion failed: nested stack should have a "setParameter" method'); } - (nested as any).setParameter(param.logicalId, Token.asString(reference)); + (nested as any).setParameter(param.logicalId, Token.asString(value)); } return param.value as CfnReference;