forked from aws/aws-cdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cloud-assembly.ts
468 lines (400 loc) · 14.6 KB
/
cloud-assembly.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { LoadManifestOptions } from '@aws-cdk/cloud-assembly-schema';
import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact';
import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact';
import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact';
import { CloudArtifact } from './cloud-artifact';
import { topologicalSort } from './toposort';
/**
* The name of the root manifest file of the assembly.
*/
const MANIFEST_FILE = 'manifest.json';
/**
* Represents a deployable cloud application.
*/
export class CloudAssembly {
/**
* The root directory of the cloud assembly.
*/
public readonly directory: string;
/**
* The schema version of the assembly manifest.
*/
public readonly version: string;
/**
* All artifacts included in this assembly.
*/
public readonly artifacts: CloudArtifact[];
/**
* Runtime information such as module versions used to synthesize this assembly.
*/
public readonly runtime: cxschema.RuntimeInfo;
/**
* The raw assembly manifest.
*/
public readonly manifest: cxschema.AssemblyManifest;
/**
* Reads a cloud assembly from the specified directory.
* @param directory The root directory of the assembly.
*/
constructor(directory: string, loadOptions?: LoadManifestOptions) {
this.directory = directory;
this.manifest = cxschema.Manifest.loadAssemblyManifest(path.join(directory, MANIFEST_FILE), loadOptions);
this.version = this.manifest.version;
this.artifacts = this.renderArtifacts();
this.runtime = this.manifest.runtime || { libraries: { } };
// force validation of deps by accessing 'depends' on all artifacts
this.validateDeps();
}
/**
* Attempts to find an artifact with a specific identity.
* @returns A `CloudArtifact` object or `undefined` if the artifact does not exist in this assembly.
* @param id The artifact ID
*/
public tryGetArtifact(id: string): CloudArtifact | undefined {
return this.artifacts.find(a => a.id === id);
}
/**
* Returns a CloudFormation stack artifact from this assembly.
*
* Will only search the current assembly.
*
* @param stackName the name of the CloudFormation stack.
* @throws if there is no stack artifact by that name
* @throws if there is more than one stack with the same stack name. You can
* use `getStackArtifact(stack.artifactId)` instead.
* @returns a `CloudFormationStackArtifact` object.
*/
public getStackByName(stackName: string): CloudFormationStackArtifact {
const artifacts = this.artifacts.filter(a => a instanceof CloudFormationStackArtifact && a.stackName === stackName);
if (!artifacts || artifacts.length === 0) {
throw new Error(`Unable to find stack with stack name "${stackName}"`);
}
if (artifacts.length > 1) {
// eslint-disable-next-line max-len
throw new Error(`There are multiple stacks with the stack name "${stackName}" (${artifacts.map(a => a.id).join(',')}). Use "getStackArtifact(id)" instead`);
}
return artifacts[0] as CloudFormationStackArtifact;
}
/**
* Returns a CloudFormation stack artifact by name from this assembly.
* @deprecated renamed to `getStackByName` (or `getStackArtifact(id)`)
*/
public getStack(stackName: string) {
return this.getStackByName(stackName);
}
/**
* Returns a CloudFormation stack artifact from this assembly.
*
* @param artifactId the artifact id of the stack (can be obtained through `stack.artifactId`).
* @throws if there is no stack artifact with that id
* @returns a `CloudFormationStackArtifact` object.
*/
public getStackArtifact(artifactId: string): CloudFormationStackArtifact {
const artifact = this.tryGetArtifactRecursively(artifactId);
if (!artifact) {
throw new Error(`Unable to find artifact with id "${artifactId}"`);
}
if (!(artifact instanceof CloudFormationStackArtifact)) {
throw new Error(`Artifact ${artifactId} is not a CloudFormation stack`);
}
return artifact;
}
private tryGetArtifactRecursively(artifactId: string): CloudArtifact | undefined {
return this.stacksRecursively.find(a => a.id === artifactId);
}
/**
* Returns all the stacks, including the ones in nested assemblies
*/
public get stacksRecursively(): CloudFormationStackArtifact[] {
function search(stackArtifacts: CloudFormationStackArtifact[], assemblies: CloudAssembly[]): CloudFormationStackArtifact[] {
if (assemblies.length === 0) {
return stackArtifacts;
}
const [head, ...tail] = assemblies;
const nestedAssemblies = head.nestedAssemblies.map(asm => asm.nestedAssembly);
return search(stackArtifacts.concat(head.stacks), tail.concat(nestedAssemblies));
};
return search([], [this]);
}
/**
* Returns a nested assembly artifact.
*
* @param artifactId The artifact ID of the nested assembly
*/
public getNestedAssemblyArtifact(artifactId: string): NestedCloudAssemblyArtifact {
const artifact = this.tryGetArtifact(artifactId);
if (!artifact) {
throw new Error(`Unable to find artifact with id "${artifactId}"`);
}
if (!(artifact instanceof NestedCloudAssemblyArtifact)) {
throw new Error(`Found artifact '${artifactId}' but it's not a nested cloud assembly`);
}
return artifact;
}
/**
* Returns a nested assembly.
*
* @param artifactId The artifact ID of the nested assembly
*/
public getNestedAssembly(artifactId: string): CloudAssembly {
return this.getNestedAssemblyArtifact(artifactId).nestedAssembly;
}
/**
* Returns the tree metadata artifact from this assembly.
* @throws if there is no metadata artifact by that name
* @returns a `TreeCloudArtifact` object if there is one defined in the manifest, `undefined` otherwise.
*/
public tree(): TreeCloudArtifact | undefined {
const trees = this.artifacts.filter(a => a.manifest.type === cxschema.ArtifactType.CDK_TREE);
if (trees.length === 0) {
return undefined;
} else if (trees.length > 1) {
throw new Error(`Multiple artifacts of type ${cxschema.ArtifactType.CDK_TREE} found in manifest`);
}
const tree = trees[0];
if (!(tree instanceof TreeCloudArtifact)) {
throw new Error('"Tree" artifact is not of expected type');
}
return tree;
}
/**
* @returns all the CloudFormation stack artifacts that are included in this assembly.
*/
public get stacks(): CloudFormationStackArtifact[] {
return this.artifacts.filter(isCloudFormationStackArtifact);
function isCloudFormationStackArtifact(x: any): x is CloudFormationStackArtifact {
return x instanceof CloudFormationStackArtifact;
}
}
/**
* The nested assembly artifacts in this assembly
*/
public get nestedAssemblies(): NestedCloudAssemblyArtifact[] {
return this.artifacts.filter(isNestedCloudAssemblyArtifact);
function isNestedCloudAssemblyArtifact(x: any): x is NestedCloudAssemblyArtifact {
return x instanceof NestedCloudAssemblyArtifact;
}
}
private validateDeps() {
for (const artifact of this.artifacts) {
ignore(artifact.dependencies);
}
}
private renderArtifacts() {
const result = new Array<CloudArtifact>();
for (const [name, artifact] of Object.entries(this.manifest.artifacts || { })) {
const cloudartifact = CloudArtifact.fromManifest(this, name, artifact);
if (cloudartifact) {
result.push(cloudartifact);
}
}
return topologicalSort(result, x => x.id, x => x._dependencyIDs);
}
}
/**
* Construction properties for CloudAssemblyBuilder
*/
export interface CloudAssemblyBuilderProps {
/**
* Use the given asset output directory
*
* @default - Same as the manifest outdir
*/
readonly assetOutdir?: string;
/**
* If this builder is for a nested assembly, the parent assembly builder
*
* @default - This is a root assembly
*/
readonly parentBuilder?: CloudAssemblyBuilder;
}
/**
* Can be used to build a cloud assembly.
*/
export class CloudAssemblyBuilder {
/**
* The root directory of the resulting cloud assembly.
*/
public readonly outdir: string;
/**
* The directory where assets of this Cloud Assembly should be stored
*/
public readonly assetOutdir: string;
private readonly artifacts: { [id: string]: cxschema.ArtifactManifest } = { };
private readonly missing = new Array<cxschema.MissingContext>();
private readonly parentBuilder?: CloudAssemblyBuilder;
/**
* Initializes a cloud assembly builder.
* @param outdir The output directory, uses temporary directory if undefined
*/
constructor(outdir?: string, props: CloudAssemblyBuilderProps = {}) {
this.outdir = determineOutputDirectory(outdir);
this.assetOutdir = props.assetOutdir ?? this.outdir;
this.parentBuilder = props.parentBuilder;
// we leverage the fact that outdir is long-lived to avoid staging assets into it
// that were already staged (copying can be expensive). this is achieved by the fact
// that assets use a source hash as their name. other artifacts, and the manifest itself,
// will overwrite existing files as needed.
ensureDirSync(this.outdir);
}
/**
* Adds an artifact into the cloud assembly.
* @param id The ID of the artifact.
* @param manifest The artifact manifest
*/
public addArtifact(id: string, manifest: cxschema.ArtifactManifest) {
this.artifacts[id] = filterUndefined(manifest);
}
/**
* Reports that some context is missing in order for this cloud assembly to be fully synthesized.
* @param missing Missing context information.
*/
public addMissing(missing: cxschema.MissingContext) {
if (this.missing.every(m => m.key !== missing.key)) {
this.missing.push(missing);
}
// Also report in parent
this.parentBuilder?.addMissing(missing);
}
/**
* Finalizes the cloud assembly into the output directory returns a
* `CloudAssembly` object that can be used to inspect the assembly.
* @param options
*/
public buildAssembly(options: AssemblyBuildOptions = { }): CloudAssembly {
// explicitly initializing this type will help us detect
// breaking changes. (For example adding a required property will break compilation).
let manifest: cxschema.AssemblyManifest = {
version: cxschema.Manifest.version(),
artifacts: this.artifacts,
runtime: options.runtimeInfo,
missing: this.missing.length > 0 ? this.missing : undefined,
};
// now we can filter
manifest = filterUndefined(manifest);
const manifestFilePath = path.join(this.outdir, MANIFEST_FILE);
cxschema.Manifest.saveAssemblyManifest(manifest, manifestFilePath);
// "backwards compatibility": in order for the old CLI to tell the user they
// need a new version, we'll emit the legacy manifest with only "version".
// this will result in an error "CDK Toolkit >= CLOUD_ASSEMBLY_VERSION is required in order to interact with this program."
fs.writeFileSync(path.join(this.outdir, 'cdk.out'), JSON.stringify({ version: manifest.version }));
return new CloudAssembly(this.outdir);
}
/**
* Creates a nested cloud assembly
*/
public createNestedAssembly(artifactId: string, displayName: string) {
const directoryName = artifactId;
const innerAsmDir = path.join(this.outdir, directoryName);
this.addArtifact(artifactId, {
type: cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY,
properties: {
directoryName,
displayName,
} as cxschema.NestedCloudAssemblyProperties,
});
return new CloudAssemblyBuilder(innerAsmDir, {
// Reuse the same asset output directory as the current Casm builder
assetOutdir: this.assetOutdir,
parentBuilder: this,
});
}
}
/**
* Backwards compatibility for when `RuntimeInfo`
* was defined here. This is necessary because its used as an input in the stable
* @aws-cdk/core library.
*
* @deprecated moved to package 'cloud-assembly-schema'
* @see core.ConstructNode.synth
*/
export interface RuntimeInfo extends cxschema.RuntimeInfo {
}
/**
* Backwards compatibility for when `MetadataEntry`
* was defined here. This is necessary because its used as an input in the stable
* @aws-cdk/core library.
*
* @deprecated moved to package 'cloud-assembly-schema'
* @see core.ConstructNode.metadata
*/
export interface MetadataEntry extends cxschema.MetadataEntry {
}
/**
* Backwards compatibility for when `MissingContext`
* was defined here. This is necessary because its used as an input in the stable
* @aws-cdk/core library.
*
* @deprecated moved to package 'cloud-assembly-schema'
* @see core.Stack.reportMissingContext
*/
export interface MissingContext {
/**
* The missing context key.
*/
readonly key: string;
/**
* The provider from which we expect this context key to be obtained.
*
* (This is the old untyped definition, which is necessary for backwards compatibility.
* See cxschema for a type definition.)
*/
readonly provider: string;
/**
* A set of provider-specific options.
*
* (This is the old untyped definition, which is necessary for backwards compatibility.
* See cxschema for a type definition.)
*/
readonly props: Record<string, any>;
}
export interface AssemblyBuildOptions {
/**
* Include the specified runtime information (module versions) in manifest.
* @default - if this option is not specified, runtime info will not be included
* @deprecated All template modifications that should result from this should
* have already been inserted into the template.
*/
readonly runtimeInfo?: RuntimeInfo;
}
/**
* Returns a copy of `obj` without undefined values in maps or arrays.
*/
function filterUndefined(obj: any): any {
if (Array.isArray(obj)) {
return obj.filter(x => x !== undefined).map(x => filterUndefined(x));
}
if (typeof(obj) === 'object') {
const ret: any = { };
for (const [key, value] of Object.entries(obj)) {
if (value === undefined) {
continue;
}
ret[key] = filterUndefined(value);
}
return ret;
}
return obj;
}
function ignore(_x: any) {
return;
}
/**
* Turn the given optional output directory into a fixed output directory
*/
function determineOutputDirectory(outdir?: string) {
return outdir ?? fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out'));
}
function ensureDirSync(dir: string) {
if (fs.existsSync(dir)) {
if (!fs.statSync(dir).isDirectory()) {
throw new Error(`${dir} must be a directory`);
}
} else {
fs.mkdirSync(dir, { recursive: true });
}
}