-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathsource-builder.ts
More file actions
547 lines (498 loc) · 19.2 KB
/
source-builder.ts
File metadata and controls
547 lines (498 loc) · 19.2 KB
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
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
import * as path from 'path';
import { format } from 'util';
import * as cxapi from '@aws-cdk/cloud-assembly-api';
import type * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as fs from 'fs-extra';
import { CdkAppMultiContext, MemoryContext, type IContextStore } from './context-store';
import { RWLock } from '../rwlock';
import { CachedCloudAssembly } from './cached-source';
import type { ContextAwareCloudAssemblyProps } from './private/context-aware-source';
import { ContextAwareCloudAssemblySource } from './private/context-aware-source';
import { execInChildProcess, toCommand } from './private/exec';
import { ExecutionEnvironment, assemblyFromDirectory, parametersFromSynthOptions, writeContextToEnv } from './private/prepare-source';
import { ReadableCloudAssembly } from './private/readable-assembly';
import type { ICloudAssemblySource } from './types';
import type { ToolkitServices } from '../../toolkit/private';
import { ToolkitError, AssemblyError } from '../../toolkit/toolkit-error';
import { noUndefined } from '../../util';
import { IO } from '../io/private';
import { missingContextKeys, temporarilyWriteEnv } from './private/helpers';
/**
* Properties the builder function receives.
*/
export interface AssemblyBuilderProps {
/**
* The output directory into which to the builder app will emit synthesized artifacts.
*/
readonly outdir?: string;
/**
* The context provided tp the builder app to synthesize the Cloud Assembly, including looked-up context.
*/
readonly context?: { [key: string]: any };
/**
* Additional configuration that would normally be passed to a CDK app using environment variables
*
* This contains variables intended for the user portion of a CDK app (notably
* `CDK_DEFAULT_ACCOUNT` and `CDK_DEFAULT_REGION`), which you can freely read.
*
* It also contains variables intended for the CDK Toolkit to communicate with
* the internals of the construct library, like `CDK_DEBUG` and
* `CDK_CLI_ASM_VERSION`. Reading these latter variables is possible but not
* recommended, as their meaning may change without notice.
*/
readonly env: Record<string, string>;
}
/**
* A function that takes synthesis parameters and produces a Cloud Assembly
*
* Most typically, the properties passed here will be used to construct a
* `cdk.App`, and the return value is the return value of `app.synth()`.
*/
export type AssemblyBuilder = (props: AssemblyBuilderProps) => Promise<cxschema.ICloudAssembly>;
/**
* Configuration for creating a CLI from an AWS CDK App directory
*/
export interface AssemblyDirectoryProps {
/**
* Options to configure loading of the assembly after it has been synthesized
*/
readonly loadAssemblyOptions?: LoadAssemblyOptions;
/**
* Whether or not to fail if the synthesized assembly contains
* missing context
*
* @default true
*/
readonly failOnMissingContext?: boolean;
}
/**
* Configuration for creating a CLI from an AWS CDK App directory
*/
export interface AssemblySourceProps {
/**
* Emits the synthesized cloud assembly into the given directory
*
* @default "cdk.out"
*/
readonly outdir?: string;
/**
* Perform context lookups.
*
* Synthesis fails if this is disabled and context lookups need to be performed.
*
* @default true
*/
readonly lookups?: boolean;
/**
* A context store for this operation
*
* The context store will be used to source initial context values,
* and updated values will be stored here.
*
* @default - Depends on the operation
*/
readonly contextStore?: IContextStore;
/**
* Options that are passed through the context to a CDK app on synth
*/
readonly synthOptions?: AppSynthOptions;
/**
* Options to configure loading of the assembly after it has been synthesized
*/
readonly loadAssemblyOptions?: LoadAssemblyOptions;
/**
* Delete the `outdir` when the assembly is disposed
*
* @default - `true` if `outdir` is not given, `false` otherwise
*/
readonly disposeOutdir?: boolean;
/**
* Resolve the current default environment an provide as environment variables to the app.
*
* This will make a (cached) call to STS to resolve the current account using
* base credentials. The behavior is not always desirable and can add
* unnecessary delays, e.g. when an app specifies an environment explicitly
* or when local actions are be performed without internet access.
*
* @default true
*/
readonly resolveDefaultEnvironment?: boolean;
}
/**
* Options for the `fromAssemblyBuilder` Assembly Source constructor
*/
export interface FromAssemblyBuilderOptions extends AssemblySourceProps {
/**
* Mutate current process' environment variables to communicate with CDK app
*
* There are a number of environment variables the Toolkit uses to pass
* information to the CDK app.
*
* By default, these environment variables will be written to the current
* process' global shared environment variables before the builder is invoked,
* and you don't need to do anything else. However, because this mutates
* shared state it is not safe to run multiple builders concurrently.
*
* Set this to `false` to avoid mutating the shared environment. Instead,
* you will need to pass the `outdir` and `context` to the `App` constructor
* directly in your builder, and inspect the `env` map directly
* for information like the `CDK_DEFAULT_ACCOUNT` and `CDK_DEFAULT_REGION`.
*
* ```ts
* const cx = await toolkit.fromAssemblyBuilder(async (props) => {
* // Important: pass on synthesis parameters
* const app = new core.App({
* outdir: props.outdir,
* context: props.context,
* });
*
* new MyStack(app, 'MyStack', {
* env: {
* account: props.env.CDK_DEFAULT_ACCOUNT,
* region: props.env.CDK_DEFAULT_REGION,
* },
* });
*
* // ...
* }, {
* clobberEnv: false,
* });
* ```
*
* @default true
*/
readonly clobberEnv?: boolean;
}
/**
* Options for the `fromCdkApp` Assembly Source constructor
*/
export interface FromCdkAppOptions extends AssemblySourceProps {
/**
* Execute the application in this working directory.
*
* @default - Current working directory
*/
readonly workingDirectory?: string;
/**
* Additional environment variables
*
* These environment variables will be set in addition to the environment
* variables currently set in the process. A value of `undefined` will
* unset a particular environment variable.
*/
readonly env?: Record<string, string | undefined>;
}
/**
* Settings that are passed to a CDK app via the context
*/
export interface AppSynthOptions {
/**
* Debug the CDK app.
* Logs additional information during synthesis, such as creation stack traces of tokens.
* This also sets the `CDK_DEBUG` env variable and will slow down synthesis.
*
* @default false
*/
readonly debug?: boolean;
/**
* Enables the embedding of the "aws:cdk:path" in CloudFormation template metadata.
*
* @default true
*/
readonly pathMetadata?: boolean;
/**
* Enable the collection and reporting of version information.
*
* @default true
*/
readonly versionReporting?: boolean;
/**
* Whe enabled, `aws:asset:xxx` metadata entries are added to the template.
*
* Disabling this can be useful in certain cases like integration tests.
*
* @default true
*/
readonly assetMetadata?: boolean;
/**
* Enable asset staging.
*
* Disabling asset staging means that copyable assets will not be copied to the
* output directory and will be referenced with absolute paths.
*
* Not copied to the output directory: this is so users can iterate on the
* Lambda source and run SAM CLI without having to re-run CDK (note: we
* cannot achieve this for bundled assets, if assets are bundled they
* will have to re-run CDK CLI to re-bundle updated versions).
*
* Absolute path: SAM CLI expects `cwd`-relative paths in a resource's
* `aws:asset:path` metadata. In order to be predictable, we will always output
* absolute paths.
*
* @default true
*/
readonly assetStaging?: boolean;
/**
* Select which stacks should have asset bundling enabled
*
* @default ["**"] - all stacks
*/
readonly bundlingForStacks?: string;
}
/**
* Options to configure loading of the assembly after it has been synthesized
*/
export interface LoadAssemblyOptions {
/**
* Check the Toolkit supports the Cloud Assembly Schema version
*
* When disabled, allows to Toolkit to read a newer cloud assembly than the CX API is designed
* to support. Your application may not be aware of all features that in use in the Cloud Assembly.
*
* @default true
*/
readonly checkVersion?: boolean;
/**
* Validate enums to only have known values
*
* When disabled, the Toolkit may read enum values it doesn't know about yet.
* You will have to make sure to always check the values of enums you encounter in the manifest.
*
* @default true
*/
readonly checkEnums?: boolean;
}
export abstract class CloudAssemblySourceBuilder {
/**
* Helper to provide the CloudAssemblySourceBuilder with required toolkit services
* @internal
*/
protected abstract sourceBuilderServices(): Promise<ToolkitServices>;
/**
* Create a Cloud Assembly from a Cloud Assembly builder function.
*
* ## Outdir
*
* If no output directory is given, it will synthesize into a temporary system
* directory. The temporary directory will be cleaned up, unless
* `disposeOutdir: false`.
*
* A write lock will be acquired on the output directory for the duration of
* the CDK app synthesis (which means that no two apps can synthesize at the
* same time), and after synthesis a read lock will be acquired on the
* directory. This means that while the CloudAssembly is being used, no CDK
* app synthesis can take place into that directory.
*
* ## Context
*
* If no `contextStore` is given, a `MemoryContext` will be used. This means
* no provider lookups will be persisted anywhere by default. Use a different
* type of context store if you want persistence between synth operations.
*
* @param builder - the builder function
* @param props - additional configuration properties
* @returns the CloudAssembly source
*/
public async fromAssemblyBuilder(
builder: AssemblyBuilder,
props: FromAssemblyBuilderOptions = {},
): Promise<ICloudAssemblySource> {
const services = await this.sourceBuilderServices();
const contextStore = props.contextStore ?? new MemoryContext();
const contextAssemblyProps: ContextAwareCloudAssemblyProps = {
services,
contextStore,
lookups: props.lookups,
};
const outdir = props.outdir ? path.resolve(props.outdir) : undefined;
return new ContextAwareCloudAssemblySource(
{
produce: async () => {
await using execution = await ExecutionEnvironment.create(services, {
outdir,
resolveDefaultAppEnv: props.resolveDefaultEnvironment ?? true,
});
const synthParams = parametersFromSynthOptions(props.synthOptions);
const fullContext = {
...await contextStore.read(),
...synthParams.context,
};
await services.ioHelper.defaults.debug(format('context:', fullContext));
const env = noUndefined({
// Versioning, outdir, default account and region
...await execution.defaultEnvVars(),
// Environment variables derived from settings
...synthParams.env,
});
const cleanupContextTemp = writeContextToEnv(env, fullContext, 'env-is-complete');
using _cleanupEnv = (props.clobberEnv ?? true) ? temporarilyWriteEnv(env) : undefined;
let assembly;
try {
assembly = await builder({
outdir: execution.outdir,
context: fullContext,
env,
});
} catch (error: unknown) {
// re-throw toolkit errors unchanged
if (ToolkitError.isToolkitError(error)) {
throw error;
}
// otherwise, wrap into an assembly error
throw AssemblyError.withCause('Assembly builder failed', error);
} finally {
await cleanupContextTemp();
}
// Convert what we got to the definitely correct type we're expecting, a cxapi.CloudAssembly
const asm = cxapi.CloudAssembly.isCloudAssembly(assembly)
? assembly
: await assemblyFromDirectory(assembly.directory, services.ioHelper, props.loadAssemblyOptions);
const success = await execution.markSuccessful();
const deleteOnDispose = props.disposeOutdir ?? execution.shouldDisposeOutDir;
return new ReadableCloudAssembly(asm, success.readLock, { deleteOnDispose });
},
},
contextAssemblyProps,
);
}
/**
* Creates a Cloud Assembly from an existing assembly directory.
*
* A read lock will be acquired for the directory. This means that while
* the CloudAssembly is being used, no CDK app synthesis can take place into
* that directory.
*
* @param directory - directory the directory of a already produced Cloud Assembly.
* @returns the CloudAssembly source
*/
public async fromAssemblyDirectory(directory: string, props: AssemblyDirectoryProps = {}): Promise<ICloudAssemblySource> {
const services: ToolkitServices = await this.sourceBuilderServices();
return {
async produce() {
await services.ioHelper.notify(IO.CDK_ASSEMBLY_I0150.msg('--app points to a cloud assembly, so we bypass synth'));
const readLock = await new RWLock(directory).acquireRead();
try {
const asm = await assemblyFromDirectory(directory, services.ioHelper, props.loadAssemblyOptions);
const assembly = new ReadableCloudAssembly(asm, readLock, { deleteOnDispose: false });
if (assembly.cloudAssembly.manifest.missing && assembly.cloudAssembly.manifest.missing.length > 0) {
if (props.failOnMissingContext ?? true) {
const missingKeysSet = missingContextKeys(assembly.cloudAssembly.manifest.missing);
const missingKeys = Array.from(missingKeysSet);
throw AssemblyError.withCause(
'Assembly contains missing context. ' +
"Make sure all necessary context is already in 'cdk.context.json' by running 'cdk synth' on a machine with sufficient AWS credentials and committing the result. " +
`Missing context keys: '${missingKeys.join(', ')}'`,
'Error producing assembly',
);
}
}
return new CachedCloudAssembly(assembly);
} catch (e) {
await readLock.release();
throw e;
}
},
};
}
/**
* Use an AWS CDK app exectuable as source.
*
* `app` is a command line that will be executed to produce a Cloud Assembly.
* The command will be executed in a shell, so it must come from a trusted source.
*
* The subprocess will execute in `workingDirectory`, which defaults to
* the current process' working directory if not given.
*
* ## Outdir
*
* If an output directory is supplied, relative paths are evaluated with
* respect to the current process' working directory. If an output directory
* is not supplied, the default is a `cdk.out` directory underneath
* `workingDirectory`. The output directory will not be cleaned up unless
* `disposeOutdir: true`.
*
* A write lock will be acquired on the output directory for the duration of
* the CDK app synthesis (which means that no two apps can synthesize at the
* same time), and after synthesis a read lock will be acquired on the
* directory. This means that while the CloudAssembly is being used, no CDK
* app synthesis can take place into that directory.
*
* ## Context
*
* If no `contextStore` is given, a `CdkAppMultiContext` will be used, initialized
* to the app's `workingDirectory`. This means that context will be loaded from
* all the CDK's default context sources, and updates will be written to
* `cdk.context.json`.
*
* @param props - additional configuration properties
* @returns the CloudAssembly source
*/
public async fromCdkApp(app: string, props: FromCdkAppOptions = {}): Promise<ICloudAssemblySource> {
const services: ToolkitServices = await this.sourceBuilderServices();
const workingDirectory = props.workingDirectory ?? process.cwd();
const outdir = props.outdir ? path.resolve(props.outdir) : path.resolve(workingDirectory, 'cdk.out');
const contextStore = props.contextStore ?? new CdkAppMultiContext(workingDirectory);
const contextAssemblyProps: ContextAwareCloudAssemblyProps = {
services,
contextStore,
lookups: props.lookups,
};
return new ContextAwareCloudAssemblySource(
{
produce: async () => {
try {
fs.mkdirpSync(outdir);
} catch (e: any) {
throw new ToolkitError(`Could not create output directory at '${outdir}' (${e.message}).`);
}
await using execution = await ExecutionEnvironment.create(services, {
outdir,
resolveDefaultAppEnv: props.resolveDefaultEnvironment ?? true,
});
const commandLine = await execution.guessExecutable(toCommand(app));
const synthParams = parametersFromSynthOptions(props.synthOptions);
const fullContext = {
...await contextStore.read(),
...synthParams.context,
};
await services.ioHelper.defaults.debug(format('context:', fullContext));
const env = noUndefined({
// Need to start with full env of `writeContextToEnv` will not be able to do the size
// calculation correctly.
...process.env,
// User gave us something
...props.env,
// Versioning, outdir, default account and region
...await execution.defaultEnvVars(),
// Environment variables derived from settings
...synthParams.env,
});
const cleanupTemp = writeContextToEnv(env, fullContext, 'env-is-complete');
try {
await execInChildProcess(commandLine, {
eventPublisher: async (type, line) => {
switch (type) {
case 'data_stdout':
await services.ioHelper.notify(IO.CDK_ASSEMBLY_I1001.msg(line));
break;
case 'data_stderr':
await services.ioHelper.notify(IO.CDK_ASSEMBLY_E1002.msg(line));
break;
}
},
env,
cwd: workingDirectory,
});
} finally {
await cleanupTemp();
}
const asm = await assemblyFromDirectory(outdir, services.ioHelper, props.loadAssemblyOptions);
const success = await execution.markSuccessful();
const deleteOnDispose = props.disposeOutdir ?? execution.shouldDisposeOutDir;
return new ReadableCloudAssembly(asm, success.readLock, { deleteOnDispose });
},
},
contextAssemblyProps,
);
}
}