-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathsynthesis.ts
251 lines (216 loc) · 7.88 KB
/
synthesis.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
import * as cxapi from '@aws-cdk/cx-api';
import { IConstruct } from 'constructs';
import { Annotations } from '../annotations';
import { App } from '../app';
import { Aspects, IAspect } from '../aspect';
import { Stack } from '../stack';
import { ISynthesisSession } from '../stack-synthesizers/types';
import { Stage, StageSynthesisOptions } from '../stage';
import { MetadataResource } from './metadata-resource';
import { prepareApp } from './prepare-app';
import { TreeMetadata } from './tree-metadata';
/**
* Options for `synthesize()`
*/
export interface SynthesisOptions extends StageSynthesisOptions {
/**
* The output directory into which to synthesize the cloud assembly.
* @default - creates a temporary directory
*/
readonly outdir?: string;
}
export function synthesize(root: IConstruct, options: SynthesisOptions = { }): cxapi.CloudAssembly {
// add the TreeMetadata resource to the App first
injectTreeMetadata(root);
// we start by calling "synth" on all nested assemblies (which will take care of all their children)
synthNestedAssemblies(root, options);
invokeAspects(root);
injectMetadataResources(root);
// resolve references
prepareApp(root);
// give all children an opportunity to validate now that we've finished prepare
if (!options.skipValidation) {
validateTree(root);
}
// in unit tests, we support creating free-standing stacks, so we create the
// assembly builder here.
const builder = Stage.isStage(root)
? root._assemblyBuilder
: new cxapi.CloudAssemblyBuilder(options.outdir);
// next, we invoke "onSynthesize" on all of our children. this will allow
// stacks to add themselves to the synthesized cloud assembly.
synthesizeTree(root, builder, options.validateOnSynthesis);
return builder.buildAssembly();
}
const CUSTOM_SYNTHESIS_SYM = Symbol.for('@aws-cdk/core:customSynthesis');
/**
* Interface for constructs that want to do something custom during synthesis
*
* This feature is intended for use by official AWS CDK libraries only; 3rd party
* library authors and CDK users should not use this function.
*/
export interface ICustomSynthesis {
/**
* Called when the construct is synthesized
*/
onSynthesize(session: ISynthesisSession): void;
}
export function addCustomSynthesis(construct: IConstruct, synthesis: ICustomSynthesis): void {
Object.defineProperty(construct, CUSTOM_SYNTHESIS_SYM, {
value: synthesis,
enumerable: false,
});
}
function getCustomSynthesis(construct: IConstruct): ICustomSynthesis | undefined {
return (construct as any)[CUSTOM_SYNTHESIS_SYM];
}
/**
* Find Assemblies inside the construct and call 'synth' on them
*
* (They will in turn recurse again)
*/
function synthNestedAssemblies(root: IConstruct, options: StageSynthesisOptions) {
for (const child of root.node.children) {
if (Stage.isStage(child)) {
child.synth(options);
} else {
synthNestedAssemblies(child, options);
}
}
}
/**
* Invoke aspects on the given construct tree.
*
* Aspects are not propagated across Assembly boundaries. The same Aspect will not be invoked
* twice for the same construct.
*/
function invokeAspects(root: IConstruct) {
const invokedByPath: { [nodePath: string]: IAspect[] } = { };
let nestedAspectWarning = false;
recurse(root, []);
function recurse(construct: IConstruct, inheritedAspects: IAspect[]) {
const node = construct.node;
const aspects = Aspects.of(construct);
const allAspectsHere = [...inheritedAspects ?? [], ...aspects.all];
const nodeAspectsCount = aspects.all.length;
for (const aspect of allAspectsHere) {
let invoked = invokedByPath[node.path];
if (!invoked) {
invoked = invokedByPath[node.path] = [];
}
if (invoked.includes(aspect)) { continue; }
aspect.visit(construct);
// if an aspect was added to the node while invoking another aspect it will not be invoked, emit a warning
// the `nestedAspectWarning` flag is used to prevent the warning from being emitted for every child
if (!nestedAspectWarning && nodeAspectsCount !== aspects.all.length) {
Annotations.of(construct).addWarning('We detected an Aspect was added via another Aspect, and will not be applied');
nestedAspectWarning = true;
}
// mark as invoked for this node
invoked.push(aspect);
}
for (const child of construct.node.children) {
if (!Stage.isStage(child)) {
recurse(child, allAspectsHere);
}
}
}
}
/**
* Find all stacks and add Metadata Resources to all of them
*
* There is no good generic place to do this. Can't do it in the constructor
* (because adding a child construct makes it impossible to set context on the
* node), and the generic prepare phase is deprecated.
*
* Only do this on [parent] stacks (not nested stacks), don't do this when
* disabled by the user.
*
* Also, only when running via the CLI. If we do it unconditionally,
* all unit tests everywhere are going to break massively. I've spent a day
* fixing our own, but downstream users would be affected just as badly.
*
* Stop at Assembly boundaries.
*/
function injectMetadataResources(root: IConstruct) {
visit(root, 'post', construct => {
if (!Stack.isStack(construct) || !construct._versionReportingEnabled) { return; }
// Because of https://github.com/aws/aws-cdk/blob/main/packages/assert-internal/lib/synth-utils.ts#L74
// synthesize() may be called more than once on a stack in unit tests, and the below would break
// if we execute it a second time. Guard against the constructs already existing.
const CDKMetadata = 'CDKMetadata';
if (construct.node.tryFindChild(CDKMetadata)) { return; }
new MetadataResource(construct, CDKMetadata);
});
}
/**
* Find the root App and add the TreeMetadata resource (if enabled).
*
* There is no good generic place to do this. Can't do it in the constructor
* (because adding a child construct makes it impossible to set context on the
* node), and the generic prepare phase is deprecated.
*/
function injectTreeMetadata(root: IConstruct) {
visit(root, 'post', construct => {
if (!App.isApp(construct) || !construct._treeMetadata) return;
const CDKTreeMetadata = 'Tree';
if (construct.node.tryFindChild(CDKTreeMetadata)) return;
new TreeMetadata(construct);
});
}
/**
* Synthesize children in post-order into the given builder
*
* Stop at Assembly boundaries.
*/
function synthesizeTree(root: IConstruct, builder: cxapi.CloudAssemblyBuilder, validateOnSynth: boolean = false) {
visit(root, 'post', construct => {
const session = {
outdir: builder.outdir,
assembly: builder,
validateOnSynth,
};
if (Stack.isStack(construct)) {
construct.synthesizer.synthesize(session);
} else if (construct instanceof TreeMetadata) {
construct._synthesizeTree(session);
} else {
const custom = getCustomSynthesis(construct);
custom?.onSynthesize(session);
}
});
}
interface ValidationError {
readonly message: string;
readonly source: IConstruct;
}
/**
* Validate all constructs in the given construct tree
*/
function validateTree(root: IConstruct) {
const errors = new Array<ValidationError>();
visit(root, 'pre', construct => {
for (const message of construct.node.validate()) {
errors.push({ message, source: construct });
}
});
if (errors.length > 0) {
const errorList = errors.map(e => `[${e.source.node.path}] ${e.message}`).join('\n ');
throw new Error(`Validation failed with the following errors:\n ${errorList}`);
}
}
/**
* Visit the given construct tree in either pre or post order, stopping at Assemblies
*/
function visit(root: IConstruct, order: 'pre' | 'post', cb: (x: IConstruct) => void) {
if (order === 'pre') {
cb(root);
}
for (const child of root.node.children) {
if (Stage.isStage(child)) { continue; }
visit(child, order, cb);
}
if (order === 'post') {
cb(root);
}
}