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..bc546994b8e41 --- /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": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3BucketDB605F9E" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetonestedstacksmultirefsLevel19FB2466DTopicName": { + "Fn::GetAtt": [ + "Level1ABBD39B3", + "TopicName" + ] + }, + "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket03F0C3B1Ref": { + "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket58724FCA" + }, + "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey5F9CF809Ref": { + "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey2CCE0573" + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket8F1E17B9Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket9A14AA6D" + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKey9EEEF950Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKeyF124C0D9" + } + } + } + } + }, + "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\"" + }, + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3BucketDB605F9E": { + "Type": "String", + "Description": "S3 bucket for asset \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" + }, + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906": { + "Type": "String", + "Description": "S3 key for asset version \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" + }, + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95ArtifactHashAF8D54FC": { + "Type": "String", + "Description": "Artifact hash for asset \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" + } + } +} \ 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..62027ba6a37e2 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'; @@ -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(); @@ -909,4 +933,123 @@ 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(); + }, + + '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/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 6294cb1892b2a..655bd172880a6 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 @@ -2231,7 +2231,7 @@ }, "/", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3Bucket41E299BC" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3Bucket522186DC" }, "/", { @@ -2241,7 +2241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498" } ] } @@ -2254,7 +2254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498" } ] } @@ -2292,7 +2292,7 @@ }, "/", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3Bucket186FBEF7" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3Bucket9B10D651" }, "/", { @@ -2302,7 +2302,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143" } ] } @@ -2315,7 +2315,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143" } ] } @@ -2449,29 +2449,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..cb268a88b4069 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -0,0 +1,65 @@ +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'); + } + + // 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); + + while (true) { + resolveReferences(root); + + const nested = queue.shift(); + if (!nested) { + break; + } + + defineNestedStackAsset(nested); + } +} + +/** + * 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 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) { + 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..51a2fc8902a30 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -0,0 +1,273 @@ +// ---------------------------------------------------- +// 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 { 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): void { + const edges = findAllReferences(scope); + + for (const { source, value } of edges) { + const consumer = Stack.of(source); + + // resolve the value in the context of the consumer + if (!value.hasValueForStack(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 nested within the producer (directly or indirectly), + // 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); + } + + // ---------------------------------------------------------------------- + // 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. + + // 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); +} + +/** + * 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): Intrinsic { + 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(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; + if (!param) { + param = new CfnParameter(nested, paramId, { type: 'String' }); + + // Ugly little hack until we move NestedStack to this module. + if (!('setParameter' in nested)) { + throw new Error('assertion failed: nested stack should have a "setParameter" method'); + } + + (nested as any).setParameter(param.logicalId, Token.asString(value)); + } + + 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. + */ +function isNested(nested: Stack, parent: Stack): boolean { + // if the parent is a direct parent + if (nested.nestedStackParent === parent) { + return true; + } + + // we reached a top-level (non-nested) stack without finding the parent + if (!nested.nestedStackParent) { + return false; + } + + // recurse with the child's direct parent + return isNested(nested.nestedStackParent, parent); +} 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).