@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.
- package/dist/chunk-NAAIDR4U.js +8499 -0
- package/dist/chunk-NAAIDR4U.js.map +1 -0
- package/dist/chunk-OU5OQBLB.js +74 -0
- package/dist/chunk-OU5OQBLB.js.map +1 -0
- package/dist/chunk-Y7DXREVO.js +1745 -0
- package/dist/chunk-Y7DXREVO.js.map +1 -0
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +7227 -2501
- package/dist/index.js.map +1 -1
- package/dist/library/package-resolution-worker.js +7 -5
- package/dist/library/package-resolution-worker.js.map +1 -1
- package/dist/library/worker/main.js +76 -185
- package/dist/library/worker/main.js.map +1 -1
- package/dist/magic-string.es-5ABAC4JN.js +1292 -0
- package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
- package/dist/shared/index.js +3 -98
- package/dist/shared/index.js.map +1 -1
- package/package.json +31 -10
- package/src/artifact/abstractions.ts +46 -0
- package/src/artifact/encryption.ts +109 -0
- package/src/artifact/factory.ts +36 -0
- package/src/artifact/index.ts +3 -0
- package/src/artifact/local.ts +138 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
- package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
- package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
- package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
- package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
- package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
- package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
- package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
- package/src/business/api-key.ts +65 -0
- package/src/business/artifact.ts +289 -0
- package/src/business/backend-unlock.ts +10 -0
- package/src/business/index.ts +10 -0
- package/src/business/instance-lock.ts +125 -0
- package/src/business/instance-state.ts +434 -0
- package/src/business/operation.ts +251 -0
- package/src/business/project-unlock.ts +260 -0
- package/src/business/project.ts +299 -0
- package/src/business/secret.test.ts +178 -0
- package/src/business/secret.ts +281 -0
- package/src/business/worker.test.ts +614 -0
- package/src/business/worker.ts +398 -0
- package/src/common/clock.ts +18 -0
- package/src/common/index.ts +5 -1
- package/src/common/performance.ts +44 -0
- package/src/common/random.ts +68 -0
- package/src/common/test/index.ts +2 -0
- package/src/common/test/render.ts +98 -0
- package/src/common/test/tracer.ts +359 -0
- package/src/common/tree.ts +33 -0
- package/src/common/utils.ts +40 -1
- package/src/config.ts +19 -11
- package/src/hotstate/abstractions.ts +48 -0
- package/src/hotstate/factory.ts +17 -0
- package/src/{secret → hotstate}/index.ts +1 -0
- package/src/hotstate/manager.ts +192 -0
- package/src/hotstate/memory.ts +100 -0
- package/src/hotstate/validation.ts +100 -0
- package/src/index.ts +2 -1
- package/src/library/abstractions.ts +24 -28
- package/src/library/factory.ts +2 -2
- package/src/library/local.ts +91 -111
- package/src/library/worker/evaluator.ts +36 -73
- package/src/library/worker/loader.lite.ts +54 -0
- package/src/library/worker/main.ts +15 -66
- package/src/library/worker/protocol.ts +6 -33
- package/src/lock/abstractions.ts +6 -0
- package/src/lock/factory.ts +15 -0
- package/src/lock/index.ts +4 -0
- package/src/lock/manager.ts +97 -0
- package/src/lock/memory.ts +19 -0
- package/src/lock/test.ts +108 -0
- package/src/orchestrator/manager.ts +118 -90
- package/src/orchestrator/operation-workset.ts +181 -93
- package/src/orchestrator/operation.ts +1021 -283
- package/src/project/abstractions.ts +27 -38
- package/src/project/evaluation.ts +248 -0
- package/src/project/factory.ts +1 -1
- package/src/project/index.ts +1 -2
- package/src/project/local.ts +107 -103
- package/src/pubsub/abstractions.ts +13 -0
- package/src/pubsub/factory.ts +19 -0
- package/src/{workspace → pubsub}/index.ts +1 -0
- package/src/pubsub/local.ts +36 -0
- package/src/pubsub/manager.ts +108 -0
- package/src/pubsub/validation.ts +33 -0
- package/src/runner/abstractions.ts +155 -68
- package/src/runner/artifact-env.ts +160 -0
- package/src/runner/factory.ts +20 -5
- package/src/runner/force-abort.ts +117 -0
- package/src/runner/local.ts +292 -372
- package/src/{common → runner}/pulumi.ts +89 -37
- package/src/services.ts +251 -40
- package/src/shared/index.ts +3 -11
- package/src/shared/models/backend/index.ts +3 -0
- package/src/shared/{library.ts → models/backend/library.ts} +4 -4
- package/src/shared/models/backend/project.ts +82 -0
- package/src/shared/models/backend/unlock-method.ts +20 -0
- package/src/shared/models/base.ts +68 -0
- package/src/shared/models/errors.ts +5 -0
- package/src/shared/models/index.ts +4 -0
- package/src/shared/models/project/api-key.ts +65 -0
- package/src/shared/models/project/artifact.ts +83 -0
- package/src/shared/models/project/index.ts +13 -0
- package/src/shared/models/project/lock.ts +91 -0
- package/src/shared/models/project/model.ts +14 -0
- package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
- package/src/shared/models/project/page.ts +57 -0
- package/src/shared/models/project/secret.ts +98 -0
- package/src/shared/models/project/service-account.ts +22 -0
- package/src/shared/models/project/state.ts +449 -0
- package/src/shared/models/project/terminal.ts +98 -0
- package/src/shared/models/project/trigger.ts +56 -0
- package/src/shared/models/project/unlock-method.ts +38 -0
- package/src/shared/models/project/worker.ts +107 -0
- package/src/shared/resolvers/graph-resolver.ts +61 -18
- package/src/shared/resolvers/index.ts +5 -0
- package/src/shared/resolvers/input-hash.ts +53 -15
- package/src/shared/resolvers/input.ts +47 -13
- package/src/shared/resolvers/registry.ts +3 -2
- package/src/shared/resolvers/state.ts +2 -2
- package/src/shared/resolvers/validation.ts +82 -25
- package/src/shared/utils/args.ts +25 -0
- package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
- package/src/shared/utils/hash.ts +6 -0
- package/src/shared/utils/index.ts +4 -0
- package/src/shared/utils/promise-tracker.ts +23 -0
- package/src/state/abstractions.ts +199 -131
- package/src/state/encryption.ts +98 -0
- package/src/state/factory.ts +3 -5
- package/src/state/index.ts +4 -0
- package/src/state/keyring.ts +22 -0
- package/src/state/local/backend.ts +106 -0
- package/src/state/local/collection.ts +361 -0
- package/src/state/local/index.ts +2 -0
- package/src/state/manager.ts +875 -18
- package/src/state/memory/backend.ts +70 -0
- package/src/state/memory/collection.ts +270 -0
- package/src/state/memory/index.ts +2 -0
- package/src/state/repository/index.ts +2 -0
- package/src/state/repository/repository.index.ts +193 -0
- package/src/state/repository/repository.ts +507 -0
- package/src/state/test.ts +457 -0
- package/src/terminal/{shared.ts → abstractions.ts} +3 -3
- package/src/terminal/docker.ts +18 -14
- package/src/terminal/factory.ts +3 -3
- package/src/terminal/index.ts +1 -1
- package/src/terminal/manager.ts +131 -79
- package/src/terminal/run.sh.ts +21 -11
- package/src/unlock/abstractions.ts +49 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +32 -0
- package/src/worker/abstractions.ts +42 -0
- package/src/worker/docker.ts +83 -0
- package/src/worker/factory.ts +20 -0
- package/src/worker/index.ts +3 -0
- package/src/worker/manager.ts +167 -0
- package/dist/chunk-KTGKNSKM.js +0 -979
- package/dist/chunk-KTGKNSKM.js.map +0 -1
- package/dist/chunk-WXDYCRTT.js +0 -234
- package/dist/chunk-WXDYCRTT.js.map +0 -1
- package/src/library/worker/loader.ts +0 -114
- package/src/preferences/shared.ts +0 -1
- package/src/project/lock.ts +0 -39
- package/src/project/manager.ts +0 -433
- package/src/secret/abstractions.ts +0 -59
- package/src/secret/factory.ts +0 -22
- package/src/secret/local.ts +0 -152
- package/src/shared/project.ts +0 -62
- package/src/shared/state.ts +0 -247
- package/src/shared/terminal.ts +0 -14
- package/src/state/local.ts +0 -612
- package/src/workspace/abstractions.ts +0 -41
- package/src/workspace/factory.ts +0 -14
- 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
|
|
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
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
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(
|
|
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
|
+
}
|
|
@@ -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 {
|
|
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:
|
|
16
|
+
sourceHash: number | undefined
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export type InputHashOutput = {
|
|
16
|
-
inputHash:
|
|
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
|
-
|
|
41
|
+
processNode({
|
|
37
42
|
instance,
|
|
38
43
|
component,
|
|
39
44
|
resolvedInputs,
|
|
40
45
|
sourceHash,
|
|
41
46
|
state,
|
|
42
|
-
}: InputHashNode):
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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:
|
|
79
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "../
|
|
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 "../
|
|
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: "
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
49
|
+
try {
|
|
50
|
+
const value = parseArgumentValue(instance.args?.[name])
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
+
}
|