@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
@@ -0,0 +1,698 @@
|
|
1
|
+
import type { ConfigMap, Stack } from "@pulumi/pulumi/automation"
|
2
|
+
import type { LibraryBackend } from "../library"
|
3
|
+
import { EventEmitter, on } from "node:events"
|
4
|
+
import { resolve } from "node:path"
|
5
|
+
import { getInstanceId } from "@highstate/contract"
|
6
|
+
import { ensureDependencyInstalled } from "nypm"
|
7
|
+
import { mapValues, omit, pick, pickBy } from "remeda"
|
8
|
+
import { z } from "zod"
|
9
|
+
import { sha256 } from "crypto-hash"
|
10
|
+
import { errorToString, LocalPulumiHost, runWithRetryOnError, updateResourceCount } from "../common"
|
11
|
+
import {
|
12
|
+
createInstanceState,
|
13
|
+
instanceFileSchema,
|
14
|
+
instancePageSchema,
|
15
|
+
instanceStatusFieldSchema,
|
16
|
+
instanceTerminalSchema,
|
17
|
+
instanceTriggerSchema,
|
18
|
+
type InstancePageBlock,
|
19
|
+
type InstanceState,
|
20
|
+
type InstanceStateUpdate,
|
21
|
+
type InstanceStatus,
|
22
|
+
type InstanceTerminal,
|
23
|
+
} from "../shared"
|
24
|
+
import {
|
25
|
+
type RunnerBackend,
|
26
|
+
type RunnerBaseOptions,
|
27
|
+
type InstanceUpdateOptions,
|
28
|
+
type InstanceWatchOptions,
|
29
|
+
InvalidInstanceStatusError,
|
30
|
+
type InstanceDestroyOptions,
|
31
|
+
} from "./abstractions"
|
32
|
+
|
33
|
+
type Events = {
|
34
|
+
[K in `state:${string}`]: [Partial<InstanceState>]
|
35
|
+
}
|
36
|
+
|
37
|
+
export const localRunnerBackendConfig = z.object({
|
38
|
+
HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK: z.boolean({ coerce: true }).default(false),
|
39
|
+
HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT: z.boolean({ coerce: true }).default(true),
|
40
|
+
HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR: z.string().optional(),
|
41
|
+
})
|
42
|
+
|
43
|
+
export class LocalRunnerBackend implements RunnerBackend {
|
44
|
+
private readonly events = new EventEmitter<Events>()
|
45
|
+
|
46
|
+
constructor(
|
47
|
+
private readonly skipStateCheck: boolean,
|
48
|
+
private readonly printOutput: boolean,
|
49
|
+
private readonly cacheDir: string,
|
50
|
+
private readonly pulumiProjectHost: LocalPulumiHost,
|
51
|
+
private readonly libraryBackend: LibraryBackend,
|
52
|
+
) {}
|
53
|
+
|
54
|
+
async *watch(options: InstanceWatchOptions): AsyncIterable<InstanceStateUpdate> {
|
55
|
+
const stream = on(
|
56
|
+
//
|
57
|
+
this.events,
|
58
|
+
`state:${LocalRunnerBackend.getInstanceId(options)}`,
|
59
|
+
{ signal: options.signal },
|
60
|
+
) as AsyncIterable<[InstanceStateUpdate]>
|
61
|
+
|
62
|
+
for await (const [statePatch] of stream) {
|
63
|
+
yield statePatch
|
64
|
+
|
65
|
+
if (statePatch.status && options.finalStatuses?.includes(statePatch.status)) {
|
66
|
+
return
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
getState(options: RunnerBaseOptions): Promise<InstanceState> {
|
72
|
+
return this.pulumiProjectHost.runEmpty(
|
73
|
+
{
|
74
|
+
projectId: options.projectId,
|
75
|
+
pulumiProjectName: options.instanceType,
|
76
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
77
|
+
},
|
78
|
+
async stack => {
|
79
|
+
const info = await stack.info()
|
80
|
+
const instanceId = getInstanceId(options.instanceType, options.instanceName)
|
81
|
+
|
82
|
+
if (!info) {
|
83
|
+
return createInstanceState(instanceId)
|
84
|
+
}
|
85
|
+
|
86
|
+
if (info.result === "failed") {
|
87
|
+
return createInstanceState(instanceId, "error")
|
88
|
+
}
|
89
|
+
|
90
|
+
if (info.result !== "succeeded") {
|
91
|
+
return createInstanceState(instanceId, "unknown")
|
92
|
+
}
|
93
|
+
|
94
|
+
const summary = await stack.workspace.stack()
|
95
|
+
const resourceCount = summary?.resourceCount
|
96
|
+
|
97
|
+
if (!resourceCount) {
|
98
|
+
return createInstanceState(instanceId, "not_created")
|
99
|
+
}
|
100
|
+
|
101
|
+
return createInstanceState(instanceId, "created", {
|
102
|
+
currentResourceCount: resourceCount,
|
103
|
+
totalResourceCount: resourceCount,
|
104
|
+
})
|
105
|
+
},
|
106
|
+
)
|
107
|
+
}
|
108
|
+
|
109
|
+
getTerminalFactory(
|
110
|
+
options: RunnerBaseOptions,
|
111
|
+
terminalName: string,
|
112
|
+
): Promise<InstanceTerminal | null> {
|
113
|
+
return this.pulumiProjectHost.runEmpty(
|
114
|
+
{
|
115
|
+
projectId: options.projectId,
|
116
|
+
pulumiProjectName: options.instanceType,
|
117
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
118
|
+
},
|
119
|
+
async stack => {
|
120
|
+
const outputs = await stack.outputs()
|
121
|
+
|
122
|
+
if (!outputs["$terminals"]) {
|
123
|
+
return null
|
124
|
+
}
|
125
|
+
|
126
|
+
const terminals = z.array(instanceTerminalSchema).parse(outputs["$terminals"].value)
|
127
|
+
const terminal = terminals.find(t => t.name === terminalName)
|
128
|
+
|
129
|
+
if (!terminal) {
|
130
|
+
return null
|
131
|
+
}
|
132
|
+
|
133
|
+
return terminal
|
134
|
+
},
|
135
|
+
)
|
136
|
+
}
|
137
|
+
|
138
|
+
getPageContent(
|
139
|
+
options: RunnerBaseOptions,
|
140
|
+
pageName: string,
|
141
|
+
): Promise<InstancePageBlock[] | null> {
|
142
|
+
return this.pulumiProjectHost.runEmpty(
|
143
|
+
{
|
144
|
+
projectId: options.projectId,
|
145
|
+
pulumiProjectName: options.instanceType,
|
146
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
147
|
+
},
|
148
|
+
async stack => {
|
149
|
+
const outputs = await stack.outputs()
|
150
|
+
|
151
|
+
if (!outputs["$pages"]) {
|
152
|
+
return null
|
153
|
+
}
|
154
|
+
|
155
|
+
const pages = z.array(instancePageSchema).parse(outputs["$pages"].value)
|
156
|
+
const page = pages.find(p => p.name === pageName)
|
157
|
+
|
158
|
+
if (!page) {
|
159
|
+
return null
|
160
|
+
}
|
161
|
+
|
162
|
+
return page.content
|
163
|
+
},
|
164
|
+
)
|
165
|
+
}
|
166
|
+
|
167
|
+
getFileContent(options: RunnerBaseOptions, fileName: string): Promise<string | null> {
|
168
|
+
return this.pulumiProjectHost.runEmpty(
|
169
|
+
{
|
170
|
+
projectId: options.projectId,
|
171
|
+
pulumiProjectName: options.instanceType,
|
172
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
173
|
+
},
|
174
|
+
async stack => {
|
175
|
+
const outputs = await stack.outputs()
|
176
|
+
|
177
|
+
if (!outputs["$files"]) {
|
178
|
+
return null
|
179
|
+
}
|
180
|
+
|
181
|
+
const files = z.array(instanceFileSchema).parse(outputs["$files"].value)
|
182
|
+
const file = files.find(f => f.meta.name === fileName)
|
183
|
+
|
184
|
+
if (!file) {
|
185
|
+
return null
|
186
|
+
}
|
187
|
+
|
188
|
+
return file.content
|
189
|
+
},
|
190
|
+
)
|
191
|
+
}
|
192
|
+
|
193
|
+
async update(options: InstanceUpdateOptions): Promise<void> {
|
194
|
+
const currentStatus = await this.validateStatus(options, [
|
195
|
+
"not_created",
|
196
|
+
"updating",
|
197
|
+
"created",
|
198
|
+
"error",
|
199
|
+
])
|
200
|
+
|
201
|
+
if (currentStatus === "updating") {
|
202
|
+
return
|
203
|
+
}
|
204
|
+
|
205
|
+
const configMap: ConfigMap = {
|
206
|
+
...mapValues(options.config, value => ({ value })),
|
207
|
+
...mapValues(options.secrets, value => ({ value, secret: true })),
|
208
|
+
}
|
209
|
+
|
210
|
+
void this.updateWorker(options, configMap, false)
|
211
|
+
}
|
212
|
+
|
213
|
+
async preview(options: InstanceUpdateOptions): Promise<void> {
|
214
|
+
const currentStatus = await this.validateStatus(options, [
|
215
|
+
"not_created",
|
216
|
+
"previewing",
|
217
|
+
"created",
|
218
|
+
"error",
|
219
|
+
])
|
220
|
+
|
221
|
+
if (currentStatus === "previewing") {
|
222
|
+
return
|
223
|
+
}
|
224
|
+
|
225
|
+
const configMap: ConfigMap = {
|
226
|
+
...mapValues(options.config, value => ({ value })),
|
227
|
+
...mapValues(options.secrets, value => ({ value, secret: true })),
|
228
|
+
}
|
229
|
+
|
230
|
+
void this.updateWorker(options, configMap, true)
|
231
|
+
}
|
232
|
+
|
233
|
+
private async updateWorker(
|
234
|
+
options: InstanceUpdateOptions,
|
235
|
+
configMap: ConfigMap,
|
236
|
+
preview: boolean,
|
237
|
+
): Promise<void> {
|
238
|
+
const instanceId = LocalRunnerBackend.getInstanceId(options)
|
239
|
+
|
240
|
+
try {
|
241
|
+
const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType)
|
242
|
+
if (!resolvedSource) {
|
243
|
+
throw new Error(`Resolved unit source not found for ${options.instanceType}`)
|
244
|
+
}
|
245
|
+
|
246
|
+
await this.pulumiProjectHost.runLocal(
|
247
|
+
{
|
248
|
+
projectId: options.projectId,
|
249
|
+
pulumiProjectName: options.instanceType,
|
250
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
251
|
+
projectPath: resolvedSource.projectPath,
|
252
|
+
stackConfig: configMap,
|
253
|
+
envVars: {
|
254
|
+
HIGHSTATE_CACHE_DIR: this.cacheDir,
|
255
|
+
},
|
256
|
+
},
|
257
|
+
async stack => {
|
258
|
+
await this.setStackConfig(stack, configMap)
|
259
|
+
|
260
|
+
this.updateState({
|
261
|
+
id: instanceId,
|
262
|
+
status: preview ? "previewing" : "updating",
|
263
|
+
currentResourceCount: 0,
|
264
|
+
totalResourceCount: 0,
|
265
|
+
})
|
266
|
+
|
267
|
+
let currentResourceCount = 0
|
268
|
+
let totalResourceCount = 0
|
269
|
+
|
270
|
+
await runWithRetryOnError(
|
271
|
+
async () => {
|
272
|
+
await stack[preview ? "preview" : "up"]({
|
273
|
+
color: "always",
|
274
|
+
refresh: options.refresh,
|
275
|
+
signal: options.signal,
|
276
|
+
diff: preview,
|
277
|
+
|
278
|
+
onEvent: event => {
|
279
|
+
if (event.resourcePreEvent) {
|
280
|
+
totalResourceCount = updateResourceCount(
|
281
|
+
event.resourcePreEvent.metadata.op,
|
282
|
+
totalResourceCount,
|
283
|
+
)
|
284
|
+
|
285
|
+
this.updateState({ id: instanceId, totalResourceCount })
|
286
|
+
return
|
287
|
+
}
|
288
|
+
|
289
|
+
if (event.resOutputsEvent) {
|
290
|
+
currentResourceCount = updateResourceCount(
|
291
|
+
event.resOutputsEvent.metadata.op,
|
292
|
+
currentResourceCount,
|
293
|
+
)
|
294
|
+
|
295
|
+
this.updateState({ id: instanceId, currentResourceCount })
|
296
|
+
return
|
297
|
+
}
|
298
|
+
},
|
299
|
+
|
300
|
+
onOutput: message => {
|
301
|
+
this.updateState({ id: instanceId, logLine: message })
|
302
|
+
|
303
|
+
if (this.printOutput) {
|
304
|
+
console.log(message)
|
305
|
+
}
|
306
|
+
},
|
307
|
+
})
|
308
|
+
|
309
|
+
const extraOutputs = await this.getExtraOutputsStatePatch(stack)
|
310
|
+
|
311
|
+
this.updateState({
|
312
|
+
id: instanceId,
|
313
|
+
status: "created",
|
314
|
+
totalResourceCount: currentResourceCount,
|
315
|
+
...extraOutputs,
|
316
|
+
})
|
317
|
+
},
|
318
|
+
async error => {
|
319
|
+
const isUnlocked = await this.pulumiProjectHost.tryUnlockStack(stack, error)
|
320
|
+
if (isUnlocked) return true
|
321
|
+
|
322
|
+
const isResolved = await this.tryInstallMissingDependencies(
|
323
|
+
error,
|
324
|
+
resolvedSource.allowedDependencies,
|
325
|
+
)
|
326
|
+
if (isResolved) return true
|
327
|
+
|
328
|
+
return false
|
329
|
+
},
|
330
|
+
)
|
331
|
+
},
|
332
|
+
)
|
333
|
+
} catch (error) {
|
334
|
+
this.updateState({
|
335
|
+
id: instanceId,
|
336
|
+
status: "error",
|
337
|
+
error: errorToString(error),
|
338
|
+
})
|
339
|
+
}
|
340
|
+
}
|
341
|
+
|
342
|
+
private async setStackConfig(stack: Stack, configMap: ConfigMap): Promise<void> {
|
343
|
+
const currentConfig = await stack.getAllConfig()
|
344
|
+
const currentConfigKeys = Object.keys(currentConfig)
|
345
|
+
|
346
|
+
await stack.removeAllConfig(currentConfigKeys)
|
347
|
+
await stack.setAllConfig(configMap)
|
348
|
+
}
|
349
|
+
|
350
|
+
async destroy(options: InstanceDestroyOptions): Promise<void> {
|
351
|
+
const currentStatus = await this.validateStatus(options, [
|
352
|
+
"not_created",
|
353
|
+
"destroying",
|
354
|
+
"created",
|
355
|
+
"error",
|
356
|
+
])
|
357
|
+
|
358
|
+
if (currentStatus === "destroying") {
|
359
|
+
return
|
360
|
+
}
|
361
|
+
|
362
|
+
void this.destroyWorker(options)
|
363
|
+
}
|
364
|
+
|
365
|
+
private async destroyWorker(options: InstanceDestroyOptions): Promise<void> {
|
366
|
+
const instanceId = LocalRunnerBackend.getInstanceId(options)
|
367
|
+
|
368
|
+
try {
|
369
|
+
const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType)
|
370
|
+
if (!resolvedSource) {
|
371
|
+
throw new Error(`Resolved unit source not found for ${options.instanceType}`)
|
372
|
+
}
|
373
|
+
|
374
|
+
await this.pulumiProjectHost.runLocal(
|
375
|
+
{
|
376
|
+
projectId: options.projectId,
|
377
|
+
pulumiProjectName: options.instanceType,
|
378
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
379
|
+
projectPath: resolvedSource.projectPath,
|
380
|
+
envVars: {
|
381
|
+
HIGHSTATE_CACHE_DIR: this.cacheDir,
|
382
|
+
PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : "",
|
383
|
+
},
|
384
|
+
},
|
385
|
+
async stack => {
|
386
|
+
const summary = await stack.workspace.stack()
|
387
|
+
let currentResourceCount = summary?.resourceCount ?? 0
|
388
|
+
|
389
|
+
this.updateState({
|
390
|
+
id: instanceId,
|
391
|
+
status: "destroying",
|
392
|
+
currentResourceCount,
|
393
|
+
totalResourceCount: currentResourceCount,
|
394
|
+
})
|
395
|
+
|
396
|
+
await runWithRetryOnError(
|
397
|
+
async () => {
|
398
|
+
await stack.destroy({
|
399
|
+
color: "always",
|
400
|
+
refresh: options.refresh,
|
401
|
+
remove: true,
|
402
|
+
signal: options.signal,
|
403
|
+
|
404
|
+
onEvent: event => {
|
405
|
+
if (event.resOutputsEvent) {
|
406
|
+
currentResourceCount = updateResourceCount(
|
407
|
+
event.resOutputsEvent.metadata.op,
|
408
|
+
currentResourceCount,
|
409
|
+
)
|
410
|
+
|
411
|
+
this.updateState({ id: instanceId, currentResourceCount })
|
412
|
+
return
|
413
|
+
}
|
414
|
+
},
|
415
|
+
|
416
|
+
onOutput: message => {
|
417
|
+
this.updateState({ id: instanceId, logLine: message })
|
418
|
+
|
419
|
+
if (this.printOutput) {
|
420
|
+
console.log(message)
|
421
|
+
}
|
422
|
+
},
|
423
|
+
})
|
424
|
+
|
425
|
+
const extraOutputs = await this.getExtraOutputsStatePatch(stack)
|
426
|
+
|
427
|
+
this.updateState({
|
428
|
+
id: instanceId,
|
429
|
+
status: "not_created",
|
430
|
+
totalResourceCount: currentResourceCount,
|
431
|
+
...extraOutputs,
|
432
|
+
})
|
433
|
+
},
|
434
|
+
error => this.pulumiProjectHost.tryUnlockStack(stack, error),
|
435
|
+
)
|
436
|
+
},
|
437
|
+
)
|
438
|
+
} catch (error) {
|
439
|
+
const { StackNotFoundError } = await import("@pulumi/pulumi/automation/index.js")
|
440
|
+
|
441
|
+
if (error instanceof StackNotFoundError) {
|
442
|
+
this.updateState({
|
443
|
+
id: instanceId,
|
444
|
+
status: "not_created",
|
445
|
+
totalResourceCount: 0,
|
446
|
+
currentResourceCount: 0,
|
447
|
+
statusFields: [],
|
448
|
+
pages: [],
|
449
|
+
files: [],
|
450
|
+
terminals: [],
|
451
|
+
triggers: [],
|
452
|
+
})
|
453
|
+
return
|
454
|
+
}
|
455
|
+
|
456
|
+
this.updateState({
|
457
|
+
id: instanceId,
|
458
|
+
status: "error",
|
459
|
+
error: errorToString(error),
|
460
|
+
})
|
461
|
+
}
|
462
|
+
}
|
463
|
+
|
464
|
+
async refresh(options: RunnerBaseOptions): Promise<void> {
|
465
|
+
const currentStatus = await this.validateStatus(options, [
|
466
|
+
"not_created",
|
467
|
+
"created",
|
468
|
+
"refreshing",
|
469
|
+
"error",
|
470
|
+
])
|
471
|
+
|
472
|
+
if (currentStatus === "refreshing") {
|
473
|
+
return
|
474
|
+
}
|
475
|
+
|
476
|
+
void this.refreshWorker(options)
|
477
|
+
}
|
478
|
+
|
479
|
+
private async refreshWorker(options: RunnerBaseOptions): Promise<void> {
|
480
|
+
const instanceId = LocalRunnerBackend.getInstanceId(options)
|
481
|
+
|
482
|
+
try {
|
483
|
+
await this.pulumiProjectHost.runEmpty(
|
484
|
+
{
|
485
|
+
projectId: options.projectId,
|
486
|
+
pulumiProjectName: options.instanceType,
|
487
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
488
|
+
},
|
489
|
+
async stack => {
|
490
|
+
const summary = await stack.workspace.stack()
|
491
|
+
|
492
|
+
let currentResourceCount = 0
|
493
|
+
let totalResourceCount = summary?.resourceCount ?? 0
|
494
|
+
|
495
|
+
this.updateState({
|
496
|
+
id: instanceId,
|
497
|
+
status: "refreshing",
|
498
|
+
currentResourceCount,
|
499
|
+
totalResourceCount,
|
500
|
+
})
|
501
|
+
|
502
|
+
await runWithRetryOnError(
|
503
|
+
async () => {
|
504
|
+
await stack.refresh({
|
505
|
+
color: "always",
|
506
|
+
onEvent: event => {
|
507
|
+
if (event.resourcePreEvent) {
|
508
|
+
totalResourceCount = updateResourceCount(
|
509
|
+
event.resourcePreEvent.metadata.op,
|
510
|
+
totalResourceCount,
|
511
|
+
)
|
512
|
+
|
513
|
+
this.updateState({ id: instanceId, totalResourceCount })
|
514
|
+
return
|
515
|
+
}
|
516
|
+
|
517
|
+
if (event.resOutputsEvent) {
|
518
|
+
currentResourceCount = updateResourceCount(
|
519
|
+
event.resOutputsEvent.metadata.op,
|
520
|
+
currentResourceCount,
|
521
|
+
)
|
522
|
+
|
523
|
+
this.updateState({ id: instanceId, currentResourceCount })
|
524
|
+
return
|
525
|
+
}
|
526
|
+
},
|
527
|
+
onOutput: message => {
|
528
|
+
this.updateState({ id: instanceId, logLine: message })
|
529
|
+
|
530
|
+
if (this.printOutput) {
|
531
|
+
console.log(message)
|
532
|
+
}
|
533
|
+
},
|
534
|
+
signal: options.signal,
|
535
|
+
})
|
536
|
+
|
537
|
+
const extraOutputs = await this.getExtraOutputsStatePatch(stack)
|
538
|
+
|
539
|
+
this.updateState({
|
540
|
+
id: instanceId,
|
541
|
+
status: "created",
|
542
|
+
totalResourceCount: currentResourceCount,
|
543
|
+
...extraOutputs,
|
544
|
+
})
|
545
|
+
},
|
546
|
+
error => this.pulumiProjectHost.tryUnlockStack(stack, error),
|
547
|
+
)
|
548
|
+
},
|
549
|
+
)
|
550
|
+
} catch (error) {
|
551
|
+
this.updateState({
|
552
|
+
id: instanceId,
|
553
|
+
status: "error",
|
554
|
+
error: errorToString(error),
|
555
|
+
})
|
556
|
+
}
|
557
|
+
}
|
558
|
+
|
559
|
+
private async getExtraOutputsStatePatch(stack: Stack) {
|
560
|
+
const outputs = await stack.outputs()
|
561
|
+
const patch: Omit<InstanceStateUpdate, "id"> = {}
|
562
|
+
|
563
|
+
if (outputs["$status"]) {
|
564
|
+
patch.statusFields = z.array(instanceStatusFieldSchema).parse(outputs["$status"].value)
|
565
|
+
} else {
|
566
|
+
patch.statusFields = []
|
567
|
+
}
|
568
|
+
|
569
|
+
if (outputs["$pages"]) {
|
570
|
+
const pages = z.array(instancePageSchema).parse(outputs["$pages"].value)
|
571
|
+
|
572
|
+
patch.pages = pages.map(page => omit(page, ["content"]))
|
573
|
+
} else {
|
574
|
+
patch.pages = []
|
575
|
+
}
|
576
|
+
|
577
|
+
if (outputs["$files"]) {
|
578
|
+
const files = z.array(instanceFileSchema).parse(outputs["$files"].value)
|
579
|
+
|
580
|
+
patch.files = files.map(file => file.meta)
|
581
|
+
} else {
|
582
|
+
patch.files = []
|
583
|
+
}
|
584
|
+
|
585
|
+
if (outputs["$terminals"]) {
|
586
|
+
const terminals = z.array(instanceTerminalSchema).parse(outputs["$terminals"].value)
|
587
|
+
|
588
|
+
patch.terminals = terminals.map(terminal => pick(terminal, ["name", "title", "description"]))
|
589
|
+
} else {
|
590
|
+
patch.terminals = []
|
591
|
+
}
|
592
|
+
|
593
|
+
if (outputs["$triggers"]) {
|
594
|
+
patch.triggers = z.array(instanceTriggerSchema).parse(outputs["$triggers"].value)
|
595
|
+
} else {
|
596
|
+
patch.triggers = []
|
597
|
+
}
|
598
|
+
|
599
|
+
if (outputs["$secrets"]) {
|
600
|
+
patch.secrets = pickBy(
|
601
|
+
z.record(z.string().nullish()).parse(outputs["$secrets"].value),
|
602
|
+
v => !!v,
|
603
|
+
) as Record<string, string>
|
604
|
+
} else {
|
605
|
+
patch.secrets = null
|
606
|
+
}
|
607
|
+
|
608
|
+
patch.outputHash = await sha256(JSON.stringify(outputs))
|
609
|
+
|
610
|
+
return patch
|
611
|
+
}
|
612
|
+
|
613
|
+
private updateState(patch: InstanceStateUpdate) {
|
614
|
+
this.events.emit(`state:${patch.id}`, patch)
|
615
|
+
}
|
616
|
+
|
617
|
+
private async validateStatus(
|
618
|
+
options: RunnerBaseOptions,
|
619
|
+
expectedStatuses: InstanceStatus[],
|
620
|
+
): Promise<InstanceStatus | undefined> {
|
621
|
+
if (this.skipStateCheck) {
|
622
|
+
return
|
623
|
+
}
|
624
|
+
|
625
|
+
const existingState = await this.getState(options)
|
626
|
+
|
627
|
+
if (!existingState) {
|
628
|
+
return
|
629
|
+
}
|
630
|
+
|
631
|
+
if (!expectedStatuses.includes(existingState.status)) {
|
632
|
+
throw new InvalidInstanceStatusError(existingState.status, expectedStatuses)
|
633
|
+
}
|
634
|
+
|
635
|
+
return existingState.status
|
636
|
+
}
|
637
|
+
|
638
|
+
private static getInstanceId(options: RunnerBaseOptions) {
|
639
|
+
return getInstanceId(options.instanceType, options.instanceName)
|
640
|
+
}
|
641
|
+
|
642
|
+
private async tryInstallMissingDependencies(
|
643
|
+
error: unknown,
|
644
|
+
allowedDependencies: string[],
|
645
|
+
): Promise<boolean> {
|
646
|
+
if (!(error instanceof Error)) {
|
647
|
+
return false
|
648
|
+
}
|
649
|
+
|
650
|
+
const pattern = /Cannot find module '(.*)'/
|
651
|
+
const match = error.message.match(pattern)
|
652
|
+
|
653
|
+
if (!match) {
|
654
|
+
return false
|
655
|
+
}
|
656
|
+
|
657
|
+
const packageName = match[1]
|
658
|
+
|
659
|
+
if (!allowedDependencies.includes(packageName)) {
|
660
|
+
throw new Error(
|
661
|
+
`Dependency '${packageName}' was requested to be auto-installed, but it is not allowed. Please add it to the 'peerDependencies' in the package.json of the unit.`,
|
662
|
+
)
|
663
|
+
}
|
664
|
+
|
665
|
+
await ensureDependencyInstalled(packageName)
|
666
|
+
return true
|
667
|
+
}
|
668
|
+
|
669
|
+
private static getStackName(options: RunnerBaseOptions) {
|
670
|
+
return `${options.projectId}_${options.instanceName}`
|
671
|
+
}
|
672
|
+
|
673
|
+
public static create(
|
674
|
+
config: z.infer<typeof localRunnerBackendConfig>,
|
675
|
+
pulumiProjectHost: LocalPulumiHost,
|
676
|
+
libraryBackend: LibraryBackend,
|
677
|
+
): RunnerBackend {
|
678
|
+
let cacheDir = config.HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR
|
679
|
+
if (!cacheDir) {
|
680
|
+
const homeDir = process.env.HOME ?? process.env.USERPROFILE
|
681
|
+
if (!homeDir) {
|
682
|
+
throw new Error(
|
683
|
+
"Failed to determine the home directory, please set HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR",
|
684
|
+
)
|
685
|
+
}
|
686
|
+
|
687
|
+
cacheDir = resolve(homeDir, ".cache", "highstate")
|
688
|
+
}
|
689
|
+
|
690
|
+
return new LocalRunnerBackend(
|
691
|
+
config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK,
|
692
|
+
config.HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT,
|
693
|
+
cacheDir,
|
694
|
+
pulumiProjectHost,
|
695
|
+
libraryBackend,
|
696
|
+
)
|
697
|
+
}
|
698
|
+
}
|