-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
refs.ts
308 lines (258 loc) · 11.7 KB
/
refs.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
// ----------------------------------------------------
// CROSS REFERENCES
// ----------------------------------------------------
import { CfnElement } from '../cfn-element';
import { CfnOutput } from '../cfn-output';
import { CfnParameter } from '../cfn-parameter';
import { CfnResource } from '../cfn-resource';
import { Construct, IConstruct } from '../construct-compat';
import { Reference } from '../reference';
import { IResolvable } from '../resolvable';
import { Stack } from '../stack';
import { Token } from '../token';
import { CfnReference } from './cfn-reference';
import { Intrinsic } from './intrinsic';
import { findTokens } from './resolve';
import { SsmStringParameter } from './ssm-parameter';
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: IConstruct): 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}`);
if (CfnResource.isCfnResource(reference.target) && reference.target.weakReference) {
return createSsmParameter(consumer, reference);
}
return createImportValue(reference);
}
/**
* Finds all the CloudFormation references in a construct tree.
*/
function findAllReferences(root: IConstruct) {
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
// ------------------------------------------------------------------------------------------------
function createSsmParameter(consumer: Stack, reference: CfnReference): Intrinsic {
const exportingStack = Stack.of(reference.target);
const id = makeUniqueId([ JSON.stringify(exportingStack.resolve(reference)) ]);
// include the stack name, SSM parameters are not stack-local.
// use SSM hierarchy format - https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-su-organize.html
const parameterName = `/stacks/${exportingStack.stackName}/${id}`;
const constructId = `SSMExport${id}`;
const existing = exportingStack.node.tryFindChild(constructId);
if (!existing) {
new SsmStringParameter(exportingStack, constructId, {
name: parameterName,
value: Token.asString(reference),
description: `[cdk] exported from stack "${exportingStack.stackName}" for use as parameter in a different stack`,
});
}
const parameterId = `SsmParameterValue:${parameterName}`;
let cfnparameter = consumer.node.tryFindChild(parameterId) as CfnParameter | undefined;
if (!cfnparameter) {
cfnparameter = new CfnParameter(consumer, parameterId, {
type: 'AWS::SSM::Parameter::Value<String>',
default: parameterName,
});
}
return new Intrinsic({ Ref: cfnparameter.logicalId });
}
/**
* Imports a value from another stack by creating an "Output" with an "ExportName"
* and returning an "Fn::ImportValue" token.
*/
function createImportValue(reference: CfnReference): 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);
}