@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.
Files changed (74) hide show
  1. package/dist/{index.mjs → index.js} +1255 -916
  2. package/dist/library/source-resolution-worker.js +55 -0
  3. package/dist/library/worker/main.js +207 -0
  4. package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
  5. package/dist/shared/index.js +6 -0
  6. package/dist/utils-ByadNcv4.js +102 -0
  7. package/package.json +14 -18
  8. package/src/common/index.ts +3 -0
  9. package/src/common/local.ts +22 -0
  10. package/src/common/pulumi.ts +230 -0
  11. package/src/common/utils.ts +137 -0
  12. package/src/config.ts +40 -0
  13. package/src/index.ts +6 -0
  14. package/src/library/abstractions.ts +83 -0
  15. package/src/library/factory.ts +20 -0
  16. package/src/library/index.ts +2 -0
  17. package/src/library/local.ts +404 -0
  18. package/src/library/source-resolution-worker.ts +96 -0
  19. package/src/library/worker/evaluator.ts +119 -0
  20. package/src/library/worker/loader.ts +93 -0
  21. package/src/library/worker/main.ts +82 -0
  22. package/src/library/worker/protocol.ts +38 -0
  23. package/src/orchestrator/index.ts +1 -0
  24. package/src/orchestrator/manager.ts +165 -0
  25. package/src/orchestrator/operation-workset.ts +483 -0
  26. package/src/orchestrator/operation.ts +647 -0
  27. package/src/preferences/shared.ts +1 -0
  28. package/src/project/abstractions.ts +89 -0
  29. package/src/project/factory.ts +11 -0
  30. package/src/project/index.ts +4 -0
  31. package/src/project/local.ts +412 -0
  32. package/src/project/lock.ts +39 -0
  33. package/src/project/manager.ts +374 -0
  34. package/src/runner/abstractions.ts +146 -0
  35. package/src/runner/factory.ts +22 -0
  36. package/src/runner/index.ts +2 -0
  37. package/src/runner/local.ts +698 -0
  38. package/src/secret/abstractions.ts +59 -0
  39. package/src/secret/factory.ts +22 -0
  40. package/src/secret/index.ts +2 -0
  41. package/src/secret/local.ts +152 -0
  42. package/src/services.ts +133 -0
  43. package/src/shared/index.ts +10 -0
  44. package/src/shared/library.ts +77 -0
  45. package/src/shared/operation.ts +85 -0
  46. package/src/shared/project.ts +62 -0
  47. package/src/shared/resolvers/graph-resolver.ts +111 -0
  48. package/src/shared/resolvers/input-hash.ts +77 -0
  49. package/src/shared/resolvers/input.ts +314 -0
  50. package/src/shared/resolvers/registry.ts +10 -0
  51. package/src/shared/resolvers/validation.ts +94 -0
  52. package/src/shared/state.ts +262 -0
  53. package/src/shared/terminal.ts +13 -0
  54. package/src/state/abstractions.ts +222 -0
  55. package/src/state/factory.ts +22 -0
  56. package/src/state/index.ts +3 -0
  57. package/src/state/local.ts +605 -0
  58. package/src/state/manager.ts +33 -0
  59. package/src/terminal/docker.ts +90 -0
  60. package/src/terminal/factory.ts +20 -0
  61. package/src/terminal/index.ts +3 -0
  62. package/src/terminal/manager.ts +330 -0
  63. package/src/terminal/run.sh.ts +37 -0
  64. package/src/terminal/shared.ts +50 -0
  65. package/src/workspace/abstractions.ts +41 -0
  66. package/src/workspace/factory.ts +14 -0
  67. package/src/workspace/index.ts +2 -0
  68. package/src/workspace/local.ts +54 -0
  69. package/dist/index.d.ts +0 -760
  70. package/dist/library/worker/main.mjs +0 -164
  71. package/dist/runner/source-resolution-worker.mjs +0 -22
  72. package/dist/shared/index.d.ts +0 -85
  73. package/dist/shared/index.mjs +0 -54
  74. package/dist/terminal-Cm2WqcyB.d.ts +0 -1589
@@ -0,0 +1,374 @@
1
+ import type { StateBackend, StateManager } from "../state"
2
+ import type { ProjectBackend } from "./abstractions"
3
+ import type { Logger } from "pino"
4
+ import type { LibraryBackend } from "../library"
5
+ import type { ProjectLockManager } from "./lock"
6
+ import { EventEmitter, on } from "node:events"
7
+ import { isUnitModel, type InstanceModel } from "@highstate/contract"
8
+ import {
9
+ createInputResolver,
10
+ createInputHashResolver,
11
+ type InputResolverInput,
12
+ type InputHashResolverInput,
13
+ type InstanceModelPatch,
14
+ type LibraryUpdate,
15
+ createInstanceState,
16
+ type CompositeInstance,
17
+ type ResolvedInstanceInput,
18
+ } from "../shared"
19
+
20
+ type CompositeInstanceEvent =
21
+ | {
22
+ type: "updated"
23
+ instance: CompositeInstance
24
+ }
25
+ | {
26
+ type: "deleted"
27
+ instanceId: string
28
+ }
29
+
30
+ type CompositeInstanceEvents = {
31
+ [K in string]: [CompositeInstanceEvent]
32
+ }
33
+
34
+ export class ProjectManager {
35
+ private constructor(
36
+ private readonly projectBackend: ProjectBackend,
37
+ private readonly stateBackend: StateBackend,
38
+ private readonly libraryBackend: LibraryBackend,
39
+ private readonly projectLockManager: ProjectLockManager,
40
+ private readonly stateManager: StateManager,
41
+ private readonly logger: Logger,
42
+ ) {
43
+ void this.watchLibraryChanges()
44
+ }
45
+
46
+ private readonly compositeInstanceEE = new EventEmitter<CompositeInstanceEvents>()
47
+
48
+ async *watchCompositeInstances(
49
+ projectId: string,
50
+ signal?: AbortSignal,
51
+ ): AsyncIterable<CompositeInstanceEvent> {
52
+ for await (const [children] of on(this.compositeInstanceEE, projectId, {
53
+ signal,
54
+ })) {
55
+ yield children
56
+ }
57
+ }
58
+
59
+ async createInstance(projectId: string, instance: InstanceModel): Promise<InstanceModel> {
60
+ const createdInstance = await this.projectBackend.createInstance(projectId, instance)
61
+ await this.updateCompositeInstance(projectId, createdInstance)
62
+
63
+ return createdInstance
64
+ }
65
+
66
+ async updateInstance(
67
+ projectId: string,
68
+ instanceId: string,
69
+ patch: InstanceModelPatch,
70
+ ): Promise<InstanceModel> {
71
+ const instance = await this.projectBackend.updateInstance(projectId, instanceId, patch)
72
+ await this.updateCompositeInstance(projectId, instance)
73
+
74
+ return instance
75
+ }
76
+
77
+ async renameInstance(
78
+ projectId: string,
79
+ instanceId: string,
80
+ newName: string,
81
+ ): Promise<InstanceModel> {
82
+ const instance = await this.projectBackend.renameInstance(projectId, instanceId, newName)
83
+ await this.updateCompositeInstance(projectId, instance)
84
+
85
+ return instance
86
+ }
87
+
88
+ async deleteInstance(projectId: string, instanceId: string): Promise<void> {
89
+ await Promise.all([
90
+ this.projectBackend.deleteInstance(projectId, instanceId),
91
+ this.stateBackend.clearCompositeInstances(projectId, [instanceId]),
92
+ ])
93
+ }
94
+
95
+ private async updateCompositeInstance(projectId: string, instance: InstanceModel): Promise<void> {
96
+ const { resolveInputHash, library } = await this.prepareInputHashResolver(projectId)
97
+
98
+ const component = library.components[instance.type]
99
+ if (!component) {
100
+ return
101
+ }
102
+
103
+ if (isUnitModel(component)) {
104
+ return
105
+ }
106
+
107
+ const { inputHash: expectedInputHash } = await resolveInputHash(instance.id)
108
+ const inputHash = await this.stateBackend.getCompositeInstanceInputHash(projectId, instance.id)
109
+
110
+ if (inputHash !== expectedInputHash) {
111
+ this.logger.info("re-evaluating instance since input hash has changed", {
112
+ projectId,
113
+ instanceId: instance.id,
114
+ })
115
+
116
+ await this.evaluateCompositeInstances(projectId, [instance.id])
117
+ }
118
+ }
119
+
120
+ private async evaluateCompositeInstances(projectId: string, instanceIds: string[]) {
121
+ await this.projectLockManager.getLock(projectId).lockInstances(instanceIds, async () => {
122
+ const [
123
+ { instances, resolvedInputs, stateMap, resolveInputHash },
124
+ topLevelCompositeChildrenIds,
125
+ ] = await Promise.all([
126
+ this.prepareInputHashResolver(projectId),
127
+ this.stateBackend.getTopLevelCompositeChildrenIds(projectId, instanceIds),
128
+ ])
129
+
130
+ const results = await this.libraryBackend.evaluateCompositeInstances(
131
+ instances,
132
+ resolvedInputs,
133
+ instanceIds,
134
+ )
135
+
136
+ const newStates = results.map(result => {
137
+ const existingState = stateMap.get(result.instanceId)
138
+ const newState = existingState ?? createInstanceState(result.instanceId)
139
+
140
+ newState.evaluationError = result.success ? null : result.error
141
+
142
+ return newState
143
+ })
144
+
145
+ const inputHashes = new Map<string, string>()
146
+ for (const instanceId of instanceIds) {
147
+ const { inputHash } = await resolveInputHash(instanceId)
148
+ inputHashes.set(instanceId, inputHash)
149
+ }
150
+
151
+ const compositeInstances = results
152
+ .filter(result => result.success)
153
+ .flatMap(result =>
154
+ result.compositeInstances.map(instance => ({
155
+ ...instance,
156
+ inputHash: inputHashes.get(instance.instance.id),
157
+ })),
158
+ )
159
+
160
+ const newTopLevelCompositeChildrenIds = Object.fromEntries(
161
+ results
162
+ .filter(result => result.success)
163
+ .map(result => [
164
+ result.instanceId,
165
+ result.compositeInstances
166
+ .filter(instance => instance.instance.id !== result.instanceId)
167
+ .map(instance => instance.instance.id),
168
+ ]),
169
+ )
170
+
171
+ const deletedCompositeInstanceIds = new Set(
172
+ Object.values(topLevelCompositeChildrenIds).flat(),
173
+ )
174
+
175
+ for (const childInstanceId of Object.values(newTopLevelCompositeChildrenIds).flat()) {
176
+ deletedCompositeInstanceIds.delete(childInstanceId)
177
+ }
178
+
179
+ await Promise.all([
180
+ this.stateBackend.clearCompositeInstances(
181
+ projectId,
182
+ Array.from(deletedCompositeInstanceIds),
183
+ ),
184
+ this.stateBackend.putTopLevelCompositeChildrenIds(
185
+ projectId,
186
+ newTopLevelCompositeChildrenIds,
187
+ ),
188
+ ])
189
+
190
+ for (const state of newStates) {
191
+ this.stateManager.emitStatePatch(projectId, state)
192
+ }
193
+
194
+ for (const instance of compositeInstances) {
195
+ this.compositeInstanceEE.emit(projectId, { type: "updated", instance })
196
+ }
197
+
198
+ for (const instanceId of deletedCompositeInstanceIds) {
199
+ this.compositeInstanceEE.emit(projectId, { type: "deleted", instanceId })
200
+ }
201
+
202
+ const promises: Promise<void>[] = []
203
+
204
+ if (newStates.length > 0) {
205
+ promises.push(this.stateBackend.putInstanceStates(projectId, newStates))
206
+ }
207
+
208
+ if (compositeInstances.length > 0) {
209
+ promises.push(this.stateBackend.putCompositeInstances(projectId, compositeInstances))
210
+ }
211
+
212
+ this.logger.info(
213
+ { projectId },
214
+ "instance evaluation completed, %d instances persisted",
215
+ compositeInstances.length,
216
+ )
217
+
218
+ await Promise.all(promises)
219
+ })
220
+ }
221
+
222
+ private async prepareInputHashResolver(projectId: string) {
223
+ const { instances, hubs } = await this.projectBackend.getProject(projectId)
224
+
225
+ const library = await this.libraryBackend.loadLibrary()
226
+ const filteredInstances = instances.filter(instance => instance.type in library.components)
227
+
228
+ const states = await this.stateBackend.getAllInstanceStates(projectId)
229
+ const stateMap = new Map(states.map(state => [state.id, state]))
230
+
231
+ const inputResolverNodes = new Map<string, InputResolverInput>()
232
+
233
+ for (const instance of filteredInstances) {
234
+ inputResolverNodes.set(`instance:${instance.id}`, {
235
+ kind: "instance",
236
+ instance,
237
+ component: library.components[instance.type],
238
+ })
239
+ }
240
+
241
+ for (const hub of hubs) {
242
+ inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub })
243
+ }
244
+
245
+ const resolveInputs = createInputResolver(inputResolverNodes, this.logger)
246
+
247
+ const inputHashInputs = new Map<string, InputHashResolverInput>()
248
+ const resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>> = {}
249
+
250
+ for (const instance of filteredInstances) {
251
+ const output = await resolveInputs(`instance:${instance.id}`)
252
+ if (output.kind !== "instance") {
253
+ throw new Error("Expected instance node")
254
+ }
255
+
256
+ let sourceHash: string | undefined
257
+ if (isUnitModel(library.components[instance.type])) {
258
+ const resolvedUnit = await this.libraryBackend.getResolvedUnitSource(instance.type)
259
+ if (!resolvedUnit) {
260
+ throw new Error(`Resolved unit not found: ${instance.type}`)
261
+ }
262
+
263
+ sourceHash = resolvedUnit.sourceHash
264
+ }
265
+
266
+ inputHashInputs.set(instance.id, {
267
+ instance,
268
+ component: library.components[instance.type],
269
+ resolvedInputs: output.resolvedInputs,
270
+ state: stateMap.get(instance.id),
271
+ sourceHash,
272
+ })
273
+
274
+ resolvedInputs[instance.id] = output.resolvedInputs
275
+ }
276
+
277
+ const resolveInputHash = createInputHashResolver(inputHashInputs, this.logger)
278
+
279
+ return {
280
+ resolveInputHash,
281
+ library,
282
+ instances,
283
+ stateMap,
284
+ resolvedInputs,
285
+ }
286
+ }
287
+
288
+ private async watchLibraryChanges(): Promise<void> {
289
+ for await (const updates of this.libraryBackend.watchLibrary()) {
290
+ try {
291
+ await this.handleLibraryUpdates(updates)
292
+ } catch (error) {
293
+ this.logger.error({ error }, "failed to handle library updates")
294
+ }
295
+ }
296
+ }
297
+
298
+ private async handleLibraryUpdates(updates: LibraryUpdate[]): Promise<void> {
299
+ const changedComponents = new Set<string>()
300
+ const library = await this.libraryBackend.loadLibrary()
301
+
302
+ // TODO: handle entity updates
303
+
304
+ for (const update of updates) {
305
+ switch (update.type) {
306
+ case "component-updated":
307
+ changedComponents.add(update.component.type)
308
+ break
309
+ case "component-removed":
310
+ changedComponents.add(update.componentType)
311
+ break
312
+ }
313
+ }
314
+
315
+ if (changedComponents.size === 0) {
316
+ return
317
+ }
318
+
319
+ this.logger.info(
320
+ { changedComponents },
321
+ "library components changed, updating composite instances",
322
+ )
323
+
324
+ const projects = await this.projectBackend.getProjectIds()
325
+ for (const projectId of projects) {
326
+ const { resolveInputHash, instances } = await this.prepareInputHashResolver(projectId)
327
+
328
+ const filteredInstances = instances.filter(
329
+ instance =>
330
+ changedComponents.has(instance.type) &&
331
+ library.components[instance.type] &&
332
+ !isUnitModel(library.components[instance.type]),
333
+ )
334
+
335
+ this.logger.info(
336
+ { projectId, filteredInstanceIds: filteredInstances.map(instance => instance.id) },
337
+ "updating composite instances for project",
338
+ )
339
+
340
+ const inputHashMap = new Map<string, string>()
341
+ for (const instance of filteredInstances) {
342
+ const { inputHash } = await resolveInputHash(instance.id)
343
+ inputHashMap.set(instance.id, inputHash)
344
+ }
345
+
346
+ try {
347
+ await this.evaluateCompositeInstances(
348
+ projectId,
349
+ filteredInstances.map(instance => instance.id),
350
+ )
351
+ } catch (error) {
352
+ this.logger.error({ error }, "failed to evaluate composite instances")
353
+ }
354
+ }
355
+ }
356
+
357
+ static create(
358
+ projectBackend: ProjectBackend,
359
+ stateBackend: StateBackend,
360
+ libraryBackend: LibraryBackend,
361
+ projectLockManager: ProjectLockManager,
362
+ stateManager: StateManager,
363
+ logger: Logger,
364
+ ): ProjectManager {
365
+ return new ProjectManager(
366
+ projectBackend,
367
+ stateBackend,
368
+ libraryBackend,
369
+ projectLockManager,
370
+ stateManager,
371
+ logger.child({ service: "ProjectManager" }),
372
+ )
373
+ }
374
+ }
@@ -0,0 +1,146 @@
1
+ import type {
2
+ InstancePageBlock,
3
+ InstanceState,
4
+ InstanceStateUpdate,
5
+ InstanceStatus,
6
+ InstanceTerminal,
7
+ } from "../shared"
8
+
9
+ export type RunnerBaseOptions = {
10
+ /**
11
+ * The HighState project ID.
12
+ */
13
+ projectId: string
14
+
15
+ /**
16
+ * The type of the instance to run.
17
+ */
18
+ instanceType: string
19
+
20
+ /**
21
+ * The name of the instance to run.
22
+ */
23
+ instanceName: string
24
+
25
+ /**
26
+ * The signal to abort the operation.
27
+ */
28
+ signal?: AbortSignal
29
+ }
30
+
31
+ export type InstanceUpdateOptions = RunnerBaseOptions & {
32
+ /**
33
+ * The configuration of the stack.
34
+ */
35
+ config: Record<string, string>
36
+
37
+ /**
38
+ * The values of the secrets.
39
+ */
40
+ secrets: Record<string, string>
41
+
42
+ /**
43
+ * Whether to refresh the state before updating.
44
+ */
45
+ refresh?: boolean
46
+ }
47
+
48
+ export type InstanceDestroyOptions = RunnerBaseOptions & {
49
+ /**
50
+ * Whether to refresh the state before updating.
51
+ */
52
+ refresh?: boolean
53
+
54
+ /**
55
+ * Whether to delete the unreachable resources (e.g. k8s resources in unreachable clusters).
56
+ */
57
+ deleteUnreachable?: boolean
58
+ }
59
+
60
+ export type InstanceWatchOptions = RunnerBaseOptions & {
61
+ /**
62
+ * The final statuses to watch.
63
+ * If the instance reaches one of these statuses, it should return the current state and exit.
64
+ */
65
+ finalStatuses?: InstanceStatus[]
66
+ }
67
+
68
+ export class InvalidInstanceStatusError extends Error {
69
+ constructor(
70
+ public readonly currentStatus: InstanceStatus,
71
+ public readonly expectedStatuses: InstanceStatus[],
72
+ ) {
73
+ const expectedString = expectedStatuses.join(", ")
74
+ super(`The current state is "${currentStatus}", but it should be one of "${expectedString}".`)
75
+ }
76
+ }
77
+
78
+ export interface RunnerBackend {
79
+ /**
80
+ * Watches the instance state.
81
+ *
82
+ * Returns the async iterable of the instance state patches.
83
+ * The fields with `undefined` values should be ignored, and the fields with `null` values should be removed.
84
+ */
85
+ watch(options: InstanceWatchOptions): AsyncIterable<InstanceStateUpdate>
86
+
87
+ /**
88
+ * Gets the actual state of the instance.
89
+ */
90
+ getState(options: RunnerBaseOptions): Promise<InstanceState>
91
+
92
+ /**
93
+ * Gets the content of the page.
94
+ */
95
+ getPageContent(options: RunnerBaseOptions, pageName: string): Promise<InstancePageBlock[] | null>
96
+
97
+ /**
98
+ * Gets the content of the file.
99
+ */
100
+ getFileContent(options: RunnerBaseOptions, fileName: string): Promise<string | null>
101
+
102
+ /**
103
+ * Gets the terminal factory.
104
+ */
105
+ getTerminalFactory(
106
+ options: RunnerBaseOptions,
107
+ terminalName: string,
108
+ ): Promise<InstanceTerminal | null>
109
+
110
+ /**
111
+ * Updates the instance.
112
+ *
113
+ * The operation must only be aborted by the signal, not even when the connection is closed.
114
+ * If the instance is already updating, it should exit immediately.
115
+ * If the instance is running another operation, it should throw an error.
116
+ */
117
+ update(options: InstanceUpdateOptions): Promise<void>
118
+
119
+ /**
120
+ * Previews the instance update without actually applying the changes.
121
+ *
122
+ * The operation must only be aborted by the signal, not even when the connection is closed.
123
+ * If the instance is already updating, it should exit immediately.
124
+ * If the instance is running another operation, it should throw an error.
125
+ */
126
+ preview(options: InstanceUpdateOptions): Promise<void>
127
+
128
+ /**
129
+ * Destroys the instance.
130
+ *
131
+ * The operation must only be aborted by the signal, not even when the connection is closed.
132
+ * If the instance is not created, it should exit immediately.
133
+ * If the instance is running another operation, it should throw an error.
134
+ */
135
+ destroy(options: InstanceDestroyOptions): Promise<void>
136
+
137
+ /**
138
+ * Refreshes the instance.
139
+ *
140
+ * The operation must only be aborted by the signal, not even when the connection is closed.
141
+ * If the instance is not created, it should exit immediately.
142
+ * If the instance is running another operation, it should throw an error.
143
+ * If the instance is not running, it should exit immediately.
144
+ */
145
+ refresh(options: RunnerBaseOptions): Promise<void>
146
+ }
@@ -0,0 +1,22 @@
1
+ import type { RunnerBackend } from "./abstractions"
2
+ import type { LocalPulumiHost } from "../common"
3
+ import type { LibraryBackend } from "../library"
4
+ import { z } from "zod"
5
+ import { LocalRunnerBackend, localRunnerBackendConfig } from "./local"
6
+
7
+ export const runnerBackendConfig = z.object({
8
+ HIGHSTATE_BACKEND_RUNNER_TYPE: z.enum(["local"]).default("local"),
9
+ ...localRunnerBackendConfig.shape,
10
+ })
11
+
12
+ export function createRunnerBackend(
13
+ config: z.infer<typeof runnerBackendConfig>,
14
+ pulumiProjectHost: LocalPulumiHost,
15
+ libraryBackend: LibraryBackend,
16
+ ): RunnerBackend {
17
+ switch (config.HIGHSTATE_BACKEND_RUNNER_TYPE) {
18
+ case "local": {
19
+ return LocalRunnerBackend.create(config, pulumiProjectHost, libraryBackend)
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./abstractions"
2
+ export * from "./factory"