@highstate/backend 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index.mjs → index.js} +1255 -916
- package/dist/library/source-resolution-worker.js +55 -0
- package/dist/library/worker/main.js +207 -0
- package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
- package/dist/shared/index.js +6 -0
- package/dist/utils-ByadNcv4.js +102 -0
- package/package.json +14 -18
- package/src/common/index.ts +3 -0
- package/src/common/local.ts +22 -0
- package/src/common/pulumi.ts +230 -0
- package/src/common/utils.ts +137 -0
- package/src/config.ts +40 -0
- package/src/index.ts +6 -0
- package/src/library/abstractions.ts +83 -0
- package/src/library/factory.ts +20 -0
- package/src/library/index.ts +2 -0
- package/src/library/local.ts +404 -0
- package/src/library/source-resolution-worker.ts +96 -0
- package/src/library/worker/evaluator.ts +119 -0
- package/src/library/worker/loader.ts +93 -0
- package/src/library/worker/main.ts +82 -0
- package/src/library/worker/protocol.ts +38 -0
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/manager.ts +165 -0
- package/src/orchestrator/operation-workset.ts +483 -0
- package/src/orchestrator/operation.ts +647 -0
- package/src/preferences/shared.ts +1 -0
- package/src/project/abstractions.ts +89 -0
- package/src/project/factory.ts +11 -0
- package/src/project/index.ts +4 -0
- package/src/project/local.ts +412 -0
- package/src/project/lock.ts +39 -0
- package/src/project/manager.ts +374 -0
- package/src/runner/abstractions.ts +146 -0
- package/src/runner/factory.ts +22 -0
- package/src/runner/index.ts +2 -0
- package/src/runner/local.ts +698 -0
- package/src/secret/abstractions.ts +59 -0
- package/src/secret/factory.ts +22 -0
- package/src/secret/index.ts +2 -0
- package/src/secret/local.ts +152 -0
- package/src/services.ts +133 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/library.ts +77 -0
- package/src/shared/operation.ts +85 -0
- package/src/shared/project.ts +62 -0
- package/src/shared/resolvers/graph-resolver.ts +111 -0
- package/src/shared/resolvers/input-hash.ts +77 -0
- package/src/shared/resolvers/input.ts +314 -0
- package/src/shared/resolvers/registry.ts +10 -0
- package/src/shared/resolvers/validation.ts +94 -0
- package/src/shared/state.ts +262 -0
- package/src/shared/terminal.ts +13 -0
- package/src/state/abstractions.ts +222 -0
- package/src/state/factory.ts +22 -0
- package/src/state/index.ts +3 -0
- package/src/state/local.ts +605 -0
- package/src/state/manager.ts +33 -0
- package/src/terminal/docker.ts +90 -0
- package/src/terminal/factory.ts +20 -0
- package/src/terminal/index.ts +3 -0
- package/src/terminal/manager.ts +330 -0
- package/src/terminal/run.sh.ts +37 -0
- package/src/terminal/shared.ts +50 -0
- package/src/workspace/abstractions.ts +41 -0
- package/src/workspace/factory.ts +14 -0
- package/src/workspace/index.ts +2 -0
- package/src/workspace/local.ts +54 -0
- package/dist/index.d.ts +0 -760
- package/dist/library/worker/main.mjs +0 -164
- package/dist/runner/source-resolution-worker.mjs +0 -22
- package/dist/shared/index.d.ts +0 -85
- package/dist/shared/index.mjs +0 -54
- package/dist/terminal-Cm2WqcyB.d.ts +0 -1589
@@ -1,25 +1,26 @@
|
|
1
1
|
import { z } from 'zod';
|
2
2
|
import { pickBy, mapKeys, mapValues, omit, pick } from 'remeda';
|
3
|
+
import { r as runWithRetryOnError, A as AbortError, s as stringArrayType, c as createAsyncBatcher, i as isAbortErrorLike, e as errorToString, a as isAbortError, t as tryWrapAbortErrorLike } from './utils-ByadNcv4.js';
|
3
4
|
import { BetterLock } from 'better-lock';
|
4
|
-
import { basename, relative,
|
5
|
-
import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
|
5
|
+
import { basename, relative, resolve as resolve$1, dirname } from 'node:path';
|
6
|
+
import { findWorkspaceDir, readPackageJSON, resolvePackageJSON } from 'pkg-types';
|
6
7
|
import { fileURLToPath } from 'node:url';
|
7
|
-
import { EventEmitter, on } from 'node:events';
|
8
|
+
import EventEmitter$1, { EventEmitter, on } from 'node:events';
|
8
9
|
import { Worker } from 'node:worker_threads';
|
10
|
+
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
|
11
|
+
import { isUnitModel, getInstanceId, parseInstanceId } from '@highstate/contract';
|
9
12
|
import Watcher from 'watcher';
|
10
13
|
import { resolve } from 'import-meta-resolve';
|
11
|
-
import {
|
12
|
-
import { getInstanceId, isUnitModel, parseInstanceId } from '@highstate/contract';
|
13
|
-
import { h as hubModelSchema, i as instanceModelSchema, c as createInputResolver, a as createInputHashResolver, b as createInstanceState, d as instanceTerminalSchema, e as instancePageSchema, f as instanceFileSchema, g as instanceStatusFieldSchema, j as instanceTriggerSchema, k as compositeInstanceSchema, p as projectOperationSchema, l as instanceStateSchema, m as isFinalOperationStatus, t as terminalSessionSchema, n as createInstanceStateFrontendPatch, o as applyPartialInstanceState } from './terminal-CqIsctlZ.mjs';
|
14
|
-
import { sha256 } from 'crypto-hash';
|
15
|
-
import 'ajv';
|
14
|
+
import { U as diffLibraries, e as hubModelSchema, b as instanceModelSchema, x as createInstanceState, M as createInputResolver, R as createInputHashResolver, r as instanceTerminalSchema, n as instancePageSchema, k as instanceFileSchema, g as instanceStatusFieldSchema, t as instanceTriggerSchema, G as projectOperationSchema, v as instanceStateSchema, H as isFinalOperationStatus, c as compositeInstanceSchema, T as terminalSessionSchema, y as applyPartialInstanceState, z as createInstanceStatePatch } from './library-BW5oPM7V.js';
|
16
15
|
import { Readable, PassThrough } from 'node:stream';
|
17
16
|
import { tmpdir, homedir } from 'node:os';
|
18
17
|
import spawn from 'nano-spawn';
|
19
|
-
import { randomUUID } from 'node:crypto';
|
20
18
|
import { ensureDependencyInstalled } from 'nypm';
|
19
|
+
import { sha256 } from 'crypto-hash';
|
21
20
|
import { uuidv7 } from 'uuidv7';
|
21
|
+
import { webcrypto } from 'node:crypto';
|
22
22
|
import { pino } from 'pino';
|
23
|
+
import 'ajv';
|
23
24
|
|
24
25
|
class SecretAccessDeniedError extends Error {
|
25
26
|
constructor(projectId, key) {
|
@@ -27,92 +28,6 @@ class SecretAccessDeniedError extends Error {
|
|
27
28
|
}
|
28
29
|
}
|
29
30
|
|
30
|
-
async function runWithRetryOnError(runner, tryHandleError, maxRetries = 1) {
|
31
|
-
let lastError;
|
32
|
-
for (let i = 0; i < maxRetries + 1; i++) {
|
33
|
-
try {
|
34
|
-
return await runner();
|
35
|
-
} catch (e) {
|
36
|
-
lastError = e;
|
37
|
-
if (await tryHandleError(e)) {
|
38
|
-
continue;
|
39
|
-
}
|
40
|
-
throw e;
|
41
|
-
}
|
42
|
-
}
|
43
|
-
throw lastError;
|
44
|
-
}
|
45
|
-
class AbortError extends Error {
|
46
|
-
constructor() {
|
47
|
-
super("Operation aborted");
|
48
|
-
}
|
49
|
-
}
|
50
|
-
function isAbortError(error) {
|
51
|
-
return error instanceof Error && error.name === "AbortError";
|
52
|
-
}
|
53
|
-
const stringArrayType = z.string().transform((args) => args.split(",").map((arg) => arg.trim()));
|
54
|
-
function errorToString(error) {
|
55
|
-
if (error instanceof Error) {
|
56
|
-
return error.stack || error.message;
|
57
|
-
}
|
58
|
-
return JSON.stringify(error);
|
59
|
-
}
|
60
|
-
function createAsyncBatcher(fn, { waitMs = 100, maxWaitTimeMs = 1e3 } = {}) {
|
61
|
-
let batch = [];
|
62
|
-
let activeTimeout = null;
|
63
|
-
let maxWaitTimeout = null;
|
64
|
-
let firstCallTimestamp = null;
|
65
|
-
async function processBatch() {
|
66
|
-
if (batch.length === 0) return;
|
67
|
-
const currentBatch = batch;
|
68
|
-
batch = [];
|
69
|
-
await fn(currentBatch);
|
70
|
-
if (maxWaitTimeout) {
|
71
|
-
clearTimeout(maxWaitTimeout);
|
72
|
-
maxWaitTimeout = null;
|
73
|
-
}
|
74
|
-
firstCallTimestamp = null;
|
75
|
-
}
|
76
|
-
function schedule() {
|
77
|
-
if (activeTimeout) clearTimeout(activeTimeout);
|
78
|
-
activeTimeout = setTimeout(() => {
|
79
|
-
activeTimeout = null;
|
80
|
-
void processBatch();
|
81
|
-
}, waitMs);
|
82
|
-
if (!firstCallTimestamp) {
|
83
|
-
firstCallTimestamp = Date.now();
|
84
|
-
maxWaitTimeout = setTimeout(() => {
|
85
|
-
if (activeTimeout) clearTimeout(activeTimeout);
|
86
|
-
activeTimeout = null;
|
87
|
-
void processBatch();
|
88
|
-
}, maxWaitTimeMs);
|
89
|
-
}
|
90
|
-
}
|
91
|
-
return {
|
92
|
-
/**
|
93
|
-
* Add an item to the batch.
|
94
|
-
*/
|
95
|
-
call(item) {
|
96
|
-
batch.push(item);
|
97
|
-
schedule();
|
98
|
-
},
|
99
|
-
/**
|
100
|
-
* Immediately flush the pending batch (if any).
|
101
|
-
*/
|
102
|
-
async flush() {
|
103
|
-
if (activeTimeout) {
|
104
|
-
clearTimeout(activeTimeout);
|
105
|
-
activeTimeout = null;
|
106
|
-
}
|
107
|
-
if (maxWaitTimeout) {
|
108
|
-
clearTimeout(maxWaitTimeout);
|
109
|
-
maxWaitTimeout = null;
|
110
|
-
}
|
111
|
-
await processBatch();
|
112
|
-
}
|
113
|
-
};
|
114
|
-
}
|
115
|
-
|
116
31
|
class LocalPulumiHost {
|
117
32
|
constructor(logger) {
|
118
33
|
this.logger = logger;
|
@@ -128,7 +43,7 @@ class LocalPulumiHost {
|
|
128
43
|
return null;
|
129
44
|
}
|
130
45
|
}
|
131
|
-
async runEmpty(options, fn) {
|
46
|
+
async runEmpty(options, fn, signal) {
|
132
47
|
const { projectId, pulumiProjectName, pulumiStackName, envVars } = options;
|
133
48
|
return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
134
49
|
const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
|
@@ -150,6 +65,7 @@ class LocalPulumiHost {
|
|
150
65
|
}
|
151
66
|
}
|
152
67
|
);
|
68
|
+
signal?.throwIfAborted();
|
153
69
|
try {
|
154
70
|
return await runWithRetryOnError(
|
155
71
|
() => fn(stack),
|
@@ -163,7 +79,7 @@ class LocalPulumiHost {
|
|
163
79
|
}
|
164
80
|
});
|
165
81
|
}
|
166
|
-
async runLocal(options, fn) {
|
82
|
+
async runLocal(options, fn, signal) {
|
167
83
|
const { projectId, pulumiProjectName, pulumiStackName, projectPath, stackConfig, envVars } = options;
|
168
84
|
return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
169
85
|
const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
|
@@ -189,6 +105,7 @@ class LocalPulumiHost {
|
|
189
105
|
}
|
190
106
|
}
|
191
107
|
);
|
108
|
+
signal?.throwIfAborted();
|
192
109
|
try {
|
193
110
|
return await runWithRetryOnError(
|
194
111
|
() => fn(stack),
|
@@ -295,7 +212,7 @@ class LocalSecretBackend {
|
|
295
212
|
isLocked(projectId) {
|
296
213
|
return Promise.resolve(!this.pulumiProjectHost.hasPassword(projectId));
|
297
214
|
}
|
298
|
-
async unlock(projectId, password) {
|
215
|
+
async unlock(projectId, password, signal) {
|
299
216
|
this.pulumiProjectHost.setPassword(projectId, password);
|
300
217
|
try {
|
301
218
|
await this.pulumiProjectHost.runLocal(
|
@@ -308,7 +225,8 @@ class LocalSecretBackend {
|
|
308
225
|
async (stack) => {
|
309
226
|
this.logger.debug({ projectId }, "checking password");
|
310
227
|
await stack.info(true);
|
311
|
-
}
|
228
|
+
},
|
229
|
+
signal
|
312
230
|
);
|
313
231
|
return true;
|
314
232
|
} catch (error) {
|
@@ -322,7 +240,7 @@ class LocalSecretBackend {
|
|
322
240
|
throw error;
|
323
241
|
}
|
324
242
|
}
|
325
|
-
get(projectId, instanceId) {
|
243
|
+
get(projectId, instanceId, signal) {
|
326
244
|
return this.pulumiProjectHost.runLocal(
|
327
245
|
{
|
328
246
|
projectId,
|
@@ -333,14 +251,16 @@ class LocalSecretBackend {
|
|
333
251
|
async (stack) => {
|
334
252
|
this.logger.debug({ projectId, instanceId }, "getting secrets");
|
335
253
|
const config = await stack.getAllConfig();
|
254
|
+
signal?.throwIfAborted();
|
336
255
|
const prefix = this.getPrefix(projectId, instanceId);
|
337
256
|
const secrets = pickBy(config, (_, key) => key.startsWith(prefix));
|
338
257
|
const trimmedSecrets = mapKeys(secrets, (key) => key.slice(prefix.length));
|
339
258
|
return mapValues(trimmedSecrets, (value) => stringToValue(value.value));
|
340
|
-
}
|
259
|
+
},
|
260
|
+
signal
|
341
261
|
);
|
342
262
|
}
|
343
|
-
set(projectId, instanceId, values) {
|
263
|
+
set(projectId, instanceId, values, signal) {
|
344
264
|
return this.pulumiProjectHost.runLocal(
|
345
265
|
{
|
346
266
|
projectId,
|
@@ -358,9 +278,11 @@ class LocalSecretBackend {
|
|
358
278
|
})
|
359
279
|
);
|
360
280
|
const config = await stack.getAllConfig();
|
281
|
+
signal?.throwIfAborted();
|
361
282
|
Object.assign(config, componentSecrets);
|
362
283
|
await stack.setAllConfig(config);
|
363
|
-
}
|
284
|
+
},
|
285
|
+
signal
|
364
286
|
);
|
365
287
|
}
|
366
288
|
getPrefix(projectId, instanceId) {
|
@@ -394,25 +316,41 @@ function createSecretBackend(config, localPulumiHost, logger) {
|
|
394
316
|
}
|
395
317
|
|
396
318
|
const localLibraryBackendConfig = z.object({
|
397
|
-
HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES: stringArrayType.default("@highstate/library")
|
319
|
+
HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES: stringArrayType.default("@highstate/library"),
|
320
|
+
HIGHSTATE_BACKEND_LIBRARY_LOCAL_SOURCE_BASE_PATH: z.string().optional(),
|
321
|
+
HIGHSTATE_BACKEND_LIBRARY_LOCAL_EXTRA_SOURCE_WATCH_PATHS: stringArrayType.default("")
|
398
322
|
});
|
399
323
|
class LocalLibraryBackend {
|
400
|
-
constructor(modulePaths, logger) {
|
324
|
+
constructor(modulePaths, sourceBasePath, extraSourceWatchPaths, logger) {
|
401
325
|
this.modulePaths = modulePaths;
|
326
|
+
this.sourceBasePath = sourceBasePath;
|
402
327
|
this.logger = logger;
|
403
|
-
this.
|
404
|
-
this.
|
328
|
+
this.libraryWatcher = new Watcher(modulePaths, { recursive: true, ignoreInitial: true });
|
329
|
+
this.libraryWatcher.on("all", (event, path) => {
|
405
330
|
const prefixPath = modulePaths.find((modulePath) => path.startsWith(modulePath));
|
406
331
|
this.logger.info({ msg: "library event", event, path: relative(prefixPath, path) });
|
407
332
|
void this.lock.acquire(() => this.updateLibrary());
|
408
333
|
});
|
334
|
+
this.sourceWatcher = new Watcher([sourceBasePath, ...extraSourceWatchPaths], {
|
335
|
+
recursive: true,
|
336
|
+
ignoreInitial: true,
|
337
|
+
ignore: /\.git|node_modules/
|
338
|
+
});
|
339
|
+
this.sourceWatcher.on("all", (_, path) => {
|
340
|
+
if (!path.endsWith("highstate.manifest.json")) {
|
341
|
+
return;
|
342
|
+
}
|
343
|
+
void this.updateUnitSourceHashes(path);
|
344
|
+
});
|
409
345
|
this.logger.debug({ msg: "initialized", modulePaths });
|
410
346
|
}
|
411
|
-
|
347
|
+
libraryWatcher;
|
348
|
+
sourceWatcher;
|
412
349
|
lock = new BetterLock();
|
413
350
|
eventEmitter = new EventEmitter();
|
414
351
|
library = null;
|
415
352
|
worker = null;
|
353
|
+
resolvedUnitSources = /* @__PURE__ */ new Map();
|
416
354
|
async loadLibrary() {
|
417
355
|
return await this.lock.acquire(async () => {
|
418
356
|
const [library] = await this.getLibrary();
|
@@ -424,36 +362,130 @@ class LocalLibraryBackend {
|
|
424
362
|
yield library;
|
425
363
|
}
|
426
364
|
}
|
427
|
-
async
|
365
|
+
async getResolvedUnitSources() {
|
366
|
+
return await this.lock.acquire(async () => {
|
367
|
+
const [library] = await this.getLibrary();
|
368
|
+
if (!this.resolvedUnitSources.size) {
|
369
|
+
await this.syncUnitSources(library);
|
370
|
+
}
|
371
|
+
return Array.from(this.resolvedUnitSources.values());
|
372
|
+
});
|
373
|
+
}
|
374
|
+
async getResolvedUnitSource(unitType) {
|
375
|
+
return await this.lock.acquire(async () => {
|
376
|
+
const [library] = await this.getLibrary();
|
377
|
+
if (!this.resolvedUnitSources.size) {
|
378
|
+
await this.syncUnitSources(library);
|
379
|
+
}
|
380
|
+
return this.resolvedUnitSources.get(unitType) ?? null;
|
381
|
+
});
|
382
|
+
}
|
383
|
+
async *watchResolvedUnitSources(signal) {
|
384
|
+
for await (const [resolvedUnitSource] of on(this.eventEmitter, "resolvedUnitSource", {
|
385
|
+
signal
|
386
|
+
})) {
|
387
|
+
yield resolvedUnitSource;
|
388
|
+
}
|
389
|
+
}
|
390
|
+
async syncUnitSources(library) {
|
391
|
+
const unitsToResolve = /* @__PURE__ */ new Map();
|
392
|
+
for (const component of Object.values(library.components)) {
|
393
|
+
if (!isUnitModel(component)) {
|
394
|
+
continue;
|
395
|
+
}
|
396
|
+
const existingResolvedSource = this.resolvedUnitSources.get(component.type);
|
397
|
+
const expectedSource = JSON.stringify(component.source);
|
398
|
+
if (existingResolvedSource?.serializedSource !== expectedSource) {
|
399
|
+
unitsToResolve.set(component.type, component);
|
400
|
+
}
|
401
|
+
}
|
402
|
+
await this.runSourceResolution(unitsToResolve);
|
403
|
+
}
|
404
|
+
async runSourceResolution(units) {
|
405
|
+
const workerPathUrl = resolve(
|
406
|
+
`@highstate/backend/source-resolution-worker`,
|
407
|
+
import.meta.url
|
408
|
+
);
|
409
|
+
const workerPath = fileURLToPath(workerPathUrl);
|
410
|
+
const worker = new Worker(workerPath, {
|
411
|
+
workerData: {
|
412
|
+
requests: Array.from(units.values()).map((unit) => ({
|
413
|
+
unitType: unit.type,
|
414
|
+
source: unit.source
|
415
|
+
})),
|
416
|
+
sourceBasePath: this.sourceBasePath,
|
417
|
+
logLevel: "error"
|
418
|
+
}
|
419
|
+
});
|
420
|
+
for await (const [event] of on(worker, "message")) {
|
421
|
+
const eventData = event;
|
422
|
+
if (eventData.type !== "result") {
|
423
|
+
throw new Error(`Unexpected message type '${eventData.type}', expected 'result'`);
|
424
|
+
}
|
425
|
+
for (const result of eventData.results) {
|
426
|
+
const unit = units.get(result.unitType);
|
427
|
+
if (!unit) {
|
428
|
+
this.logger.warn("unit not found for resolved source", { unitType: result.unitType });
|
429
|
+
continue;
|
430
|
+
}
|
431
|
+
const resolvedSource = {
|
432
|
+
unitType: result.unitType,
|
433
|
+
serializedSource: JSON.stringify(unit.source),
|
434
|
+
projectPath: result.projectPath,
|
435
|
+
packageJsonPath: result.packageJsonPath,
|
436
|
+
allowedDependencies: result.allowedDependencies,
|
437
|
+
sourceHash: result.sourceHash
|
438
|
+
};
|
439
|
+
this.resolvedUnitSources.set(result.unitType, resolvedSource);
|
440
|
+
this.eventEmitter.emit("resolvedUnitSource", resolvedSource);
|
441
|
+
}
|
442
|
+
this.logger.info("unit sources synced");
|
443
|
+
return;
|
444
|
+
}
|
445
|
+
throw new Error("Worker ended without sending the result");
|
446
|
+
}
|
447
|
+
async evaluateCompositeInstances(allInstances, resolvedInputs, instanceIds) {
|
428
448
|
return await this.lock.acquire(async () => {
|
429
|
-
this.logger.info("evaluating composite instances",
|
449
|
+
this.logger.info("evaluating %d composite instances", instanceIds.length);
|
430
450
|
const [, worker] = await this.getLibrary();
|
431
|
-
worker.postMessage({
|
451
|
+
worker.postMessage({
|
452
|
+
type: "evaluate-composite-instances",
|
453
|
+
allInstances,
|
454
|
+
resolvedInputs,
|
455
|
+
instanceIds
|
456
|
+
});
|
432
457
|
this.logger.debug("evaluation request sent");
|
433
|
-
|
458
|
+
const { results } = await this.getResult(worker, "instance-evaluation-results");
|
459
|
+
return results;
|
434
460
|
});
|
435
461
|
}
|
436
462
|
async evaluateModules(modulePaths) {
|
437
463
|
return await this.lock.acquire(async () => {
|
438
464
|
this.logger.info({ msg: "evaluating modules", modulePaths });
|
439
465
|
const [, worker] = await this.getLibrary();
|
440
|
-
worker.postMessage({
|
466
|
+
worker.postMessage({
|
467
|
+
type: "evaluate-modules",
|
468
|
+
modulePaths
|
469
|
+
});
|
441
470
|
this.logger.debug("evaluation request sent");
|
442
|
-
|
471
|
+
const { result } = await this.getResult(worker, "module-evaluation-result");
|
472
|
+
return result;
|
443
473
|
});
|
444
474
|
}
|
445
|
-
async
|
475
|
+
async getResult(worker, expectedType) {
|
446
476
|
for await (const [event] of on(worker, "message")) {
|
447
477
|
const eventData = event;
|
448
478
|
if (eventData.type === "error") {
|
449
479
|
throw new Error(`Worker error: ${eventData.error}`);
|
450
480
|
}
|
451
|
-
if (eventData.type !==
|
452
|
-
throw new Error(
|
481
|
+
if (eventData.type !== expectedType) {
|
482
|
+
throw new Error(
|
483
|
+
`Unexpected response message type "${eventData.type}", expected "${expectedType}"`
|
484
|
+
);
|
453
485
|
}
|
454
|
-
return eventData
|
486
|
+
return eventData;
|
455
487
|
}
|
456
|
-
throw new Error("Worker ended without sending
|
488
|
+
throw new Error("Worker ended without sending any response");
|
457
489
|
}
|
458
490
|
async getLibrary() {
|
459
491
|
if (this.library && this.worker) {
|
@@ -472,9 +504,14 @@ class LocalLibraryBackend {
|
|
472
504
|
if (eventData.type !== "library") {
|
473
505
|
throw new Error(`Unexpected message type '${eventData.type}', expected 'library'`);
|
474
506
|
}
|
475
|
-
|
507
|
+
const updates = diffLibraries(
|
508
|
+
this.library ?? { components: {}, entities: {} },
|
509
|
+
eventData.library
|
510
|
+
);
|
511
|
+
this.eventEmitter.emit("library", updates);
|
476
512
|
this.library = eventData.library;
|
477
513
|
this.logger.info("library reloaded");
|
514
|
+
await this.syncUnitSources(eventData.library);
|
478
515
|
return [this.library, this.worker];
|
479
516
|
}
|
480
517
|
throw new Error("Worker ended without sending library model");
|
@@ -484,7 +521,49 @@ class LocalLibraryBackend {
|
|
484
521
|
const workerPath = fileURLToPath(workerPathUrl);
|
485
522
|
return new Worker(workerPath, { workerData });
|
486
523
|
}
|
487
|
-
|
524
|
+
async updateUnitSourceHashes(path) {
|
525
|
+
const packageJsonPath = await resolvePackageJSON(path);
|
526
|
+
const packageJson = await readPackageJSON(path);
|
527
|
+
const library = await this.loadLibrary();
|
528
|
+
const manifestPath = resolve$1(dirname(packageJsonPath), "dist", "highstate.manifest.json");
|
529
|
+
let manifest;
|
530
|
+
try {
|
531
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
532
|
+
} catch (error) {
|
533
|
+
this.logger.debug(
|
534
|
+
{ error },
|
535
|
+
`failed to read highstate manifest for package "%s"`,
|
536
|
+
packageJson.name
|
537
|
+
);
|
538
|
+
}
|
539
|
+
for (const unit of Object.values(library.components)) {
|
540
|
+
if (!isUnitModel(unit)) {
|
541
|
+
continue;
|
542
|
+
}
|
543
|
+
if (unit.source.package !== packageJson.name) {
|
544
|
+
continue;
|
545
|
+
}
|
546
|
+
const relativePath = unit.source.path ? `./dist/${unit.source.path}/index.js` : `./dist/index.js`;
|
547
|
+
const sourceHash = manifest?.sourceHashes?.[relativePath];
|
548
|
+
if (!sourceHash) {
|
549
|
+
this.logger.warn(`source hash not found for unit: "%s"`, unit.type);
|
550
|
+
continue;
|
551
|
+
}
|
552
|
+
const resolvedSource = this.resolvedUnitSources.get(unit.type);
|
553
|
+
if (!resolvedSource) {
|
554
|
+
this.logger.warn(`resolved source not found for unit: "%s"`, unit.type);
|
555
|
+
continue;
|
556
|
+
}
|
557
|
+
const newResolvedSource = {
|
558
|
+
...resolvedSource,
|
559
|
+
sourceHash
|
560
|
+
};
|
561
|
+
this.resolvedUnitSources.set(unit.type, newResolvedSource);
|
562
|
+
this.eventEmitter.emit("resolvedUnitSource", newResolvedSource);
|
563
|
+
this.logger.info(`updated source hash for unit: "%s"`, unit.type);
|
564
|
+
}
|
565
|
+
}
|
566
|
+
static async create(config, logger) {
|
488
567
|
const modulePaths = [];
|
489
568
|
for (const module of config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES) {
|
490
569
|
const url = resolve(module, import.meta.url);
|
@@ -494,8 +573,17 @@ class LocalLibraryBackend {
|
|
494
573
|
}
|
495
574
|
modulePaths.push(path);
|
496
575
|
}
|
576
|
+
let sourceBasePath = config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_SOURCE_BASE_PATH;
|
577
|
+
const extraSourceWatchPaths = config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_EXTRA_SOURCE_WATCH_PATHS;
|
578
|
+
if (!sourceBasePath) {
|
579
|
+
const [projectPath] = await resolveMainLocalProject();
|
580
|
+
sourceBasePath = resolve$1(projectPath, "units");
|
581
|
+
extraSourceWatchPaths.push(projectPath);
|
582
|
+
}
|
497
583
|
return new LocalLibraryBackend(
|
498
584
|
modulePaths,
|
585
|
+
sourceBasePath,
|
586
|
+
extraSourceWatchPaths,
|
499
587
|
logger.child({ backend: "LibraryBackend", service: "LocalLibraryBackend" })
|
500
588
|
);
|
501
589
|
}
|
@@ -505,10 +593,10 @@ const libraryBackendConfig = z.object({
|
|
505
593
|
HIGHSTATE_BACKEND_LIBRARY_TYPE: z.enum(["local"]).default("local"),
|
506
594
|
...localLibraryBackendConfig.shape
|
507
595
|
});
|
508
|
-
function createLibraryBackend(config, logger) {
|
596
|
+
async function createLibraryBackend(config, logger) {
|
509
597
|
switch (config.HIGHSTATE_BACKEND_LIBRARY_TYPE) {
|
510
598
|
case "local": {
|
511
|
-
return LocalLibraryBackend.create(config, logger);
|
599
|
+
return await LocalLibraryBackend.create(config, logger);
|
512
600
|
}
|
513
601
|
}
|
514
602
|
}
|
@@ -524,7 +612,7 @@ class LocalProjectBackend {
|
|
524
612
|
constructor(projectsDir) {
|
525
613
|
this.projectsDir = projectsDir;
|
526
614
|
}
|
527
|
-
async
|
615
|
+
async getProjectIds() {
|
528
616
|
try {
|
529
617
|
const files = await readdir(this.projectsDir);
|
530
618
|
return files.filter((file) => file.endsWith(".json")).map((file) => file.replace(/\.json$/, ""));
|
@@ -825,6 +913,14 @@ class ProjectLock {
|
|
825
913
|
canImmediatelyAcquireLock(instanceId) {
|
826
914
|
return this.lock.canAcquire(`${this.projectId}/${instanceId}`);
|
827
915
|
}
|
916
|
+
canImmediatelyAcquireLocks(instanceIds) {
|
917
|
+
for (const instanceId of instanceIds) {
|
918
|
+
if (!this.canImmediatelyAcquireLock(instanceId)) {
|
919
|
+
return false;
|
920
|
+
}
|
921
|
+
}
|
922
|
+
return true;
|
923
|
+
}
|
828
924
|
lockInstance(instanceId, fn) {
|
829
925
|
return this.lock.acquire(`${this.projectId}/${instanceId}`, fn);
|
830
926
|
}
|
@@ -843,52 +939,36 @@ class ProjectLockManager {
|
|
843
939
|
}
|
844
940
|
|
845
941
|
class ProjectManager {
|
846
|
-
constructor(projectBackend, stateBackend,
|
942
|
+
constructor(projectBackend, stateBackend, libraryBackend, projectLockManager, stateManager, logger) {
|
847
943
|
this.projectBackend = projectBackend;
|
848
944
|
this.stateBackend = stateBackend;
|
849
|
-
this.
|
850
|
-
this.
|
945
|
+
this.libraryBackend = libraryBackend;
|
946
|
+
this.projectLockManager = projectLockManager;
|
947
|
+
this.stateManager = stateManager;
|
851
948
|
this.logger = logger;
|
949
|
+
void this.watchLibraryChanges();
|
852
950
|
}
|
853
|
-
|
854
|
-
|
855
|
-
const [
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
if (compositeInstance && compositeInstance.inputHash === actualInputHash) {
|
860
|
-
return compositeInstance;
|
951
|
+
compositeInstanceEE = new EventEmitter();
|
952
|
+
async *watchCompositeInstances(projectId, signal) {
|
953
|
+
for await (const [children] of on(this.compositeInstanceEE, projectId, {
|
954
|
+
signal
|
955
|
+
})) {
|
956
|
+
yield children;
|
861
957
|
}
|
862
|
-
this.logger.info("re-evaluating instance since input hash has changed", {
|
863
|
-
projectId,
|
864
|
-
instanceId
|
865
|
-
});
|
866
|
-
const instancePromise = this.waitForCompositeInstance(
|
867
|
-
projectId,
|
868
|
-
instanceId,
|
869
|
-
actualInputHash,
|
870
|
-
resolveInputHash
|
871
|
-
);
|
872
|
-
await this.operationManager.launch({
|
873
|
-
type: "evaluate",
|
874
|
-
projectId,
|
875
|
-
instanceIds: [instanceId]
|
876
|
-
});
|
877
|
-
return await instancePromise;
|
878
958
|
}
|
879
959
|
async createInstance(projectId, instance) {
|
880
960
|
const createdInstance = await this.projectBackend.createInstance(projectId, instance);
|
881
|
-
await this.
|
961
|
+
await this.updateCompositeInstance(projectId, createdInstance);
|
882
962
|
return createdInstance;
|
883
963
|
}
|
884
964
|
async updateInstance(projectId, instanceId, patch) {
|
885
965
|
const instance = await this.projectBackend.updateInstance(projectId, instanceId, patch);
|
886
|
-
await this.
|
966
|
+
await this.updateCompositeInstance(projectId, instance);
|
887
967
|
return instance;
|
888
968
|
}
|
889
969
|
async renameInstance(projectId, instanceId, newName) {
|
890
970
|
const instance = await this.projectBackend.renameInstance(projectId, instanceId, newName);
|
891
|
-
await this.
|
971
|
+
await this.updateCompositeInstance(projectId, instance);
|
892
972
|
return instance;
|
893
973
|
}
|
894
974
|
async deleteInstance(projectId, instanceId) {
|
@@ -897,11 +977,8 @@ class ProjectManager {
|
|
897
977
|
this.stateBackend.clearCompositeInstances(projectId, [instanceId])
|
898
978
|
]);
|
899
979
|
}
|
900
|
-
async
|
901
|
-
const { resolveInputHash, library } = await this.prepareInputHashResolver(
|
902
|
-
projectId,
|
903
|
-
instance.id
|
904
|
-
);
|
980
|
+
async updateCompositeInstance(projectId, instance) {
|
981
|
+
const { resolveInputHash, library } = await this.prepareInputHashResolver(projectId);
|
905
982
|
const component = library.components[instance.type];
|
906
983
|
if (!component) {
|
907
984
|
return;
|
@@ -909,87 +986,205 @@ class ProjectManager {
|
|
909
986
|
if (isUnitModel(component)) {
|
910
987
|
return;
|
911
988
|
}
|
912
|
-
const expectedInputHash = await resolveInputHash(instance.id);
|
989
|
+
const { inputHash: expectedInputHash } = await resolveInputHash(instance.id);
|
913
990
|
const inputHash = await this.stateBackend.getCompositeInstanceInputHash(projectId, instance.id);
|
914
991
|
if (inputHash !== expectedInputHash) {
|
915
992
|
this.logger.info("re-evaluating instance since input hash has changed", {
|
916
993
|
projectId,
|
917
994
|
instanceId: instance.id
|
918
995
|
});
|
919
|
-
await this.
|
920
|
-
type: "evaluate",
|
921
|
-
projectId,
|
922
|
-
instanceIds: [instance.id]
|
923
|
-
});
|
996
|
+
await this.evaluateCompositeInstances(projectId, [instance.id]);
|
924
997
|
}
|
925
998
|
}
|
926
|
-
async
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
999
|
+
async evaluateCompositeInstances(projectId, instanceIds) {
|
1000
|
+
await this.projectLockManager.getLock(projectId).lockInstances(instanceIds, async () => {
|
1001
|
+
const [
|
1002
|
+
{ instances, resolvedInputs, stateMap, resolveInputHash },
|
1003
|
+
topLevelCompositeChildrenIds
|
1004
|
+
] = await Promise.all([
|
1005
|
+
this.prepareInputHashResolver(projectId),
|
1006
|
+
this.stateBackend.getTopLevelCompositeChildrenIds(projectId, instanceIds)
|
1007
|
+
]);
|
1008
|
+
const results = await this.libraryBackend.evaluateCompositeInstances(
|
1009
|
+
instances,
|
1010
|
+
resolvedInputs,
|
1011
|
+
instanceIds
|
1012
|
+
);
|
1013
|
+
const newStates = results.map((result) => {
|
1014
|
+
const existingState = stateMap.get(result.instanceId);
|
1015
|
+
const newState = existingState ?? createInstanceState(result.instanceId);
|
1016
|
+
newState.evaluationError = result.success ? null : result.error;
|
1017
|
+
return newState;
|
1018
|
+
});
|
1019
|
+
const inputHashes = /* @__PURE__ */ new Map();
|
1020
|
+
for (const instanceId of instanceIds) {
|
1021
|
+
const { inputHash } = await resolveInputHash(instanceId);
|
1022
|
+
inputHashes.set(instanceId, inputHash);
|
1023
|
+
}
|
1024
|
+
const compositeInstances = results.filter((result) => result.success).flatMap(
|
1025
|
+
(result) => result.compositeInstances.map((instance) => ({
|
1026
|
+
...instance,
|
1027
|
+
inputHash: inputHashes.get(instance.instance.id)
|
1028
|
+
}))
|
1029
|
+
);
|
1030
|
+
const newTopLevelCompositeChildrenIds = Object.fromEntries(
|
1031
|
+
results.filter((result) => result.success).map((result) => [
|
1032
|
+
result.instanceId,
|
1033
|
+
result.compositeInstances.filter((instance) => instance.instance.id !== result.instanceId).map((instance) => instance.instance.id)
|
1034
|
+
])
|
1035
|
+
);
|
1036
|
+
const deletedCompositeInstanceIds = new Set(
|
1037
|
+
Object.values(topLevelCompositeChildrenIds).flat()
|
1038
|
+
);
|
1039
|
+
for (const childInstanceId of Object.values(newTopLevelCompositeChildrenIds).flat()) {
|
1040
|
+
deletedCompositeInstanceIds.delete(childInstanceId);
|
934
1041
|
}
|
935
|
-
|
936
|
-
|
937
|
-
|
1042
|
+
await Promise.all([
|
1043
|
+
this.stateBackend.clearCompositeInstances(
|
1044
|
+
projectId,
|
1045
|
+
Array.from(deletedCompositeInstanceIds)
|
1046
|
+
),
|
1047
|
+
this.stateBackend.putTopLevelCompositeChildrenIds(
|
1048
|
+
projectId,
|
1049
|
+
newTopLevelCompositeChildrenIds
|
1050
|
+
)
|
1051
|
+
]);
|
1052
|
+
for (const state of newStates) {
|
1053
|
+
this.stateManager.emitStatePatch(projectId, state);
|
1054
|
+
}
|
1055
|
+
for (const instance of compositeInstances) {
|
1056
|
+
this.compositeInstanceEE.emit(projectId, { type: "updated", instance });
|
1057
|
+
}
|
1058
|
+
for (const instanceId of deletedCompositeInstanceIds) {
|
1059
|
+
this.compositeInstanceEE.emit(projectId, { type: "deleted", instanceId });
|
1060
|
+
}
|
1061
|
+
const promises = [];
|
1062
|
+
if (newStates.length > 0) {
|
1063
|
+
promises.push(this.stateBackend.putInstanceStates(projectId, newStates));
|
1064
|
+
}
|
1065
|
+
if (compositeInstances.length > 0) {
|
1066
|
+
promises.push(this.stateBackend.putCompositeInstances(projectId, compositeInstances));
|
1067
|
+
}
|
1068
|
+
this.logger.info(
|
1069
|
+
{ projectId },
|
1070
|
+
"instance evaluation completed, %d instances persisted",
|
1071
|
+
compositeInstances.length
|
1072
|
+
);
|
1073
|
+
await Promise.all(promises);
|
1074
|
+
});
|
938
1075
|
}
|
939
|
-
async prepareInputHashResolver(projectId
|
1076
|
+
async prepareInputHashResolver(projectId) {
|
940
1077
|
const { instances, hubs } = await this.projectBackend.getProject(projectId);
|
941
|
-
const library = await this.
|
942
|
-
const filteredInstances = instances.filter((
|
943
|
-
const states = await this.stateBackend.
|
1078
|
+
const library = await this.libraryBackend.loadLibrary();
|
1079
|
+
const filteredInstances = instances.filter((instance) => instance.type in library.components);
|
1080
|
+
const states = await this.stateBackend.getAllInstanceStates(projectId);
|
944
1081
|
const stateMap = new Map(states.map((state) => [state.id, state]));
|
945
|
-
const instance = filteredInstances.find((instance2) => instance2.id === instanceId);
|
946
|
-
if (!instance) {
|
947
|
-
throw new Error(`Instance not found: ${instanceId}`);
|
948
|
-
}
|
949
1082
|
const inputResolverNodes = /* @__PURE__ */ new Map();
|
950
|
-
for (const
|
951
|
-
inputResolverNodes.set(`instance:${
|
1083
|
+
for (const instance of filteredInstances) {
|
1084
|
+
inputResolverNodes.set(`instance:${instance.id}`, {
|
952
1085
|
kind: "instance",
|
953
|
-
instance
|
954
|
-
component: library.components[
|
1086
|
+
instance,
|
1087
|
+
component: library.components[instance.type]
|
955
1088
|
});
|
956
1089
|
}
|
957
1090
|
for (const hub of hubs) {
|
958
1091
|
inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub });
|
959
1092
|
}
|
960
|
-
const resolveInputs = createInputResolver(
|
961
|
-
|
962
|
-
|
963
|
-
)
|
964
|
-
|
965
|
-
for (const instance2 of filteredInstances) {
|
966
|
-
const output = await resolveInputs(`instance:${instance2.id}`);
|
1093
|
+
const resolveInputs = createInputResolver(inputResolverNodes, this.logger);
|
1094
|
+
const inputHashInputs = /* @__PURE__ */ new Map();
|
1095
|
+
const resolvedInputs = {};
|
1096
|
+
for (const instance of filteredInstances) {
|
1097
|
+
const output = await resolveInputs(`instance:${instance.id}`);
|
967
1098
|
if (output.kind !== "instance") {
|
968
1099
|
throw new Error("Expected instance node");
|
969
1100
|
}
|
970
|
-
|
971
|
-
|
1101
|
+
let sourceHash;
|
1102
|
+
if (isUnitModel(library.components[instance.type])) {
|
1103
|
+
const resolvedUnit = await this.libraryBackend.getResolvedUnitSource(instance.type);
|
1104
|
+
if (!resolvedUnit) {
|
1105
|
+
throw new Error(`Resolved unit not found: ${instance.type}`);
|
1106
|
+
}
|
1107
|
+
sourceHash = resolvedUnit.sourceHash;
|
1108
|
+
}
|
1109
|
+
inputHashInputs.set(instance.id, {
|
1110
|
+
instance,
|
1111
|
+
component: library.components[instance.type],
|
972
1112
|
resolvedInputs: output.resolvedInputs,
|
973
|
-
state: stateMap.get(
|
974
|
-
sourceHash
|
975
|
-
// implement source hash
|
1113
|
+
state: stateMap.get(instance.id),
|
1114
|
+
sourceHash
|
976
1115
|
});
|
1116
|
+
resolvedInputs[instance.id] = output.resolvedInputs;
|
977
1117
|
}
|
978
|
-
const resolveInputHash = createInputHashResolver(
|
979
|
-
inputHashNodes,
|
980
|
-
this.logger.child({ resolver: "input-hash-resolver" })
|
981
|
-
);
|
1118
|
+
const resolveInputHash = createInputHashResolver(inputHashInputs, this.logger);
|
982
1119
|
return {
|
983
1120
|
resolveInputHash,
|
984
|
-
library
|
1121
|
+
library,
|
1122
|
+
instances,
|
1123
|
+
stateMap,
|
1124
|
+
resolvedInputs
|
985
1125
|
};
|
986
1126
|
}
|
987
|
-
|
1127
|
+
async watchLibraryChanges() {
|
1128
|
+
for await (const updates of this.libraryBackend.watchLibrary()) {
|
1129
|
+
try {
|
1130
|
+
await this.handleLibraryUpdates(updates);
|
1131
|
+
} catch (error) {
|
1132
|
+
this.logger.error({ error }, "failed to handle library updates");
|
1133
|
+
}
|
1134
|
+
}
|
1135
|
+
}
|
1136
|
+
async handleLibraryUpdates(updates) {
|
1137
|
+
const changedComponents = /* @__PURE__ */ new Set();
|
1138
|
+
const library = await this.libraryBackend.loadLibrary();
|
1139
|
+
for (const update of updates) {
|
1140
|
+
switch (update.type) {
|
1141
|
+
case "component-updated":
|
1142
|
+
changedComponents.add(update.component.type);
|
1143
|
+
break;
|
1144
|
+
case "component-removed":
|
1145
|
+
changedComponents.add(update.componentType);
|
1146
|
+
break;
|
1147
|
+
}
|
1148
|
+
}
|
1149
|
+
if (changedComponents.size === 0) {
|
1150
|
+
return;
|
1151
|
+
}
|
1152
|
+
this.logger.info(
|
1153
|
+
{ changedComponents },
|
1154
|
+
"library components changed, updating composite instances"
|
1155
|
+
);
|
1156
|
+
const projects = await this.projectBackend.getProjectIds();
|
1157
|
+
for (const projectId of projects) {
|
1158
|
+
const { resolveInputHash, instances } = await this.prepareInputHashResolver(projectId);
|
1159
|
+
const filteredInstances = instances.filter(
|
1160
|
+
(instance) => changedComponents.has(instance.type) && library.components[instance.type] && !isUnitModel(library.components[instance.type])
|
1161
|
+
);
|
1162
|
+
this.logger.info(
|
1163
|
+
{ projectId, filteredInstanceIds: filteredInstances.map((instance) => instance.id) },
|
1164
|
+
"updating composite instances for project"
|
1165
|
+
);
|
1166
|
+
const inputHashMap = /* @__PURE__ */ new Map();
|
1167
|
+
for (const instance of filteredInstances) {
|
1168
|
+
const { inputHash } = await resolveInputHash(instance.id);
|
1169
|
+
inputHashMap.set(instance.id, inputHash);
|
1170
|
+
}
|
1171
|
+
try {
|
1172
|
+
await this.evaluateCompositeInstances(
|
1173
|
+
projectId,
|
1174
|
+
filteredInstances.map((instance) => instance.id)
|
1175
|
+
);
|
1176
|
+
} catch (error) {
|
1177
|
+
this.logger.error({ error }, "failed to evaluate composite instances");
|
1178
|
+
}
|
1179
|
+
}
|
1180
|
+
}
|
1181
|
+
static create(projectBackend, stateBackend, libraryBackend, projectLockManager, stateManager, logger) {
|
988
1182
|
return new ProjectManager(
|
989
1183
|
projectBackend,
|
990
1184
|
stateBackend,
|
991
|
-
|
992
|
-
|
1185
|
+
libraryBackend,
|
1186
|
+
projectLockManager,
|
1187
|
+
stateManager,
|
993
1188
|
logger.child({ service: "ProjectManager" })
|
994
1189
|
);
|
995
1190
|
}
|
@@ -1002,6 +1197,8 @@ read -r data
|
|
1002
1197
|
envKeys=($(jq -r '.env | keys[]' <<<"$data"))
|
1003
1198
|
filesKeys=($(jq -r '.files | keys[]' <<<"$data"))
|
1004
1199
|
commandArr=($(jq -r '.command[]' <<<"$data"))
|
1200
|
+
cols=$(jq -r '.screenSize.cols' <<<"$data")
|
1201
|
+
rows=$(jq -r '.screenSize.rows' <<<"$data")
|
1005
1202
|
|
1006
1203
|
# Set environment variables
|
1007
1204
|
for key in "\${envKeys[@]}"; do
|
@@ -1026,20 +1223,24 @@ for key in "\${filesKeys[@]}"; do
|
|
1026
1223
|
fi
|
1027
1224
|
done
|
1028
1225
|
|
1029
|
-
# Execute the command, keeping stdin/stdout open
|
1030
|
-
|
1226
|
+
# Execute the command, keeping stdin/stdout open and spawnin a new TTY
|
1227
|
+
cmd=$(printf "%q " "\${commandArr[@]}")
|
1228
|
+
exec script -q -c "stty cols $cols rows $rows; $cmd" /dev/null
|
1229
|
+
`;
|
1031
1230
|
|
1032
1231
|
const dockerTerminalBackendConfig = z.object({
|
1033
1232
|
HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY: z.string().default("docker"),
|
1233
|
+
HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
|
1034
1234
|
HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST: z.string().optional()
|
1035
1235
|
});
|
1036
1236
|
class DockerTerminalBackend {
|
1037
|
-
constructor(binary, host, logger) {
|
1237
|
+
constructor(binary, useSudo, host, logger) {
|
1038
1238
|
this.binary = binary;
|
1239
|
+
this.useSudo = useSudo;
|
1039
1240
|
this.host = host;
|
1040
1241
|
this.logger = logger;
|
1041
1242
|
}
|
1042
|
-
async run({ factory, stdin, stdout, signal }) {
|
1243
|
+
async run({ factory, stdin, stdout, screenSize, signal }) {
|
1043
1244
|
const hsTempDir = resolve$1(tmpdir(), "highstate");
|
1044
1245
|
await mkdir(hsTempDir, { recursive: true });
|
1045
1246
|
const runScriptPath = resolve$1(hsTempDir, "run.sh");
|
@@ -1056,11 +1257,18 @@ class DockerTerminalBackend {
|
|
1056
1257
|
const initData = {
|
1057
1258
|
command: factory.command,
|
1058
1259
|
cwd: factory.cwd,
|
1059
|
-
env:
|
1060
|
-
|
1260
|
+
env: {
|
1261
|
+
...factory.env,
|
1262
|
+
TERM: "xterm-256color"
|
1263
|
+
},
|
1264
|
+
files: factory.files ?? {},
|
1265
|
+
screenSize
|
1061
1266
|
};
|
1062
1267
|
const initDataStream = Readable.from(JSON.stringify(initData) + "\n");
|
1063
|
-
|
1268
|
+
if (this.useSudo) {
|
1269
|
+
args.unshift(this.binary);
|
1270
|
+
}
|
1271
|
+
const process = spawn(this.useSudo ? "sudo" : this.binary, args, {
|
1064
1272
|
env: {
|
1065
1273
|
DOCKER_HOST: this.host
|
1066
1274
|
},
|
@@ -1071,12 +1279,13 @@ class DockerTerminalBackend {
|
|
1071
1279
|
initDataStream.on("end", () => stdin.pipe(childProcess.stdin));
|
1072
1280
|
childProcess.stdout.pipe(stdout);
|
1073
1281
|
childProcess.stderr.pipe(stdout);
|
1074
|
-
this.logger.info(
|
1282
|
+
this.logger.info({ pid: childProcess.pid }, "process started");
|
1075
1283
|
await process;
|
1076
1284
|
}
|
1077
1285
|
static create(config, logger) {
|
1078
1286
|
return new DockerTerminalBackend(
|
1079
1287
|
config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY,
|
1288
|
+
config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO,
|
1080
1289
|
config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST,
|
1081
1290
|
logger.child({ backend: "TerminalBackend", service: "DockerTerminalBackend" })
|
1082
1291
|
);
|
@@ -1095,39 +1304,95 @@ function createTerminalBackend(config, logger) {
|
|
1095
1304
|
}
|
1096
1305
|
}
|
1097
1306
|
|
1098
|
-
const
|
1307
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
1308
|
+
|
1309
|
+
const POOL_SIZE_MULTIPLIER = 128;
|
1310
|
+
let pool, poolOffset;
|
1311
|
+
function fillPool(bytes) {
|
1312
|
+
if (!pool || pool.length < bytes) {
|
1313
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
1314
|
+
webcrypto.getRandomValues(pool);
|
1315
|
+
poolOffset = 0;
|
1316
|
+
} else if (poolOffset + bytes > pool.length) {
|
1317
|
+
webcrypto.getRandomValues(pool);
|
1318
|
+
poolOffset = 0;
|
1319
|
+
}
|
1320
|
+
poolOffset += bytes;
|
1321
|
+
}
|
1322
|
+
function nanoid(size = 21) {
|
1323
|
+
fillPool(size |= 0);
|
1324
|
+
let id = "";
|
1325
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
1326
|
+
id += urlAlphabet[pool[i] & 63];
|
1327
|
+
}
|
1328
|
+
return id;
|
1329
|
+
}
|
1330
|
+
|
1331
|
+
const notAttachedTerminalLifetime = 5 * 60 * 1e3;
|
1099
1332
|
class TerminalManager {
|
1100
1333
|
constructor(terminalBackend, stateBackend, runnerBackend, logger) {
|
1101
1334
|
this.terminalBackend = terminalBackend;
|
1102
1335
|
this.stateBackend = stateBackend;
|
1103
1336
|
this.runnerBackend = runnerBackend;
|
1104
1337
|
this.logger = logger;
|
1338
|
+
void this.markFinishedSessions();
|
1105
1339
|
}
|
1106
1340
|
managedTerminals = /* @__PURE__ */ new Map();
|
1341
|
+
existingSessions = /* @__PURE__ */ new Map();
|
1342
|
+
terminalEE = new EventEmitter();
|
1343
|
+
isSessionActive(sessionId) {
|
1344
|
+
return this.managedTerminals.has(sessionId);
|
1345
|
+
}
|
1346
|
+
async *watchSession(projectId, instanceId, sessionId, signal) {
|
1347
|
+
const managedTerminal = this.managedTerminals.get(sessionId);
|
1348
|
+
if (!managedTerminal) {
|
1349
|
+
const session = await this.stateBackend.getTerminalSession(projectId, instanceId, sessionId);
|
1350
|
+
if (!session) {
|
1351
|
+
throw new Error(`Terminal session "${sessionId}" not found`);
|
1352
|
+
}
|
1353
|
+
yield session;
|
1354
|
+
return;
|
1355
|
+
}
|
1356
|
+
yield managedTerminal.session;
|
1357
|
+
for await (const [terminalSession] of on(this.terminalEE, sessionId, {
|
1358
|
+
signal
|
1359
|
+
})) {
|
1360
|
+
yield terminalSession;
|
1361
|
+
if (terminalSession.finishedAt) {
|
1362
|
+
return;
|
1363
|
+
}
|
1364
|
+
}
|
1365
|
+
}
|
1107
1366
|
async createSession(projectId, instanceId, terminalName) {
|
1108
|
-
const
|
1109
|
-
|
1367
|
+
const [instanceType, instanceName] = parseInstanceId(instanceId);
|
1368
|
+
const factory = await this.runnerBackend.getTerminalFactory(
|
1369
|
+
{ projectId, instanceType, instanceName },
|
1110
1370
|
terminalName
|
1371
|
+
);
|
1372
|
+
if (!factory) {
|
1373
|
+
throw new Error(
|
1374
|
+
`Terminal factory for instance "${instanceId}" with name "${terminalName}" not found`
|
1375
|
+
);
|
1376
|
+
}
|
1377
|
+
const terminalSession = {
|
1378
|
+
id: nanoid(),
|
1379
|
+
projectId,
|
1380
|
+
instanceId,
|
1381
|
+
terminalName,
|
1382
|
+
terminalTitle: factory.title,
|
1383
|
+
createdAt: /* @__PURE__ */ new Date()
|
1111
1384
|
};
|
1112
1385
|
await this.stateBackend.putTerminalSession(projectId, instanceId, terminalSession);
|
1113
1386
|
this.logger.info({ msg: "terminal session created", id: terminalSession.id });
|
1114
|
-
|
1387
|
+
this.createManagedTerminal(factory, terminalSession);
|
1115
1388
|
return terminalSession;
|
1116
1389
|
}
|
1117
|
-
async
|
1118
|
-
const
|
1119
|
-
|
1120
|
-
|
1121
|
-
this.logger.info({ msg: "reusing existing terminal session", id: session.id });
|
1122
|
-
} else {
|
1123
|
-
this.logger.info("creating new terminal session");
|
1124
|
-
session = await this.createSession(projectId, instanceId, terminalName);
|
1125
|
-
}
|
1126
|
-
if (!this.managedTerminals.has(session.id)) {
|
1127
|
-
this.logger.info({ msg: "no managed terminal found, creating new one", id: session.id });
|
1128
|
-
await this.createManagedTerminal(projectId, instanceId, session, false);
|
1390
|
+
async getOrCreateSession(projectId, instanceId, terminalName) {
|
1391
|
+
const existingSession = this.existingSessions.get(`${projectId}:${instanceId}.${terminalName}`);
|
1392
|
+
if (existingSession) {
|
1393
|
+
return existingSession;
|
1129
1394
|
}
|
1130
|
-
return
|
1395
|
+
return await this.createSession(projectId, instanceId, terminalName);
|
1131
1396
|
}
|
1132
1397
|
close(sessionId) {
|
1133
1398
|
this.logger.info({ msg: "closing terminal session", id: sessionId });
|
@@ -1136,95 +1401,117 @@ class TerminalManager {
|
|
1136
1401
|
managedTerminal.abortController.abort();
|
1137
1402
|
}
|
1138
1403
|
}
|
1139
|
-
|
1140
|
-
this.
|
1141
|
-
let terminal = this.managedTerminals.get(sessionId);
|
1404
|
+
attach(sessionId, stdin, stdout, screenSize, signal) {
|
1405
|
+
const terminal = this.managedTerminals.get(sessionId);
|
1142
1406
|
if (!terminal) {
|
1143
|
-
|
1144
|
-
const session = await this.stateBackend.getTerminalSession(projectId, instanceId, sessionId);
|
1145
|
-
if (!session) {
|
1146
|
-
throw new Error(`Terminal session "${sessionId}" not found`);
|
1147
|
-
}
|
1148
|
-
terminal = await this.createManagedTerminal(projectId, instanceId, session, false);
|
1149
|
-
terminal.history = await this.stateBackend.getTerminalSessionHistory(sessionId);
|
1150
|
-
}
|
1151
|
-
if (terminal.attached) {
|
1152
|
-
throw new Error(`Terminal session "${sessionId}" is already attached`);
|
1153
|
-
}
|
1154
|
-
terminal.attached = true;
|
1155
|
-
for (const line of terminal.history) {
|
1156
|
-
stdout.write(line);
|
1407
|
+
throw new Error(`Terminal session "${sessionId}" not found, check if it's still active`);
|
1157
1408
|
}
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1409
|
+
const handleStdin = (data) => {
|
1410
|
+
terminal.stdin.write(data);
|
1411
|
+
};
|
1412
|
+
const handleStdout = (data) => {
|
1413
|
+
stdout.write(data);
|
1414
|
+
};
|
1415
|
+
terminal.stdout.on("data", handleStdout);
|
1416
|
+
stdin.on("data", handleStdin);
|
1417
|
+
terminal.refCount += 1;
|
1418
|
+
this.logger.info(
|
1419
|
+
"terminal attached (sessionId: %s, refCount: %d)",
|
1420
|
+
sessionId,
|
1421
|
+
terminal.refCount
|
1422
|
+
);
|
1162
1423
|
signal.addEventListener("abort", () => {
|
1163
|
-
terminal.
|
1164
|
-
|
1424
|
+
terminal.refCount -= 1;
|
1425
|
+
terminal.stdout.off("data", handleStdout);
|
1426
|
+
stdin.off("data", handleStdin);
|
1427
|
+
this.logger.info(
|
1428
|
+
"terminal detached (sessionId: %s, refCount: %d)",
|
1429
|
+
sessionId,
|
1430
|
+
terminal.refCount
|
1431
|
+
);
|
1165
1432
|
});
|
1433
|
+
if (!terminal.started) {
|
1434
|
+
terminal.start(screenSize);
|
1435
|
+
terminal.started = true;
|
1436
|
+
}
|
1166
1437
|
}
|
1167
|
-
async
|
1168
|
-
|
1169
|
-
|
1170
|
-
{ projectId, instanceType, instanceName },
|
1171
|
-
terminalSession.terminalName
|
1438
|
+
async updateActiveTerminalSessions() {
|
1439
|
+
await this.stateBackend.putActiveTerminalSessions(
|
1440
|
+
Array.from(this.managedTerminals.values()).map((t) => t.session)
|
1172
1441
|
);
|
1173
|
-
|
1174
|
-
|
1175
|
-
`Terminal factory for instance "${instanceId}" with name "${terminalSession.terminalName}" not found`
|
1176
|
-
);
|
1177
|
-
}
|
1442
|
+
}
|
1443
|
+
createManagedTerminal(factory, terminalSession) {
|
1178
1444
|
const managedTerminal = {
|
1179
|
-
|
1445
|
+
session: terminalSession,
|
1180
1446
|
abortController: new AbortController(),
|
1181
|
-
attached: false,
|
1182
1447
|
stdin: new PassThrough(),
|
1183
1448
|
stdout: new PassThrough(),
|
1184
|
-
|
1449
|
+
refCount: 0,
|
1450
|
+
started: false,
|
1451
|
+
start: (screenSize) => {
|
1452
|
+
void this.terminalBackend.run({
|
1453
|
+
factory,
|
1454
|
+
stdin: managedTerminal.stdin,
|
1455
|
+
stdout: managedTerminal.stdout,
|
1456
|
+
screenSize,
|
1457
|
+
signal: managedTerminal.abortController.signal
|
1458
|
+
}).catch((error) => {
|
1459
|
+
if (isAbortErrorLike(error)) {
|
1460
|
+
return;
|
1461
|
+
}
|
1462
|
+
this.logger.error({
|
1463
|
+
msg: "managed terminal failed",
|
1464
|
+
id: managedTerminal.session.id,
|
1465
|
+
error
|
1466
|
+
});
|
1467
|
+
}).finally(() => {
|
1468
|
+
this.logger.info({ msg: "managed terminal closed", id: managedTerminal.session.id });
|
1469
|
+
this.managedTerminals.delete(managedTerminal.session.id);
|
1470
|
+
this.existingSessions.delete(
|
1471
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`
|
1472
|
+
);
|
1473
|
+
managedTerminal.session.finishedAt = /* @__PURE__ */ new Date();
|
1474
|
+
this.terminalEE.emit(managedTerminal.session.id, managedTerminal.session);
|
1475
|
+
void this.stateBackend.putTerminalSession(
|
1476
|
+
managedTerminal.session.projectId,
|
1477
|
+
managedTerminal.session.instanceId,
|
1478
|
+
managedTerminal.session
|
1479
|
+
);
|
1480
|
+
void this.updateActiveTerminalSessions();
|
1481
|
+
});
|
1482
|
+
setTimeout(
|
1483
|
+
() => this.closeTerminalIfNotAttached(managedTerminal),
|
1484
|
+
notAttachedTerminalLifetime
|
1485
|
+
);
|
1486
|
+
this.logger.info({ msg: "managed terminal created", id: managedTerminal.session.id });
|
1487
|
+
}
|
1185
1488
|
};
|
1186
1489
|
managedTerminal.stdout.on("data", (data) => {
|
1187
|
-
const
|
1188
|
-
|
1189
|
-
void this.persistHistory.call([terminalSession.id, line]);
|
1190
|
-
});
|
1191
|
-
this.managedTerminals.set(managedTerminal.sessionId, managedTerminal);
|
1192
|
-
void this.terminalBackend.run({
|
1193
|
-
factory: {
|
1194
|
-
...factory,
|
1195
|
-
env: {
|
1196
|
-
...factory.env,
|
1197
|
-
HIGHSTATE_TERMINAL_FIRST_LAUNCH: firstLaunch ? "1" : "0"
|
1198
|
-
}
|
1199
|
-
},
|
1200
|
-
stdin: managedTerminal.stdin,
|
1201
|
-
stdout: managedTerminal.stdout,
|
1202
|
-
signal: managedTerminal.abortController.signal
|
1203
|
-
}).catch((error) => {
|
1204
|
-
this.logger.error({
|
1205
|
-
msg: "managed terminal failed",
|
1206
|
-
id: managedTerminal.sessionId,
|
1207
|
-
error
|
1208
|
-
});
|
1209
|
-
}).finally(() => {
|
1210
|
-
this.logger.info({ msg: "managed terminal closed", id: managedTerminal.sessionId });
|
1211
|
-
this.managedTerminals.delete(managedTerminal.sessionId);
|
1490
|
+
const entry = String(data);
|
1491
|
+
void this.persistHistory.call([terminalSession.id, entry]);
|
1212
1492
|
});
|
1213
|
-
|
1214
|
-
this.
|
1493
|
+
this.managedTerminals.set(managedTerminal.session.id, managedTerminal);
|
1494
|
+
this.existingSessions.set(
|
1495
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`,
|
1496
|
+
managedTerminal.session
|
1497
|
+
);
|
1498
|
+
void this.updateActiveTerminalSessions();
|
1215
1499
|
return managedTerminal;
|
1216
1500
|
}
|
1217
1501
|
closeTerminalIfNotAttached(terminal) {
|
1218
|
-
if (!this.managedTerminals.has(terminal.
|
1502
|
+
if (!this.managedTerminals.has(terminal.session.id)) {
|
1219
1503
|
return;
|
1220
1504
|
}
|
1221
|
-
if (
|
1505
|
+
if (terminal.refCount <= 0) {
|
1222
1506
|
this.logger.info({
|
1223
1507
|
msg: "terminal not attached for too long, closing",
|
1224
|
-
id: terminal.
|
1508
|
+
id: terminal.session.id
|
1225
1509
|
});
|
1226
1510
|
terminal.abortController.abort();
|
1227
|
-
this.managedTerminals.delete(terminal.
|
1511
|
+
this.managedTerminals.delete(terminal.session.id);
|
1512
|
+
this.existingSessions.delete(
|
1513
|
+
`${terminal.session.projectId}:${terminal.session.instanceId}.${terminal.session.terminalName}`
|
1514
|
+
);
|
1228
1515
|
return;
|
1229
1516
|
}
|
1230
1517
|
setTimeout(() => this.closeTerminalIfNotAttached(terminal), notAttachedTerminalLifetime);
|
@@ -1238,9 +1525,19 @@ class TerminalManager {
|
|
1238
1525
|
);
|
1239
1526
|
}
|
1240
1527
|
persistHistory = createAsyncBatcher(async (entries) => {
|
1241
|
-
this.logger.trace({ msg: "persisting history
|
1528
|
+
this.logger.trace({ msg: "persisting history entries", count: entries.length });
|
1242
1529
|
await this.stateBackend.appendTerminalSessionHistory(entries);
|
1243
1530
|
});
|
1531
|
+
async markFinishedSessions() {
|
1532
|
+
const sessions = await this.stateBackend.getActiveTerminalSessions();
|
1533
|
+
for (const session of sessions) {
|
1534
|
+
await this.stateBackend.putTerminalSession(session.projectId, session.instanceId, {
|
1535
|
+
...session,
|
1536
|
+
finishedAt: /* @__PURE__ */ new Date()
|
1537
|
+
});
|
1538
|
+
}
|
1539
|
+
await this.updateActiveTerminalSessions();
|
1540
|
+
}
|
1244
1541
|
}
|
1245
1542
|
|
1246
1543
|
class InvalidInstanceStatusError extends Error {
|
@@ -1253,20 +1550,17 @@ class InvalidInstanceStatusError extends Error {
|
|
1253
1550
|
}
|
1254
1551
|
|
1255
1552
|
const localRunnerBackendConfig = z.object({
|
1256
|
-
HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_SOURCE_CHECK: z.boolean({ coerce: true }).default(false),
|
1257
1553
|
HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK: z.boolean({ coerce: true }).default(false),
|
1258
1554
|
HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT: z.boolean({ coerce: true }).default(true),
|
1259
|
-
HIGHSTATE_BACKEND_RUNNER_LOCAL_SOURCE_BASE_PATH: z.string().optional(),
|
1260
1555
|
HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR: z.string().optional()
|
1261
1556
|
});
|
1262
1557
|
class LocalRunnerBackend {
|
1263
|
-
constructor(
|
1264
|
-
this.skipSourceCheck = skipSourceCheck;
|
1558
|
+
constructor(skipStateCheck, printOutput, cacheDir, pulumiProjectHost, libraryBackend) {
|
1265
1559
|
this.skipStateCheck = skipStateCheck;
|
1266
1560
|
this.printOutput = printOutput;
|
1267
|
-
this.sourceBasePath = sourceBasePath;
|
1268
1561
|
this.cacheDir = cacheDir;
|
1269
1562
|
this.pulumiProjectHost = pulumiProjectHost;
|
1563
|
+
this.libraryBackend = libraryBackend;
|
1270
1564
|
}
|
1271
1565
|
events = new EventEmitter();
|
1272
1566
|
async *watch(options) {
|
@@ -1369,7 +1663,7 @@ class LocalRunnerBackend {
|
|
1369
1663
|
return null;
|
1370
1664
|
}
|
1371
1665
|
const files = z.array(instanceFileSchema).parse(outputs["$files"].value);
|
1372
|
-
const file = files.find((f) => f.name === fileName);
|
1666
|
+
const file = files.find((f) => f.meta.name === fileName);
|
1373
1667
|
if (!file) {
|
1374
1668
|
return null;
|
1375
1669
|
}
|
@@ -1412,16 +1706,16 @@ class LocalRunnerBackend {
|
|
1412
1706
|
async updateWorker(options, configMap, preview) {
|
1413
1707
|
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1414
1708
|
try {
|
1415
|
-
const
|
1416
|
-
|
1417
|
-
options.
|
1418
|
-
|
1709
|
+
const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType);
|
1710
|
+
if (!resolvedSource) {
|
1711
|
+
throw new Error(`Resolved unit source not found for ${options.instanceType}`);
|
1712
|
+
}
|
1419
1713
|
await this.pulumiProjectHost.runLocal(
|
1420
1714
|
{
|
1421
1715
|
projectId: options.projectId,
|
1422
1716
|
pulumiProjectName: options.instanceType,
|
1423
1717
|
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
1424
|
-
projectPath,
|
1718
|
+
projectPath: resolvedSource.projectPath,
|
1425
1719
|
stackConfig: configMap,
|
1426
1720
|
envVars: {
|
1427
1721
|
HIGHSTATE_CACHE_DIR: this.cacheDir
|
@@ -1482,7 +1776,7 @@ class LocalRunnerBackend {
|
|
1482
1776
|
if (isUnlocked) return true;
|
1483
1777
|
const isResolved = await this.tryInstallMissingDependencies(
|
1484
1778
|
error,
|
1485
|
-
allowedDependencies
|
1779
|
+
resolvedSource.allowedDependencies
|
1486
1780
|
);
|
1487
1781
|
if (isResolved) return true;
|
1488
1782
|
return false;
|
@@ -1519,13 +1813,16 @@ class LocalRunnerBackend {
|
|
1519
1813
|
async destroyWorker(options) {
|
1520
1814
|
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1521
1815
|
try {
|
1522
|
-
const
|
1816
|
+
const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType);
|
1817
|
+
if (!resolvedSource) {
|
1818
|
+
throw new Error(`Resolved unit source not found for ${options.instanceType}`);
|
1819
|
+
}
|
1523
1820
|
await this.pulumiProjectHost.runLocal(
|
1524
1821
|
{
|
1525
1822
|
projectId: options.projectId,
|
1526
1823
|
pulumiProjectName: options.instanceType,
|
1527
1824
|
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
1528
|
-
projectPath,
|
1825
|
+
projectPath: resolvedSource.projectPath,
|
1529
1826
|
envVars: {
|
1530
1827
|
HIGHSTATE_CACHE_DIR: this.cacheDir,
|
1531
1828
|
PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : ""
|
@@ -1577,13 +1874,18 @@ class LocalRunnerBackend {
|
|
1577
1874
|
}
|
1578
1875
|
);
|
1579
1876
|
} catch (error) {
|
1580
|
-
const { StackNotFoundError } = await import('@pulumi/pulumi/automation');
|
1877
|
+
const { StackNotFoundError } = await import('@pulumi/pulumi/automation/index.js');
|
1581
1878
|
if (error instanceof StackNotFoundError) {
|
1582
1879
|
this.updateState({
|
1583
1880
|
id: instanceId,
|
1584
1881
|
status: "not_created",
|
1585
1882
|
totalResourceCount: 0,
|
1586
|
-
currentResourceCount: 0
|
1883
|
+
currentResourceCount: 0,
|
1884
|
+
statusFields: [],
|
1885
|
+
pages: [],
|
1886
|
+
files: [],
|
1887
|
+
terminals: [],
|
1888
|
+
triggers: []
|
1587
1889
|
});
|
1588
1890
|
return;
|
1589
1891
|
}
|
@@ -1691,7 +1993,7 @@ class LocalRunnerBackend {
|
|
1691
1993
|
}
|
1692
1994
|
if (outputs["$files"]) {
|
1693
1995
|
const files = z.array(instanceFileSchema).parse(outputs["$files"].value);
|
1694
|
-
patch.files = files.map((file) =>
|
1996
|
+
patch.files = files.map((file) => file.meta);
|
1695
1997
|
} else {
|
1696
1998
|
patch.files = [];
|
1697
1999
|
}
|
@@ -1736,35 +2038,9 @@ class LocalRunnerBackend {
|
|
1736
2038
|
static getInstanceId(options) {
|
1737
2039
|
return getInstanceId(options.instanceType, options.instanceName);
|
1738
2040
|
}
|
1739
|
-
async
|
1740
|
-
if (
|
1741
|
-
|
1742
|
-
const projectPath = resolve$1(this.sourceBasePath, path);
|
1743
|
-
return [projectPath, []];
|
1744
|
-
}
|
1745
|
-
if (!this.skipSourceCheck) {
|
1746
|
-
const packageName = source.version ? `${source.package}@${source.version}` : source.package;
|
1747
|
-
await ensureDependencyInstalled(packageName);
|
1748
|
-
}
|
1749
|
-
const workerPathUrl = resolve(
|
1750
|
-
`@highstate/backend/source-resolution-worker`,
|
1751
|
-
import.meta.url
|
1752
|
-
);
|
1753
|
-
const workerPath = fileURLToPath(workerPathUrl);
|
1754
|
-
const worker = new Worker(workerPath, {
|
1755
|
-
workerData: { source, skipSourceCheck: this.skipSourceCheck }
|
1756
|
-
});
|
1757
|
-
for await (const [event] of on(worker, "message")) {
|
1758
|
-
const eventData = event;
|
1759
|
-
if (eventData.type === "result") {
|
1760
|
-
return [eventData.projectPath, eventData.allowedDependencies];
|
1761
|
-
}
|
1762
|
-
}
|
1763
|
-
throw new Error("Worker ended without sending the result");
|
1764
|
-
}
|
1765
|
-
async tryInstallMissingDependencies(error, allowedDependencies) {
|
1766
|
-
if (!(error instanceof Error)) {
|
1767
|
-
return false;
|
2041
|
+
async tryInstallMissingDependencies(error, allowedDependencies) {
|
2042
|
+
if (!(error instanceof Error)) {
|
2043
|
+
return false;
|
1768
2044
|
}
|
1769
2045
|
const pattern = /Cannot find module '(.*)'/;
|
1770
2046
|
const match = error.message.match(pattern);
|
@@ -1783,12 +2059,7 @@ class LocalRunnerBackend {
|
|
1783
2059
|
static getStackName(options) {
|
1784
2060
|
return `${options.projectId}_${options.instanceName}`;
|
1785
2061
|
}
|
1786
|
-
static
|
1787
|
-
let sourceBasePath = config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SOURCE_BASE_PATH;
|
1788
|
-
if (!sourceBasePath) {
|
1789
|
-
const [projectPath] = await resolveMainLocalProject();
|
1790
|
-
sourceBasePath = resolve$1(projectPath, "units");
|
1791
|
-
}
|
2062
|
+
static create(config, pulumiProjectHost, libraryBackend) {
|
1792
2063
|
let cacheDir = config.HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR;
|
1793
2064
|
if (!cacheDir) {
|
1794
2065
|
const homeDir = process.env.HOME ?? process.env.USERPROFILE;
|
@@ -1800,12 +2071,11 @@ class LocalRunnerBackend {
|
|
1800
2071
|
cacheDir = resolve$1(homeDir, ".cache", "highstate");
|
1801
2072
|
}
|
1802
2073
|
return new LocalRunnerBackend(
|
1803
|
-
config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_SOURCE_CHECK,
|
1804
2074
|
config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK,
|
1805
2075
|
config.HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT,
|
1806
|
-
sourceBasePath,
|
1807
2076
|
cacheDir,
|
1808
|
-
pulumiProjectHost
|
2077
|
+
pulumiProjectHost,
|
2078
|
+
libraryBackend
|
1809
2079
|
);
|
1810
2080
|
}
|
1811
2081
|
}
|
@@ -1814,18 +2084,14 @@ const runnerBackendConfig = z.object({
|
|
1814
2084
|
HIGHSTATE_BACKEND_RUNNER_TYPE: z.enum(["local"]).default("local"),
|
1815
2085
|
...localRunnerBackendConfig.shape
|
1816
2086
|
});
|
1817
|
-
function createRunnerBackend(config, pulumiProjectHost) {
|
2087
|
+
function createRunnerBackend(config, pulumiProjectHost, libraryBackend) {
|
1818
2088
|
switch (config.HIGHSTATE_BACKEND_RUNNER_TYPE) {
|
1819
2089
|
case "local": {
|
1820
|
-
return LocalRunnerBackend.create(config, pulumiProjectHost);
|
2090
|
+
return LocalRunnerBackend.create(config, pulumiProjectHost, libraryBackend);
|
1821
2091
|
}
|
1822
2092
|
}
|
1823
2093
|
}
|
1824
2094
|
|
1825
|
-
const evaluatedCompositeInstanceSchema = compositeInstanceSchema.extend({
|
1826
|
-
inputHash: z.string()
|
1827
|
-
});
|
1828
|
-
|
1829
2095
|
const localStateBackendConfig = z.object({
|
1830
2096
|
HIGHSTATE_BACKEND_STATE_LOCAL_DIR: z.string().optional()
|
1831
2097
|
});
|
@@ -1837,27 +2103,31 @@ class LocalStateBackend {
|
|
1837
2103
|
}
|
1838
2104
|
async getActiveOperations() {
|
1839
2105
|
const sublevel = this.getActiveOperationsSublevel();
|
1840
|
-
return this.
|
2106
|
+
return this.getAllSublevelItems(sublevel, projectOperationSchema);
|
1841
2107
|
}
|
1842
2108
|
async getOperations(projectId, beforeOperationId) {
|
1843
2109
|
const sublevel = this.getProjectOperationsSublevel(projectId);
|
1844
2110
|
const pageSize = 10;
|
1845
|
-
return await this.
|
2111
|
+
return await this.getAllSublevelItems(sublevel, projectOperationSchema, pageSize, {
|
1846
2112
|
lt: beforeOperationId,
|
1847
2113
|
reverse: true
|
1848
2114
|
});
|
1849
2115
|
}
|
1850
|
-
async
|
2116
|
+
async getAllInstanceStates(projectId) {
|
1851
2117
|
const sublevel = this.getProjectInstanceStatesSublevel(projectId);
|
1852
|
-
return await this.
|
2118
|
+
return await this.getAllSublevelItems(sublevel, instanceStateSchema);
|
1853
2119
|
}
|
1854
2120
|
async getInstanceState(projectId, instanceID) {
|
1855
2121
|
const sublevel = this.getProjectInstanceStatesSublevel(projectId);
|
1856
2122
|
return await this.getSublevelItem(sublevel, instanceStateSchema, instanceID);
|
1857
2123
|
}
|
2124
|
+
async getInstanceStates(projectId, instanceIds) {
|
2125
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId);
|
2126
|
+
return await this.getSublevelItems(sublevel, instanceStateSchema, instanceIds);
|
2127
|
+
}
|
1858
2128
|
async getAffectedInstanceStates(operationId) {
|
1859
2129
|
const sublevel = this.getOperationInstanceStatesSublevel(operationId);
|
1860
|
-
return await this.
|
2130
|
+
return await this.getAllSublevelItems(sublevel, instanceStateSchema);
|
1861
2131
|
}
|
1862
2132
|
async getInstanceLogs(operationId, instanceId) {
|
1863
2133
|
const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId);
|
@@ -1889,13 +2159,14 @@ class LocalStateBackend {
|
|
1889
2159
|
// this separation is also necessary because the instance states can be updated without operations
|
1890
2160
|
states.flatMap((state) => [
|
1891
2161
|
{
|
2162
|
+
// always put the state to the operation sublevel for history
|
1892
2163
|
type: "put",
|
1893
2164
|
key: state.id,
|
1894
2165
|
value: state,
|
1895
2166
|
sublevel: operationInstanceStatesSublevel
|
1896
2167
|
},
|
1897
2168
|
{
|
1898
|
-
type: "put",
|
2169
|
+
type: state.status === "not_created" ? "del" : "put",
|
1899
2170
|
key: state.id,
|
1900
2171
|
value: state,
|
1901
2172
|
sublevel: projectInstanceStatesSublevel
|
@@ -1910,7 +2181,7 @@ class LocalStateBackend {
|
|
1910
2181
|
// as i told before, we update the instance states without operations
|
1911
2182
|
// this method is used when upstream instance state changes are detected
|
1912
2183
|
states.map((state) => ({
|
1913
|
-
type: "put",
|
2184
|
+
type: state.status === "not_created" ? "del" : "put",
|
1914
2185
|
key: state.id,
|
1915
2186
|
value: state
|
1916
2187
|
}))
|
@@ -1934,9 +2205,19 @@ class LocalStateBackend {
|
|
1934
2205
|
}))
|
1935
2206
|
);
|
1936
2207
|
}
|
2208
|
+
async getCompositeInstances(projectId, signal) {
|
2209
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
|
2210
|
+
return await this.getAllSublevelItems(
|
2211
|
+
//
|
2212
|
+
sublevel,
|
2213
|
+
compositeInstanceSchema,
|
2214
|
+
void 0,
|
2215
|
+
{ signal }
|
2216
|
+
);
|
2217
|
+
}
|
1937
2218
|
async getCompositeInstance(projectId, instanceId) {
|
1938
2219
|
const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
|
1939
|
-
return this.getSublevelItem(sublevel,
|
2220
|
+
return this.getSublevelItem(sublevel, compositeInstanceSchema, instanceId);
|
1940
2221
|
}
|
1941
2222
|
async getCompositeInstanceInputHash(projectId, instanceId) {
|
1942
2223
|
const sublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId);
|
@@ -1944,22 +2225,16 @@ class LocalStateBackend {
|
|
1944
2225
|
return inputHash ?? null;
|
1945
2226
|
}
|
1946
2227
|
async putCompositeInstances(projectId, instances) {
|
1947
|
-
this.validateArray(
|
2228
|
+
this.validateArray(compositeInstanceSchema, instances);
|
1948
2229
|
const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
|
1949
|
-
|
2230
|
+
this.getProjectCompositeInstanceInputHashesSublevel(projectId);
|
1950
2231
|
await this.db.batch(
|
1951
2232
|
instances.flatMap((instance) => [
|
1952
2233
|
{
|
1953
2234
|
type: "put",
|
1954
2235
|
key: instance.instance.id,
|
1955
|
-
value:
|
2236
|
+
value: compositeInstanceSchema.parse(instance),
|
1956
2237
|
sublevel
|
1957
|
-
},
|
1958
|
-
{
|
1959
|
-
type: "put",
|
1960
|
-
key: instance.instance.id,
|
1961
|
-
value: instance.inputHash,
|
1962
|
-
sublevel: inputHashesSublevel
|
1963
2238
|
}
|
1964
2239
|
])
|
1965
2240
|
);
|
@@ -1974,11 +2249,46 @@ class LocalStateBackend {
|
|
1974
2249
|
])
|
1975
2250
|
);
|
1976
2251
|
}
|
2252
|
+
async getTopLevelCompositeChildrenIds(projectId, instanceIds) {
|
2253
|
+
const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId);
|
2254
|
+
const items = await sublevel.getMany(instanceIds);
|
2255
|
+
const schema = z.array(z.string()).optional();
|
2256
|
+
const result = {};
|
2257
|
+
for (let i = 0; i < items.length; i++) {
|
2258
|
+
const instanceId = instanceIds[i];
|
2259
|
+
const childrenIds = schema.parse(items[i]);
|
2260
|
+
result[instanceId] = childrenIds ?? [];
|
2261
|
+
}
|
2262
|
+
return result;
|
2263
|
+
}
|
2264
|
+
async putTopLevelCompositeChildrenIds(projectId, childrenIds) {
|
2265
|
+
const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId);
|
2266
|
+
const schema = z.array(z.string()).optional();
|
2267
|
+
await sublevel.batch(
|
2268
|
+
Object.entries(childrenIds).map(([instanceId, ids]) => ({
|
2269
|
+
type: "put",
|
2270
|
+
key: instanceId,
|
2271
|
+
value: schema.parse(ids)
|
2272
|
+
}))
|
2273
|
+
);
|
2274
|
+
}
|
2275
|
+
async getActiveTerminalSessions() {
|
2276
|
+
const data = await this.db.get("activeTerminalSessionIds", { valueEncoding: "json" });
|
2277
|
+
return data ? z.array(terminalSessionSchema).parse(data) : [];
|
2278
|
+
}
|
2279
|
+
putActiveTerminalSessions(sessions) {
|
2280
|
+
this.validateArray(terminalSessionSchema, sessions);
|
2281
|
+
return this.db.put("activeTerminalSessionIds", sessions, { valueEncoding: "json" });
|
2282
|
+
}
|
2283
|
+
async getTerminalSession(projectId, instanceId, sessionId) {
|
2284
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
|
2285
|
+
return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId);
|
2286
|
+
}
|
1977
2287
|
async getTerminalSessions(projectId, instanceId) {
|
1978
2288
|
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
|
1979
|
-
return await this.
|
2289
|
+
return await this.getAllSublevelItems(sublevel, terminalSessionSchema);
|
1980
2290
|
}
|
1981
|
-
async
|
2291
|
+
async getLastTerminalSession(projectId, instanceId, sessionId) {
|
1982
2292
|
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
|
1983
2293
|
return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId);
|
1984
2294
|
}
|
@@ -2009,7 +2319,7 @@ class LocalStateBackend {
|
|
2009
2319
|
}))
|
2010
2320
|
);
|
2011
2321
|
}
|
2012
|
-
async
|
2322
|
+
async getAllSublevelItems(sublevel, schema, limit, options) {
|
2013
2323
|
const result = [];
|
2014
2324
|
const iterator = options ? sublevel.iterator(options) : sublevel.iterator();
|
2015
2325
|
const invalidKeys = [];
|
@@ -2058,6 +2368,37 @@ class LocalStateBackend {
|
|
2058
2368
|
}
|
2059
2369
|
return parseResult.data;
|
2060
2370
|
}
|
2371
|
+
async getSublevelItems(sublevel, schema, keys) {
|
2372
|
+
const result = [];
|
2373
|
+
const invalidKeys = [];
|
2374
|
+
for (const key of keys) {
|
2375
|
+
const value = await sublevel.get(key);
|
2376
|
+
if (!value) {
|
2377
|
+
continue;
|
2378
|
+
}
|
2379
|
+
const parseResult = schema.safeParse(value);
|
2380
|
+
if (!parseResult.success) {
|
2381
|
+
this.logger.warn({
|
2382
|
+
msg: "failed to parse item, it will be deleted",
|
2383
|
+
error: parseResult.error,
|
2384
|
+
sublevel: sublevel.prefix,
|
2385
|
+
key
|
2386
|
+
});
|
2387
|
+
invalidKeys.push(key);
|
2388
|
+
continue;
|
2389
|
+
}
|
2390
|
+
result.push(parseResult.data);
|
2391
|
+
}
|
2392
|
+
if (invalidKeys.length > 0) {
|
2393
|
+
this.logger.info({
|
2394
|
+
msg: "deleting invalid items",
|
2395
|
+
sublevel: sublevel.prefix,
|
2396
|
+
keyCount: invalidKeys.length
|
2397
|
+
});
|
2398
|
+
await sublevel.batch(invalidKeys.map((key) => ({ type: "del", key })));
|
2399
|
+
}
|
2400
|
+
return result;
|
2401
|
+
}
|
2061
2402
|
validateItem(schema, item) {
|
2062
2403
|
const parseResult = schema.safeParse(item);
|
2063
2404
|
if (!parseResult.success) {
|
@@ -2088,6 +2429,9 @@ class LocalStateBackend {
|
|
2088
2429
|
getProjectCompositeInstanceInputHashesSublevel(projectId) {
|
2089
2430
|
return this.getStringSublevel(`projects/${projectId}/compositeInstanceInputHashes`);
|
2090
2431
|
}
|
2432
|
+
getProjectCompositeChildrenIdsSublevel(projectId) {
|
2433
|
+
return this.getJsonSublevel(`projects/${projectId}/topLevelCompositeChildrenIds`);
|
2434
|
+
}
|
2091
2435
|
getProjectInstanceStatesSublevel(projectId) {
|
2092
2436
|
return this.getJsonSublevel(`projects/${projectId}/instanceStates`);
|
2093
2437
|
}
|
@@ -2150,6 +2494,30 @@ function createStateBackend(config, localPulumiHost, logger) {
|
|
2150
2494
|
}
|
2151
2495
|
}
|
2152
2496
|
|
2497
|
+
class StateManager {
|
2498
|
+
stateEE = new EventEmitter$1();
|
2499
|
+
/**
|
2500
|
+
* Watches for all instance state changes in the project.
|
2501
|
+
*
|
2502
|
+
* @param projectId The project ID to watch.
|
2503
|
+
* @param signal The signal to abort the operation.
|
2504
|
+
*/
|
2505
|
+
async *watchInstanceStates(projectId, signal) {
|
2506
|
+
for await (const [state] of on(this.stateEE, projectId, { signal })) {
|
2507
|
+
yield state;
|
2508
|
+
}
|
2509
|
+
}
|
2510
|
+
/**
|
2511
|
+
* Emits a state patch for the instance in the project.
|
2512
|
+
*
|
2513
|
+
* @param projectId The project ID to emit the state patch for.
|
2514
|
+
* @param patch The state patch to emit.
|
2515
|
+
*/
|
2516
|
+
emitStatePatch(projectId, patch) {
|
2517
|
+
this.stateEE.emit(projectId, patch);
|
2518
|
+
}
|
2519
|
+
}
|
2520
|
+
|
2153
2521
|
const localWorkspaceBackendConfig = z.object({
|
2154
2522
|
HIGHSTATE_BACKEND_WORKSPACE_LOCAL_DIR: z.string().optional()
|
2155
2523
|
});
|
@@ -2216,8 +2584,352 @@ async function loadConfig(env = process.env, useDotenv = true) {
|
|
2216
2584
|
return configSchema.parse(env);
|
2217
2585
|
}
|
2218
2586
|
|
2587
|
+
class OperationWorkset {
|
2588
|
+
constructor(operation, library, stateManager, logger) {
|
2589
|
+
this.operation = operation;
|
2590
|
+
this.library = library;
|
2591
|
+
this.stateManager = stateManager;
|
2592
|
+
this.logger = logger;
|
2593
|
+
}
|
2594
|
+
affectedInstanceIdSet = /* @__PURE__ */ new Set();
|
2595
|
+
instanceMap = /* @__PURE__ */ new Map();
|
2596
|
+
instanceChildrenMap = /* @__PURE__ */ new Map();
|
2597
|
+
initialStateMap = /* @__PURE__ */ new Map();
|
2598
|
+
stateMap = /* @__PURE__ */ new Map();
|
2599
|
+
dependentStateMap = /* @__PURE__ */ new Map();
|
2600
|
+
stateChildIdMap = /* @__PURE__ */ new Map();
|
2601
|
+
unitSourceHashMap = /* @__PURE__ */ new Map();
|
2602
|
+
inputResolver;
|
2603
|
+
inputResolverInputs = /* @__PURE__ */ new Map();
|
2604
|
+
inputResolverPromiseCache = /* @__PURE__ */ new Map();
|
2605
|
+
inputHashResolver;
|
2606
|
+
inputHashResolverInputs = /* @__PURE__ */ new Map();
|
2607
|
+
inputHashResolverPromiseCache = /* @__PURE__ */ new Map();
|
2608
|
+
resolvedInstanceInputs = /* @__PURE__ */ new Map();
|
2609
|
+
getInstance(instanceId) {
|
2610
|
+
const instance = this.instanceMap.get(instanceId);
|
2611
|
+
if (!instance) {
|
2612
|
+
throw new Error(`Instance with ID ${instanceId} not found in the operation workset`);
|
2613
|
+
}
|
2614
|
+
return instance;
|
2615
|
+
}
|
2616
|
+
isAffected(instanceId) {
|
2617
|
+
return this.affectedInstanceIdSet.has(instanceId);
|
2618
|
+
}
|
2619
|
+
updateState(update) {
|
2620
|
+
const finalState = applyPartialInstanceState(this.stateMap, update);
|
2621
|
+
this.stateManager.emitStatePatch(this.operation.projectId, createInstanceStatePatch(update));
|
2622
|
+
if (finalState.parentId) {
|
2623
|
+
this.recalculateCompositeState(finalState.parentId);
|
2624
|
+
}
|
2625
|
+
return finalState;
|
2626
|
+
}
|
2627
|
+
setState(state) {
|
2628
|
+
this.stateMap.set(state.id, state);
|
2629
|
+
this.initialStateMap.set(state.id, state);
|
2630
|
+
if (state.parentId) {
|
2631
|
+
let children = this.stateChildIdMap.get(state.parentId);
|
2632
|
+
if (!children) {
|
2633
|
+
children = [];
|
2634
|
+
this.stateChildIdMap.set(state.parentId, children);
|
2635
|
+
}
|
2636
|
+
children.push(state.id);
|
2637
|
+
}
|
2638
|
+
}
|
2639
|
+
restoreInitialStatus(instanceId) {
|
2640
|
+
const state = this.stateMap.get(instanceId);
|
2641
|
+
const initialState = this.initialStateMap.get(instanceId);
|
2642
|
+
if (!state || !initialState) {
|
2643
|
+
this.logger.warn(
|
2644
|
+
`cannot reset status for instance "${instanceId}" because it is not in the state map`
|
2645
|
+
);
|
2646
|
+
return;
|
2647
|
+
}
|
2648
|
+
this.stateMap.set(instanceId, { ...initialState, status: initialState.status });
|
2649
|
+
}
|
2650
|
+
emitAffectedInitialStates() {
|
2651
|
+
for (const state of this.initialStateMap.values()) {
|
2652
|
+
if (this.affectedInstanceIdSet.has(state.id)) {
|
2653
|
+
this.stateManager.emitStatePatch(this.operation.projectId, state);
|
2654
|
+
}
|
2655
|
+
}
|
2656
|
+
}
|
2657
|
+
getAffectedCompositeChildren(instanceId) {
|
2658
|
+
const children = this.instanceChildrenMap.get(instanceId);
|
2659
|
+
if (!children) {
|
2660
|
+
return [];
|
2661
|
+
}
|
2662
|
+
return children.filter((child) => this.affectedInstanceIdSet.has(child.id));
|
2663
|
+
}
|
2664
|
+
getState(instanceId) {
|
2665
|
+
return this.stateMap.get(instanceId);
|
2666
|
+
}
|
2667
|
+
getDependentStates(instanceId) {
|
2668
|
+
return this.dependentStateMap.get(instanceId) ?? [];
|
2669
|
+
}
|
2670
|
+
recalculateCompositeState(instanceId) {
|
2671
|
+
const state = this.stateMap.get(instanceId) ?? createInstanceState(instanceId);
|
2672
|
+
let currentResourceCount = 0;
|
2673
|
+
let totalResourceCount = 0;
|
2674
|
+
const children = this.stateChildIdMap.get(instanceId) ?? [];
|
2675
|
+
for (const childId of children) {
|
2676
|
+
const child = this.stateMap.get(childId);
|
2677
|
+
if (child?.currentResourceCount) {
|
2678
|
+
currentResourceCount += child.currentResourceCount;
|
2679
|
+
}
|
2680
|
+
if (child?.totalResourceCount) {
|
2681
|
+
totalResourceCount += child.totalResourceCount;
|
2682
|
+
}
|
2683
|
+
}
|
2684
|
+
const updatedState = {
|
2685
|
+
...state,
|
2686
|
+
currentResourceCount,
|
2687
|
+
totalResourceCount
|
2688
|
+
};
|
2689
|
+
this.stateMap.set(instanceId, updatedState);
|
2690
|
+
this.stateManager.emitStatePatch(this.operation.projectId, updatedState);
|
2691
|
+
if (state.parentId) {
|
2692
|
+
this.recalculateCompositeState(state.parentId);
|
2693
|
+
}
|
2694
|
+
}
|
2695
|
+
addInstance(instance) {
|
2696
|
+
if (this.instanceMap.has(instance.id)) {
|
2697
|
+
throw new Error(`Found multiple instances with the same ID: ${instance.id}`);
|
2698
|
+
}
|
2699
|
+
if (!(instance.type in this.library.components)) {
|
2700
|
+
this.logger.warn(
|
2701
|
+
`ignoring instance "${instance.id}" because its type "${instance.type}" is not in the library`
|
2702
|
+
);
|
2703
|
+
return;
|
2704
|
+
}
|
2705
|
+
this.instanceMap.set(instance.id, instance);
|
2706
|
+
if (instance.parentId) {
|
2707
|
+
let children = this.instanceChildrenMap.get(instance.parentId);
|
2708
|
+
if (!children) {
|
2709
|
+
children = [];
|
2710
|
+
this.instanceChildrenMap.set(instance.parentId, children);
|
2711
|
+
}
|
2712
|
+
children.push(instance);
|
2713
|
+
}
|
2714
|
+
}
|
2715
|
+
async extendForUpdate() {
|
2716
|
+
const traverse = async (instanceId) => {
|
2717
|
+
if (this.affectedInstanceIdSet.has(instanceId)) {
|
2718
|
+
return;
|
2719
|
+
}
|
2720
|
+
const instance = this.instanceMap.get(instanceId);
|
2721
|
+
if (!instance) {
|
2722
|
+
return;
|
2723
|
+
}
|
2724
|
+
const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
|
2725
|
+
for (const inputs of Object.values(instanceInputs)) {
|
2726
|
+
for (const input of inputs) {
|
2727
|
+
await traverse(input.input.instanceId);
|
2728
|
+
}
|
2729
|
+
}
|
2730
|
+
const state = this.stateMap.get(instance.id);
|
2731
|
+
const { inputHash: expectedInputHash } = await this.inputHashResolver(instance.id);
|
2732
|
+
if (this.operation.options.forceUpdateDependencies) {
|
2733
|
+
this.affectedInstanceIdSet.add(instanceId);
|
2734
|
+
return;
|
2735
|
+
}
|
2736
|
+
if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
|
2737
|
+
this.affectedInstanceIdSet.add(instanceId);
|
2738
|
+
}
|
2739
|
+
};
|
2740
|
+
for (const instanceId of this.operation.instanceIds) {
|
2741
|
+
if (this.operation.type === "update") {
|
2742
|
+
await traverse(instanceId);
|
2743
|
+
}
|
2744
|
+
this.affectedInstanceIdSet.add(instanceId);
|
2745
|
+
}
|
2746
|
+
const compositeInstanceQueue = Array.from(this.affectedInstanceIdSet);
|
2747
|
+
while (compositeInstanceQueue.length > 0) {
|
2748
|
+
const childId = compositeInstanceQueue.pop();
|
2749
|
+
const children = this.instanceChildrenMap.get(childId) ?? [];
|
2750
|
+
for (const child of children) {
|
2751
|
+
compositeInstanceQueue.push(child.id);
|
2752
|
+
this.affectedInstanceIdSet.add(child.id);
|
2753
|
+
}
|
2754
|
+
}
|
2755
|
+
for (const instanceId of this.affectedInstanceIdSet) {
|
2756
|
+
let instance = this.instanceMap.get(instanceId);
|
2757
|
+
while (instance?.parentId) {
|
2758
|
+
this.affectedInstanceIdSet.add(instance.parentId);
|
2759
|
+
instance = this.instanceMap.get(instance.parentId);
|
2760
|
+
}
|
2761
|
+
}
|
2762
|
+
this.operation.affectedInstanceIds = Array.from(this.affectedInstanceIdSet);
|
2763
|
+
}
|
2764
|
+
extendForDestroy() {
|
2765
|
+
const traverse = (instanceKey) => {
|
2766
|
+
if (this.affectedInstanceIdSet.has(instanceKey)) {
|
2767
|
+
return;
|
2768
|
+
}
|
2769
|
+
const state = this.stateMap.get(instanceKey);
|
2770
|
+
if (!state || state.status === "not_created") {
|
2771
|
+
return;
|
2772
|
+
}
|
2773
|
+
const dependents = this.dependentStateMap.get(instanceKey) ?? [];
|
2774
|
+
for (const dependent of dependents) {
|
2775
|
+
traverse(dependent.id);
|
2776
|
+
this.affectedInstanceIdSet.add(instanceKey);
|
2777
|
+
}
|
2778
|
+
};
|
2779
|
+
for (const instanceId of this.operation.instanceIds) {
|
2780
|
+
const instance = this.instanceMap.get(instanceId);
|
2781
|
+
if (!instance) {
|
2782
|
+
throw new Error(`Instance not found: ${instanceId}`);
|
2783
|
+
}
|
2784
|
+
if (this.operation.options.destroyDependentInstances) {
|
2785
|
+
traverse(instance.id);
|
2786
|
+
}
|
2787
|
+
this.affectedInstanceIdSet.add(instanceId);
|
2788
|
+
}
|
2789
|
+
const compositeInstanceQueue = Array.from(this.affectedInstanceIdSet);
|
2790
|
+
while (compositeInstanceQueue.length > 0) {
|
2791
|
+
const childId = compositeInstanceQueue.pop();
|
2792
|
+
const children = this.stateChildIdMap.get(childId) ?? [];
|
2793
|
+
for (const child of children) {
|
2794
|
+
compositeInstanceQueue.push(child);
|
2795
|
+
this.affectedInstanceIdSet.add(child);
|
2796
|
+
}
|
2797
|
+
}
|
2798
|
+
for (const instanceId of this.affectedInstanceIdSet) {
|
2799
|
+
let state = this.stateMap.get(instanceId);
|
2800
|
+
while (state?.parentId) {
|
2801
|
+
this.affectedInstanceIdSet.add(state.parentId);
|
2802
|
+
state = this.stateMap.get(state.parentId);
|
2803
|
+
}
|
2804
|
+
}
|
2805
|
+
this.operation.affectedInstanceIds = Array.from(this.affectedInstanceIdSet);
|
2806
|
+
}
|
2807
|
+
getSourceHashIfApplicable(instance, component) {
|
2808
|
+
if (isUnitModel(component)) {
|
2809
|
+
return this.unitSourceHashMap.get(instance.type);
|
2810
|
+
}
|
2811
|
+
return void 0;
|
2812
|
+
}
|
2813
|
+
async getUpToDateInputHash(instance) {
|
2814
|
+
const component = this.library.components[instance.type];
|
2815
|
+
this.inputHashResolverInputs.set(instance.id, {
|
2816
|
+
instance,
|
2817
|
+
component,
|
2818
|
+
resolvedInputs: this.resolvedInstanceInputs.get(instance.id),
|
2819
|
+
state: this.stateMap.get(instance.id),
|
2820
|
+
sourceHash: this.getSourceHashIfApplicable(instance, component)
|
2821
|
+
});
|
2822
|
+
this.inputHashResolverPromiseCache.delete(instance.id);
|
2823
|
+
const { inputHash } = await this.inputHashResolver(instance.id);
|
2824
|
+
return inputHash;
|
2825
|
+
}
|
2826
|
+
getLockInstanceIds() {
|
2827
|
+
const instanceIds = new Set(this.operation.affectedInstanceIds);
|
2828
|
+
for (const instanceId of this.operation.affectedInstanceIds) {
|
2829
|
+
const instance = this.getInstance(instanceId);
|
2830
|
+
if (instance.parentId) {
|
2831
|
+
instanceIds.add(instance.parentId);
|
2832
|
+
}
|
2833
|
+
}
|
2834
|
+
return Array.from(instanceIds);
|
2835
|
+
}
|
2836
|
+
static async load(operation, projectBackend, libraryBackend, stateBackend, stateManager, logger, signal) {
|
2837
|
+
const [library, unitSources, project, compositeInstances, states] = await Promise.all([
|
2838
|
+
libraryBackend.loadLibrary(signal),
|
2839
|
+
libraryBackend.getResolvedUnitSources(),
|
2840
|
+
projectBackend.getProject(operation.projectId, signal),
|
2841
|
+
stateBackend.getCompositeInstances(operation.projectId, signal),
|
2842
|
+
stateBackend.getAllInstanceStates(operation.projectId, signal)
|
2843
|
+
]);
|
2844
|
+
const workset = new OperationWorkset(
|
2845
|
+
operation,
|
2846
|
+
library,
|
2847
|
+
stateManager,
|
2848
|
+
logger.child({ operationId: operation.id, service: "OperationWorkset" })
|
2849
|
+
);
|
2850
|
+
for (const unitSource of unitSources) {
|
2851
|
+
workset.unitSourceHashMap.set(unitSource.unitType, unitSource.sourceHash);
|
2852
|
+
}
|
2853
|
+
for (const instance of project.instances) {
|
2854
|
+
workset.addInstance(instance);
|
2855
|
+
}
|
2856
|
+
for (const instance of compositeInstances) {
|
2857
|
+
const worksetInstance = workset.instanceMap.get(instance.instance.id);
|
2858
|
+
if (worksetInstance) {
|
2859
|
+
worksetInstance.outputs = instance.instance.outputs;
|
2860
|
+
worksetInstance.resolvedOutputs = instance.instance.resolvedOutputs;
|
2861
|
+
} else {
|
2862
|
+
workset.addInstance(instance.instance);
|
2863
|
+
}
|
2864
|
+
for (const child of instance.children) {
|
2865
|
+
workset.addInstance(child);
|
2866
|
+
}
|
2867
|
+
}
|
2868
|
+
for (const state of states) {
|
2869
|
+
if (!workset.instanceMap.has(state.id)) {
|
2870
|
+
workset.logger.warn(
|
2871
|
+
`ignoring instance "${state.id}" from state because it is not in the project or is not a part of a composite instance`
|
2872
|
+
);
|
2873
|
+
continue;
|
2874
|
+
}
|
2875
|
+
workset.setState(state);
|
2876
|
+
}
|
2877
|
+
for (const instance of workset.instanceMap.values()) {
|
2878
|
+
workset.inputResolverInputs.set(`instance:${instance.id}`, {
|
2879
|
+
kind: "instance",
|
2880
|
+
instance,
|
2881
|
+
component: library.components[instance.type]
|
2882
|
+
});
|
2883
|
+
}
|
2884
|
+
for (const hub of project.hubs) {
|
2885
|
+
workset.inputResolverInputs.set(`hub:${hub.id}`, { kind: "hub", hub });
|
2886
|
+
}
|
2887
|
+
workset.inputResolver = createInputResolver(
|
2888
|
+
//
|
2889
|
+
workset.inputResolverInputs,
|
2890
|
+
logger,
|
2891
|
+
{ promiseCache: workset.inputResolverPromiseCache }
|
2892
|
+
);
|
2893
|
+
for (const instance of workset.instanceMap.values()) {
|
2894
|
+
const output = await workset.inputResolver(`instance:${instance.id}`);
|
2895
|
+
if (output.kind !== "instance") {
|
2896
|
+
throw new Error("Unexpected output kind");
|
2897
|
+
}
|
2898
|
+
workset.resolvedInstanceInputs.set(instance.id, output.resolvedInputs);
|
2899
|
+
const component = workset.library.components[instance.type];
|
2900
|
+
workset.inputHashResolverInputs.set(instance.id, {
|
2901
|
+
instance,
|
2902
|
+
component,
|
2903
|
+
resolvedInputs: output.resolvedInputs,
|
2904
|
+
state: workset.stateMap.get(instance.id),
|
2905
|
+
sourceHash: workset.getSourceHashIfApplicable(instance, component)
|
2906
|
+
});
|
2907
|
+
}
|
2908
|
+
workset.inputHashResolver = createInputHashResolver(
|
2909
|
+
//
|
2910
|
+
workset.inputHashResolverInputs,
|
2911
|
+
logger,
|
2912
|
+
{ promiseCache: workset.inputHashResolverPromiseCache }
|
2913
|
+
);
|
2914
|
+
switch (operation.type) {
|
2915
|
+
case "update":
|
2916
|
+
case "preview":
|
2917
|
+
case "refresh":
|
2918
|
+
await workset.extendForUpdate();
|
2919
|
+
break;
|
2920
|
+
case "recreate":
|
2921
|
+
workset.extendForDestroy();
|
2922
|
+
break;
|
2923
|
+
case "destroy":
|
2924
|
+
workset.extendForDestroy();
|
2925
|
+
break;
|
2926
|
+
}
|
2927
|
+
return workset;
|
2928
|
+
}
|
2929
|
+
}
|
2930
|
+
|
2219
2931
|
class RuntimeOperation {
|
2220
|
-
constructor(operation, runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLock,
|
2932
|
+
constructor(operation, runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLock, stateManager, operationEE, instanceLogsEE, logger) {
|
2221
2933
|
this.operation = operation;
|
2222
2934
|
this.runnerBackend = runnerBackend;
|
2223
2935
|
this.stateBackend = stateBackend;
|
@@ -2225,33 +2937,20 @@ class RuntimeOperation {
|
|
2225
2937
|
this.projectBackend = projectBackend;
|
2226
2938
|
this.secretBackend = secretBackend;
|
2227
2939
|
this.projectLock = projectLock;
|
2940
|
+
this.stateManager = stateManager;
|
2228
2941
|
this.operationEE = operationEE;
|
2229
|
-
this.stateEE = stateEE;
|
2230
|
-
this.compositeInstanceEE = compositeInstanceEE;
|
2231
2942
|
this.instanceLogsEE = instanceLogsEE;
|
2232
2943
|
this.logger = logger;
|
2233
2944
|
}
|
2234
2945
|
abortController = new AbortController();
|
2235
|
-
instanceMap = /* @__PURE__ */ new Map();
|
2236
|
-
hubMap = /* @__PURE__ */ new Map();
|
2237
|
-
resolvedInstanceInputs = /* @__PURE__ */ new Map();
|
2238
|
-
compositeInstanceLock = new BetterLock();
|
2239
|
-
compositeInstanceMap = /* @__PURE__ */ new Map();
|
2240
|
-
initialStatusMap = /* @__PURE__ */ new Map();
|
2241
|
-
stateMap = /* @__PURE__ */ new Map();
|
2242
2946
|
instancePromiseMap = /* @__PURE__ */ new Map();
|
2243
|
-
|
2244
|
-
|
2245
|
-
library;
|
2246
|
-
inputHashLock = new BetterLock();
|
2247
|
-
inputHashPromiseCache = /* @__PURE__ */ new Map();
|
2248
|
-
inputHashNodes = /* @__PURE__ */ new Map();
|
2249
|
-
resolveInputHash;
|
2947
|
+
workset;
|
2948
|
+
currentPhase;
|
2250
2949
|
async operateSafe() {
|
2251
2950
|
try {
|
2252
2951
|
await this.operate();
|
2253
2952
|
} catch (error) {
|
2254
|
-
if (
|
2953
|
+
if (isAbortError(error)) {
|
2255
2954
|
this.logger.info("the operation was cancelled");
|
2256
2955
|
this.operation.status = "cancelled";
|
2257
2956
|
await this.updateOperation();
|
@@ -2262,121 +2961,64 @@ class RuntimeOperation {
|
|
2262
2961
|
this.operation.error = errorToString(error);
|
2263
2962
|
await this.updateOperation();
|
2264
2963
|
} finally {
|
2265
|
-
this.resetPendingStateStatuses();
|
2266
2964
|
await Promise.all([
|
2267
2965
|
this.persistStates.flush(),
|
2268
2966
|
this.persistLogs.flush(),
|
2269
2967
|
this.persistSecrets.flush()
|
2270
2968
|
]);
|
2271
2969
|
this.logger.debug("operation finished, all entries persisted");
|
2272
|
-
if (this.operation.type === "preview") {
|
2273
|
-
const states = await this.stateBackend.getInstanceStates(this.operation.projectId);
|
2274
|
-
for (const state of states) {
|
2275
|
-
if (this.operation.instanceIds.includes(state.id)) {
|
2276
|
-
this.stateEE.emit(this.operation.projectId, createInstanceStateFrontendPatch(state));
|
2277
|
-
}
|
2278
|
-
}
|
2279
|
-
}
|
2280
|
-
}
|
2281
|
-
}
|
2282
|
-
resetPendingStateStatuses() {
|
2283
|
-
for (const state of this.stateMap.values()) {
|
2284
|
-
if (state.status !== "pending") {
|
2285
|
-
continue;
|
2286
|
-
}
|
2287
|
-
let initialStatus = this.initialStatusMap.get(state.id);
|
2288
|
-
let error = state.error;
|
2289
|
-
if (initialStatus === "destroying" || initialStatus === "pending" || initialStatus === "refreshing" || initialStatus === "updating") {
|
2290
|
-
initialStatus = "error";
|
2291
|
-
error ??= "unexpected progressing instance status";
|
2292
|
-
}
|
2293
|
-
if (initialStatus) {
|
2294
|
-
this.updateInstanceState({ id: state.id, status: initialStatus, error });
|
2295
|
-
}
|
2296
2970
|
}
|
2297
2971
|
}
|
2298
2972
|
async operate() {
|
2299
2973
|
this.logger.info("starting operation");
|
2300
|
-
|
2301
|
-
|
2302
|
-
|
2303
|
-
|
2304
|
-
|
2305
|
-
|
2306
|
-
|
2307
|
-
|
2308
|
-
|
2309
|
-
|
2310
|
-
|
2311
|
-
|
2312
|
-
|
2313
|
-
|
2314
|
-
this.hubMap.set(hub.id, hub);
|
2315
|
-
inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub });
|
2316
|
-
}
|
2317
|
-
for (const instance of allInstances) {
|
2318
|
-
this.instanceMap.set(instance.id, instance);
|
2319
|
-
inputResolverNodes.set(`instance:${instance.id}`, {
|
2320
|
-
kind: "instance",
|
2321
|
-
instance,
|
2322
|
-
component: this.library.components[instance.type]
|
2323
|
-
});
|
2324
|
-
}
|
2325
|
-
const resolveInputs = createInputResolver(
|
2326
|
-
inputResolverNodes,
|
2327
|
-
this.logger.child({ resolver: "input-resolver" })
|
2328
|
-
);
|
2329
|
-
for (const instance of allInstances) {
|
2330
|
-
const output = await resolveInputs(`instance:${instance.id}`);
|
2331
|
-
if (output.kind !== "instance") {
|
2332
|
-
throw new Error("Unexpected output kind, expected instance");
|
2974
|
+
let lockInstanceIds;
|
2975
|
+
while (true) {
|
2976
|
+
this.workset = await OperationWorkset.load(
|
2977
|
+
this.operation,
|
2978
|
+
this.projectBackend,
|
2979
|
+
this.libraryBackend,
|
2980
|
+
this.stateBackend,
|
2981
|
+
this.stateManager,
|
2982
|
+
this.logger,
|
2983
|
+
this.abortController.signal
|
2984
|
+
);
|
2985
|
+
lockInstanceIds = this.workset.getLockInstanceIds();
|
2986
|
+
if (this.projectLock.canImmediatelyAcquireLocks(lockInstanceIds)) {
|
2987
|
+
break;
|
2333
2988
|
}
|
2334
|
-
this.
|
2335
|
-
|
2336
|
-
for (const state of states) {
|
2337
|
-
this.stateMap.set(state.id, state);
|
2338
|
-
this.initialStatusMap.set(state.id, state.status);
|
2339
|
-
this.tryAddStateToParent(state);
|
2340
|
-
this.addDependentState(state);
|
2989
|
+
this.logger.info("waiting for locks to be available");
|
2990
|
+
await this.projectLock.lockInstances(lockInstanceIds, () => Promise.resolve());
|
2341
2991
|
}
|
2342
|
-
|
2343
|
-
this.
|
2344
|
-
|
2345
|
-
|
2346
|
-
|
2347
|
-
|
2348
|
-
// TODO: implement source hash
|
2349
|
-
});
|
2992
|
+
try {
|
2993
|
+
await this.projectLock.lockInstances(lockInstanceIds, () => this.processOperation());
|
2994
|
+
} finally {
|
2995
|
+
if (this.operation.type === "preview") {
|
2996
|
+
this.workset.emitAffectedInitialStates();
|
2997
|
+
}
|
2350
2998
|
}
|
2351
|
-
|
2352
|
-
|
2353
|
-
|
2354
|
-
|
2999
|
+
}
|
3000
|
+
async processOperation() {
|
3001
|
+
this.operation.affectedInstanceIds = this.workset.operation.affectedInstanceIds;
|
3002
|
+
this.logger.info(
|
3003
|
+
{ affectedInstanceIds: this.operation.affectedInstanceIds },
|
3004
|
+
"operation started"
|
2355
3005
|
);
|
2356
|
-
|
2357
|
-
|
2358
|
-
|
2359
|
-
} else if (this.operation.type === "destroy" && this.operation.options.destroyDependentInstances !== false) {
|
2360
|
-
this.extendWithCreatedDependents();
|
2361
|
-
await this.updateOperation();
|
2362
|
-
}
|
2363
|
-
this.logger.info("all instances loaded", {
|
2364
|
-
count: allInstances.length,
|
2365
|
-
affectedCount: this.operation.instanceIds.length
|
2366
|
-
});
|
2367
|
-
if (this.operation.type === "evaluate") {
|
2368
|
-
this.logger.info("evaluating instances");
|
2369
|
-
await this.evaluateCompositeInstances();
|
2370
|
-
} else {
|
3006
|
+
const phases = this.getOperationPhases();
|
3007
|
+
for (const phase of phases) {
|
3008
|
+
this.currentPhase = phase;
|
2371
3009
|
const promises = [];
|
2372
|
-
for (const instanceId of this.operation.
|
3010
|
+
for (const instanceId of this.operation.affectedInstanceIds) {
|
3011
|
+
const instance = this.workset.getInstance(instanceId);
|
3012
|
+
if (instance.parentId && this.workset.isAffected(instance.parentId)) {
|
3013
|
+
continue;
|
3014
|
+
}
|
2373
3015
|
promises.push(this.getInstancePromiseForOperation(instanceId));
|
2374
3016
|
}
|
2375
|
-
this.logger.info(
|
3017
|
+
this.logger.info(`all operations for phase "%s" started`, phase);
|
2376
3018
|
this.operation.status = "running";
|
2377
3019
|
await this.updateOperation();
|
2378
3020
|
await Promise.all(promises);
|
2379
|
-
this.logger.info(
|
3021
|
+
this.logger.info(`all operations for phase "%s" completed`, phase);
|
2380
3022
|
}
|
2381
3023
|
this.operation.status = "completed";
|
2382
3024
|
this.operation.error = null;
|
@@ -2388,30 +3030,33 @@ class RuntimeOperation {
|
|
2388
3030
|
this.abortController.abort();
|
2389
3031
|
}
|
2390
3032
|
getInstancePromiseForOperation(instanceId) {
|
2391
|
-
const instance = this.
|
2392
|
-
|
2393
|
-
throw new Error(`Instance not found: ${instanceId}`);
|
2394
|
-
}
|
2395
|
-
const component = this.library.components[instance.type];
|
2396
|
-
if (!component) {
|
2397
|
-
throw new Error(`Component not found: ${instance.type}`);
|
2398
|
-
}
|
3033
|
+
const instance = this.workset.getInstance(instanceId);
|
3034
|
+
const component = this.workset.library.components[instance.type];
|
2399
3035
|
if (isUnitModel(component)) {
|
2400
|
-
return this.getUnitPromise(instance
|
3036
|
+
return this.getUnitPromise(instance);
|
2401
3037
|
}
|
2402
3038
|
return this.getCompositePromise(instance);
|
2403
3039
|
}
|
2404
|
-
|
3040
|
+
getOperationPhases() {
|
2405
3041
|
switch (this.operation.type) {
|
2406
3042
|
case "update":
|
2407
|
-
case "preview":
|
2408
|
-
return
|
2409
|
-
|
2410
|
-
|
2411
|
-
|
3043
|
+
case "preview":
|
3044
|
+
return ["update"];
|
3045
|
+
case "recreate":
|
3046
|
+
return ["destroy", "update"];
|
3047
|
+
case "destroy":
|
3048
|
+
return ["destroy"];
|
3049
|
+
case "refresh":
|
3050
|
+
return ["refresh"];
|
3051
|
+
}
|
3052
|
+
}
|
3053
|
+
async getUnitPromise(instance) {
|
3054
|
+
switch (this.currentPhase) {
|
3055
|
+
case "update": {
|
3056
|
+
return this.updateUnit(instance);
|
2412
3057
|
}
|
2413
3058
|
case "destroy": {
|
2414
|
-
return this.destroyUnit(instance.id
|
3059
|
+
return this.destroyUnit(instance.id);
|
2415
3060
|
}
|
2416
3061
|
case "refresh": {
|
2417
3062
|
return this.refreshUnit(instance.id);
|
@@ -2421,39 +3066,38 @@ class RuntimeOperation {
|
|
2421
3066
|
async getCompositePromise(instance) {
|
2422
3067
|
const logger = this.logger.child({ instanceId: instance.id });
|
2423
3068
|
return this.getInstancePromise(instance.id, async () => {
|
2424
|
-
const
|
3069
|
+
const state = this.workset.getState(instance.id) ?? createInstanceState(instance.id);
|
2425
3070
|
this.updateInstanceState({
|
2426
|
-
|
3071
|
+
...state,
|
3072
|
+
parentId: instance.parentId,
|
2427
3073
|
latestOperationId: this.operation.id,
|
2428
3074
|
status: this.getStatusByOperationType(),
|
2429
|
-
|
2430
|
-
totalResourceCount: 0
|
3075
|
+
error: null
|
2431
3076
|
});
|
2432
|
-
const
|
3077
|
+
const children = this.workset.getAffectedCompositeChildren(instance.id);
|
2433
3078
|
const childPromises = [];
|
2434
|
-
if (
|
2435
|
-
logger.
|
3079
|
+
if (children.length) {
|
3080
|
+
logger.info("running %s children", children.length);
|
3081
|
+
} else {
|
3082
|
+
logger.warn("no affected children found for composite component");
|
2436
3083
|
}
|
2437
3084
|
for (const child of children) {
|
2438
|
-
logger.
|
3085
|
+
logger.debug(`waiting for child: "%s"`, child.id);
|
2439
3086
|
childPromises.push(this.getInstancePromiseForOperation(child.id));
|
2440
3087
|
}
|
2441
3088
|
try {
|
2442
3089
|
await Promise.all(childPromises);
|
2443
|
-
this.abortController.signal.throwIfAborted();
|
2444
3090
|
if (children.length > 0) {
|
2445
|
-
logger.info("all children completed"
|
3091
|
+
logger.info("all children completed");
|
2446
3092
|
}
|
2447
3093
|
this.updateInstanceState({
|
2448
3094
|
id: instance.id,
|
2449
|
-
status: "created"
|
3095
|
+
status: this.operation.type === "destroy" ? "not_created" : "created",
|
3096
|
+
inputHash: await this.workset.getUpToDateInputHash(instance)
|
2450
3097
|
});
|
2451
3098
|
} catch (error) {
|
2452
|
-
if (
|
2453
|
-
this.
|
2454
|
-
id: instance.id,
|
2455
|
-
status: initialStatus
|
2456
|
-
});
|
3099
|
+
if (isAbortErrorLike(error)) {
|
3100
|
+
this.workset.restoreInitialStatus(instance.id);
|
2457
3101
|
return;
|
2458
3102
|
}
|
2459
3103
|
this.updateInstanceState({
|
@@ -2464,31 +3108,24 @@ class RuntimeOperation {
|
|
2464
3108
|
}
|
2465
3109
|
});
|
2466
3110
|
}
|
2467
|
-
updateUnit(instance
|
2468
|
-
|
2469
|
-
return this.getInstancePromise(instance.id, async () => {
|
3111
|
+
updateUnit(instance) {
|
3112
|
+
return this.getInstancePromise(instance.id, async (logger) => {
|
2470
3113
|
this.updateInstanceState({
|
2471
3114
|
id: instance.id,
|
3115
|
+
parentId: instance.parentId,
|
2472
3116
|
latestOperationId: this.operation.id,
|
2473
3117
|
status: "pending",
|
2474
|
-
|
2475
|
-
|
3118
|
+
error: null,
|
3119
|
+
currentResourceCount: 0
|
2476
3120
|
});
|
2477
|
-
|
2478
|
-
|
2479
|
-
|
2480
|
-
|
2481
|
-
|
2482
|
-
|
2483
|
-
logger.info("waiting for dependency", { dependencyId: dependency.id });
|
2484
|
-
dependencyPromises.push(this.getInstancePromiseForOperation(dependency.id));
|
2485
|
-
}
|
2486
|
-
await Promise.all(dependencyPromises);
|
2487
|
-
this.abortController.signal.throwIfAborted();
|
2488
|
-
if (dependencies.length > 0) {
|
2489
|
-
logger.info("all dependencies completed", { count: dependencies.length });
|
3121
|
+
let dependencyIds = [];
|
3122
|
+
try {
|
3123
|
+
dependencyIds = await this.updateUnitDependencies(instance, logger);
|
3124
|
+
} catch (error) {
|
3125
|
+
this.workset.restoreInitialStatus(instance.id);
|
3126
|
+
throw error;
|
2490
3127
|
}
|
2491
|
-
logger.info("updating unit
|
3128
|
+
logger.info("updating unit");
|
2492
3129
|
const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
|
2493
3130
|
this.abortController.signal.throwIfAborted();
|
2494
3131
|
logger.debug("secrets loaded", { count: Object.keys(secrets).length });
|
@@ -2496,10 +3133,9 @@ class RuntimeOperation {
|
|
2496
3133
|
projectId: this.operation.projectId,
|
2497
3134
|
instanceType: instance.type,
|
2498
3135
|
instanceName: instance.name,
|
2499
|
-
config:
|
3136
|
+
config: this.prepareUnitConfig(instance),
|
2500
3137
|
refresh: this.operation.options.refresh,
|
2501
3138
|
secrets: mapValues(secrets, (value) => valueToString(value)),
|
2502
|
-
source: this.resolveUnitSource(component),
|
2503
3139
|
signal: this.abortController.signal
|
2504
3140
|
});
|
2505
3141
|
logger.debug("unit update requested");
|
@@ -2510,55 +3146,61 @@ class RuntimeOperation {
|
|
2510
3146
|
finalStatuses: ["created", "error"]
|
2511
3147
|
});
|
2512
3148
|
await this.watchStateStream(stream);
|
2513
|
-
const inputHash = await this.getUpToDateInputHash(instance);
|
3149
|
+
const inputHash = await this.workset.getUpToDateInputHash(instance);
|
2514
3150
|
this.updateInstanceState({
|
2515
3151
|
id: instance.id,
|
2516
3152
|
inputHash,
|
2517
|
-
dependencyIds
|
3153
|
+
dependencyIds
|
2518
3154
|
});
|
2519
3155
|
logger.debug("input hash after update", { inputHash });
|
2520
3156
|
logger.info("unit updated");
|
2521
3157
|
});
|
2522
3158
|
}
|
2523
|
-
async
|
2524
|
-
|
2525
|
-
this.
|
2526
|
-
|
2527
|
-
|
2528
|
-
|
2529
|
-
|
2530
|
-
|
2531
|
-
|
2532
|
-
|
2533
|
-
|
2534
|
-
|
3159
|
+
async updateUnitDependencies(instance, logger) {
|
3160
|
+
try {
|
3161
|
+
const dependencies = this.getInstanceDependencies(instance);
|
3162
|
+
const dependencyPromises = [];
|
3163
|
+
for (const dependency of dependencies) {
|
3164
|
+
if (!this.operation.affectedInstanceIds.includes(dependency.id)) {
|
3165
|
+
continue;
|
3166
|
+
}
|
3167
|
+
logger.debug(`waiting for dependency: ${dependency.id}`);
|
3168
|
+
dependencyPromises.push(this.getInstancePromiseForOperation(dependency.id));
|
3169
|
+
}
|
3170
|
+
await Promise.all(dependencyPromises);
|
3171
|
+
if (dependencies.length > 0) {
|
3172
|
+
logger.info("all dependencies completed");
|
3173
|
+
}
|
3174
|
+
return dependencies.map((dependency) => dependency.id);
|
3175
|
+
} catch (error) {
|
3176
|
+
this.workset.restoreInitialStatus(instance.id);
|
3177
|
+
throw error;
|
3178
|
+
}
|
2535
3179
|
}
|
2536
|
-
async processBeforeDestroyTriggers(state,
|
3180
|
+
async processBeforeDestroyTriggers(state, logger) {
|
2537
3181
|
if (!this.operation.options.invokeDestroyTriggers) {
|
2538
3182
|
logger.debug("destroy triggers are disabled for the operation");
|
2539
3183
|
return;
|
2540
3184
|
}
|
2541
|
-
const instance = this.
|
2542
|
-
if (!instance) {
|
2543
|
-
throw new Error(`Instance not found: ${state.id}`);
|
2544
|
-
}
|
3185
|
+
const instance = this.workset.getInstance(state.id);
|
2545
3186
|
const triggers = state.triggers.filter((trigger) => trigger.spec.type === "before-destroy");
|
2546
3187
|
if (triggers.length === 0) {
|
2547
3188
|
return;
|
2548
3189
|
}
|
2549
3190
|
const invokedTriggers = triggers.map((trigger) => ({ name: trigger.name }));
|
2550
3191
|
logger.info("updating unit to process before-destroy triggers...");
|
2551
|
-
const secrets = await this.secretBackend.get(
|
2552
|
-
|
2553
|
-
|
3192
|
+
const secrets = await this.secretBackend.get(
|
3193
|
+
this.operation.projectId,
|
3194
|
+
instance.id,
|
3195
|
+
this.abortController.signal
|
3196
|
+
);
|
2554
3197
|
await this.runnerBackend.update({
|
2555
3198
|
projectId: this.operation.projectId,
|
2556
3199
|
instanceType: instance.type,
|
2557
3200
|
instanceName: instance.name,
|
2558
|
-
config:
|
3201
|
+
config: this.prepareUnitConfig(instance, invokedTriggers),
|
2559
3202
|
refresh: this.operation.options.refresh,
|
2560
3203
|
secrets: mapValues(secrets, (value) => valueToString(value)),
|
2561
|
-
source: this.resolveUnitSource(component),
|
2562
3204
|
signal: this.abortController.signal
|
2563
3205
|
});
|
2564
3206
|
logger.debug("unit update requested");
|
@@ -2571,29 +3213,27 @@ class RuntimeOperation {
|
|
2571
3213
|
await this.watchStateStream(stream);
|
2572
3214
|
logger.debug("before-destroy triggers processed");
|
2573
3215
|
}
|
2574
|
-
async destroyUnit(instanceId
|
2575
|
-
|
2576
|
-
return this.getInstancePromise(instanceId, async () => {
|
3216
|
+
async destroyUnit(instanceId) {
|
3217
|
+
return this.getInstancePromise(instanceId, async (logger) => {
|
2577
3218
|
this.updateInstanceState({
|
2578
3219
|
id: instanceId,
|
2579
3220
|
latestOperationId: this.operation.id,
|
2580
3221
|
status: "pending",
|
2581
|
-
|
2582
|
-
totalResourceCount: 0
|
3222
|
+
error: null
|
2583
3223
|
});
|
2584
|
-
const state = this.
|
3224
|
+
const state = this.workset.getState(instanceId);
|
2585
3225
|
if (!state) {
|
2586
3226
|
logger.warn("state not found for unit, but destroy was requested");
|
2587
3227
|
return;
|
2588
3228
|
}
|
2589
3229
|
const dependentPromises = [];
|
2590
|
-
const dependents = this.
|
3230
|
+
const dependents = this.workset.getDependentStates(state.id);
|
2591
3231
|
for (const dependent of dependents) {
|
2592
3232
|
dependentPromises.push(this.getInstancePromiseForOperation(dependent.id));
|
2593
3233
|
}
|
2594
3234
|
await Promise.all(dependentPromises);
|
2595
3235
|
this.abortController.signal.throwIfAborted();
|
2596
|
-
await this.processBeforeDestroyTriggers(state,
|
3236
|
+
await this.processBeforeDestroyTriggers(state, logger);
|
2597
3237
|
logger.info("destroying unit...");
|
2598
3238
|
const [type, name] = parseInstanceId(instanceId);
|
2599
3239
|
await this.runnerBackend.destroy({
|
@@ -2602,7 +3242,6 @@ class RuntimeOperation {
|
|
2602
3242
|
instanceName: name,
|
2603
3243
|
refresh: this.operation.options.refresh,
|
2604
3244
|
signal: this.abortController.signal,
|
2605
|
-
source: this.resolveUnitSource(component),
|
2606
3245
|
deleteUnreachable: this.operation.options.deleteUnreachableResources
|
2607
3246
|
});
|
2608
3247
|
this.logger.debug("destroy request sent");
|
@@ -2616,74 +3255,6 @@ class RuntimeOperation {
|
|
2616
3255
|
this.logger.info("unit destroyed");
|
2617
3256
|
});
|
2618
3257
|
}
|
2619
|
-
async recreateUnit(instance, component) {
|
2620
|
-
const logger = this.logger.child({ instanceId: instance.id });
|
2621
|
-
return this.getInstancePromise(instance.id, async () => {
|
2622
|
-
this.updateInstanceState({
|
2623
|
-
id: instance.id,
|
2624
|
-
latestOperationId: this.operation.id,
|
2625
|
-
status: "pending",
|
2626
|
-
currentResourceCount: 0,
|
2627
|
-
totalResourceCount: 0
|
2628
|
-
});
|
2629
|
-
const state = this.stateMap.get(instance.id);
|
2630
|
-
if (!state) {
|
2631
|
-
logger.warn("state not found for unit, but recreate was requested");
|
2632
|
-
return;
|
2633
|
-
}
|
2634
|
-
await this.processBeforeDestroyTriggers(state, component, logger);
|
2635
|
-
logger.info("destroying unit...");
|
2636
|
-
await this.runnerBackend.destroy({
|
2637
|
-
projectId: this.operation.projectId,
|
2638
|
-
instanceType: instance.type,
|
2639
|
-
instanceName: instance.name,
|
2640
|
-
refresh: this.operation.options.refresh,
|
2641
|
-
signal: this.abortController.signal,
|
2642
|
-
source: this.resolveUnitSource(component),
|
2643
|
-
deleteUnreachable: this.operation.options.deleteUnreachableResources
|
2644
|
-
});
|
2645
|
-
logger.debug("destroy request sent");
|
2646
|
-
const destroyStream = this.runnerBackend.watch({
|
2647
|
-
projectId: this.operation.projectId,
|
2648
|
-
instanceType: instance.type,
|
2649
|
-
instanceName: instance.name,
|
2650
|
-
finalStatuses: ["not_created", "error"]
|
2651
|
-
});
|
2652
|
-
await this.watchStateStream(destroyStream);
|
2653
|
-
this.abortController.signal.throwIfAborted();
|
2654
|
-
logger.info("unit destroyed");
|
2655
|
-
logger.info("updating unit...");
|
2656
|
-
const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
|
2657
|
-
this.abortController.signal.throwIfAborted();
|
2658
|
-
logger.debug("secrets loaded", { count: Object.keys(secrets).length });
|
2659
|
-
await this.runnerBackend.update({
|
2660
|
-
projectId: this.operation.projectId,
|
2661
|
-
instanceType: instance.type,
|
2662
|
-
instanceName: instance.name,
|
2663
|
-
config: await this.prepareUnitConfig(instance),
|
2664
|
-
refresh: this.operation.options.refresh,
|
2665
|
-
secrets: mapValues(secrets, (value) => valueToString(value)),
|
2666
|
-
source: this.resolveUnitSource(component),
|
2667
|
-
signal: this.abortController.signal
|
2668
|
-
});
|
2669
|
-
logger.debug("unit update requested");
|
2670
|
-
const updateStream = this.runnerBackend.watch({
|
2671
|
-
projectId: this.operation.projectId,
|
2672
|
-
instanceType: instance.type,
|
2673
|
-
instanceName: instance.name,
|
2674
|
-
finalStatuses: ["created", "error"]
|
2675
|
-
});
|
2676
|
-
const inputHash = await this.getUpToDateInputHash(instance);
|
2677
|
-
const dependencies = this.getInstanceDependencies(instance);
|
2678
|
-
this.updateInstanceState({
|
2679
|
-
id: instance.id,
|
2680
|
-
inputHash,
|
2681
|
-
dependencyIds: dependencies.map((dependency) => dependency.id)
|
2682
|
-
});
|
2683
|
-
await this.watchStateStream(updateStream);
|
2684
|
-
logger.info("unit recreated");
|
2685
|
-
});
|
2686
|
-
}
|
2687
3258
|
async refreshUnit(instanceId) {
|
2688
3259
|
const logger = this.logger.child({ instanceId });
|
2689
3260
|
return this.getInstancePromise(instanceId, async () => {
|
@@ -2725,84 +3296,23 @@ class RuntimeOperation {
|
|
2725
3296
|
throw new Error("The stream ended without emitting any state.");
|
2726
3297
|
}
|
2727
3298
|
if (statePatch.status === "error") {
|
2728
|
-
throw
|
3299
|
+
throw tryWrapAbortErrorLike(
|
3300
|
+
new Error(`The operation on unit "${statePatch.id}" failed: ${statePatch.error}`)
|
3301
|
+
);
|
2729
3302
|
}
|
2730
3303
|
}
|
2731
|
-
|
3304
|
+
prepareUnitConfig(instance, invokedTriggers = []) {
|
2732
3305
|
const config = {};
|
2733
3306
|
for (const [key, value] of Object.entries(instance.args ?? {})) {
|
2734
3307
|
config[key] = valueToString(value);
|
2735
3308
|
}
|
2736
|
-
const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
|
3309
|
+
const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {};
|
2737
3310
|
for (const [key, value] of Object.entries(instanceInputs)) {
|
2738
|
-
|
2739
|
-
config[`input.${key}`] = JSON.stringify(resolvedInput);
|
3311
|
+
config[`input.${key}`] = JSON.stringify(value.map((input) => input.input));
|
2740
3312
|
}
|
2741
3313
|
config["$invokedTriggers"] = JSON.stringify(invokedTriggers);
|
2742
3314
|
return config;
|
2743
3315
|
}
|
2744
|
-
async resolveInstanceInput(input) {
|
2745
|
-
const promises = input.map((input2) => this.resolveSingleInstanceInput(input2));
|
2746
|
-
const results = await Promise.all(promises);
|
2747
|
-
return results.flat();
|
2748
|
-
}
|
2749
|
-
async resolveSingleInstanceInput(input) {
|
2750
|
-
const loadedInstance = this.instanceMap.get(input.instanceId);
|
2751
|
-
if (loadedInstance && !loadedInstance.parentId) {
|
2752
|
-
return [input];
|
2753
|
-
}
|
2754
|
-
const parentInstance = await this.loadCompositeInstance(input.instanceId);
|
2755
|
-
const parentOutput = parentInstance.instance.outputs?.[input.output];
|
2756
|
-
if (!parentOutput) {
|
2757
|
-
throw new Error(`Output "${input.output}" not found in "${input.instanceId}"`);
|
2758
|
-
}
|
2759
|
-
return await this.resolveInstanceInput(parentOutput);
|
2760
|
-
}
|
2761
|
-
async loadCompositeInstance(instanceId) {
|
2762
|
-
return await this.compositeInstanceLock.acquire(instanceId, async () => {
|
2763
|
-
const existingInstance = this.compositeInstanceMap.get(instanceId);
|
2764
|
-
if (existingInstance) {
|
2765
|
-
return existingInstance;
|
2766
|
-
}
|
2767
|
-
const compositeInstance = await this.stateBackend.getCompositeInstance(
|
2768
|
-
this.operation.projectId,
|
2769
|
-
instanceId
|
2770
|
-
);
|
2771
|
-
if (!compositeInstance) {
|
2772
|
-
throw new Error(`Composite instance not found: ${instanceId}`);
|
2773
|
-
}
|
2774
|
-
this.abortController.signal.throwIfAborted();
|
2775
|
-
for (const child of compositeInstance.children) {
|
2776
|
-
this.instanceMap.set(child.id, child);
|
2777
|
-
}
|
2778
|
-
this.compositeInstanceMap.set(instanceId, compositeInstance);
|
2779
|
-
return compositeInstance;
|
2780
|
-
});
|
2781
|
-
}
|
2782
|
-
async evaluateCompositeInstances() {
|
2783
|
-
await this.projectLock.lockInstances(this.operation.instanceIds, async () => {
|
2784
|
-
const compositeInstances = await this.libraryBackend.evaluateCompositeInstances(
|
2785
|
-
Array.from(this.instanceMap.values()),
|
2786
|
-
this.operation.instanceIds
|
2787
|
-
);
|
2788
|
-
this.abortController.signal.throwIfAborted();
|
2789
|
-
for (const instance of compositeInstances) {
|
2790
|
-
this.compositeInstanceEE.emit(
|
2791
|
-
`${this.operation.projectId}/${instance.instance.id}`,
|
2792
|
-
instance
|
2793
|
-
);
|
2794
|
-
}
|
2795
|
-
await this.stateBackend.putCompositeInstances(
|
2796
|
-
this.operation.projectId,
|
2797
|
-
await Promise.all(
|
2798
|
-
compositeInstances.map(async (instance) => ({
|
2799
|
-
...instance,
|
2800
|
-
inputHash: await this.resolveInputHash(instance.instance.id)
|
2801
|
-
}))
|
2802
|
-
)
|
2803
|
-
);
|
2804
|
-
});
|
2805
|
-
}
|
2806
3316
|
async updateOperation() {
|
2807
3317
|
this.operationEE.emit(this.operation.projectId, this.operation);
|
2808
3318
|
await this.stateBackend.putOperation(this.operation);
|
@@ -2812,139 +3322,32 @@ class RuntimeOperation {
|
|
2812
3322
|
throw new Error("The ID of the instance state is required.");
|
2813
3323
|
}
|
2814
3324
|
if (patch.logLine) {
|
2815
|
-
this.
|
2816
|
-
|
3325
|
+
let instance = this.workset.getInstance(patch.id);
|
3326
|
+
for (; ; ) {
|
3327
|
+
this.persistLogs.call([instance.id, patch.logLine]);
|
3328
|
+
this.instanceLogsEE.emit(`${this.operation.id}/${instance.id}`, patch.logLine);
|
3329
|
+
if (!instance.parentId) {
|
3330
|
+
break;
|
3331
|
+
}
|
3332
|
+
instance = this.workset.getInstance(instance.parentId);
|
3333
|
+
}
|
2817
3334
|
return;
|
2818
3335
|
}
|
2819
|
-
const state =
|
3336
|
+
const state = this.workset.updateState(patch);
|
2820
3337
|
if (this.operation.type !== "preview") {
|
2821
3338
|
this.persistStates.call(state);
|
2822
3339
|
if (patch.secrets) {
|
2823
3340
|
this.persistSecrets.call([patch.id, patch.secrets]);
|
2824
3341
|
}
|
2825
3342
|
}
|
2826
|
-
this.stateEE.emit(this.operation.projectId, createInstanceStateFrontendPatch(patch));
|
2827
|
-
}
|
2828
|
-
tryAddStateToParent(state) {
|
2829
|
-
if (!state.parentId) {
|
2830
|
-
return;
|
2831
|
-
}
|
2832
|
-
const children = this.childrenStateMap.get(state.parentId) ?? [];
|
2833
|
-
children.push(state);
|
2834
|
-
this.childrenStateMap.set(state.parentId, children);
|
2835
|
-
}
|
2836
|
-
removeStateFromParent(state) {
|
2837
|
-
if (!state.parentId) {
|
2838
|
-
return;
|
2839
|
-
}
|
2840
|
-
const children = this.childrenStateMap.get(state.parentId) ?? [];
|
2841
|
-
const index = children.findIndex((child) => child.id === state.id);
|
2842
|
-
if (index !== -1) {
|
2843
|
-
children.splice(index, 1);
|
2844
|
-
this.childrenStateMap.set(state.parentId, children);
|
2845
|
-
}
|
2846
|
-
}
|
2847
|
-
addDependentState(state) {
|
2848
|
-
const instance = this.instanceMap.get(state.id);
|
2849
|
-
if (!instance) {
|
2850
|
-
return;
|
2851
|
-
}
|
2852
|
-
for (const dependency of state.dependencyIds) {
|
2853
|
-
const dependents = this.dependentStateMap.get(dependency) ?? [];
|
2854
|
-
dependents.push(state);
|
2855
|
-
this.dependentStateMap.set(dependency, dependents);
|
2856
|
-
}
|
2857
|
-
}
|
2858
|
-
removeDependentState(state) {
|
2859
|
-
const instance = this.instanceMap.get(state.id);
|
2860
|
-
if (!instance) {
|
2861
|
-
return;
|
2862
|
-
}
|
2863
|
-
const instanceInputs = this.resolvedInstanceInputs.get(state.id) ?? {};
|
2864
|
-
for (const inputs of Object.values(instanceInputs)) {
|
2865
|
-
for (const input of inputs) {
|
2866
|
-
const dependents = this.dependentStateMap.get(input.input.instanceId) ?? [];
|
2867
|
-
const index = dependents.findIndex((dependent) => dependent.id === state.id);
|
2868
|
-
if (index !== -1) {
|
2869
|
-
dependents.splice(index, 1);
|
2870
|
-
this.dependentStateMap.set(input.input.instanceId, dependents);
|
2871
|
-
}
|
2872
|
-
}
|
2873
|
-
}
|
2874
|
-
}
|
2875
|
-
async extendWithDependencies() {
|
2876
|
-
const instanceIdsSet = /* @__PURE__ */ new Set();
|
2877
|
-
const traverse = async (instanceId) => {
|
2878
|
-
if (instanceIdsSet.has(instanceId)) {
|
2879
|
-
return;
|
2880
|
-
}
|
2881
|
-
const instance = this.instanceMap.get(instanceId);
|
2882
|
-
if (!instance) {
|
2883
|
-
return;
|
2884
|
-
}
|
2885
|
-
const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
|
2886
|
-
for (const inputs of Object.values(instanceInputs)) {
|
2887
|
-
for (const input of inputs) {
|
2888
|
-
await traverse(input.input.instanceId);
|
2889
|
-
}
|
2890
|
-
}
|
2891
|
-
const state = this.stateMap.get(instance.id);
|
2892
|
-
const expectedInputHash = await this.resolveInputHash(instance.id);
|
2893
|
-
if (this.operation.options.forceUpdateDependencies) {
|
2894
|
-
instanceIdsSet.add(instanceId);
|
2895
|
-
return;
|
2896
|
-
}
|
2897
|
-
if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
|
2898
|
-
instanceIdsSet.add(instanceId);
|
2899
|
-
}
|
2900
|
-
};
|
2901
|
-
for (const instanceId of this.operation.instanceIds) {
|
2902
|
-
await traverse(instanceId);
|
2903
|
-
instanceIdsSet.add(instanceId);
|
2904
|
-
}
|
2905
|
-
this.operation.instanceIds = Array.from(instanceIdsSet);
|
2906
|
-
}
|
2907
|
-
extendWithCreatedDependents() {
|
2908
|
-
const instanceIdsSet = /* @__PURE__ */ new Set();
|
2909
|
-
const traverse = (instanceKey) => {
|
2910
|
-
if (instanceIdsSet.has(instanceKey)) {
|
2911
|
-
return;
|
2912
|
-
}
|
2913
|
-
const state = this.stateMap.get(instanceKey);
|
2914
|
-
if (!state || state.status === "not_created") {
|
2915
|
-
return;
|
2916
|
-
}
|
2917
|
-
const dependents = this.dependentStateMap.get(instanceKey) ?? [];
|
2918
|
-
for (const dependent of dependents) {
|
2919
|
-
traverse(dependent.id);
|
2920
|
-
instanceIdsSet.add(instanceKey);
|
2921
|
-
}
|
2922
|
-
};
|
2923
|
-
for (const instanceId of this.operation.instanceIds) {
|
2924
|
-
const instance = this.instanceMap.get(instanceId);
|
2925
|
-
if (!instance) {
|
2926
|
-
throw new Error(`Instance not found: ${instanceId}`);
|
2927
|
-
}
|
2928
|
-
traverse(instance.id);
|
2929
|
-
instanceIdsSet.add(instanceId);
|
2930
|
-
}
|
2931
|
-
this.operation.instanceIds = Array.from(instanceIdsSet);
|
2932
3343
|
}
|
2933
3344
|
getInstancePromise(instanceId, fn) {
|
2934
3345
|
let instancePromise = this.instancePromiseMap.get(instanceId);
|
2935
3346
|
if (instancePromise) {
|
2936
3347
|
return instancePromise;
|
2937
3348
|
}
|
2938
|
-
|
2939
|
-
|
2940
|
-
await this.projectLock.lockInstance(instanceId, async () => {
|
2941
|
-
if (!wasImmediatelyAcquired) {
|
2942
|
-
await this.refetchState(instanceId);
|
2943
|
-
}
|
2944
|
-
return await fn();
|
2945
|
-
});
|
2946
|
-
})();
|
2947
|
-
instancePromise = instancePromise.finally(() => this.instancePromiseMap.delete(instanceId));
|
3349
|
+
const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` });
|
3350
|
+
instancePromise = fn(logger).finally(() => this.instancePromiseMap.delete(instanceId));
|
2948
3351
|
this.instancePromiseMap.set(instanceId, instancePromise);
|
2949
3352
|
return instancePromise;
|
2950
3353
|
}
|
@@ -2960,51 +3363,19 @@ class RuntimeOperation {
|
|
2960
3363
|
return "destroying";
|
2961
3364
|
case "refresh":
|
2962
3365
|
return "refreshing";
|
2963
|
-
default:
|
2964
|
-
throw new Error(`Unexpected operation type: ${this.operation.type}`);
|
2965
3366
|
}
|
2966
3367
|
}
|
2967
3368
|
getInstanceDependencies(instance) {
|
2968
3369
|
const dependencies = [];
|
2969
|
-
const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
|
3370
|
+
const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {};
|
2970
3371
|
for (const inputs of Object.values(instanceInputs)) {
|
2971
3372
|
for (const input of inputs) {
|
2972
|
-
const dependency = this.
|
2973
|
-
|
2974
|
-
dependencies.push(dependency);
|
2975
|
-
}
|
3373
|
+
const dependency = this.workset.getInstance(input.input.instanceId);
|
3374
|
+
dependencies.push(dependency);
|
2976
3375
|
}
|
2977
3376
|
}
|
2978
3377
|
return dependencies;
|
2979
3378
|
}
|
2980
|
-
resolveUnitSource(component) {
|
2981
|
-
if (!component.source || component.source.type === "local") {
|
2982
|
-
return {
|
2983
|
-
type: "local",
|
2984
|
-
// auto-generate path for local units
|
2985
|
-
path: component.source?.path ?? component.type.split(".").join("/")
|
2986
|
-
};
|
2987
|
-
}
|
2988
|
-
return component.source;
|
2989
|
-
}
|
2990
|
-
async refetchState(instanceId) {
|
2991
|
-
this.logger.info({ instanceId }, "refetching state since the lock was not immediately acquired");
|
2992
|
-
const state = await this.stateBackend.getInstanceState(this.operation.projectId, instanceId);
|
2993
|
-
const oldState = this.stateMap.get(instanceId);
|
2994
|
-
if (oldState) {
|
2995
|
-
this.removeStateFromParent(oldState);
|
2996
|
-
this.removeDependentState(oldState);
|
2997
|
-
}
|
2998
|
-
if (state) {
|
2999
|
-
this.stateMap.set(instanceId, state);
|
3000
|
-
this.initialStatusMap.set(instanceId, state.status);
|
3001
|
-
this.tryAddStateToParent(state);
|
3002
|
-
this.addDependentState(state);
|
3003
|
-
} else {
|
3004
|
-
this.stateMap.delete(instanceId);
|
3005
|
-
this.initialStatusMap.delete(instanceId);
|
3006
|
-
}
|
3007
|
-
}
|
3008
3379
|
persistStates = createAsyncBatcher(async (states) => {
|
3009
3380
|
this.logger.debug({ msg: "persisting states", count: states.length });
|
3010
3381
|
await this.stateBackend.putAffectedInstanceStates(
|
@@ -3027,47 +3398,22 @@ class RuntimeOperation {
|
|
3027
3398
|
}
|
3028
3399
|
}
|
3029
3400
|
);
|
3030
|
-
static abortMessagePatterns = [
|
3031
|
-
"Operation aborted",
|
3032
|
-
"Command was killed with SIGINT"
|
3033
|
-
];
|
3034
|
-
static isAbortError(err) {
|
3035
|
-
if (isAbortError(err)) {
|
3036
|
-
return true;
|
3037
|
-
}
|
3038
|
-
if (err instanceof Error) {
|
3039
|
-
return RuntimeOperation.abortMessagePatterns.some((pattern) => err.message.includes(pattern));
|
3040
|
-
}
|
3041
|
-
return false;
|
3042
|
-
}
|
3043
3401
|
}
|
3044
3402
|
|
3045
3403
|
class OperationManager {
|
3046
|
-
constructor(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, logger) {
|
3404
|
+
constructor(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, stateManager, logger) {
|
3047
3405
|
this.runnerBackend = runnerBackend;
|
3048
3406
|
this.stateBackend = stateBackend;
|
3049
3407
|
this.libraryBackend = libraryBackend;
|
3050
3408
|
this.projectBackend = projectBackend;
|
3051
3409
|
this.secretBackend = secretBackend;
|
3052
3410
|
this.projectLockManager = projectLockManager;
|
3411
|
+
this.stateManager = stateManager;
|
3053
3412
|
this.logger = logger;
|
3054
3413
|
}
|
3055
3414
|
operationEE = new EventEmitter();
|
3056
|
-
stateEE = new EventEmitter();
|
3057
|
-
compositeInstanceEE = new EventEmitter();
|
3058
3415
|
instanceLogsEE = new EventEmitter();
|
3059
3416
|
runtimeOperations = /* @__PURE__ */ new Map();
|
3060
|
-
/**
|
3061
|
-
* Watches for all instance state changes in the project.
|
3062
|
-
*
|
3063
|
-
* @param projectId The project ID to watch.
|
3064
|
-
* @param signal The signal to abort the operation.
|
3065
|
-
*/
|
3066
|
-
async *watchInstanceStates(projectId, signal) {
|
3067
|
-
for await (const [state] of on(this.stateEE, projectId, { signal })) {
|
3068
|
-
yield state;
|
3069
|
-
}
|
3070
|
-
}
|
3071
3417
|
/**
|
3072
3418
|
* Watches for all project operations in the project.
|
3073
3419
|
*
|
@@ -3079,19 +3425,6 @@ class OperationManager {
|
|
3079
3425
|
yield operation;
|
3080
3426
|
}
|
3081
3427
|
}
|
3082
|
-
/**
|
3083
|
-
* Watches for changes in the composite instance.
|
3084
|
-
*
|
3085
|
-
* @param projectId The project ID to watch.
|
3086
|
-
* @param instanceId The instance ID to watch.
|
3087
|
-
* @param signal The signal to abort the operation.
|
3088
|
-
*/
|
3089
|
-
async *watchCompositeInstance(projectId, instanceId, signal) {
|
3090
|
-
const eventKey = `${projectId}/${instanceId}`;
|
3091
|
-
for await (const [instance] of on(this.compositeInstanceEE, eventKey, { signal })) {
|
3092
|
-
yield instance;
|
3093
|
-
}
|
3094
|
-
}
|
3095
3428
|
/**
|
3096
3429
|
* Watches for logs of the instance in the operation.
|
3097
3430
|
*
|
@@ -3116,6 +3449,7 @@ class OperationManager {
|
|
3116
3449
|
projectId: request.projectId,
|
3117
3450
|
type: request.type,
|
3118
3451
|
instanceIds: request.instanceIds,
|
3452
|
+
affectedInstanceIds: request.instanceIds,
|
3119
3453
|
status: "pending",
|
3120
3454
|
options: {
|
3121
3455
|
forceUpdateDependencies: request.options?.forceUpdateDependencies ?? false,
|
@@ -3152,16 +3486,15 @@ class OperationManager {
|
|
3152
3486
|
this.projectBackend,
|
3153
3487
|
this.secretBackend,
|
3154
3488
|
this.projectLockManager.getLock(operation.projectId),
|
3489
|
+
this.stateManager,
|
3155
3490
|
this.operationEE,
|
3156
|
-
this.stateEE,
|
3157
|
-
this.compositeInstanceEE,
|
3158
3491
|
this.instanceLogsEE,
|
3159
3492
|
this.logger.child({ service: "RuntimeOperation", operationId: operation.id })
|
3160
3493
|
);
|
3161
3494
|
this.runtimeOperations.set(operation.id, runtimeOperation);
|
3162
3495
|
void runtimeOperation.operateSafe().finally(() => this.runtimeOperations.delete(operation.id));
|
3163
3496
|
}
|
3164
|
-
static async create(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, logger) {
|
3497
|
+
static async create(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, stateManager, logger) {
|
3165
3498
|
const operator = new OperationManager(
|
3166
3499
|
runnerBackend,
|
3167
3500
|
stateBackend,
|
@@ -3169,6 +3502,7 @@ class OperationManager {
|
|
3169
3502
|
projectBackend,
|
3170
3503
|
secretBackend,
|
3171
3504
|
projectLockManager,
|
3505
|
+
stateManager,
|
3172
3506
|
logger.child({ service: "OperationManager" })
|
3173
3507
|
);
|
3174
3508
|
const activeOperations = await stateBackend.getActiveOperations();
|
@@ -3191,6 +3525,7 @@ async function createServices({
|
|
3191
3525
|
projectBackend,
|
3192
3526
|
projectManager,
|
3193
3527
|
stateBackend,
|
3528
|
+
stateManager,
|
3194
3529
|
operationManager,
|
3195
3530
|
terminalBackend,
|
3196
3531
|
terminalManager,
|
@@ -3205,10 +3540,11 @@ async function createServices({
|
|
3205
3540
|
});
|
3206
3541
|
const localPulumiHost = LocalPulumiHost.create(logger);
|
3207
3542
|
const projectLockManager = new ProjectLockManager();
|
3208
|
-
|
3543
|
+
stateManager ??= new StateManager();
|
3544
|
+
libraryBackend ??= await createLibraryBackend(config, logger);
|
3209
3545
|
secretBackend ??= await createSecretBackend(config, localPulumiHost, logger);
|
3210
3546
|
stateBackend ??= await createStateBackend(config, localPulumiHost, logger);
|
3211
|
-
runnerBackend ??=
|
3547
|
+
runnerBackend ??= createRunnerBackend(config, localPulumiHost, libraryBackend);
|
3212
3548
|
projectBackend ??= await createProjectBackend(config);
|
3213
3549
|
terminalBackend ??= createTerminalBackend(config, logger);
|
3214
3550
|
terminalManager ??= TerminalManager.create(terminalBackend, stateBackend, runnerBackend, logger);
|
@@ -3219,13 +3555,15 @@ async function createServices({
|
|
3219
3555
|
projectBackend,
|
3220
3556
|
secretBackend,
|
3221
3557
|
projectLockManager,
|
3558
|
+
stateManager,
|
3222
3559
|
logger
|
3223
3560
|
);
|
3224
3561
|
projectManager ??= ProjectManager.create(
|
3225
3562
|
projectBackend,
|
3226
3563
|
stateBackend,
|
3227
|
-
operationManager,
|
3228
3564
|
libraryBackend,
|
3565
|
+
projectLockManager,
|
3566
|
+
stateManager,
|
3229
3567
|
logger
|
3230
3568
|
);
|
3231
3569
|
workspaceBackend ??= await createWorkspaceBackend(config);
|
@@ -3238,6 +3576,7 @@ async function createServices({
|
|
3238
3576
|
projectBackend,
|
3239
3577
|
projectManager,
|
3240
3578
|
stateBackend,
|
3579
|
+
stateManager,
|
3241
3580
|
operationManager,
|
3242
3581
|
terminalBackend,
|
3243
3582
|
terminalManager,
|