-
Notifications
You must be signed in to change notification settings - Fork 4k
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
feat(core): present reason for cyclic references #2061
Merged
Merged
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
3d9629c
feat(core): present reason for cyclic references
rix0rrr de68555
Satisfy jsii by exporting
rix0rrr 63cbcdf
Get rid of some trailing AwsNoValue() references
rix0rrr 04a18f4
Another ScopedAws that I missed
3db479b
And 2 more
ad48bcf
Use Symbol to test for class type
rix0rrr b233cf9
Also use a symbol for Reference
rix0rrr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { Reference } from "./reference"; | ||
|
||
/** | ||
* A Token that represents a CloudFormation reference to another resource | ||
* | ||
* If these references are used in a different stack from where they are | ||
* defined, appropriate CloudFormation `Export`s and `Fn::ImportValue`s will be | ||
* synthesized automatically instead of the regular CloudFormation references. | ||
* | ||
* Additionally, the dependency between the stacks will be recorded, and the toolkit | ||
* will make sure to deploy producing stack before the consuming stack. | ||
* | ||
* This magic happens in the prepare() phase, where consuming stacks will call | ||
* `consumeFromStack` on these Tokens and if they happen to be exported by a different | ||
* Stack, we'll register the dependency. | ||
*/ | ||
export class CfnReference extends Reference { | ||
/** | ||
* Check whether this is actually a Reference | ||
*/ | ||
public static isCfnReferenceToken(x: Token): x is CfnReference { | ||
return (x as any).consumeFromStack !== undefined; | ||
} | ||
|
||
/** | ||
* What stack this Token is pointing to | ||
*/ | ||
private readonly producingStack?: Stack; | ||
|
||
/** | ||
* The Tokens that should be returned for each consuming stack (as decided by the producing Stack) | ||
*/ | ||
private readonly replacementTokens: Map<Stack, Token>; | ||
|
||
private readonly originalDisplayName: string; | ||
|
||
constructor(value: any, displayName: string, target: Construct) { | ||
if (typeof(value) === 'function') { | ||
throw new Error('Reference can only hold CloudFormation intrinsics (not a function)'); | ||
} | ||
// prepend scope path to display name | ||
super(value, `${target.node.id}.${displayName}`, target); | ||
this.originalDisplayName = displayName; | ||
this.replacementTokens = new Map<Stack, Token>(); | ||
|
||
this.producingStack = target.node.stack; | ||
} | ||
|
||
public resolve(context: ResolveContext): any { | ||
// If we have a special token for this consuming stack, resolve that. Otherwise resolve as if | ||
// we are in the same stack. | ||
const token = this.replacementTokens.get(context.scope.node.stack); | ||
if (token) { | ||
return token.resolve(context); | ||
} else { | ||
return super.resolve(context); | ||
} | ||
} | ||
|
||
/** | ||
* Register a stack this references is being consumed from. | ||
*/ | ||
public consumeFromStack(consumingStack: Stack, consumingConstruct: IConstruct) { | ||
if (this.producingStack && this.producingStack !== consumingStack && !this.replacementTokens.has(consumingStack)) { | ||
// We're trying to resolve a cross-stack reference | ||
consumingStack.addDependency(this.producingStack, `${consumingConstruct.node.path} -> ${this.target.node.path}.${this.originalDisplayName}`); | ||
this.replacementTokens.set(consumingStack, this.exportValue(this, consumingStack)); | ||
} | ||
} | ||
|
||
/** | ||
* Export a Token value for use in another stack | ||
* | ||
* Works by mutating the producing stack in-place. | ||
*/ | ||
private exportValue(tokenValue: Token, consumingStack: Stack): Token { | ||
const producingStack = this.producingStack!; | ||
|
||
if (producingStack.env.account !== consumingStack.env.account || producingStack.env.region !== consumingStack.env.region) { | ||
throw new Error('Can only reference cross stacks in the same region and account.'); | ||
} | ||
|
||
// 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 exportsName = 'Exports'; | ||
let stackExports = producingStack.node.tryFindChild(exportsName) as Construct; | ||
if (stackExports === undefined) { | ||
stackExports = new Construct(producingStack, exportsName); | ||
} | ||
|
||
// Ensure a singleton CfnOutput for this value | ||
const resolved = producingStack.node.resolve(tokenValue); | ||
const id = 'Output' + JSON.stringify(resolved); | ||
let output = stackExports.node.tryFindChild(id) as CfnOutput; | ||
if (!output) { | ||
output = new CfnOutput(stackExports, id, { value: tokenValue }); | ||
} | ||
|
||
// We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', | ||
// so construct one in-place. | ||
return new Token({ 'Fn::ImportValue': output.obtainExportName() }); | ||
} | ||
|
||
} | ||
|
||
import { CfnOutput } from "./cfn-output"; | ||
import { Construct, IConstruct } from "./construct"; | ||
import { Stack } from "./stack"; | ||
import { ResolveContext, Token } from "./token"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could instead use a private symbol to make this 100% reliable:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
doesn't bother JSII but it does bother TypeScript, which complains about unused private members.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am also not 100% sure that a private symbol will work across two different versions of this module (which is the reason we can't use
instanceof
).My favorite (and very ugly still) way to do this is to add the marker at runtime:
Yeah, horrible. TypeScript, can you make this easier?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will work across two different versions because
Symbol.for('key')
stores the symbol in the global symbol registry by name. It would not work if you were to just callSymbol()
, which is unique for every call. By private, I mean hiding it from JSII.Your UUID approach is an emulation of the global Symbol registry which was designed to solve this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use this to avoid some of the ugly duck typing we do - mark up the core constructs with symbols and maybe even generate some for L1. Module augmentation may even allow us to generate for our L2 later.
Create a symbol for each type that would be checked by name in a nominal type system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes,
Symbol.for()
will fix it. Although I do wonder what the point of making it a Symbol is, then, since we lose the uniqueness value of it. I guess that it can't conflict with a naive string.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On top of avoiding accidental conflicts, Symbols are not considered properties so it won't show up when enumerating an object's keys. Basically, it's cleaner and simpler.