@highstate/backend 0.7.2 → 0.7.4
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} +1254 -915
- package/dist/library/source-resolution-worker.js +55 -0
- package/dist/library/worker/main.js +216 -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 +15 -19
- 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 +110 -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
@@ -0,0 +1,647 @@
|
|
1
|
+
import type { LibraryBackend } from "../library"
|
2
|
+
import type { LogEntry, StateBackend, StateManager } from "../state"
|
3
|
+
import type { ProjectBackend, ProjectLock } from "../project"
|
4
|
+
import type { SecretBackend } from "../secret"
|
5
|
+
import type { RunnerBackend } from "../runner"
|
6
|
+
import type { InstanceLogsEvents, OperationEvents } from "./manager"
|
7
|
+
import type { EventEmitter } from "node:events"
|
8
|
+
import type { Logger } from "pino"
|
9
|
+
import { isUnitModel, parseInstanceId, type InstanceModel } from "@highstate/contract"
|
10
|
+
import { mapValues } from "remeda"
|
11
|
+
import {
|
12
|
+
type InstanceState,
|
13
|
+
type InstanceStateUpdate,
|
14
|
+
type InstanceStatus,
|
15
|
+
type ProjectOperation,
|
16
|
+
type InstanceTriggerInvocation,
|
17
|
+
createInstanceState,
|
18
|
+
} from "../shared"
|
19
|
+
import {
|
20
|
+
createAsyncBatcher,
|
21
|
+
errorToString,
|
22
|
+
isAbortError,
|
23
|
+
isAbortErrorLike,
|
24
|
+
tryWrapAbortErrorLike,
|
25
|
+
valueToString,
|
26
|
+
} from "../common"
|
27
|
+
import { OperationWorkset } from "./operation-workset"
|
28
|
+
|
29
|
+
type OperationPhase = "update" | "destroy" | "refresh"
|
30
|
+
|
31
|
+
export class RuntimeOperation {
|
32
|
+
private readonly abortController = new AbortController()
|
33
|
+
private readonly instancePromiseMap = new Map<string, Promise<void>>()
|
34
|
+
private workset!: OperationWorkset
|
35
|
+
|
36
|
+
private currentPhase!: OperationPhase
|
37
|
+
|
38
|
+
constructor(
|
39
|
+
private readonly operation: ProjectOperation,
|
40
|
+
private readonly runnerBackend: RunnerBackend,
|
41
|
+
private readonly stateBackend: StateBackend,
|
42
|
+
private readonly libraryBackend: LibraryBackend,
|
43
|
+
private readonly projectBackend: ProjectBackend,
|
44
|
+
private readonly secretBackend: SecretBackend,
|
45
|
+
private readonly projectLock: ProjectLock,
|
46
|
+
private readonly stateManager: StateManager,
|
47
|
+
private readonly operationEE: EventEmitter<OperationEvents>,
|
48
|
+
private readonly instanceLogsEE: EventEmitter<InstanceLogsEvents>,
|
49
|
+
private readonly logger: Logger,
|
50
|
+
) {}
|
51
|
+
|
52
|
+
async operateSafe(): Promise<void> {
|
53
|
+
try {
|
54
|
+
await this.operate()
|
55
|
+
} catch (error) {
|
56
|
+
if (isAbortError(error)) {
|
57
|
+
this.logger.info("the operation was cancelled")
|
58
|
+
this.operation.status = "cancelled"
|
59
|
+
|
60
|
+
await this.updateOperation()
|
61
|
+
return
|
62
|
+
}
|
63
|
+
|
64
|
+
this.logger.error({ msg: "an error occurred while running the operation", error })
|
65
|
+
|
66
|
+
this.operation.status = "failed"
|
67
|
+
this.operation.error = errorToString(error)
|
68
|
+
|
69
|
+
await this.updateOperation()
|
70
|
+
} finally {
|
71
|
+
await Promise.all([
|
72
|
+
this.persistStates.flush(),
|
73
|
+
this.persistLogs.flush(),
|
74
|
+
this.persistSecrets.flush(),
|
75
|
+
])
|
76
|
+
|
77
|
+
this.logger.debug("operation finished, all entries persisted")
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
private async operate(): Promise<void> {
|
82
|
+
this.logger.info("starting operation")
|
83
|
+
let lockInstanceIds: string[]
|
84
|
+
|
85
|
+
// keep recalculating the workset until we can acquire the locks and actually start the operation
|
86
|
+
while (true) {
|
87
|
+
this.workset = await OperationWorkset.load(
|
88
|
+
this.operation,
|
89
|
+
this.projectBackend,
|
90
|
+
this.libraryBackend,
|
91
|
+
this.stateBackend,
|
92
|
+
this.stateManager,
|
93
|
+
this.logger,
|
94
|
+
this.abortController.signal,
|
95
|
+
)
|
96
|
+
|
97
|
+
lockInstanceIds = this.workset.getLockInstanceIds()
|
98
|
+
|
99
|
+
if (this.projectLock.canImmediatelyAcquireLocks(lockInstanceIds)) {
|
100
|
+
break
|
101
|
+
}
|
102
|
+
|
103
|
+
this.logger.info("waiting for locks to be available")
|
104
|
+
|
105
|
+
await this.projectLock.lockInstances(lockInstanceIds, () => Promise.resolve())
|
106
|
+
}
|
107
|
+
|
108
|
+
try {
|
109
|
+
// actually acquire the locks and start the operation
|
110
|
+
await this.projectLock.lockInstances(lockInstanceIds, () => this.processOperation())
|
111
|
+
} finally {
|
112
|
+
if (this.operation.type === "preview") {
|
113
|
+
// stream initial states for preview operations
|
114
|
+
this.workset.emitAffectedInitialStates()
|
115
|
+
}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
private async processOperation(): Promise<void> {
|
120
|
+
this.operation.affectedInstanceIds = this.workset.operation.affectedInstanceIds
|
121
|
+
|
122
|
+
this.logger.info(
|
123
|
+
{ affectedInstanceIds: this.operation.affectedInstanceIds },
|
124
|
+
"operation started",
|
125
|
+
)
|
126
|
+
|
127
|
+
const phases = this.getOperationPhases()
|
128
|
+
|
129
|
+
for (const phase of phases) {
|
130
|
+
this.currentPhase = phase
|
131
|
+
|
132
|
+
const promises: Promise<void>[] = []
|
133
|
+
for (const instanceId of this.operation.affectedInstanceIds) {
|
134
|
+
const instance = this.workset.getInstance(instanceId)
|
135
|
+
if (instance.parentId && this.workset.isAffected(instance.parentId)) {
|
136
|
+
// do not call the operation for child instances of affected composites,
|
137
|
+
// they will be called by their parent instance
|
138
|
+
continue
|
139
|
+
}
|
140
|
+
|
141
|
+
promises.push(this.getInstancePromiseForOperation(instanceId))
|
142
|
+
}
|
143
|
+
|
144
|
+
this.logger.info(`all operations for phase "%s" started`, phase)
|
145
|
+
this.operation.status = "running"
|
146
|
+
await this.updateOperation()
|
147
|
+
|
148
|
+
await Promise.all(promises)
|
149
|
+
|
150
|
+
this.logger.info(`all operations for phase "%s" completed`, phase)
|
151
|
+
}
|
152
|
+
|
153
|
+
this.operation.status = "completed"
|
154
|
+
this.operation.error = null
|
155
|
+
await this.updateOperation()
|
156
|
+
|
157
|
+
this.logger.info("operation completed")
|
158
|
+
}
|
159
|
+
|
160
|
+
cancel() {
|
161
|
+
this.logger.info("cancelling operation")
|
162
|
+
this.abortController.abort()
|
163
|
+
}
|
164
|
+
|
165
|
+
private getInstancePromiseForOperation(instanceId: string): Promise<void> {
|
166
|
+
const instance = this.workset.getInstance(instanceId)
|
167
|
+
const component = this.workset.library.components[instance.type]
|
168
|
+
|
169
|
+
if (isUnitModel(component)) {
|
170
|
+
return this.getUnitPromise(instance)
|
171
|
+
}
|
172
|
+
|
173
|
+
return this.getCompositePromise(instance)
|
174
|
+
}
|
175
|
+
|
176
|
+
private getOperationPhases(): OperationPhase[] {
|
177
|
+
switch (this.operation.type) {
|
178
|
+
case "update":
|
179
|
+
case "preview":
|
180
|
+
return ["update"]
|
181
|
+
case "recreate":
|
182
|
+
return ["destroy", "update"]
|
183
|
+
case "destroy":
|
184
|
+
return ["destroy"]
|
185
|
+
case "refresh":
|
186
|
+
return ["refresh"]
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
private async getUnitPromise(instance: InstanceModel): Promise<void> {
|
191
|
+
switch (this.currentPhase) {
|
192
|
+
case "update": {
|
193
|
+
return this.updateUnit(instance)
|
194
|
+
}
|
195
|
+
case "destroy": {
|
196
|
+
return this.destroyUnit(instance.id)
|
197
|
+
}
|
198
|
+
case "refresh": {
|
199
|
+
return this.refreshUnit(instance.id)
|
200
|
+
}
|
201
|
+
}
|
202
|
+
}
|
203
|
+
|
204
|
+
private async getCompositePromise(instance: InstanceModel): Promise<void> {
|
205
|
+
const logger = this.logger.child({ instanceId: instance.id })
|
206
|
+
|
207
|
+
return this.getInstancePromise(instance.id, async () => {
|
208
|
+
const state = this.workset.getState(instance.id) ?? createInstanceState(instance.id)
|
209
|
+
|
210
|
+
this.updateInstanceState({
|
211
|
+
...state,
|
212
|
+
parentId: instance.parentId,
|
213
|
+
latestOperationId: this.operation.id,
|
214
|
+
status: this.getStatusByOperationType(),
|
215
|
+
error: null,
|
216
|
+
})
|
217
|
+
|
218
|
+
const children = this.workset.getAffectedCompositeChildren(instance.id)
|
219
|
+
const childPromises: Promise<void>[] = []
|
220
|
+
|
221
|
+
if (children.length) {
|
222
|
+
logger.info("running %s children", children.length)
|
223
|
+
} else {
|
224
|
+
logger.warn("no affected children found for composite component")
|
225
|
+
}
|
226
|
+
|
227
|
+
for (const child of children) {
|
228
|
+
logger.debug(`waiting for child: "%s"`, child.id)
|
229
|
+
childPromises.push(this.getInstancePromiseForOperation(child.id))
|
230
|
+
}
|
231
|
+
|
232
|
+
try {
|
233
|
+
await Promise.all(childPromises)
|
234
|
+
|
235
|
+
if (children.length > 0) {
|
236
|
+
logger.info("all children completed")
|
237
|
+
}
|
238
|
+
|
239
|
+
this.updateInstanceState({
|
240
|
+
id: instance.id,
|
241
|
+
status: this.operation.type === "destroy" ? "not_created" : "created",
|
242
|
+
inputHash: await this.workset.getUpToDateInputHash(instance),
|
243
|
+
})
|
244
|
+
} catch (error) {
|
245
|
+
if (isAbortErrorLike(error)) {
|
246
|
+
this.workset.restoreInitialStatus(instance.id)
|
247
|
+
return
|
248
|
+
}
|
249
|
+
|
250
|
+
this.updateInstanceState({
|
251
|
+
id: instance.id,
|
252
|
+
status: "error",
|
253
|
+
error: errorToString(error),
|
254
|
+
})
|
255
|
+
}
|
256
|
+
})
|
257
|
+
}
|
258
|
+
|
259
|
+
private updateUnit(instance: InstanceModel): Promise<void> {
|
260
|
+
return this.getInstancePromise(instance.id, async logger => {
|
261
|
+
this.updateInstanceState({
|
262
|
+
id: instance.id,
|
263
|
+
parentId: instance.parentId,
|
264
|
+
latestOperationId: this.operation.id,
|
265
|
+
status: "pending",
|
266
|
+
error: null,
|
267
|
+
currentResourceCount: 0,
|
268
|
+
})
|
269
|
+
|
270
|
+
let dependencyIds: string[] = []
|
271
|
+
try {
|
272
|
+
dependencyIds = await this.updateUnitDependencies(instance, logger)
|
273
|
+
} catch (error) {
|
274
|
+
// restore the initial status of the instance if one of the dependencies failed
|
275
|
+
this.workset.restoreInitialStatus(instance.id)
|
276
|
+
throw error
|
277
|
+
}
|
278
|
+
|
279
|
+
logger.info("updating unit")
|
280
|
+
|
281
|
+
const secrets = await this.secretBackend.get(this.operation.projectId, instance.id)
|
282
|
+
this.abortController.signal.throwIfAborted()
|
283
|
+
|
284
|
+
logger.debug("secrets loaded", { count: Object.keys(secrets).length })
|
285
|
+
|
286
|
+
await this.runnerBackend[this.operation.type === "preview" ? "preview" : "update"]({
|
287
|
+
projectId: this.operation.projectId,
|
288
|
+
instanceType: instance.type,
|
289
|
+
instanceName: instance.name,
|
290
|
+
config: this.prepareUnitConfig(instance),
|
291
|
+
refresh: this.operation.options.refresh,
|
292
|
+
secrets: mapValues(secrets, value => valueToString(value)),
|
293
|
+
signal: this.abortController.signal,
|
294
|
+
})
|
295
|
+
|
296
|
+
logger.debug("unit update requested")
|
297
|
+
|
298
|
+
const stream = this.runnerBackend.watch({
|
299
|
+
projectId: this.operation.projectId,
|
300
|
+
instanceType: instance.type,
|
301
|
+
instanceName: instance.name,
|
302
|
+
finalStatuses: ["created", "error"],
|
303
|
+
})
|
304
|
+
|
305
|
+
await this.watchStateStream(stream)
|
306
|
+
|
307
|
+
const inputHash = await this.workset.getUpToDateInputHash(instance)
|
308
|
+
|
309
|
+
this.updateInstanceState({
|
310
|
+
id: instance.id,
|
311
|
+
inputHash,
|
312
|
+
dependencyIds,
|
313
|
+
})
|
314
|
+
|
315
|
+
logger.debug("input hash after update", { inputHash })
|
316
|
+
logger.info("unit updated")
|
317
|
+
})
|
318
|
+
}
|
319
|
+
|
320
|
+
private async updateUnitDependencies(instance: InstanceModel, logger: Logger): Promise<string[]> {
|
321
|
+
try {
|
322
|
+
const dependencies = this.getInstanceDependencies(instance)
|
323
|
+
const dependencyPromises: Promise<void>[] = []
|
324
|
+
|
325
|
+
for (const dependency of dependencies) {
|
326
|
+
if (!this.operation.affectedInstanceIds.includes(dependency.id)) {
|
327
|
+
// skip dependencies that are not affected by the operation
|
328
|
+
continue
|
329
|
+
}
|
330
|
+
|
331
|
+
logger.debug(`waiting for dependency: ${dependency.id}`)
|
332
|
+
dependencyPromises.push(this.getInstancePromiseForOperation(dependency.id))
|
333
|
+
}
|
334
|
+
|
335
|
+
await Promise.all(dependencyPromises)
|
336
|
+
|
337
|
+
if (dependencies.length > 0) {
|
338
|
+
logger.info("all dependencies completed")
|
339
|
+
}
|
340
|
+
|
341
|
+
return dependencies.map(dependency => dependency.id)
|
342
|
+
} catch (error) {
|
343
|
+
// restore the initial status of the instance if one of the dependencies failed
|
344
|
+
this.workset.restoreInitialStatus(instance.id)
|
345
|
+
throw error
|
346
|
+
}
|
347
|
+
}
|
348
|
+
|
349
|
+
private async processBeforeDestroyTriggers(state: InstanceState, logger: Logger): Promise<void> {
|
350
|
+
if (!this.operation.options.invokeDestroyTriggers) {
|
351
|
+
logger.debug("destroy triggers are disabled for the operation")
|
352
|
+
return
|
353
|
+
}
|
354
|
+
|
355
|
+
const instance = this.workset.getInstance(state.id)
|
356
|
+
|
357
|
+
const triggers = state.triggers.filter(trigger => trigger.spec.type === "before-destroy")
|
358
|
+
if (triggers.length === 0) {
|
359
|
+
return
|
360
|
+
}
|
361
|
+
|
362
|
+
const invokedTriggers = triggers.map(trigger => ({ name: trigger.name }))
|
363
|
+
|
364
|
+
logger.info("updating unit to process before-destroy triggers...")
|
365
|
+
|
366
|
+
const secrets = await this.secretBackend.get(
|
367
|
+
this.operation.projectId,
|
368
|
+
instance.id,
|
369
|
+
this.abortController.signal,
|
370
|
+
)
|
371
|
+
|
372
|
+
await this.runnerBackend.update({
|
373
|
+
projectId: this.operation.projectId,
|
374
|
+
instanceType: instance.type,
|
375
|
+
instanceName: instance.name,
|
376
|
+
config: this.prepareUnitConfig(instance, invokedTriggers),
|
377
|
+
refresh: this.operation.options.refresh,
|
378
|
+
secrets: mapValues(secrets, value => valueToString(value)),
|
379
|
+
signal: this.abortController.signal,
|
380
|
+
})
|
381
|
+
|
382
|
+
logger.debug("unit update requested")
|
383
|
+
|
384
|
+
const stream = this.runnerBackend.watch({
|
385
|
+
projectId: this.operation.projectId,
|
386
|
+
instanceType: instance.type,
|
387
|
+
instanceName: instance.name,
|
388
|
+
finalStatuses: ["created", "error"],
|
389
|
+
})
|
390
|
+
|
391
|
+
await this.watchStateStream(stream)
|
392
|
+
|
393
|
+
logger.debug("before-destroy triggers processed")
|
394
|
+
}
|
395
|
+
|
396
|
+
private async destroyUnit(instanceId: string): Promise<void> {
|
397
|
+
return this.getInstancePromise(instanceId, async logger => {
|
398
|
+
this.updateInstanceState({
|
399
|
+
id: instanceId,
|
400
|
+
latestOperationId: this.operation.id,
|
401
|
+
status: "pending",
|
402
|
+
error: null,
|
403
|
+
})
|
404
|
+
|
405
|
+
const state = this.workset.getState(instanceId)
|
406
|
+
if (!state) {
|
407
|
+
logger.warn("state not found for unit, but destroy was requested")
|
408
|
+
return
|
409
|
+
}
|
410
|
+
|
411
|
+
const dependentPromises: Promise<void>[] = []
|
412
|
+
const dependents = this.workset.getDependentStates(state.id)
|
413
|
+
|
414
|
+
for (const dependent of dependents) {
|
415
|
+
dependentPromises.push(this.getInstancePromiseForOperation(dependent.id))
|
416
|
+
}
|
417
|
+
|
418
|
+
await Promise.all(dependentPromises)
|
419
|
+
this.abortController.signal.throwIfAborted()
|
420
|
+
|
421
|
+
await this.processBeforeDestroyTriggers(state, logger)
|
422
|
+
|
423
|
+
logger.info("destroying unit...")
|
424
|
+
|
425
|
+
const [type, name] = parseInstanceId(instanceId)
|
426
|
+
|
427
|
+
await this.runnerBackend.destroy({
|
428
|
+
projectId: this.operation.projectId,
|
429
|
+
instanceType: type,
|
430
|
+
instanceName: name,
|
431
|
+
refresh: this.operation.options.refresh,
|
432
|
+
signal: this.abortController.signal,
|
433
|
+
deleteUnreachable: this.operation.options.deleteUnreachableResources,
|
434
|
+
})
|
435
|
+
|
436
|
+
this.logger.debug("destroy request sent")
|
437
|
+
|
438
|
+
const stream = this.runnerBackend.watch({
|
439
|
+
projectId: this.operation.projectId,
|
440
|
+
instanceType: type,
|
441
|
+
instanceName: name,
|
442
|
+
finalStatuses: ["not_created", "error"],
|
443
|
+
})
|
444
|
+
|
445
|
+
await this.watchStateStream(stream)
|
446
|
+
this.logger.info("unit destroyed")
|
447
|
+
})
|
448
|
+
}
|
449
|
+
|
450
|
+
private async refreshUnit(instanceId: string) {
|
451
|
+
const logger = this.logger.child({ instanceId })
|
452
|
+
|
453
|
+
return this.getInstancePromise(instanceId, async () => {
|
454
|
+
this.updateInstanceState({
|
455
|
+
id: instanceId,
|
456
|
+
latestOperationId: this.operation.id,
|
457
|
+
status: "refreshing",
|
458
|
+
currentResourceCount: 0,
|
459
|
+
totalResourceCount: 0,
|
460
|
+
})
|
461
|
+
|
462
|
+
logger.info("refreshing unit...")
|
463
|
+
|
464
|
+
const [type, name] = parseInstanceId(instanceId)
|
465
|
+
|
466
|
+
await this.runnerBackend.refresh({
|
467
|
+
projectId: this.operation.projectId,
|
468
|
+
instanceType: type,
|
469
|
+
instanceName: name,
|
470
|
+
signal: this.abortController.signal,
|
471
|
+
})
|
472
|
+
|
473
|
+
logger.debug("unit refresh requested")
|
474
|
+
|
475
|
+
const stream = this.runnerBackend.watch({
|
476
|
+
projectId: this.operation.projectId,
|
477
|
+
instanceType: type,
|
478
|
+
instanceName: name,
|
479
|
+
finalStatuses: ["created", "error"],
|
480
|
+
})
|
481
|
+
|
482
|
+
await this.watchStateStream(stream)
|
483
|
+
logger.info("unit refreshed")
|
484
|
+
})
|
485
|
+
}
|
486
|
+
|
487
|
+
private async watchStateStream(stream: AsyncIterable<InstanceStateUpdate>) {
|
488
|
+
let statePatch: InstanceStateUpdate | undefined
|
489
|
+
for await (statePatch of stream) {
|
490
|
+
if (statePatch.status === "not_created" && this.operation.type === "recreate") {
|
491
|
+
// do not stream "not_created" status for recreate operation to improve UX
|
492
|
+
continue
|
493
|
+
}
|
494
|
+
|
495
|
+
this.updateInstanceState(statePatch)
|
496
|
+
}
|
497
|
+
|
498
|
+
if (!statePatch) {
|
499
|
+
throw new Error("The stream ended without emitting any state.")
|
500
|
+
}
|
501
|
+
|
502
|
+
if (statePatch.status === "error") {
|
503
|
+
throw tryWrapAbortErrorLike(
|
504
|
+
new Error(`The operation on unit "${statePatch.id}" failed: ${statePatch.error}`),
|
505
|
+
)
|
506
|
+
}
|
507
|
+
}
|
508
|
+
|
509
|
+
private prepareUnitConfig(
|
510
|
+
instance: InstanceModel,
|
511
|
+
invokedTriggers: InstanceTriggerInvocation[] = [],
|
512
|
+
): Record<string, string> {
|
513
|
+
const config: Record<string, string> = {}
|
514
|
+
|
515
|
+
for (const [key, value] of Object.entries(instance.args ?? {})) {
|
516
|
+
config[key] = valueToString(value)
|
517
|
+
}
|
518
|
+
|
519
|
+
const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
|
520
|
+
|
521
|
+
for (const [key, value] of Object.entries(instanceInputs)) {
|
522
|
+
config[`input.${key}`] = JSON.stringify(value.map(input => input.input))
|
523
|
+
}
|
524
|
+
|
525
|
+
config["$invokedTriggers"] = JSON.stringify(invokedTriggers)
|
526
|
+
|
527
|
+
return config
|
528
|
+
}
|
529
|
+
|
530
|
+
private async updateOperation(): Promise<void> {
|
531
|
+
this.operationEE.emit(this.operation.projectId, this.operation)
|
532
|
+
await this.stateBackend.putOperation(this.operation)
|
533
|
+
}
|
534
|
+
|
535
|
+
private updateInstanceState(patch: InstanceStateUpdate): void {
|
536
|
+
if (!patch.id) {
|
537
|
+
throw new Error("The ID of the instance state is required.")
|
538
|
+
}
|
539
|
+
|
540
|
+
if (patch.logLine) {
|
541
|
+
// recursively persist logs for instance and all parent instances
|
542
|
+
|
543
|
+
let instance: InstanceModel | null = this.workset.getInstance(patch.id)
|
544
|
+
|
545
|
+
for (;;) {
|
546
|
+
this.persistLogs.call([instance.id, patch.logLine])
|
547
|
+
this.instanceLogsEE.emit(`${this.operation.id}/${instance.id}`, patch.logLine)
|
548
|
+
|
549
|
+
if (!instance.parentId) {
|
550
|
+
break
|
551
|
+
}
|
552
|
+
|
553
|
+
instance = this.workset.getInstance(instance.parentId)
|
554
|
+
}
|
555
|
+
return
|
556
|
+
}
|
557
|
+
|
558
|
+
const state = this.workset.updateState(patch)
|
559
|
+
|
560
|
+
// do not persist anyting for preview operations
|
561
|
+
if (this.operation.type !== "preview") {
|
562
|
+
this.persistStates.call(state)
|
563
|
+
|
564
|
+
if (patch.secrets) {
|
565
|
+
this.persistSecrets.call([patch.id, patch.secrets])
|
566
|
+
}
|
567
|
+
}
|
568
|
+
}
|
569
|
+
|
570
|
+
private getInstancePromise(
|
571
|
+
instanceId: string,
|
572
|
+
fn: (logger: Logger) => Promise<void>,
|
573
|
+
): Promise<void> {
|
574
|
+
let instancePromise = this.instancePromiseMap.get(instanceId)
|
575
|
+
if (instancePromise) {
|
576
|
+
return instancePromise
|
577
|
+
}
|
578
|
+
|
579
|
+
const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
|
580
|
+
|
581
|
+
instancePromise = fn(logger).finally(() => this.instancePromiseMap.delete(instanceId))
|
582
|
+
this.instancePromiseMap.set(instanceId, instancePromise)
|
583
|
+
|
584
|
+
return instancePromise
|
585
|
+
}
|
586
|
+
|
587
|
+
private getStatusByOperationType(): InstanceStatus {
|
588
|
+
switch (this.operation.type) {
|
589
|
+
case "update":
|
590
|
+
return "updating"
|
591
|
+
case "preview":
|
592
|
+
return "previewing"
|
593
|
+
case "recreate":
|
594
|
+
return "updating"
|
595
|
+
case "destroy":
|
596
|
+
return "destroying"
|
597
|
+
case "refresh":
|
598
|
+
return "refreshing"
|
599
|
+
}
|
600
|
+
}
|
601
|
+
|
602
|
+
private getInstanceDependencies(instance: InstanceModel): InstanceModel[] {
|
603
|
+
const dependencies: InstanceModel[] = []
|
604
|
+
const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
|
605
|
+
|
606
|
+
for (const inputs of Object.values(instanceInputs)) {
|
607
|
+
for (const input of inputs) {
|
608
|
+
const dependency = this.workset.getInstance(input.input.instanceId)
|
609
|
+
|
610
|
+
dependencies.push(dependency)
|
611
|
+
}
|
612
|
+
}
|
613
|
+
|
614
|
+
return dependencies
|
615
|
+
}
|
616
|
+
|
617
|
+
private persistStates = createAsyncBatcher(async (states: InstanceState[]) => {
|
618
|
+
this.logger.debug({ msg: "persisting states", count: states.length })
|
619
|
+
|
620
|
+
await this.stateBackend.putAffectedInstanceStates(
|
621
|
+
this.operation.projectId,
|
622
|
+
this.operation.id,
|
623
|
+
states,
|
624
|
+
)
|
625
|
+
})
|
626
|
+
|
627
|
+
private persistLogs = createAsyncBatcher(async (entries: LogEntry[]) => {
|
628
|
+
this.logger.trace({ msg: "persisting logs", count: entries.length })
|
629
|
+
|
630
|
+
await this.stateBackend.appendInstanceLogs(this.operation.id, entries)
|
631
|
+
})
|
632
|
+
|
633
|
+
private persistSecrets = createAsyncBatcher(
|
634
|
+
async (entries: [string, Record<string, string>][]) => {
|
635
|
+
this.logger.debug({ msg: "persisting secrets", count: entries.length })
|
636
|
+
|
637
|
+
// TODO: may be batched (and patched without reading)
|
638
|
+
for (const [instanceId, secrets] of entries) {
|
639
|
+
const existingSecrets = await this.secretBackend.get(this.operation.projectId, instanceId)
|
640
|
+
|
641
|
+
Object.assign(existingSecrets, secrets)
|
642
|
+
|
643
|
+
await this.secretBackend.set(this.operation.projectId, instanceId, existingSecrets)
|
644
|
+
}
|
645
|
+
},
|
646
|
+
)
|
647
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export interface PreferencesBackend {}
|