@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.
Files changed (74) hide show
  1. package/dist/{index.mjs → index.js} +1254 -915
  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,230 @@
1
+ import type { ConfigMap, OpMap, OpType, Stack, WhoAmIResult } from "@pulumi/pulumi/automation"
2
+ import type { Logger } from "pino"
3
+ import { BetterLock } from "better-lock"
4
+ import { AbortError, runWithRetryOnError } from "./utils"
5
+
6
+ export type RunOptions = {
7
+ projectId: string
8
+ pulumiProjectName: string
9
+ pulumiStackName: string
10
+ envVars?: Record<string, string>
11
+ }
12
+
13
+ export type RunLocalOptions = RunOptions & {
14
+ projectPath: string
15
+ stackConfig?: ConfigMap
16
+ }
17
+
18
+ export class LocalPulumiHost {
19
+ private lock = new BetterLock()
20
+
21
+ private constructor(private readonly logger: Logger) {}
22
+
23
+ async getCurrentUser(): Promise<WhoAmIResult | null> {
24
+ const { LocalWorkspace } = await import("@pulumi/pulumi/automation/index.js")
25
+ const workspace = await LocalWorkspace.create({})
26
+
27
+ try {
28
+ return await workspace.whoAmI()
29
+ } catch (error) {
30
+ this.logger.error({ msg: "failed to get current user", error })
31
+
32
+ return null
33
+ }
34
+ }
35
+
36
+ async runEmpty<T>(
37
+ options: RunOptions,
38
+ fn: (stack: Stack) => Promise<T>,
39
+ signal?: AbortSignal,
40
+ ): Promise<T> {
41
+ const { projectId, pulumiProjectName, pulumiStackName, envVars } = options
42
+
43
+ return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
44
+ const { LocalWorkspace } = await import("@pulumi/pulumi/automation/index.js")
45
+
46
+ const stack = await LocalWorkspace.createOrSelectStack(
47
+ {
48
+ projectName: pulumiProjectName,
49
+ stackName: pulumiStackName,
50
+ program: () => Promise.resolve(),
51
+ },
52
+ {
53
+ projectSettings: {
54
+ name: pulumiProjectName,
55
+ runtime: "nodejs",
56
+ },
57
+ envVars: {
58
+ PULUMI_CONFIG_PASSPHRASE: this.getPassword(projectId),
59
+ PULUMI_K8S_AWAIT_ALL: "true",
60
+ ...envVars,
61
+ },
62
+ },
63
+ )
64
+
65
+ signal?.throwIfAborted()
66
+
67
+ try {
68
+ return await runWithRetryOnError(
69
+ () => fn(stack),
70
+ error => this.tryUnlockStack(stack, error),
71
+ )
72
+ } catch (e) {
73
+ if (e instanceof Error && e.message.includes("canceled")) {
74
+ throw new AbortError()
75
+ }
76
+
77
+ throw e
78
+ }
79
+ })
80
+ }
81
+
82
+ async runLocal<T>(
83
+ options: RunLocalOptions,
84
+ fn: (stack: Stack) => Promise<T>,
85
+ signal?: AbortSignal,
86
+ ): Promise<T> {
87
+ const { projectId, pulumiProjectName, pulumiStackName, projectPath, stackConfig, envVars } =
88
+ options
89
+
90
+ return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
91
+ const { LocalWorkspace } = await import("@pulumi/pulumi/automation/index.js")
92
+
93
+ const stack = await LocalWorkspace.createOrSelectStack(
94
+ {
95
+ stackName: pulumiStackName,
96
+ workDir: projectPath,
97
+ },
98
+ {
99
+ projectSettings: {
100
+ name: pulumiProjectName,
101
+ runtime: "nodejs",
102
+ },
103
+ stackSettings: stackConfig
104
+ ? {
105
+ [pulumiStackName]: {
106
+ config: stackConfig,
107
+ },
108
+ }
109
+ : undefined,
110
+ envVars: {
111
+ PULUMI_CONFIG_PASSPHRASE: this.getPassword(projectId),
112
+ PULUMI_K8S_AWAIT_ALL: "true",
113
+ ...envVars,
114
+ },
115
+ },
116
+ )
117
+
118
+ signal?.throwIfAborted()
119
+
120
+ try {
121
+ return await runWithRetryOnError(
122
+ () => fn(stack),
123
+ error => this.tryUnlockStack(stack, error),
124
+ )
125
+ } catch (e) {
126
+ if (e instanceof Error && e.message.includes("canceled")) {
127
+ throw new AbortError()
128
+ }
129
+
130
+ throw e
131
+ }
132
+ })
133
+ }
134
+
135
+ private sharedPassword: string = process.env.PULUMI_CONFIG_PASSPHRASE ?? ""
136
+ private passwords = new Map<string, string>()
137
+
138
+ hasPassword(projectId: string) {
139
+ return !!this.sharedPassword || this.passwords.has(projectId)
140
+ }
141
+
142
+ setPassword(projectId: string, password: string) {
143
+ this.passwords.set(projectId, password)
144
+ }
145
+
146
+ removePassword(projectId: string) {
147
+ this.passwords.delete(projectId)
148
+ }
149
+
150
+ private getPassword(projectId: string) {
151
+ return this.sharedPassword || this.passwords.get(projectId) || ""
152
+ }
153
+
154
+ async tryUnlockStack(stack: Stack, error: unknown) {
155
+ if (error instanceof Error && error.message.includes("the stack is currently locked")) {
156
+ // TODO: kill the process if the hostname matches the current hostname
157
+
158
+ this.logger.warn({ stackName: stack.name }, "inlocking stack")
159
+ await stack.cancel()
160
+ return true
161
+ }
162
+
163
+ return false
164
+ }
165
+
166
+ static create(logger: Logger) {
167
+ return new LocalPulumiHost(logger.child({ service: "LocalPulumiHost" }))
168
+ }
169
+ }
170
+
171
+ export function valueToString(value: unknown): string {
172
+ if (typeof value === "string") {
173
+ return value
174
+ }
175
+
176
+ return JSON.stringify(value)
177
+ }
178
+
179
+ export function stringToValue(value: string): unknown {
180
+ try {
181
+ return JSON.parse(value)
182
+ } catch {
183
+ return value
184
+ }
185
+ }
186
+
187
+ export function updateResourceCount(opType: OpType, currentCount: number): number {
188
+ switch (opType) {
189
+ case "same":
190
+ case "create":
191
+ case "update":
192
+ case "replace":
193
+ case "create-replacement":
194
+ case "import":
195
+ case "import-replacement":
196
+ return currentCount + 1
197
+
198
+ case "delete":
199
+ case "delete-replaced":
200
+ case "discard":
201
+ case "discard-replaced":
202
+ case "remove-pending-replace":
203
+ return currentCount - 1
204
+
205
+ case "refresh":
206
+ case "read-replacement":
207
+ case "read":
208
+ return currentCount
209
+
210
+ default:
211
+ throw new Error(`Unknown operation type: ${opType as string}`)
212
+ }
213
+ }
214
+
215
+ export function calculateTotalResources(opMap: OpMap | undefined): number {
216
+ if (!opMap) {
217
+ return 0 // No operations imply no resources
218
+ }
219
+
220
+ let total = 0
221
+
222
+ for (const [op, count] of Object.entries(opMap)) {
223
+ const opType = op as OpType
224
+ const value = count ?? 0
225
+
226
+ total = updateResourceCount(opType, value)
227
+ }
228
+
229
+ return total
230
+ }
@@ -0,0 +1,137 @@
1
+ import { z } from "zod"
2
+
3
+ export async function runWithRetryOnError<T>(
4
+ runner: () => T | Promise<T>,
5
+ tryHandleError: (error: unknown) => boolean | Promise<boolean>,
6
+ maxRetries: number = 1,
7
+ ): Promise<T> {
8
+ let lastError: unknown
9
+
10
+ for (let i = 0; i < maxRetries + 1; i++) {
11
+ try {
12
+ return await runner()
13
+ } catch (e) {
14
+ lastError = e
15
+
16
+ if (await tryHandleError(e)) {
17
+ continue
18
+ }
19
+
20
+ throw e
21
+ }
22
+ }
23
+
24
+ throw lastError
25
+ }
26
+
27
+ export class AbortError extends Error {
28
+ constructor(options?: ErrorOptions) {
29
+ super("Operation aborted", options)
30
+ }
31
+ }
32
+
33
+ export function isAbortError(error: unknown): boolean {
34
+ return error instanceof Error && error.name === "AbortError"
35
+ }
36
+
37
+ const abortMessagePatterns = ["Operation aborted", "Command was killed with SIGINT"]
38
+
39
+ export function isAbortErrorLike(error: unknown): boolean {
40
+ if (error instanceof Error) {
41
+ return abortMessagePatterns.some(pattern => error.message.includes(pattern))
42
+ }
43
+
44
+ return false
45
+ }
46
+
47
+ export function tryWrapAbortErrorLike(error: unknown): unknown {
48
+ if (isAbortErrorLike(error)) {
49
+ return new AbortError({ cause: error })
50
+ }
51
+
52
+ return error
53
+ }
54
+
55
+ export const stringArrayType = z.string().transform(args => args.split(",").map(arg => arg.trim()))
56
+
57
+ export function errorToString(error: unknown): string {
58
+ if (error instanceof Error) {
59
+ return error.stack || error.message
60
+ }
61
+
62
+ return JSON.stringify(error)
63
+ }
64
+
65
+ export type AsyncBatcherOptions = {
66
+ waitMs?: number
67
+ maxWaitTimeMs?: number
68
+ }
69
+
70
+ export function createAsyncBatcher<T>(
71
+ fn: (items: T[]) => Promise<void>,
72
+ { waitMs = 100, maxWaitTimeMs = 1000 }: AsyncBatcherOptions = {},
73
+ ) {
74
+ let batch: T[] = []
75
+ let activeTimeout: NodeJS.Timeout | null = null
76
+ let maxWaitTimeout: NodeJS.Timeout | null = null
77
+ let firstCallTimestamp: number | null = null
78
+
79
+ async function processBatch() {
80
+ if (batch.length === 0) return
81
+
82
+ const currentBatch = batch
83
+ batch = [] // Reset batch before async call
84
+
85
+ await fn(currentBatch)
86
+
87
+ // Clear max wait timer since batch has been processed
88
+ if (maxWaitTimeout) {
89
+ clearTimeout(maxWaitTimeout)
90
+ maxWaitTimeout = null
91
+ }
92
+ firstCallTimestamp = null
93
+ }
94
+
95
+ function schedule() {
96
+ if (activeTimeout) clearTimeout(activeTimeout)
97
+ activeTimeout = setTimeout(() => {
98
+ activeTimeout = null
99
+ void processBatch()
100
+ }, waitMs)
101
+
102
+ // Ensure batch is executed within maxWaitTimeMs
103
+ if (!firstCallTimestamp) {
104
+ firstCallTimestamp = Date.now()
105
+ maxWaitTimeout = setTimeout(() => {
106
+ if (activeTimeout) clearTimeout(activeTimeout)
107
+ activeTimeout = null
108
+ void processBatch()
109
+ }, maxWaitTimeMs)
110
+ }
111
+ }
112
+
113
+ return {
114
+ /**
115
+ * Add an item to the batch.
116
+ */
117
+ call(item: T): void {
118
+ batch.push(item)
119
+ schedule()
120
+ },
121
+
122
+ /**
123
+ * Immediately flush the pending batch (if any).
124
+ */
125
+ async flush(): Promise<void> {
126
+ if (activeTimeout) {
127
+ clearTimeout(activeTimeout)
128
+ activeTimeout = null
129
+ }
130
+ if (maxWaitTimeout) {
131
+ clearTimeout(maxWaitTimeout)
132
+ maxWaitTimeout = null
133
+ }
134
+ await processBatch()
135
+ },
136
+ }
137
+ }
package/src/config.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { z } from "zod"
2
+ import { libraryBackendConfig } from "./library"
3
+ import { projectBackendConfig } from "./project"
4
+ import { secretBackendConfig } from "./secret"
5
+ import { terminalBackendConfig } from "./terminal"
6
+ import { runnerBackendConfig } from "./runner"
7
+ import { stateBackendConfig } from "./state"
8
+ import { workspaceBackendConfig } from "./workspace"
9
+
10
+ const loggerConfig = z.object({
11
+ HIGHSTATE_BACKEND_LOGGER_NAME: z.string().default("highstate-backend"),
12
+
13
+ HIGHSTATE_BACKEND_LOGGER_LEVEL: z
14
+ .enum(["fatal", "error", "warn", "info", "debug", "trace"])
15
+ .default("info"),
16
+ })
17
+
18
+ const configSchema = z.object({
19
+ ...libraryBackendConfig.shape,
20
+ ...projectBackendConfig.shape,
21
+ ...secretBackendConfig.shape,
22
+ ...stateBackendConfig.shape,
23
+ ...runnerBackendConfig.shape,
24
+ ...terminalBackendConfig.shape,
25
+ ...workspaceBackendConfig.shape,
26
+ ...loggerConfig.shape,
27
+ })
28
+
29
+ export type Config = z.infer<typeof configSchema>
30
+
31
+ export async function loadConfig(
32
+ env: NodeJS.ProcessEnv = process.env,
33
+ useDotenv = true,
34
+ ): Promise<Config> {
35
+ if (useDotenv) {
36
+ await import("dotenv/config")
37
+ }
38
+
39
+ return configSchema.parse(env)
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./secret"
2
+ export * from "./library"
3
+ export * from "./config"
4
+ export * from "./orchestrator"
5
+ export * from "./terminal"
6
+ export * from "./services"
@@ -0,0 +1,83 @@
1
+ import type { InstanceModel, CompositeInstance } from "@highstate/contract"
2
+ import type { LibraryModel, LibraryUpdate, ResolvedInstanceInput } from "../shared"
3
+
4
+ export type ResolvedUnitSource = {
5
+ unitType: string
6
+ serializedSource: string
7
+ projectPath: string
8
+ packageJsonPath: string
9
+ allowedDependencies: string[]
10
+ sourceHash: string
11
+ }
12
+
13
+ export type ModuleEvaluationResult =
14
+ | {
15
+ success: true
16
+ compositeInstances: CompositeInstance[]
17
+ }
18
+ | {
19
+ success: false
20
+ modulePath: string
21
+ error: string
22
+ }
23
+
24
+ export type InstanceEvaluationResult =
25
+ | {
26
+ success: true
27
+ instanceId: string
28
+ compositeInstances: CompositeInstance[]
29
+ }
30
+ | {
31
+ success: false
32
+ instanceId: string
33
+ error: string
34
+ }
35
+
36
+ export interface LibraryBackend {
37
+ /**
38
+ * Loads the library.
39
+ */
40
+ loadLibrary(signal?: AbortSignal): Promise<LibraryModel>
41
+
42
+ /**
43
+ * Watches the library for changes.
44
+ */
45
+ watchLibrary(signal?: AbortSignal): AsyncIterable<LibraryUpdate[]>
46
+
47
+ /**
48
+ * Gets the resolved unit sources for all units in the library.
49
+ */
50
+ getResolvedUnitSources(): Promise<readonly ResolvedUnitSource[]>
51
+
52
+ /**
53
+ * Gets the resolved unit source for the specified unit type.
54
+ */
55
+ getResolvedUnitSource(unitType: string): Promise<ResolvedUnitSource | null>
56
+
57
+ /**
58
+ * Watches the resolved unit sources for changes.
59
+ * Returns an async iterable that emits each resolved unit source whenever it changes.
60
+ * Does not emit the resolved unit sources for units that have not changed even if the library was reloaded.
61
+ */
62
+ watchResolvedUnitSources(signal?: AbortSignal): AsyncIterable<ResolvedUnitSource>
63
+
64
+ /**
65
+ * Evaluates the instances and returns the evaluated composite instances.
66
+ *
67
+ * @param allInstances The all instances of the project.
68
+ * @param resolvedInputs The resolved inputs of the instances.
69
+ * @param instanceIds The instance ids to evaluate.
70
+ */
71
+ evaluateCompositeInstances(
72
+ allInstances: InstanceModel[],
73
+ resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,
74
+ instanceIds: string[],
75
+ ): Promise<InstanceEvaluationResult[]>
76
+
77
+ /**
78
+ * Evaluates the modules and returns the evaluated instances.
79
+ *
80
+ * @param modulePaths The module paths to evaluate.
81
+ */
82
+ evaluateModules(modulePaths: string[]): Promise<ModuleEvaluationResult>
83
+ }
@@ -0,0 +1,20 @@
1
+ import type { LibraryBackend } from "./abstractions"
2
+ import type { Logger } from "pino"
3
+ import { z } from "zod"
4
+ import { LocalLibraryBackend, localLibraryBackendConfig } from "./local"
5
+
6
+ export const libraryBackendConfig = z.object({
7
+ HIGHSTATE_BACKEND_LIBRARY_TYPE: z.enum(["local"]).default("local"),
8
+ ...localLibraryBackendConfig.shape,
9
+ })
10
+
11
+ export async function createLibraryBackend(
12
+ config: z.infer<typeof libraryBackendConfig>,
13
+ logger: Logger,
14
+ ): Promise<LibraryBackend> {
15
+ switch (config.HIGHSTATE_BACKEND_LIBRARY_TYPE) {
16
+ case "local": {
17
+ return await LocalLibraryBackend.create(config, logger)
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./abstractions"
2
+ export * from "./factory"