@highstate/backend 0.7.2 → 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} +1254 -915
- 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,605 @@
|
|
1
|
+
import type { ClassicLevel } from "classic-level"
|
2
|
+
import type { AbstractSublevel, AbstractValueIteratorOptions } from "abstract-level"
|
3
|
+
import type { Logger } from "pino"
|
4
|
+
import { resolve } from "node:path"
|
5
|
+
import { z } from "zod"
|
6
|
+
import { uuidv7 } from "uuidv7"
|
7
|
+
import {
|
8
|
+
type ProjectOperation,
|
9
|
+
type InstanceState,
|
10
|
+
projectOperationSchema,
|
11
|
+
instanceStateSchema,
|
12
|
+
isFinalOperationStatus,
|
13
|
+
type TerminalSession,
|
14
|
+
terminalSessionSchema,
|
15
|
+
compositeInstanceSchema,
|
16
|
+
type CompositeInstance,
|
17
|
+
} from "../shared"
|
18
|
+
import { LocalPulumiHost } from "../common"
|
19
|
+
import { type LogEntry, type StateBackend, type TerminalHistoryEntry } from "./abstractions"
|
20
|
+
|
21
|
+
export const localStateBackendConfig = z.object({
|
22
|
+
HIGHSTATE_BACKEND_STATE_LOCAL_DIR: z.string().optional(),
|
23
|
+
})
|
24
|
+
|
25
|
+
type Sublevel<TValue = unknown> = AbstractSublevel<
|
26
|
+
ClassicLevel<string, unknown>,
|
27
|
+
string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>,
|
28
|
+
string,
|
29
|
+
TValue
|
30
|
+
>
|
31
|
+
|
32
|
+
/**
|
33
|
+
* A state backend that stores the state in a local LevelDB database.
|
34
|
+
*
|
35
|
+
* It uses the following structure:
|
36
|
+
*
|
37
|
+
* - `activeOperations/{operationId}` - all active operations, denormalized from `projects/{projectId}/operations`;
|
38
|
+
* - `activeTerminalSessionIds` - the IDs of the active terminal sessions;
|
39
|
+
*
|
40
|
+
* - `projects/{projectId}/operations/{operationId}` - the operations of the project;
|
41
|
+
* - `projects/{projectId}/instanceStates/{instanceId}` - the latest states of the instances of the project;
|
42
|
+
* - `projects/{projectId}/compositeInstances/{instanceId}` - all evaluated composite instances of the project;
|
43
|
+
* - `projects/{projectId}/compositeInstanceInputHashes/{instanceId}` - the input hashes of the composite instances, can be fetched independently;
|
44
|
+
* - `projects/{projectId}/topLevelCompositeChildrenIds/{instanceId}` - the IDs of the composite children of the top-level composite instance;
|
45
|
+
* - `projects/{projectId}/instances/{instanceId}/terminalSessions/{sessionId}` - the terminal sessions of the instances;
|
46
|
+
*
|
47
|
+
* - `operations/{operationId}/instanceStates/{instanceID}` - the states of the instances affected by the operation (at the moment of the operation completion);
|
48
|
+
* - `operations/{operationId}/instances/{instanceID}/logs/{logId}` - the logs of the instances affected by the operation, logId is a UUIDv7;
|
49
|
+
*
|
50
|
+
* - `terminalSessions/{sessionId}/history/{lineId}` - the history lines of the terminal session, lineId is a UUIDv7;
|
51
|
+
*/
|
52
|
+
export class LocalStateBackend implements StateBackend {
|
53
|
+
constructor(
|
54
|
+
private readonly db: ClassicLevel<string, unknown>,
|
55
|
+
private readonly logger: Logger,
|
56
|
+
) {
|
57
|
+
this.logger.debug({ msg: "initialized", dbLocation: db.location })
|
58
|
+
}
|
59
|
+
|
60
|
+
async getActiveOperations(): Promise<ProjectOperation[]> {
|
61
|
+
const sublevel = this.getActiveOperationsSublevel()
|
62
|
+
|
63
|
+
return this.getAllSublevelItems(sublevel, projectOperationSchema)
|
64
|
+
}
|
65
|
+
|
66
|
+
async getOperations(projectId: string, beforeOperationId?: string): Promise<ProjectOperation[]> {
|
67
|
+
const sublevel = this.getProjectOperationsSublevel(projectId)
|
68
|
+
const pageSize = 10
|
69
|
+
|
70
|
+
return await this.getAllSublevelItems(sublevel, projectOperationSchema, pageSize, {
|
71
|
+
lt: beforeOperationId,
|
72
|
+
reverse: true,
|
73
|
+
})
|
74
|
+
}
|
75
|
+
|
76
|
+
async getAllInstanceStates(projectId: string): Promise<InstanceState[]> {
|
77
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId)
|
78
|
+
|
79
|
+
return await this.getAllSublevelItems(sublevel, instanceStateSchema)
|
80
|
+
}
|
81
|
+
|
82
|
+
async getInstanceState(projectId: string, instanceID: string): Promise<InstanceState | null> {
|
83
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId)
|
84
|
+
|
85
|
+
return await this.getSublevelItem(sublevel, instanceStateSchema, instanceID)
|
86
|
+
}
|
87
|
+
|
88
|
+
async getInstanceStates(projectId: string, instanceIds: string[]): Promise<InstanceState[]> {
|
89
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId)
|
90
|
+
|
91
|
+
return await this.getSublevelItems(sublevel, instanceStateSchema, instanceIds)
|
92
|
+
}
|
93
|
+
|
94
|
+
async getAffectedInstanceStates(operationId: string): Promise<InstanceState[]> {
|
95
|
+
const sublevel = this.getOperationInstanceStatesSublevel(operationId)
|
96
|
+
|
97
|
+
return await this.getAllSublevelItems(sublevel, instanceStateSchema)
|
98
|
+
}
|
99
|
+
|
100
|
+
async getInstanceLogs(operationId: string, instanceId: string): Promise<string[]> {
|
101
|
+
const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId)
|
102
|
+
|
103
|
+
return await Array.fromAsync(sublevel.values())
|
104
|
+
}
|
105
|
+
|
106
|
+
async putOperation(operation: ProjectOperation): Promise<void> {
|
107
|
+
this.validateItem(projectOperationSchema, operation)
|
108
|
+
|
109
|
+
const operationsSublevel = this.getProjectOperationsSublevel(operation.projectId)
|
110
|
+
const activeOperationsSublevel = this.getActiveOperationsSublevel()
|
111
|
+
|
112
|
+
if (!isFinalOperationStatus(operation.status)) {
|
113
|
+
// if the operation is not completed or failed, it is active,
|
114
|
+
// so we put it to the active operations sublevel as well
|
115
|
+
await this.db.batch([
|
116
|
+
{ type: "put", key: operation.id, value: operation, sublevel: operationsSublevel },
|
117
|
+
{ type: "put", key: operation.id, value: operation, sublevel: activeOperationsSublevel },
|
118
|
+
])
|
119
|
+
} else {
|
120
|
+
// otherwise, we put it only to the operations sublevel
|
121
|
+
// and ensure it is not in the active operations
|
122
|
+
await this.db.batch([
|
123
|
+
{ type: "put", key: operation.id, value: operation, sublevel: operationsSublevel },
|
124
|
+
{ type: "del", key: operation.id, sublevel: activeOperationsSublevel },
|
125
|
+
])
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
async putAffectedInstanceStates(
|
130
|
+
projectId: string,
|
131
|
+
operationId: string,
|
132
|
+
states: InstanceState[],
|
133
|
+
): Promise<void> {
|
134
|
+
this.validateArray(instanceStateSchema, states)
|
135
|
+
|
136
|
+
const operationInstanceStatesSublevel = this.getOperationInstanceStatesSublevel(operationId)
|
137
|
+
const projectInstanceStatesSublevel = this.getProjectInstanceStatesSublevel(projectId)
|
138
|
+
|
139
|
+
await this.db.batch(
|
140
|
+
// put the states to both the operation and project sublevels
|
141
|
+
// denormalization is cool
|
142
|
+
// this separation is also necessary because the instance states can be updated without operations
|
143
|
+
states.flatMap(state => [
|
144
|
+
{
|
145
|
+
// always put the state to the operation sublevel for history
|
146
|
+
type: "put",
|
147
|
+
key: state.id,
|
148
|
+
value: state,
|
149
|
+
sublevel: operationInstanceStatesSublevel,
|
150
|
+
},
|
151
|
+
{
|
152
|
+
type: state.status === "not_created" ? "del" : "put",
|
153
|
+
key: state.id,
|
154
|
+
value: state,
|
155
|
+
sublevel: projectInstanceStatesSublevel,
|
156
|
+
},
|
157
|
+
]),
|
158
|
+
)
|
159
|
+
}
|
160
|
+
|
161
|
+
async putInstanceStates(projectId: string, states: InstanceState[]): Promise<void> {
|
162
|
+
this.validateArray(instanceStateSchema, states)
|
163
|
+
|
164
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId)
|
165
|
+
|
166
|
+
await sublevel.batch(
|
167
|
+
// as i told before, we update the instance states without operations
|
168
|
+
// this method is used when upstream instance state changes are detected
|
169
|
+
states.map(state => ({
|
170
|
+
type: state.status === "not_created" ? "del" : "put",
|
171
|
+
key: state.id,
|
172
|
+
value: state,
|
173
|
+
})),
|
174
|
+
)
|
175
|
+
}
|
176
|
+
|
177
|
+
async appendInstanceLogs(operationId: string, logs: LogEntry[]): Promise<void> {
|
178
|
+
const sublevels = new Map<string, Sublevel<string>>()
|
179
|
+
for (const [instanceId] of logs) {
|
180
|
+
if (sublevels.has(instanceId)) {
|
181
|
+
continue
|
182
|
+
}
|
183
|
+
|
184
|
+
const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId)
|
185
|
+
sublevels.set(instanceId, sublevel)
|
186
|
+
}
|
187
|
+
|
188
|
+
await this.db.batch(
|
189
|
+
logs.map(([instanceID, line]) => ({
|
190
|
+
type: "put",
|
191
|
+
key: uuidv7(),
|
192
|
+
value: line,
|
193
|
+
sublevel: sublevels.get(instanceID)!,
|
194
|
+
})),
|
195
|
+
)
|
196
|
+
}
|
197
|
+
|
198
|
+
async getCompositeInstances(
|
199
|
+
projectId: string,
|
200
|
+
signal?: AbortSignal,
|
201
|
+
): Promise<CompositeInstance[]> {
|
202
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
|
203
|
+
|
204
|
+
return await this.getAllSublevelItems(
|
205
|
+
//
|
206
|
+
sublevel,
|
207
|
+
compositeInstanceSchema,
|
208
|
+
undefined,
|
209
|
+
{ signal },
|
210
|
+
)
|
211
|
+
}
|
212
|
+
|
213
|
+
async getCompositeInstance(
|
214
|
+
projectId: string,
|
215
|
+
instanceId: string,
|
216
|
+
): Promise<CompositeInstance | null> {
|
217
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
|
218
|
+
|
219
|
+
return this.getSublevelItem(sublevel, compositeInstanceSchema, instanceId)
|
220
|
+
}
|
221
|
+
|
222
|
+
async getCompositeInstanceInputHash(
|
223
|
+
projectId: string,
|
224
|
+
instanceId: string,
|
225
|
+
): Promise<string | null> {
|
226
|
+
const sublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId)
|
227
|
+
const inputHash = await sublevel.get(instanceId)
|
228
|
+
|
229
|
+
return inputHash ?? null
|
230
|
+
}
|
231
|
+
|
232
|
+
async putCompositeInstances(projectId: string, instances: CompositeInstance[]): Promise<void> {
|
233
|
+
this.validateArray(compositeInstanceSchema, instances)
|
234
|
+
|
235
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
|
236
|
+
const inputHashesSublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId)
|
237
|
+
|
238
|
+
await this.db.batch(
|
239
|
+
instances.flatMap(instance => [
|
240
|
+
{
|
241
|
+
type: "put",
|
242
|
+
key: instance.instance.id,
|
243
|
+
value: compositeInstanceSchema.parse(instance),
|
244
|
+
sublevel: sublevel,
|
245
|
+
},
|
246
|
+
]),
|
247
|
+
)
|
248
|
+
}
|
249
|
+
|
250
|
+
async clearCompositeInstances(projectId: string, instanceIds: string[]): Promise<void> {
|
251
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
|
252
|
+
const inputHashesSublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId)
|
253
|
+
|
254
|
+
await this.db.batch(
|
255
|
+
instanceIds.flatMap(instanceId => [
|
256
|
+
{ type: "del", key: instanceId, sublevel },
|
257
|
+
{ type: "del", key: instanceId, sublevel: inputHashesSublevel },
|
258
|
+
]),
|
259
|
+
)
|
260
|
+
}
|
261
|
+
|
262
|
+
async getTopLevelCompositeChildrenIds(
|
263
|
+
projectId: string,
|
264
|
+
instanceIds: string[],
|
265
|
+
): Promise<Record<string, string[]>> {
|
266
|
+
const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId)
|
267
|
+
const items = await sublevel.getMany(instanceIds)
|
268
|
+
const schema = z.array(z.string()).optional()
|
269
|
+
|
270
|
+
const result: Record<string, string[]> = {}
|
271
|
+
for (let i = 0; i < items.length; i++) {
|
272
|
+
const instanceId = instanceIds[i]
|
273
|
+
const childrenIds = schema.parse(items[i])
|
274
|
+
|
275
|
+
result[instanceId] = childrenIds ?? []
|
276
|
+
}
|
277
|
+
|
278
|
+
return result
|
279
|
+
}
|
280
|
+
|
281
|
+
async putTopLevelCompositeChildrenIds(
|
282
|
+
projectId: string,
|
283
|
+
childrenIds: Record<string, string[]>,
|
284
|
+
): Promise<void> {
|
285
|
+
const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId)
|
286
|
+
const schema = z.array(z.string()).optional()
|
287
|
+
|
288
|
+
await sublevel.batch(
|
289
|
+
Object.entries(childrenIds).map(([instanceId, ids]) => ({
|
290
|
+
type: "put",
|
291
|
+
key: instanceId,
|
292
|
+
value: schema.parse(ids),
|
293
|
+
})),
|
294
|
+
)
|
295
|
+
}
|
296
|
+
|
297
|
+
async getActiveTerminalSessions(): Promise<TerminalSession[]> {
|
298
|
+
const data = await this.db.get("activeTerminalSessionIds", { valueEncoding: "json" })
|
299
|
+
|
300
|
+
return data ? z.array(terminalSessionSchema).parse(data) : []
|
301
|
+
}
|
302
|
+
|
303
|
+
putActiveTerminalSessions(sessions: TerminalSession[]): Promise<void> {
|
304
|
+
this.validateArray(terminalSessionSchema, sessions)
|
305
|
+
|
306
|
+
return this.db.put("activeTerminalSessionIds", sessions, { valueEncoding: "json" })
|
307
|
+
}
|
308
|
+
|
309
|
+
async getTerminalSession(
|
310
|
+
projectId: string,
|
311
|
+
instanceId: string,
|
312
|
+
sessionId: string,
|
313
|
+
): Promise<TerminalSession | null> {
|
314
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
|
315
|
+
|
316
|
+
return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId)
|
317
|
+
}
|
318
|
+
|
319
|
+
async getTerminalSessions(projectId: string, instanceId: string): Promise<TerminalSession[]> {
|
320
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
|
321
|
+
|
322
|
+
return await this.getAllSublevelItems(sublevel, terminalSessionSchema)
|
323
|
+
}
|
324
|
+
|
325
|
+
async getLastTerminalSession(
|
326
|
+
projectId: string,
|
327
|
+
instanceId: string,
|
328
|
+
sessionId: string,
|
329
|
+
): Promise<TerminalSession | null> {
|
330
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
|
331
|
+
|
332
|
+
return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId)
|
333
|
+
}
|
334
|
+
|
335
|
+
async putTerminalSession(
|
336
|
+
projectId: string,
|
337
|
+
instanceId: string,
|
338
|
+
session: TerminalSession,
|
339
|
+
): Promise<void> {
|
340
|
+
this.validateItem(terminalSessionSchema, session)
|
341
|
+
|
342
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
|
343
|
+
await sublevel.put(session.id, session)
|
344
|
+
}
|
345
|
+
|
346
|
+
async getTerminalSessionHistory(sessionId: string): Promise<string[]> {
|
347
|
+
const sublevel = this.getTerminalSessionHistorySublevel(sessionId)
|
348
|
+
|
349
|
+
return await Array.fromAsync(sublevel.values())
|
350
|
+
}
|
351
|
+
|
352
|
+
async appendTerminalSessionHistory(lines: TerminalHistoryEntry[]): Promise<void> {
|
353
|
+
const sublevels = new Map<string, Sublevel<string>>()
|
354
|
+
for (const [sessionId] of lines) {
|
355
|
+
if (sublevels.has(sessionId)) {
|
356
|
+
continue
|
357
|
+
}
|
358
|
+
|
359
|
+
const sublevel = this.getTerminalSessionHistorySublevel(sessionId)
|
360
|
+
sublevels.set(sessionId, sublevel)
|
361
|
+
}
|
362
|
+
|
363
|
+
await this.db.batch(
|
364
|
+
lines.map(([sessionId, line]) => ({
|
365
|
+
type: "put",
|
366
|
+
key: uuidv7(),
|
367
|
+
value: line,
|
368
|
+
sublevel: sublevels.get(sessionId)!,
|
369
|
+
})),
|
370
|
+
)
|
371
|
+
}
|
372
|
+
|
373
|
+
private async getAllSublevelItems<TSchema extends z.ZodType>(
|
374
|
+
sublevel: Sublevel,
|
375
|
+
schema: TSchema,
|
376
|
+
limit?: number,
|
377
|
+
options?: AbstractValueIteratorOptions<string, unknown>,
|
378
|
+
): Promise<z.infer<TSchema>[]> {
|
379
|
+
const result: z.infer<TSchema>[] = []
|
380
|
+
const iterator = options ? sublevel.iterator(options) : sublevel.iterator()
|
381
|
+
const invalidKeys: string[] = []
|
382
|
+
|
383
|
+
for await (const [key, value] of iterator) {
|
384
|
+
const parseResult = schema.safeParse(value)
|
385
|
+
|
386
|
+
if (!parseResult.success) {
|
387
|
+
this.logger.warn({
|
388
|
+
msg: "failed to parse item, it will be deleted",
|
389
|
+
error: parseResult.error,
|
390
|
+
sublevel: sublevel.prefix,
|
391
|
+
key,
|
392
|
+
})
|
393
|
+
|
394
|
+
invalidKeys.push(key)
|
395
|
+
continue
|
396
|
+
}
|
397
|
+
|
398
|
+
result.push(parseResult.data as z.infer<TSchema>)
|
399
|
+
if (limit && result.length >= limit) {
|
400
|
+
break
|
401
|
+
}
|
402
|
+
}
|
403
|
+
|
404
|
+
if (invalidKeys.length > 0) {
|
405
|
+
this.logger.info({
|
406
|
+
msg: "deleting invalid items",
|
407
|
+
sublevel: sublevel.prefix,
|
408
|
+
keyCount: invalidKeys.length,
|
409
|
+
})
|
410
|
+
|
411
|
+
await sublevel.batch(invalidKeys.map(key => ({ type: "del", key })))
|
412
|
+
}
|
413
|
+
|
414
|
+
return result
|
415
|
+
}
|
416
|
+
|
417
|
+
private async getSublevelItem<TSchema extends z.ZodType>(
|
418
|
+
sublevel: Sublevel,
|
419
|
+
schema: TSchema,
|
420
|
+
key: string,
|
421
|
+
): Promise<z.infer<TSchema> | null> {
|
422
|
+
const value = await sublevel.get(key)
|
423
|
+
|
424
|
+
if (!value) {
|
425
|
+
return null
|
426
|
+
}
|
427
|
+
|
428
|
+
const parseResult = schema.safeParse(value)
|
429
|
+
|
430
|
+
if (!parseResult.success) {
|
431
|
+
this.logger.warn({
|
432
|
+
msg: "failed to parse item, it will be deleted",
|
433
|
+
error: parseResult.error,
|
434
|
+
sublevel: sublevel.prefix,
|
435
|
+
key,
|
436
|
+
})
|
437
|
+
|
438
|
+
await sublevel.del(key)
|
439
|
+
return null
|
440
|
+
}
|
441
|
+
|
442
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
443
|
+
return parseResult.data as z.infer<TSchema>
|
444
|
+
}
|
445
|
+
|
446
|
+
private async getSublevelItems<TSchema extends z.ZodType>(
|
447
|
+
sublevel: Sublevel,
|
448
|
+
schema: TSchema,
|
449
|
+
keys: string[],
|
450
|
+
): Promise<z.infer<TSchema>[]> {
|
451
|
+
const result: z.infer<TSchema>[] = []
|
452
|
+
const invalidKeys: string[] = []
|
453
|
+
|
454
|
+
for (const key of keys) {
|
455
|
+
const value = await sublevel.get(key)
|
456
|
+
|
457
|
+
if (!value) {
|
458
|
+
continue
|
459
|
+
}
|
460
|
+
|
461
|
+
const parseResult = schema.safeParse(value)
|
462
|
+
|
463
|
+
if (!parseResult.success) {
|
464
|
+
this.logger.warn({
|
465
|
+
msg: "failed to parse item, it will be deleted",
|
466
|
+
error: parseResult.error,
|
467
|
+
sublevel: sublevel.prefix,
|
468
|
+
key,
|
469
|
+
})
|
470
|
+
|
471
|
+
invalidKeys.push(key)
|
472
|
+
continue
|
473
|
+
}
|
474
|
+
|
475
|
+
result.push(parseResult.data as z.infer<TSchema>)
|
476
|
+
}
|
477
|
+
|
478
|
+
if (invalidKeys.length > 0) {
|
479
|
+
this.logger.info({
|
480
|
+
msg: "deleting invalid items",
|
481
|
+
sublevel: sublevel.prefix,
|
482
|
+
keyCount: invalidKeys.length,
|
483
|
+
})
|
484
|
+
|
485
|
+
await sublevel.batch(invalidKeys.map(key => ({ type: "del", key })))
|
486
|
+
}
|
487
|
+
|
488
|
+
return result
|
489
|
+
}
|
490
|
+
|
491
|
+
private validateItem<TSchema extends z.ZodType>(schema: TSchema, item: z.infer<TSchema>): void {
|
492
|
+
const parseResult = schema.safeParse(item)
|
493
|
+
|
494
|
+
if (!parseResult.success) {
|
495
|
+
this.logger.error({
|
496
|
+
msg: "failed to validate item",
|
497
|
+
error: parseResult.error,
|
498
|
+
item,
|
499
|
+
})
|
500
|
+
|
501
|
+
throw new Error(`Failed to validate item: ${parseResult.error.errors[0].message}`, {
|
502
|
+
cause: parseResult.error,
|
503
|
+
})
|
504
|
+
}
|
505
|
+
}
|
506
|
+
|
507
|
+
private validateArray<TSchema extends z.ZodType>(
|
508
|
+
schema: TSchema,
|
509
|
+
items: z.infer<TSchema>[],
|
510
|
+
): void {
|
511
|
+
for (const item of items) {
|
512
|
+
this.validateItem(schema, item)
|
513
|
+
}
|
514
|
+
}
|
515
|
+
|
516
|
+
private getActiveOperationsSublevel(): Sublevel {
|
517
|
+
return this.getJsonSublevel("activeOperations")
|
518
|
+
}
|
519
|
+
|
520
|
+
private getProjectOperationsSublevel(projectId: string): Sublevel {
|
521
|
+
return this.getJsonSublevel(`projects/${projectId}/operations`)
|
522
|
+
}
|
523
|
+
|
524
|
+
private getProjectCompositeInstancesSublevel(projectId: string): Sublevel {
|
525
|
+
return this.getJsonSublevel(`projects/${projectId}/compositeInstances`)
|
526
|
+
}
|
527
|
+
|
528
|
+
private getProjectCompositeInstanceInputHashesSublevel(projectId: string): Sublevel<string> {
|
529
|
+
return this.getStringSublevel(`projects/${projectId}/compositeInstanceInputHashes`)
|
530
|
+
}
|
531
|
+
|
532
|
+
private getProjectCompositeChildrenIdsSublevel(projectId: string): Sublevel {
|
533
|
+
return this.getJsonSublevel(`projects/${projectId}/topLevelCompositeChildrenIds`)
|
534
|
+
}
|
535
|
+
|
536
|
+
private getProjectInstanceStatesSublevel(projectId: string): Sublevel {
|
537
|
+
return this.getJsonSublevel(`projects/${projectId}/instanceStates`)
|
538
|
+
}
|
539
|
+
|
540
|
+
private getOperationInstanceStatesSublevel(operationId: string): Sublevel {
|
541
|
+
return this.getJsonSublevel(`operations/${operationId}/instanceStates`)
|
542
|
+
}
|
543
|
+
|
544
|
+
private getOperationInstanceLogsSublevel(
|
545
|
+
operationId: string,
|
546
|
+
instanceId: string,
|
547
|
+
): Sublevel<string> {
|
548
|
+
return this.getStringSublevel(`operations/${operationId}/instances/${instanceId}/logs`)
|
549
|
+
}
|
550
|
+
|
551
|
+
private getTerminalSessionsSublevel(projectId: string, instanceId: string): Sublevel {
|
552
|
+
return this.getJsonSublevel(`projects/${projectId}/instances/${instanceId}/terminalSessions`)
|
553
|
+
}
|
554
|
+
|
555
|
+
private getTerminalSessionHistorySublevel(sessionId: string): Sublevel<string> {
|
556
|
+
return this.getStringSublevel(`terminalSessions/${sessionId}/history`)
|
557
|
+
}
|
558
|
+
|
559
|
+
private getStringSublevel(path: string): Sublevel<string> {
|
560
|
+
return this.db.sublevel<string, string>(path, { valueEncoding: "utf8" })
|
561
|
+
}
|
562
|
+
|
563
|
+
private getJsonSublevel(path: string): Sublevel {
|
564
|
+
return this.db.sublevel<string, unknown>(path, { valueEncoding: "json" })
|
565
|
+
}
|
566
|
+
|
567
|
+
static async create(
|
568
|
+
config: z.infer<typeof localStateBackendConfig>,
|
569
|
+
localPulumiHost: LocalPulumiHost,
|
570
|
+
logger: Logger,
|
571
|
+
): Promise<StateBackend> {
|
572
|
+
const childLogger = logger.child({ backend: "StateBackend", service: "LocalStateBackend" })
|
573
|
+
|
574
|
+
let location = config.HIGHSTATE_BACKEND_STATE_LOCAL_DIR
|
575
|
+
if (!location) {
|
576
|
+
const currentUser = await localPulumiHost.getCurrentUser()
|
577
|
+
|
578
|
+
if (!currentUser) {
|
579
|
+
throw new Error(
|
580
|
+
"The pulumi state is not specified, please run `pulumi login` or specify the state location manually before restarting the service",
|
581
|
+
)
|
582
|
+
}
|
583
|
+
|
584
|
+
if (!currentUser.url) {
|
585
|
+
throw new Error(
|
586
|
+
"The pulumi user does not have a URL, please specify the state location manually",
|
587
|
+
)
|
588
|
+
}
|
589
|
+
|
590
|
+
const path = currentUser.url.replace("file://", "").replace("~", process.env.HOME!)
|
591
|
+
location = resolve(path, ".pulumi", ".highstate")
|
592
|
+
|
593
|
+
childLogger.debug({
|
594
|
+
msg: "auto-detected state location",
|
595
|
+
pulumiStateUrl: currentUser.url,
|
596
|
+
location,
|
597
|
+
})
|
598
|
+
}
|
599
|
+
|
600
|
+
const { ClassicLevel } = await import("classic-level")
|
601
|
+
const db = new ClassicLevel<string, unknown>(location)
|
602
|
+
|
603
|
+
return new LocalStateBackend(db, childLogger)
|
604
|
+
}
|
605
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import type { InstanceState } from "../shared"
|
2
|
+
import EventEmitter, { on } from "node:events"
|
3
|
+
|
4
|
+
export type StateEvents = Record<string, [Partial<InstanceState>]>
|
5
|
+
|
6
|
+
export class StateManager {
|
7
|
+
private readonly stateEE = new EventEmitter<StateEvents>()
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Watches for all instance state changes in the project.
|
11
|
+
*
|
12
|
+
* @param projectId The project ID to watch.
|
13
|
+
* @param signal The signal to abort the operation.
|
14
|
+
*/
|
15
|
+
public async *watchInstanceStates(
|
16
|
+
projectId: string,
|
17
|
+
signal?: AbortSignal,
|
18
|
+
): AsyncIterable<Partial<InstanceState>> {
|
19
|
+
for await (const [state] of on(this.stateEE, projectId, { signal })) {
|
20
|
+
yield state
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Emits a state patch for the instance in the project.
|
26
|
+
*
|
27
|
+
* @param projectId The project ID to emit the state patch for.
|
28
|
+
* @param patch The state patch to emit.
|
29
|
+
*/
|
30
|
+
public emitStatePatch(projectId: string, patch: Partial<InstanceState>): void {
|
31
|
+
this.stateEE.emit(projectId, patch)
|
32
|
+
}
|
33
|
+
}
|