Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(core): unable to reference resources across multiple nested stacks #7187

Merged
merged 15 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 42 additions & 53 deletions packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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
Expand Down Expand Up @@ -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');

Expand All @@ -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 || '<unresolved>' }),
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,
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
},
"/",
{
"Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3BucketE3660F43"
"Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3Bucket1DDC9C52"
},
"/",
{
Expand All @@ -168,7 +168,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470"
"Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1"
}
]
}
Expand All @@ -181,7 +181,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470"
"Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1"
}
]
}
Expand Down Expand Up @@ -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\""
}
}
}
Original file line number Diff line number Diff line change
@@ -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\""
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
Loading