Skip to content

Commit

Permalink
fix(core): unable to reference resources across multiple nested stacks (
Browse files Browse the repository at this point in the history
#7187)

Fixes #6473 by centralizing the logic to resolve cross-references and prepare nested stack template assets in `App.prepare`, which has a global view of the app and is the last prepare to execute before synthesis. This dramatically simplified reference resolution and allows dealing with nested stack assets only after all cross references have been resolved (the root cause for #7059).

This logic is implemented in a function called `prepareApp` which is normally called from `App.prepare()` but if a `Stack` is created as a root (normally in unit tests, we invoke this logic from there to retain current behavior).

This algorithm takes care of both resolving cross references and add template assets for nested stacks. This is because assets are currently addressed using CFN parameters, which means that when we adding them to the parent of a nested stack, the parent is mutated, so we need to rectify references again. To make sure this is done correctly, we always create assets in DFS order.

All changes to the test snapshots stem from new asset IDs of nested stack templates, not the template themselves. The change is a result of the fact that the refactor caused the "Parameters" section to appear in a different place in the template, but the template itself is identical.

Fixes #7059 by first resolving all references in the app and only then calculating the hash of the nested stack templates for their assets.

Fixes #5888 but this was not verified.
  • Loading branch information
Elad Ben-Israel authored Apr 16, 2020
1 parent 4ab3ffa commit 000f0c2
Show file tree
Hide file tree
Showing 17 changed files with 882 additions and 289 deletions.
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

0 comments on commit 000f0c2

Please sign in to comment.