Skip to content

Commit e82e208

Browse files
authored
feat(core): present reason for cyclic references (#2061)
To help people debugging cyclic references, we now trace the "reason" a cyclic reference got added, so that we can present the conflicting references instead of just presenting an error. Fix the order of the error message, which was the wrong way around. Clean up references a little: - Split out `Reference` and `CfnReference`, which got conflated in an undesirable way. `Reference` is now officially the base class for all references, and `CfnReference` is only one implementation of it for CloudFormation references. - Make 'scope' required for references, the only place where it was potentially empty was for CFN Pseudo Parameters, refactored those to not use classes anymore (because there's no need to). - Get rid of 'Ref', the class wasn't being very useful. - Make a dependency Construct => Stack lazy (it was conflicting at load time with the Stack => Construct dependency which is more important).
1 parent 99ab46d commit e82e208

File tree

16 files changed

+231
-225
lines changed

16 files changed

+231
-225
lines changed

packages/@aws-cdk/aws-codebuild/lib/project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ export class Project extends ProjectBase {
602602

603603
let cache: CfnProject.ProjectCacheProperty | undefined;
604604
if (props.cacheBucket) {
605-
const cacheDir = props.cacheDir != null ? props.cacheDir : new cdk.AwsNoValue().toString();
605+
const cacheDir = props.cacheDir != null ? props.cacheDir : cdk.Aws.noValue;
606606
cache = {
607607
type: 'S3',
608608
location: cdk.Fn.join('/', [props.cacheBucket.bucketName, cacheDir]),

packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/imported.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export abstract class ImportedTargetGroupBase extends cdk.Construct implements I
2424
super(scope, id);
2525

2626
this.targetGroupArn = props.targetGroupArn;
27-
this.loadBalancerArns = props.loadBalancerArns || new cdk.AwsNoValue().toString();
27+
this.loadBalancerArns = props.loadBalancerArns || cdk.Aws.noValue;
2828
}
2929

3030
public export() {

packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export class ReceiptRuleLambdaAction implements IReceiptRuleAction {
280280
this.props.function.addPermission(permissionId, {
281281
action: 'lambda:InvokeFunction',
282282
principal: new iam.ServicePrincipal('ses.amazonaws.com'),
283-
sourceAccount: new cdk.ScopedAws().accountId
283+
sourceAccount: cdk.Aws.accountId
284284
});
285285
}
286286

@@ -344,7 +344,7 @@ export class ReceiptRuleS3Action implements IReceiptRuleAction {
344344
.addServicePrincipal('ses.amazonaws.com')
345345
.addResource(this.props.bucket.arnForObjects(`${keyPattern}*`))
346346
.addCondition('StringEquals', {
347-
'aws:Referer': new cdk.ScopedAws().accountId
347+
'aws:Referer': cdk.Aws.accountId
348348
});
349349

350350
this.props.bucket.addToResourcePolicy(s3Statement);
@@ -362,7 +362,7 @@ export class ReceiptRuleS3Action implements IReceiptRuleAction {
362362
'kms:EncryptionContext:aws:ses:message-id': 'false'
363363
},
364364
StringEquals: {
365-
'kms:EncryptionContext:aws:ses:source-account': new cdk.ScopedAws().accountId
365+
'kms:EncryptionContext:aws:ses:source-account': cdk.Aws.accountId
366366
}
367367
});
368368

packages/@aws-cdk/cdk/lib/cfn-element.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,6 @@ export abstract class CfnElement extends Construct {
126126
}
127127
}
128128

129-
import { Reference } from "./reference";
130-
131-
/**
132-
* A generic, untyped reference to a Stack Element
133-
*/
134-
export class Ref extends Reference {
135-
constructor(element: CfnElement) {
136-
super({ Ref: element.logicalId }, 'Ref', element);
137-
}
138-
}
139-
140129
/**
141130
* Base class for referenceable CloudFormation constructs which are not Resources
142131
*
@@ -152,8 +141,16 @@ export abstract class CfnRefElement extends CfnElement {
152141
* Returns a token to a CloudFormation { Ref } that references this entity based on it's logical ID.
153142
*/
154143
public get ref(): string {
155-
return new Ref(this).toString();
144+
return this.referenceToken.toString();
145+
}
146+
147+
/**
148+
* Return a token that will CloudFormation { Ref } this stack element
149+
*/
150+
protected get referenceToken(): Token {
151+
return new CfnReference({ Ref: this.logicalId }, 'Ref', this);
156152
}
157153
}
158154

159-
import { findTokens } from "./resolve";
155+
import { CfnReference } from "./cfn-reference";
156+
import { findTokens } from "./resolve";

packages/@aws-cdk/cdk/lib/cfn-parameter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CfnRefElement, Ref } from './cfn-element';
1+
import { CfnRefElement } from './cfn-element';
22
import { Construct } from './construct';
33
import { Token } from './token';
44

@@ -99,7 +99,7 @@ export class CfnParameter extends CfnRefElement {
9999
constructor(scope: Construct, id: string, props: CfnParameterProps) {
100100
super(scope, id);
101101
this.properties = props;
102-
this.value = new Ref(this);
102+
this.value = this.referenceToken;
103103
this.stringValue = this.value.toString();
104104
this.stringListValue = this.value.toList();
105105
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Reference } from "./reference";
2+
3+
const CFN_REFERENCE_SYMBOL = Symbol('@aws-cdk/cdk.CfnReference');
4+
5+
/**
6+
* A Token that represents a CloudFormation reference to another resource
7+
*
8+
* If these references are used in a different stack from where they are
9+
* defined, appropriate CloudFormation `Export`s and `Fn::ImportValue`s will be
10+
* synthesized automatically instead of the regular CloudFormation references.
11+
*
12+
* Additionally, the dependency between the stacks will be recorded, and the toolkit
13+
* will make sure to deploy producing stack before the consuming stack.
14+
*
15+
* This magic happens in the prepare() phase, where consuming stacks will call
16+
* `consumeFromStack` on these Tokens and if they happen to be exported by a different
17+
* Stack, we'll register the dependency.
18+
*/
19+
export class CfnReference extends Reference {
20+
/**
21+
* Check whether this is actually a Reference
22+
*/
23+
public static isCfnReference(x: Token): x is CfnReference {
24+
return (x as any)[CFN_REFERENCE_SYMBOL] === true;
25+
}
26+
27+
/**
28+
* What stack this Token is pointing to
29+
*/
30+
private readonly producingStack?: Stack;
31+
32+
/**
33+
* The Tokens that should be returned for each consuming stack (as decided by the producing Stack)
34+
*/
35+
private readonly replacementTokens: Map<Stack, Token>;
36+
37+
private readonly originalDisplayName: string;
38+
39+
constructor(value: any, displayName: string, target: Construct) {
40+
if (typeof(value) === 'function') {
41+
throw new Error('Reference can only hold CloudFormation intrinsics (not a function)');
42+
}
43+
// prepend scope path to display name
44+
super(value, `${target.node.id}.${displayName}`, target);
45+
this.originalDisplayName = displayName;
46+
this.replacementTokens = new Map<Stack, Token>();
47+
48+
this.producingStack = target.node.stack;
49+
Object.defineProperty(this, CFN_REFERENCE_SYMBOL, { value: true });
50+
}
51+
52+
public resolve(context: ResolveContext): any {
53+
// If we have a special token for this consuming stack, resolve that. Otherwise resolve as if
54+
// we are in the same stack.
55+
const token = this.replacementTokens.get(context.scope.node.stack);
56+
if (token) {
57+
return token.resolve(context);
58+
} else {
59+
return super.resolve(context);
60+
}
61+
}
62+
63+
/**
64+
* Register a stack this references is being consumed from.
65+
*/
66+
public consumeFromStack(consumingStack: Stack, consumingConstruct: IConstruct) {
67+
if (this.producingStack && this.producingStack !== consumingStack && !this.replacementTokens.has(consumingStack)) {
68+
// We're trying to resolve a cross-stack reference
69+
consumingStack.addDependency(this.producingStack, `${consumingConstruct.node.path} -> ${this.target.node.path}.${this.originalDisplayName}`);
70+
this.replacementTokens.set(consumingStack, this.exportValue(this, consumingStack));
71+
}
72+
}
73+
74+
/**
75+
* Export a Token value for use in another stack
76+
*
77+
* Works by mutating the producing stack in-place.
78+
*/
79+
private exportValue(tokenValue: Token, consumingStack: Stack): Token {
80+
const producingStack = this.producingStack!;
81+
82+
if (producingStack.env.account !== consumingStack.env.account || producingStack.env.region !== consumingStack.env.region) {
83+
throw new Error('Can only reference cross stacks in the same region and account.');
84+
}
85+
86+
// Ensure a singleton "Exports" scoping Construct
87+
// This mostly exists to trigger LogicalID munging, which would be
88+
// disabled if we parented constructs directly under Stack.
89+
// Also it nicely prevents likely construct name clashes
90+
91+
const exportsName = 'Exports';
92+
let stackExports = producingStack.node.tryFindChild(exportsName) as Construct;
93+
if (stackExports === undefined) {
94+
stackExports = new Construct(producingStack, exportsName);
95+
}
96+
97+
// Ensure a singleton CfnOutput for this value
98+
const resolved = producingStack.node.resolve(tokenValue);
99+
const id = 'Output' + JSON.stringify(resolved);
100+
let output = stackExports.node.tryFindChild(id) as CfnOutput;
101+
if (!output) {
102+
output = new CfnOutput(stackExports, id, { value: tokenValue });
103+
}
104+
105+
// We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string',
106+
// so construct one in-place.
107+
return new Token({ 'Fn::ImportValue': output.obtainExportName() });
108+
}
109+
110+
}
111+
112+
import { CfnOutput } from "./cfn-output";
113+
import { Construct, IConstruct } from "./construct";
114+
import { Stack } from "./stack";
115+
import { ResolveContext, Token } from "./token";

packages/@aws-cdk/cdk/lib/cfn-resource.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import cxapi = require('@aws-cdk/cx-api');
22
import { CfnCondition } from './cfn-condition';
33
import { Construct, IConstruct } from './construct';
4-
import { Reference } from './reference';
54
import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy';
65
import { TagManager } from './tag-manager';
76
import { capitalizePropertyNames, ignoreEmpty, PostResolveToken } from './util';
87
// import required to be here, otherwise causes a cycle when running the generated JavaScript
98
// tslint:disable-next-line:ordered-imports
109
import { CfnRefElement } from './cfn-element';
10+
import { CfnReference } from './cfn-reference';
1111

1212
export interface CfnResourceProps {
1313
/**
@@ -133,7 +133,7 @@ export class CfnResource extends CfnRefElement {
133133
* @param attributeName The name of the attribute.
134134
*/
135135
public getAtt(attributeName: string) {
136-
return new Reference({ 'Fn::GetAtt': [this.logicalId, attributeName] }, attributeName, this);
136+
return new CfnReference({ 'Fn::GetAtt': [this.logicalId, attributeName] }, attributeName, this);
137137
}
138138

139139
/**

packages/@aws-cdk/cdk/lib/construct.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ export class ConstructNode {
4646
private readonly _children: { [name: string]: IConstruct } = { };
4747
private readonly context: { [key: string]: any } = { };
4848
private readonly _metadata = new Array<MetadataEntry>();
49-
private readonly references = new Set<Token>();
49+
private readonly references = new Set<Reference>();
5050
private readonly dependencies = new Set<IDependable>();
5151

5252
/** Will be used to cache the value of ``this.stack``. */
53-
private _stack?: Stack;
53+
private _stack?: import('./stack').Stack;
5454

5555
/**
5656
* If this is set to 'true'. addChild() calls for this construct and any child
@@ -91,11 +91,13 @@ export class ConstructNode {
9191
/**
9292
* The stack the construct is a part of.
9393
*/
94-
public get stack(): Stack {
94+
public get stack(): import('./stack').Stack {
95+
// Lazy import to break cyclic import
96+
const stack: typeof import('./stack') = require('./stack');
9597
return this._stack || (this._stack = _lookStackUp(this));
9698

97-
function _lookStackUp(_this: ConstructNode) {
98-
if (Stack.isStack(_this.host)) {
99+
function _lookStackUp(_this: ConstructNode): import('./stack').Stack {
100+
if (stack.Stack.isStack(_this.host)) {
99101
return _this.host;
100102
}
101103
if (!_this.scope) {
@@ -471,7 +473,7 @@ export class ConstructNode {
471473
*/
472474
public recordReference(...refs: Token[]) {
473475
for (const ref of refs) {
474-
if (ref.isReference) {
476+
if (Reference.isReference(ref)) {
475477
this.references.add(ref);
476478
}
477479
}
@@ -480,12 +482,12 @@ export class ConstructNode {
480482
/**
481483
* Return all references of the given type originating from this node or any of its children
482484
*/
483-
public findReferences(): Token[] {
484-
const ret = new Set<Token>();
485+
public findReferences(): OutgoingReference[] {
486+
const ret = new Set<OutgoingReference>();
485487

486488
function recurse(node: ConstructNode) {
487-
for (const ref of node.references) {
488-
ret.add(ref);
489+
for (const reference of node.references) {
490+
ret.add({ source: node.host, reference });
489491
}
490492

491493
for (const child of node.children) {
@@ -729,5 +731,10 @@ export interface Dependency {
729731
target: IConstruct;
730732
}
731733

734+
export interface OutgoingReference {
735+
source: IConstruct;
736+
reference: Reference;
737+
}
738+
732739
// Import this _after_ everything else to help node work the classes out in the correct order...
733-
import { Stack } from './stack';
740+
import { Reference } from './reference';

packages/@aws-cdk/cdk/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './logical-id';
1616
export * from './cfn-mapping';
1717
export * from './cfn-output';
1818
export * from './cfn-parameter';
19+
export * from './cfn-reference';
1920
export * from './pseudo';
2021
export * from './cfn-resource';
2122
export * from './resource-policy';

0 commit comments

Comments
 (0)