@highstate/backend 0.9.15 → 0.9.18

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 (187) hide show
  1. package/dist/chunk-NAAIDR4U.js +8499 -0
  2. package/dist/chunk-NAAIDR4U.js.map +1 -0
  3. package/dist/chunk-OU5OQBLB.js +74 -0
  4. package/dist/chunk-OU5OQBLB.js.map +1 -0
  5. package/dist/chunk-Y7DXREVO.js +1745 -0
  6. package/dist/chunk-Y7DXREVO.js.map +1 -0
  7. package/dist/highstate.manifest.json +4 -4
  8. package/dist/index.js +7227 -2501
  9. package/dist/index.js.map +1 -1
  10. package/dist/library/package-resolution-worker.js +7 -5
  11. package/dist/library/package-resolution-worker.js.map +1 -1
  12. package/dist/library/worker/main.js +76 -185
  13. package/dist/library/worker/main.js.map +1 -1
  14. package/dist/magic-string.es-5ABAC4JN.js +1292 -0
  15. package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
  16. package/dist/shared/index.js +3 -98
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +31 -10
  19. package/src/artifact/abstractions.ts +46 -0
  20. package/src/artifact/encryption.ts +109 -0
  21. package/src/artifact/factory.ts +36 -0
  22. package/src/artifact/index.ts +3 -0
  23. package/src/artifact/local.ts +138 -0
  24. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
  25. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
  26. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
  27. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
  28. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
  29. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
  30. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
  31. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
  32. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
  33. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
  34. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
  35. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
  36. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
  37. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
  38. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
  39. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
  40. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
  41. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
  42. package/src/business/api-key.ts +65 -0
  43. package/src/business/artifact.ts +289 -0
  44. package/src/business/backend-unlock.ts +10 -0
  45. package/src/business/index.ts +10 -0
  46. package/src/business/instance-lock.ts +125 -0
  47. package/src/business/instance-state.ts +434 -0
  48. package/src/business/operation.ts +251 -0
  49. package/src/business/project-unlock.ts +260 -0
  50. package/src/business/project.ts +299 -0
  51. package/src/business/secret.test.ts +178 -0
  52. package/src/business/secret.ts +281 -0
  53. package/src/business/worker.test.ts +614 -0
  54. package/src/business/worker.ts +398 -0
  55. package/src/common/clock.ts +18 -0
  56. package/src/common/index.ts +5 -1
  57. package/src/common/performance.ts +44 -0
  58. package/src/common/random.ts +68 -0
  59. package/src/common/test/index.ts +2 -0
  60. package/src/common/test/render.ts +98 -0
  61. package/src/common/test/tracer.ts +359 -0
  62. package/src/common/tree.ts +33 -0
  63. package/src/common/utils.ts +40 -1
  64. package/src/config.ts +19 -11
  65. package/src/hotstate/abstractions.ts +48 -0
  66. package/src/hotstate/factory.ts +17 -0
  67. package/src/{secret → hotstate}/index.ts +1 -0
  68. package/src/hotstate/manager.ts +192 -0
  69. package/src/hotstate/memory.ts +100 -0
  70. package/src/hotstate/validation.ts +100 -0
  71. package/src/index.ts +2 -1
  72. package/src/library/abstractions.ts +24 -28
  73. package/src/library/factory.ts +2 -2
  74. package/src/library/local.ts +91 -111
  75. package/src/library/worker/evaluator.ts +36 -73
  76. package/src/library/worker/loader.lite.ts +54 -0
  77. package/src/library/worker/main.ts +15 -66
  78. package/src/library/worker/protocol.ts +6 -33
  79. package/src/lock/abstractions.ts +6 -0
  80. package/src/lock/factory.ts +15 -0
  81. package/src/lock/index.ts +4 -0
  82. package/src/lock/manager.ts +97 -0
  83. package/src/lock/memory.ts +19 -0
  84. package/src/lock/test.ts +108 -0
  85. package/src/orchestrator/manager.ts +118 -90
  86. package/src/orchestrator/operation-workset.ts +181 -93
  87. package/src/orchestrator/operation.ts +1021 -283
  88. package/src/project/abstractions.ts +27 -38
  89. package/src/project/evaluation.ts +248 -0
  90. package/src/project/factory.ts +1 -1
  91. package/src/project/index.ts +1 -2
  92. package/src/project/local.ts +107 -103
  93. package/src/pubsub/abstractions.ts +13 -0
  94. package/src/pubsub/factory.ts +19 -0
  95. package/src/{workspace → pubsub}/index.ts +1 -0
  96. package/src/pubsub/local.ts +36 -0
  97. package/src/pubsub/manager.ts +108 -0
  98. package/src/pubsub/validation.ts +33 -0
  99. package/src/runner/abstractions.ts +155 -68
  100. package/src/runner/artifact-env.ts +160 -0
  101. package/src/runner/factory.ts +20 -5
  102. package/src/runner/force-abort.ts +117 -0
  103. package/src/runner/local.ts +292 -372
  104. package/src/{common → runner}/pulumi.ts +89 -37
  105. package/src/services.ts +251 -40
  106. package/src/shared/index.ts +3 -11
  107. package/src/shared/models/backend/index.ts +3 -0
  108. package/src/shared/{library.ts → models/backend/library.ts} +4 -4
  109. package/src/shared/models/backend/project.ts +82 -0
  110. package/src/shared/models/backend/unlock-method.ts +20 -0
  111. package/src/shared/models/base.ts +68 -0
  112. package/src/shared/models/errors.ts +5 -0
  113. package/src/shared/models/index.ts +4 -0
  114. package/src/shared/models/project/api-key.ts +65 -0
  115. package/src/shared/models/project/artifact.ts +83 -0
  116. package/src/shared/models/project/index.ts +13 -0
  117. package/src/shared/models/project/lock.ts +91 -0
  118. package/src/shared/models/project/model.ts +14 -0
  119. package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
  120. package/src/shared/models/project/page.ts +57 -0
  121. package/src/shared/models/project/secret.ts +98 -0
  122. package/src/shared/models/project/service-account.ts +22 -0
  123. package/src/shared/models/project/state.ts +449 -0
  124. package/src/shared/models/project/terminal.ts +98 -0
  125. package/src/shared/models/project/trigger.ts +56 -0
  126. package/src/shared/models/project/unlock-method.ts +38 -0
  127. package/src/shared/models/project/worker.ts +107 -0
  128. package/src/shared/resolvers/graph-resolver.ts +61 -18
  129. package/src/shared/resolvers/index.ts +5 -0
  130. package/src/shared/resolvers/input-hash.ts +53 -15
  131. package/src/shared/resolvers/input.ts +47 -13
  132. package/src/shared/resolvers/registry.ts +3 -2
  133. package/src/shared/resolvers/state.ts +2 -2
  134. package/src/shared/resolvers/validation.ts +82 -25
  135. package/src/shared/utils/args.ts +25 -0
  136. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  137. package/src/shared/utils/hash.ts +6 -0
  138. package/src/shared/utils/index.ts +4 -0
  139. package/src/shared/utils/promise-tracker.ts +23 -0
  140. package/src/state/abstractions.ts +199 -131
  141. package/src/state/encryption.ts +98 -0
  142. package/src/state/factory.ts +3 -5
  143. package/src/state/index.ts +4 -0
  144. package/src/state/keyring.ts +22 -0
  145. package/src/state/local/backend.ts +106 -0
  146. package/src/state/local/collection.ts +361 -0
  147. package/src/state/local/index.ts +2 -0
  148. package/src/state/manager.ts +875 -18
  149. package/src/state/memory/backend.ts +70 -0
  150. package/src/state/memory/collection.ts +270 -0
  151. package/src/state/memory/index.ts +2 -0
  152. package/src/state/repository/index.ts +2 -0
  153. package/src/state/repository/repository.index.ts +193 -0
  154. package/src/state/repository/repository.ts +507 -0
  155. package/src/state/test.ts +457 -0
  156. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  157. package/src/terminal/docker.ts +18 -14
  158. package/src/terminal/factory.ts +3 -3
  159. package/src/terminal/index.ts +1 -1
  160. package/src/terminal/manager.ts +131 -79
  161. package/src/terminal/run.sh.ts +21 -11
  162. package/src/unlock/abstractions.ts +49 -0
  163. package/src/unlock/index.ts +2 -0
  164. package/src/unlock/memory.ts +32 -0
  165. package/src/worker/abstractions.ts +42 -0
  166. package/src/worker/docker.ts +83 -0
  167. package/src/worker/factory.ts +20 -0
  168. package/src/worker/index.ts +3 -0
  169. package/src/worker/manager.ts +167 -0
  170. package/dist/chunk-KTGKNSKM.js +0 -979
  171. package/dist/chunk-KTGKNSKM.js.map +0 -1
  172. package/dist/chunk-WXDYCRTT.js +0 -234
  173. package/dist/chunk-WXDYCRTT.js.map +0 -1
  174. package/src/library/worker/loader.ts +0 -114
  175. package/src/preferences/shared.ts +0 -1
  176. package/src/project/lock.ts +0 -39
  177. package/src/project/manager.ts +0 -433
  178. package/src/secret/abstractions.ts +0 -59
  179. package/src/secret/factory.ts +0 -22
  180. package/src/secret/local.ts +0 -152
  181. package/src/shared/project.ts +0 -62
  182. package/src/shared/state.ts +0 -247
  183. package/src/shared/terminal.ts +0 -14
  184. package/src/state/local.ts +0 -612
  185. package/src/workspace/abstractions.ts +0 -41
  186. package/src/workspace/factory.ts +0 -14
  187. package/src/workspace/local.ts +0 -54
@@ -0,0 +1,107 @@
1
+ import { z } from "zod"
2
+ import { objectMetaSchema } from "@highstate/contract"
3
+
4
+ export const workerStatus = z.enum([
5
+ // transient statuses
6
+ "running",
7
+ "starting",
8
+ ])
9
+
10
+ export const MAX_WORKER_START_ATTEMPTS = 5
11
+
12
+ export const workerSchema = z.object({
13
+ id: z.uuidv7(),
14
+
15
+ meta: objectMetaSchema.pick({
16
+ title: true,
17
+ description: true,
18
+ icon: true,
19
+ iconColor: true,
20
+ createdAt: true,
21
+ updatedAt: true,
22
+ }),
23
+
24
+ /**
25
+ * The status of the worker.
26
+ */
27
+ status: workerStatus,
28
+
29
+ /**
30
+ * The number of failed attempts to start the worker. When a worker fails to start,
31
+ * this count is incremented.
32
+ * If the worker fails to start more than 5 times in a row, it is
33
+ * considered unhealthy and will not be started again
34
+ *
35
+ * This count is reset when the worker is successfully started and connected.
36
+ */
37
+ failedStartAttempts: z.number(),
38
+
39
+ /**
40
+ * The fully qualified base docker image without tag and digest.
41
+ *
42
+ * It serves as the identity of the worker and can be shared across workers.
43
+ *
44
+ * Multiple workers sharing the same identity also share the same service accoun
45
+ * and have equal access to all its resources.
46
+ *
47
+ * Example: `ghcr.io/highstate-io/kubernetes-worker`
48
+ */
49
+ identity: z.string(),
50
+
51
+ /**
52
+ * The fully qualified docker image name for this worker.
53
+ *
54
+ * Example: `ghcr.io/highstate-io/kubernetes-worker:v1.0.0@sha256:abc123...`
55
+ */
56
+ image: z.string(),
57
+
58
+ /**
59
+ * The ID of the service account owned by this worker.
60
+ */
61
+ serviceAccountId: z.uuidv7(),
62
+
63
+ /**
64
+ * The ID of the API key used to authenticate this worker with the system.
65
+ *
66
+ * Will be generated on the first run of the worker and will be rotated every time the worker is restarted.
67
+ */
68
+ apiKeyId: z.uuidv7(),
69
+ })
70
+
71
+ export const workerUnitRegistrationSchema = z.object({
72
+ id: z.uuidv7(),
73
+
74
+ meta: objectMetaSchema.pick({ createdAt: true, updatedAt: true }),
75
+
76
+ workerId: z.uuidv7(),
77
+ instanceId: z.string(),
78
+
79
+ /**
80
+ * The fully qualified base docker image with tag and digest.
81
+ */
82
+ image: z.string(),
83
+
84
+ /**
85
+ * The parameters to pass to the worker.
86
+ */
87
+ params: z.record(z.string(), z.unknown()),
88
+ })
89
+
90
+ export const unitWorkerSchema = z.object({
91
+ name: z.string(),
92
+ image: z.string(),
93
+ params: z.record(z.string(), z.unknown()),
94
+ })
95
+
96
+ export type Worker = z.infer<typeof workerSchema>
97
+ export type WorkerUnitRegistration = z.infer<typeof workerUnitRegistrationSchema>
98
+ export type UnitWorker = z.infer<typeof unitWorkerSchema>
99
+
100
+ export function getWorkerIdentity(image: string): string {
101
+ const parts = image.split(":")
102
+ if (parts.length < 2) {
103
+ throw new Error(`Invalid image format: ${image}`)
104
+ }
105
+
106
+ return parts.slice(0, -1).join(":") // Remove the tag and digest
107
+ }
@@ -2,6 +2,7 @@ import type { Logger } from "pino"
2
2
  import { unique } from "remeda"
3
3
 
4
4
  export type ResolverOutputHandler<TOutput> = (id: string, value: TOutput) => void
5
+ export type DependentSetHandler = (id: string, dependentSet: Set<string> | undefined) => void
5
6
 
6
7
  export abstract class GraphResolver<TNode, TOutput> {
7
8
  private readonly workset: Set<string> = new Set()
@@ -13,6 +14,7 @@ export abstract class GraphResolver<TNode, TOutput> {
13
14
  private readonly nodes: ReadonlyMap<string, TNode>,
14
15
  protected readonly logger: Logger,
15
16
  private readonly outputHandler?: ResolverOutputHandler<TOutput>,
17
+ private readonly dependentSetHandler?: DependentSetHandler,
16
18
  ) {}
17
19
 
18
20
  addToWorkset(nodeId: string): void {
@@ -32,6 +34,15 @@ export abstract class GraphResolver<TNode, TOutput> {
32
34
  return this.outputMap
33
35
  }
34
36
 
37
+ /**
38
+ * The map of dependencies for each node.
39
+ *
40
+ * The key is the node identifier, and the value is an array of identifiers of the nodes which depend on it.
41
+ */
42
+ get dependents(): ReadonlyMap<string, Set<string>> {
43
+ return this.dependentMap
44
+ }
45
+
35
46
  requireOutput(nodeId: string): TOutput {
36
47
  const output = this.outputMap.get(nodeId)
37
48
  if (!output) {
@@ -54,27 +65,17 @@ export abstract class GraphResolver<TNode, TOutput> {
54
65
  protected abstract processNode(node: TNode, logger: Logger): TOutput | Promise<TOutput>
55
66
 
56
67
  /**
57
- * Gets all identifiers of the nodes that depend on the given node directly or indirectly.
68
+ * Gets the identifiers of the nodes that depend on the given node directly.
69
+ *
70
+ * Returns an empty array if there are no dependents.
58
71
  */
59
- getAllDependents(nodeId: string): string[] {
60
- const result = new Set<string>()
61
- const stack: string[] = [nodeId]
62
-
63
- while (stack.length > 0) {
64
- const dependents = this.dependentMap.get(stack.pop()!)
65
- if (!dependents) {
66
- continue
67
- }
68
-
69
- for (const dependentId of dependents) {
70
- if (!result.has(dependentId)) {
71
- result.add(dependentId)
72
- stack.push(dependentId)
73
- }
74
- }
72
+ getDependents(nodeId: string): string[] {
73
+ const dependents = this.dependentMap.get(nodeId)
74
+ if (!dependents) {
75
+ return []
75
76
  }
76
77
 
77
- return Array.from(result)
78
+ return Array.from(dependents)
78
79
  }
79
80
 
80
81
  /**
@@ -175,12 +176,15 @@ export abstract class GraphResolver<TNode, TOutput> {
175
176
  return
176
177
  }
177
178
 
179
+ const changedDependentMaps = new Set<string>()
180
+
178
181
  // remove all dependent nodes
179
182
  const oldDependencies = this.dependencyMap.get(nodeId) ?? []
180
183
  for (const depId of oldDependencies) {
181
184
  const dependantSet = this.dependentMap.get(depId)
182
185
  if (dependantSet) {
183
186
  dependantSet.delete(nodeId)
187
+ changedDependentMaps.add(depId)
184
188
  if (dependantSet.size === 0) {
185
189
  this.dependentMap.delete(depId)
186
190
  }
@@ -196,6 +200,15 @@ export abstract class GraphResolver<TNode, TOutput> {
196
200
  }
197
201
 
198
202
  dependantSet.add(nodeId)
203
+ changedDependentMaps.add(depId)
204
+ }
205
+
206
+ // if the dependent map has changed, notify the handler
207
+ if (this.dependentSetHandler) {
208
+ for (const depId of changedDependentMaps) {
209
+ const dependents = this.dependentMap.get(depId)
210
+ this.dependentSetHandler(depId, dependents)
211
+ }
199
212
  }
200
213
 
201
214
  this.outputMap.set(nodeId, output)
@@ -209,3 +222,33 @@ export abstract class GraphResolver<TNode, TOutput> {
209
222
  }
210
223
  }
211
224
  }
225
+
226
+ /**
227
+ * Gets all identifiers of the nodes that depend on the given node directly or indirectly.
228
+ *
229
+ * @param dependentMap The map of dependents, received from the graph resolver.
230
+ * @param nodeId The identifier of the node to get dependents for.
231
+ */
232
+ export function getAllDependents(
233
+ dependentMap: ReadonlyMap<string, Iterable<string>>,
234
+ nodeId: string,
235
+ ): string[] {
236
+ const result = new Set<string>()
237
+ const stack: string[] = [nodeId]
238
+
239
+ while (stack.length > 0) {
240
+ const dependents = dependentMap.get(stack.pop()!)
241
+ if (!dependents) {
242
+ continue
243
+ }
244
+
245
+ for (const dependentId of dependents) {
246
+ if (!result.has(dependentId)) {
247
+ result.add(dependentId)
248
+ stack.push(dependentId)
249
+ }
250
+ }
251
+ }
252
+
253
+ return Array.from(result)
254
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./graph-resolver"
2
+ export * from "./registry"
3
+ export * from "./input"
4
+ export * from "./input-hash"
5
+ export * from "./validation"
@@ -1,7 +1,11 @@
1
- import type { InstanceState } from "../state"
2
1
  import type { ResolvedInstanceInput } from "./input"
2
+ import type { InstanceState } from "../models/project"
3
3
  import { isUnitModel, type ComponentModel, type InstanceModel } from "@highstate/contract"
4
- import { sha256 } from "crypto-hash"
4
+ import { crc32 } from "@aws-crypto/crc32"
5
+ import { encode } from "@msgpack/msgpack"
6
+ import { sha256 } from "@noble/hashes/sha2"
7
+ import { bytesToHex } from "@noble/ciphers/utils"
8
+ import { int32ToBytes } from "../utils"
5
9
  import { GraphResolver } from "./graph-resolver"
6
10
 
7
11
  export type InputHashNode = {
@@ -9,11 +13,12 @@ export type InputHashNode = {
9
13
  component: ComponentModel
10
14
  resolvedInputs: Record<string, ResolvedInstanceInput[]>
11
15
  state: InstanceState | undefined
12
- sourceHash: string | undefined
16
+ sourceHash: number | undefined
13
17
  }
14
18
 
15
19
  export type InputHashOutput = {
16
- inputHash: string
20
+ inputHash: number
21
+ dependencyOutputHash: string
17
22
  outputHash: string
18
23
  }
19
24
 
@@ -33,17 +38,31 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
33
38
  return dependencies
34
39
  }
35
40
 
36
- async processNode({
41
+ processNode({
37
42
  instance,
38
43
  component,
39
44
  resolvedInputs,
40
45
  sourceHash,
41
46
  state,
42
- }: InputHashNode): Promise<InputHashOutput> {
43
- let sink = component.definitionHash + JSON.stringify(instance.args ?? {})
47
+ }: InputHashNode): InputHashOutput {
48
+ const inputHashSink: Uint8Array[] = []
44
49
 
50
+ // 1. include the component definition hash
51
+ inputHashSink.push(int32ToBytes(component.definitionHash))
52
+
53
+ // 2. include the input hash nonce if available
54
+ if (state?.operationStatus?.inputHashNonce) {
55
+ inputHashSink.push(int32ToBytes(state.operationStatus.inputHashNonce))
56
+ }
57
+
58
+ // 3. include instance args encoded as msgpack
59
+ if (instance.args) {
60
+ inputHashSink.push(encode(instance.args))
61
+ }
62
+
63
+ // 4. include the source hash if available
45
64
  if (sourceHash) {
46
- sink += sourceHash
65
+ inputHashSink.push(int32ToBytes(sourceHash))
47
66
  } else if (isUnitModel(component)) {
48
67
  this.logger.warn(
49
68
  { instanceId: instance.id },
@@ -55,28 +74,47 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
55
74
  //
56
75
  .sort(([a], [b]) => a.localeCompare(b))
57
76
 
77
+ const dependencyInstanceIds = new Set<string>()
78
+
79
+ // 5. include the sorted resolved inputs
58
80
  for (const [inputKey, inputs] of sortedInputs) {
59
81
  if (Object.keys(inputs).length === 0) {
60
82
  continue
61
83
  }
62
84
 
63
- sink += inputKey
85
+ // 5.1. include the input key to distinguish different inputs with possibly the same instanceId
86
+ inputHashSink.push(Buffer.from(inputKey))
64
87
 
65
- const instanceIds = inputs.map(input => input.input.instanceId)
66
- instanceIds.sort()
88
+ // the instances inside the input should also have stable order
89
+ const instanceIds = inputs.map(input => input.input.instanceId).sort()
67
90
 
68
91
  for (const instanceId of instanceIds) {
69
92
  const dependency = this.outputs.get(instanceId)
70
93
  if (!dependency) continue
71
94
 
72
- sink += dependency.inputHash
73
- sink += dependency.outputHash
95
+ // 5.2 include both input and output hashes of the dependency
96
+ inputHashSink.push(int32ToBytes(dependency.inputHash))
97
+ inputHashSink.push(Buffer.from(dependency.outputHash))
98
+
99
+ dependencyInstanceIds.add(instanceId)
74
100
  }
75
101
  }
76
102
 
103
+ // also calculate the dependency output hash as the combined output hashes of all unique dependencies
104
+ const dependencyOutputHashSink: Uint8Array[] = []
105
+ const sortedDependencyInstanceIds = Array.from(dependencyInstanceIds).sort()
106
+
107
+ for (const dependencyInstanceId of sortedDependencyInstanceIds) {
108
+ const dependency = this.outputs.get(dependencyInstanceId)
109
+ if (!dependency) continue
110
+
111
+ dependencyOutputHashSink.push(Buffer.from(dependencyInstanceId))
112
+ }
113
+
77
114
  return {
78
- inputHash: await sha256(sink),
79
- outputHash: state?.outputHash ?? "",
115
+ inputHash: crc32(Buffer.concat(inputHashSink)),
116
+ dependencyOutputHash: bytesToHex(sha256(Buffer.concat(dependencyOutputHashSink))),
117
+ outputHash: state?.operationStatus?.outputHash ?? "",
80
118
  }
81
119
  }
82
120
  }
@@ -1,8 +1,8 @@
1
- import type { HubModel } from "../project"
2
1
  import {
3
2
  isUnitModel,
4
3
  type ComponentModel,
5
4
  type HubInput,
5
+ type HubModel,
6
6
  type InstanceInput,
7
7
  type InstanceModel,
8
8
  } from "@highstate/contract"
@@ -44,14 +44,6 @@ export type InputResolverOutput =
44
44
  * Resolves the all recursive instance and hub inputs based on its direct inputs and injected inputs.
45
45
  */
46
46
  export class InputResolver extends GraphResolver<InputResolverNode, InputResolverOutput> {
47
- // constructor(
48
- // nodes: ReadonlyMap<string, InputResolverNode>,
49
- // logger: Logger,
50
- // outputHandler?: ResolverOutputHandler<InputResolverOutput>,
51
- // ) {
52
- // super(nodes, logger, outputHandler)
53
- // }
54
-
55
47
  getNodeDependencies(node: InputResolverNode): string[] {
56
48
  const dependencies: string[] = []
57
49
 
@@ -284,7 +276,15 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
284
276
  }
285
277
  }
286
278
 
287
- export function getResolvedHubInputs(output: InputResolverOutput): ResolvedInstanceInput[] {
279
+ export function getResolvedHubInputs(
280
+ outputMap: ReadonlyMap<string, InputResolverOutput>,
281
+ hubId: string,
282
+ ): ResolvedInstanceInput[] {
283
+ const output = outputMap.get(`hub:${hubId}`)
284
+ if (!output) {
285
+ return []
286
+ }
287
+
288
288
  if (output.kind !== "hub") {
289
289
  throw new Error("Expected hub node")
290
290
  }
@@ -293,8 +293,14 @@ export function getResolvedHubInputs(output: InputResolverOutput): ResolvedInsta
293
293
  }
294
294
 
295
295
  export function getResolvedInstanceInputs(
296
- output: InputResolverOutput,
296
+ outputMap: ReadonlyMap<string, InputResolverOutput>,
297
+ instanceId: string,
297
298
  ): Record<string, ResolvedInstanceInput[]> {
299
+ const output = outputMap.get(`instance:${instanceId}`)
300
+ if (!output) {
301
+ return {}
302
+ }
303
+
298
304
  if (output.kind !== "instance") {
299
305
  throw new Error("Expected instance node")
300
306
  }
@@ -303,8 +309,14 @@ export function getResolvedInstanceInputs(
303
309
  }
304
310
 
305
311
  export function getResolvedInjectionInstanceInputs(
306
- output: InputResolverOutput,
312
+ outputMap: ReadonlyMap<string, InputResolverOutput>,
313
+ instanceId: string,
307
314
  ): ResolvedInstanceInput[] {
315
+ const output = outputMap.get(`instance:${instanceId}`)
316
+ if (!output) {
317
+ return []
318
+ }
319
+
308
320
  if (output.kind !== "instance") {
309
321
  throw new Error("Expected instance node")
310
322
  }
@@ -313,11 +325,33 @@ export function getResolvedInjectionInstanceInputs(
313
325
  }
314
326
 
315
327
  export function getMatchedInjectionInstanceInputs(
316
- output: InputResolverOutput,
328
+ outputMap: ReadonlyMap<string, InputResolverOutput>,
329
+ instanceId: string,
317
330
  ): ResolvedInstanceInput[] {
331
+ const output = outputMap.get(`instance:${instanceId}`)
332
+ if (!output) {
333
+ return []
334
+ }
335
+
318
336
  if (output.kind !== "instance") {
319
337
  throw new Error("Expected instance node")
320
338
  }
321
339
 
322
340
  return output.matchedInjectionInputs
323
341
  }
342
+
343
+ export function getResolvedInstanceOutputs(
344
+ outputMap: ReadonlyMap<string, InputResolverOutput>,
345
+ instanceId: string,
346
+ ): Record<string, InstanceInput[]> | undefined {
347
+ const output = outputMap.get(`instance:${instanceId}`)
348
+ if (!output) {
349
+ return undefined
350
+ }
351
+
352
+ if (output.kind !== "instance") {
353
+ throw new Error("Expected instance node")
354
+ }
355
+
356
+ return output.resolvedOutputs
357
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from "pino"
2
- import type { GraphResolver, ResolverOutputHandler } from "./graph-resolver"
3
- import type { InstanceState } from "../state"
2
+ import type { DependentSetHandler, GraphResolver, ResolverOutputHandler } from "./graph-resolver"
3
+ import type { InstanceState } from "../models/project"
4
4
  import { StateResolver } from "./state"
5
5
  import { InputResolver, type InputResolverNode, type InputResolverOutput } from "./input"
6
6
  import { InputHashResolver, type InputHashNode, type InputHashOutput } from "./input-hash"
@@ -26,5 +26,6 @@ export const resolverFactories = {
26
26
  nodes: ReadonlyMap<string, unknown>,
27
27
  logger: Logger,
28
28
  outputHandler?: ResolverOutputHandler<unknown>,
29
+ dependentSetHandler?: DependentSetHandler,
29
30
  ) => GraphResolver<unknown, unknown>
30
31
  >
@@ -1,9 +1,9 @@
1
- import type { InstanceState } from "../state"
1
+ import type { InstanceState } from "../models/project"
2
2
  import { GraphResolver } from "./graph-resolver"
3
3
 
4
4
  export class StateResolver extends GraphResolver<InstanceState, void> {
5
5
  protected getNodeDependencies(node: InstanceState): string[] {
6
- return node.dependencyIds
6
+ return node.operationStatus?.dependencyIds ?? []
7
7
  }
8
8
 
9
9
  protected processNode(): void {
@@ -1,11 +1,15 @@
1
- import type { ComponentModel, InstanceModel } from "@highstate/contract"
2
1
  import type { ResolvedInstanceInput } from "./input"
2
+ import type { InstanceState } from "../models"
3
+ import { isUnitModel, type ComponentModel, type InstanceModel } from "@highstate/contract"
3
4
  import { Ajv } from "ajv"
5
+ import styles from "ansi-styles"
6
+ import { parseArgumentValue } from "../utils"
4
7
  import { GraphResolver } from "./graph-resolver"
5
8
 
6
9
  export type ValidationNode = {
7
10
  instance: InstanceModel
8
11
  component: ComponentModel
12
+ state: InstanceState | undefined
9
13
  resolvedInputs: Record<string, ResolvedInstanceInput[]>
10
14
  }
11
15
 
@@ -14,11 +18,7 @@ export type ValidationOutput =
14
18
  status: "ok"
15
19
  }
16
20
  | {
17
- status: "invalid-args"
18
- errorText: string
19
- }
20
- | {
21
- status: "invalid-inputs" | "missing-inputs"
21
+ status: "error"
22
22
  errorText: string
23
23
  }
24
24
 
@@ -38,34 +38,65 @@ export class ValidationResolver extends GraphResolver<ValidationNode, Validation
38
38
  return dependencies
39
39
  }
40
40
 
41
- processNode({ instance, component, resolvedInputs }: ValidationNode): ValidationOutput {
41
+ processNode({ instance, component, state, resolvedInputs }: ValidationNode): ValidationOutput {
42
42
  const ajv = new Ajv({ strict: false })
43
43
 
44
44
  this.logger.debug({ instanceId: instance.id }, "validating instance")
45
45
 
46
+ const validationErrors: string[] = []
47
+
46
48
  for (const [name, argument] of Object.entries(component.args)) {
47
- if (!argument.required && !instance.args?.[name]) {
48
- continue
49
- }
49
+ try {
50
+ const value = parseArgumentValue(instance.args?.[name])
50
51
 
51
- if (!ajv.validate(argument.schema, instance.args?.[name])) {
52
- this.logger.debug({ instanceId: instance.id, argumentName: name }, "invalid argument")
52
+ if (!argument.required && value === undefined) {
53
+ continue
54
+ }
55
+
56
+ if (!ajv.validate(argument.schema, value)) {
57
+ this.logger.debug({ instanceId: instance.id, argumentName: name }, "invalid argument")
53
58
 
54
- return {
55
- status: "invalid-args",
56
- errorText: `invalid argument "${name}": ${ajv.errorsText()}`,
59
+ validationErrors.push(
60
+ `Invalid argument ` +
61
+ `"${styles.blueBright.open}${name}${styles.reset.close}": ` +
62
+ ajv.errorsText(),
63
+ )
57
64
  }
65
+ } catch (error) {
66
+ this.logger.debug(
67
+ { instanceId: instance.id, argumentName: name, error },
68
+ "failed to validate argument",
69
+ )
70
+
71
+ validationErrors.push(
72
+ `Failed to validate argument ` +
73
+ `"${styles.blueBright.open}${name}${styles.reset.close}": ` +
74
+ (error instanceof Error ? error.message : String(error)),
75
+ )
58
76
  }
59
77
  }
60
78
 
61
- for (const inputs of Object.values(resolvedInputs)) {
79
+ if (isUnitModel(component)) {
80
+ for (const [secret, secretSchema] of Object.entries(component.secrets)) {
81
+ if (secretSchema.required && !state?.secretNames?.includes(secret)) {
82
+ validationErrors.push(
83
+ `Missing required secret ` +
84
+ `"${styles.blueBright.open}${secret}${styles.reset.close}"`,
85
+ )
86
+ }
87
+ }
88
+ }
89
+
90
+ for (const [key, inputs] of Object.entries(resolvedInputs)) {
62
91
  for (const input of inputs) {
63
92
  const inputInstance = this.outputs.get(input.input.instanceId)
64
93
  if (inputInstance?.status !== "ok") {
65
- return {
66
- status: "invalid-inputs",
67
- errorText: `instance "${input.input.instanceId}" has errors`,
68
- }
94
+ validationErrors.push(
95
+ `Invalid input ` +
96
+ `"${styles.blueBright.open}${key}${styles.reset.close}":\n` +
97
+ `"${styles.blueBright.open}${input.input.instanceId}${styles.reset.close}" ` +
98
+ `has validation errors`,
99
+ )
69
100
  }
70
101
  }
71
102
  }
@@ -76,13 +107,39 @@ export class ValidationResolver extends GraphResolver<ValidationNode, Validation
76
107
  }
77
108
 
78
109
  if (!resolvedInputs[name] || !resolvedInputs[name].length) {
79
- return {
80
- status: "missing-inputs",
81
- errorText: `input "${name}" is missing`,
82
- }
110
+ validationErrors.push(
111
+ `Missing required input ` +
112
+ `"${styles.blueBright.open}${name}${styles.reset.close}" ` +
113
+ `of type ` +
114
+ `"${styles.greenBright.open}${input.type}${styles.reset.close}"`,
115
+ )
83
116
  }
84
117
  }
85
118
 
86
- return { status: "ok" }
119
+ if (validationErrors.length === 0) {
120
+ return { status: "ok" }
121
+ }
122
+
123
+ const numberPrefixLength = (validationErrors.length + 1).toString().length + 2 // +2 for the ". " prefix
124
+
125
+ const formattedError = validationErrors
126
+ .map((error, index) => {
127
+ const lines = error.split("\n")
128
+ const prefix = `${index + 1}. `
129
+
130
+ return lines
131
+ .map((line, lineIndex) => {
132
+ const linePrefix = lineIndex === 0 ? prefix : " ".repeat(numberPrefixLength)
133
+
134
+ return `${linePrefix}${line}`
135
+ })
136
+ .join("\r\n")
137
+ })
138
+ .join("\r\n")
139
+
140
+ return {
141
+ status: "error",
142
+ errorText: formattedError,
143
+ }
87
144
  }
88
145
  }
@@ -0,0 +1,25 @@
1
+ import { yamlValueSchema } from "@highstate/contract"
2
+ import { parse } from "yaml"
3
+
4
+ const yamlResultCache = new WeakMap<object, unknown>()
5
+
6
+ /**
7
+ * Parses an argument value which can be wrapped in a YAML structure.
8
+ *
9
+ * @param value The value to parse.
10
+ */
11
+ export function parseArgumentValue(value: unknown): unknown {
12
+ const yamlResult = yamlValueSchema.safeParse(value)
13
+ if (!yamlResult.success) {
14
+ return value
15
+ }
16
+
17
+ const existingResult = yamlResultCache.get(value as object)
18
+ if (existingResult !== undefined) {
19
+ return existingResult
20
+ }
21
+
22
+ const result = parse(yamlResult.data.value) as unknown
23
+ yamlResultCache.set(value as object, result)
24
+ return result
25
+ }