@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
package/src/project/manager.ts
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import type { StateBackend, StateManager } from "../state"
|
|
2
|
-
import type { ProjectBackend } from "./abstractions"
|
|
3
|
-
import type { Logger } from "pino"
|
|
4
|
-
import type { LibraryBackend } from "../library"
|
|
5
|
-
import type { ProjectLockManager } from "./lock"
|
|
6
|
-
import { EventEmitter, on } from "node:events"
|
|
7
|
-
import { isUnitModel, type InstanceModel } from "@highstate/contract"
|
|
8
|
-
import {
|
|
9
|
-
type InputResolverNode,
|
|
10
|
-
type InputHashNode,
|
|
11
|
-
type InstanceModelPatch,
|
|
12
|
-
type LibraryUpdate,
|
|
13
|
-
createInstanceState,
|
|
14
|
-
type CompositeInstance,
|
|
15
|
-
type ResolvedInstanceInput,
|
|
16
|
-
type HubModel,
|
|
17
|
-
InputResolver,
|
|
18
|
-
InputHashResolver,
|
|
19
|
-
} from "../shared"
|
|
20
|
-
|
|
21
|
-
type CompositeInstanceEvent =
|
|
22
|
-
| {
|
|
23
|
-
type: "evaluation-started"
|
|
24
|
-
instanceId: string
|
|
25
|
-
}
|
|
26
|
-
| {
|
|
27
|
-
type: "updated"
|
|
28
|
-
instance: CompositeInstance
|
|
29
|
-
}
|
|
30
|
-
| {
|
|
31
|
-
type: "deleted"
|
|
32
|
-
instanceId: string
|
|
33
|
-
}
|
|
34
|
-
| {
|
|
35
|
-
type: "failed"
|
|
36
|
-
instanceId: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type CompositeInstanceEvents = {
|
|
40
|
-
[K in string]: [CompositeInstanceEvent]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type FullProjectModel = {
|
|
44
|
-
instances: InstanceModel[]
|
|
45
|
-
hubs: HubModel[]
|
|
46
|
-
compositeInstances: CompositeInstance[]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export class ProjectManager {
|
|
50
|
-
private constructor(
|
|
51
|
-
private readonly projectBackend: ProjectBackend,
|
|
52
|
-
private readonly stateBackend: StateBackend,
|
|
53
|
-
private readonly libraryBackend: LibraryBackend,
|
|
54
|
-
private readonly projectLockManager: ProjectLockManager,
|
|
55
|
-
private readonly stateManager: StateManager,
|
|
56
|
-
private readonly logger: Logger,
|
|
57
|
-
) {
|
|
58
|
-
void this.watchLibraryChanges()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private readonly compositeInstanceEE = new EventEmitter<CompositeInstanceEvents>()
|
|
62
|
-
|
|
63
|
-
async *watchCompositeInstances(
|
|
64
|
-
projectId: string,
|
|
65
|
-
signal?: AbortSignal,
|
|
66
|
-
): AsyncIterable<CompositeInstanceEvent> {
|
|
67
|
-
for await (const [children] of on(this.compositeInstanceEE, projectId, {
|
|
68
|
-
signal,
|
|
69
|
-
})) {
|
|
70
|
-
yield children
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Loads the full info of a project, including instances, hubs, and composite instances.
|
|
76
|
-
*
|
|
77
|
-
* Also filters out instances that are not in the library.
|
|
78
|
-
*
|
|
79
|
-
* @param projectId The ID of the project to load.
|
|
80
|
-
*/
|
|
81
|
-
async getProject(projectId: string): Promise<FullProjectModel> {
|
|
82
|
-
const [{ instances, hubs }, compositeInstances, library] = await Promise.all([
|
|
83
|
-
this.projectBackend.getProject(projectId),
|
|
84
|
-
this.stateBackend.getCompositeInstances(projectId),
|
|
85
|
-
this.libraryBackend.loadLibrary(),
|
|
86
|
-
])
|
|
87
|
-
|
|
88
|
-
const filteredInstances = instances.filter(instance => instance.type in library.components)
|
|
89
|
-
const filteredCompositeInstances = compositeInstances
|
|
90
|
-
.filter(instance => instance.instance.type in library.components)
|
|
91
|
-
.map(instance => ({
|
|
92
|
-
...instance,
|
|
93
|
-
children: instance.children.filter(child => child.type in library.components),
|
|
94
|
-
}))
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
instances: filteredInstances,
|
|
98
|
-
hubs,
|
|
99
|
-
compositeInstances: filteredCompositeInstances,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async createInstance(projectId: string, instance: InstanceModel): Promise<InstanceModel> {
|
|
104
|
-
const createdInstance = await this.projectBackend.createInstance(projectId, instance)
|
|
105
|
-
await this.evaluateChangedCompositeInstances(projectId)
|
|
106
|
-
|
|
107
|
-
return createdInstance
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async updateInstance(
|
|
111
|
-
projectId: string,
|
|
112
|
-
instanceId: string,
|
|
113
|
-
patch: InstanceModelPatch,
|
|
114
|
-
): Promise<InstanceModel> {
|
|
115
|
-
const instance = await this.projectBackend.updateInstance(projectId, instanceId, patch)
|
|
116
|
-
await this.evaluateChangedCompositeInstances(projectId)
|
|
117
|
-
|
|
118
|
-
return instance
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async renameInstance(
|
|
122
|
-
projectId: string,
|
|
123
|
-
instanceId: string,
|
|
124
|
-
newName: string,
|
|
125
|
-
): Promise<InstanceModel> {
|
|
126
|
-
const instance = await this.projectBackend.renameInstance(projectId, instanceId, newName)
|
|
127
|
-
await this.evaluateChangedCompositeInstances(projectId)
|
|
128
|
-
|
|
129
|
-
return instance
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async deleteInstance(projectId: string, instanceId: string): Promise<void> {
|
|
133
|
-
await Promise.all([
|
|
134
|
-
this.projectBackend.deleteInstance(projectId, instanceId),
|
|
135
|
-
this.stateBackend.clearCompositeInstances(projectId, [instanceId]),
|
|
136
|
-
])
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private async evaluateChangedCompositeInstances(projectId: string): Promise<void> {
|
|
140
|
-
const { inputHashResolver, instances, library, evaluatedInputHashes } =
|
|
141
|
-
await this.prepareResolvers(projectId)
|
|
142
|
-
|
|
143
|
-
inputHashResolver.addAllNodesToWorkset()
|
|
144
|
-
await inputHashResolver.process()
|
|
145
|
-
|
|
146
|
-
const instanceIds = instances
|
|
147
|
-
.filter(instance => !isUnitModel(library.components[instance.type]))
|
|
148
|
-
.filter(
|
|
149
|
-
instance =>
|
|
150
|
-
evaluatedInputHashes[instance.id] !==
|
|
151
|
-
inputHashResolver.requireOutput(instance.id).inputHash,
|
|
152
|
-
)
|
|
153
|
-
.map(instance => instance.id)
|
|
154
|
-
|
|
155
|
-
await this.projectLockManager.getLock(projectId).lockInstances(instanceIds, async () => {
|
|
156
|
-
this.logger.debug({ instanceIds }, "evaluating composite instances")
|
|
157
|
-
|
|
158
|
-
for (const instanceId of instanceIds) {
|
|
159
|
-
this.compositeInstanceEE.emit(projectId, { type: "evaluation-started", instanceId })
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const [
|
|
163
|
-
{ instances, resolvedInputs, stateMap, inputHashResolver },
|
|
164
|
-
topLevelCompositeChildrenIds,
|
|
165
|
-
] = await Promise.all([
|
|
166
|
-
this.prepareResolvers(projectId),
|
|
167
|
-
this.stateBackend.getTopLevelCompositeChildrenIds(projectId, instanceIds),
|
|
168
|
-
])
|
|
169
|
-
|
|
170
|
-
const results = await this.libraryBackend.evaluateCompositeInstances(
|
|
171
|
-
instances,
|
|
172
|
-
resolvedInputs,
|
|
173
|
-
instanceIds,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
const newStates = results.map(result => {
|
|
177
|
-
const existingState = stateMap.get(result.instanceId)
|
|
178
|
-
const newState = existingState ?? createInstanceState(result.instanceId)
|
|
179
|
-
|
|
180
|
-
newState.evaluationError = result.success ? null : result.error
|
|
181
|
-
|
|
182
|
-
return newState
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
inputHashResolver.addAllNodesToWorkset()
|
|
186
|
-
await inputHashResolver.process()
|
|
187
|
-
|
|
188
|
-
const compositeInstances = results
|
|
189
|
-
.filter(result => result.success)
|
|
190
|
-
.flatMap(result =>
|
|
191
|
-
result.compositeInstances.map(instance => ({
|
|
192
|
-
...instance,
|
|
193
|
-
inputHash:
|
|
194
|
-
// only store inputHash for top-level composite instances
|
|
195
|
-
instance.instance.id === result.instanceId
|
|
196
|
-
? inputHashResolver.requireOutput(instance.instance.id).inputHash
|
|
197
|
-
: "",
|
|
198
|
-
})),
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
const newTopLevelCompositeChildrenIds = Object.fromEntries(
|
|
202
|
-
results
|
|
203
|
-
.filter(result => result.success)
|
|
204
|
-
.map(result => [
|
|
205
|
-
result.instanceId,
|
|
206
|
-
result.compositeInstances
|
|
207
|
-
.filter(instance => instance.instance.id !== result.instanceId)
|
|
208
|
-
.map(instance => instance.instance.id),
|
|
209
|
-
]),
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
const deletedCompositeInstanceIds = new Set(
|
|
213
|
-
Object.values(topLevelCompositeChildrenIds).flat(),
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
for (const childInstanceId of Object.values(newTopLevelCompositeChildrenIds).flat()) {
|
|
217
|
-
deletedCompositeInstanceIds.delete(childInstanceId)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
await Promise.all([
|
|
221
|
-
this.stateBackend.clearCompositeInstances(
|
|
222
|
-
projectId,
|
|
223
|
-
Array.from(deletedCompositeInstanceIds),
|
|
224
|
-
),
|
|
225
|
-
this.stateBackend.putTopLevelCompositeChildrenIds(
|
|
226
|
-
projectId,
|
|
227
|
-
newTopLevelCompositeChildrenIds,
|
|
228
|
-
),
|
|
229
|
-
])
|
|
230
|
-
|
|
231
|
-
for (const state of newStates) {
|
|
232
|
-
this.stateManager.emitStatePatch(projectId, state)
|
|
233
|
-
|
|
234
|
-
if (state.evaluationError) {
|
|
235
|
-
this.logger.error(
|
|
236
|
-
{ projectId, instanceId: state.id, error: state.evaluationError },
|
|
237
|
-
"instance evaluation failed",
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
this.compositeInstanceEE.emit(projectId, {
|
|
241
|
-
type: "failed",
|
|
242
|
-
instanceId: state.id,
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
for (const instance of compositeInstances) {
|
|
248
|
-
this.compositeInstanceEE.emit(projectId, { type: "updated", instance })
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
for (const instanceId of deletedCompositeInstanceIds) {
|
|
252
|
-
this.compositeInstanceEE.emit(projectId, { type: "deleted", instanceId })
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const promises: Promise<void>[] = []
|
|
256
|
-
|
|
257
|
-
if (newStates.length > 0) {
|
|
258
|
-
promises.push(this.stateBackend.putInstanceStates(projectId, newStates))
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (compositeInstances.length > 0) {
|
|
262
|
-
promises.push(this.stateBackend.putCompositeInstances(projectId, compositeInstances))
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
this.logger.info(
|
|
266
|
-
{ projectId },
|
|
267
|
-
"instance evaluation completed, %d instances persisted",
|
|
268
|
-
compositeInstances.length,
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
this.logger.debug(
|
|
272
|
-
{ compositeInstanceIds: compositeInstances.map(instance => instance.instance.id) },
|
|
273
|
-
"persisted composite instances",
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
await Promise.all(promises)
|
|
277
|
-
})
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private async prepareResolvers(projectId: string) {
|
|
281
|
-
const [{ instances, hubs }, states, library, evaluatedInputHashes] = await Promise.all([
|
|
282
|
-
this.projectBackend.getProject(projectId),
|
|
283
|
-
this.stateBackend.getAllInstanceStates(projectId),
|
|
284
|
-
this.libraryBackend.loadLibrary(),
|
|
285
|
-
this.stateBackend.getCompositeInstanceInputHashes(projectId),
|
|
286
|
-
])
|
|
287
|
-
|
|
288
|
-
const filteredInstances = instances.filter(instance => instance.type in library.components)
|
|
289
|
-
const stateMap = new Map(states.map(state => [state.id, state]))
|
|
290
|
-
|
|
291
|
-
const inputResolverNodes = new Map<string, InputResolverNode>()
|
|
292
|
-
|
|
293
|
-
for (const instance of filteredInstances) {
|
|
294
|
-
inputResolverNodes.set(`instance:${instance.id}`, {
|
|
295
|
-
kind: "instance",
|
|
296
|
-
instance,
|
|
297
|
-
component: library.components[instance.type],
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
for (const hub of hubs) {
|
|
302
|
-
inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub })
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const inputResolver = new InputResolver(inputResolverNodes, this.logger)
|
|
306
|
-
inputResolver.addAllNodesToWorkset()
|
|
307
|
-
|
|
308
|
-
const inputHashInputs = new Map<string, InputHashNode>()
|
|
309
|
-
const resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>> = {}
|
|
310
|
-
|
|
311
|
-
await inputResolver.process()
|
|
312
|
-
|
|
313
|
-
for (const instance of filteredInstances) {
|
|
314
|
-
const output = inputResolver.requireOutput(`instance:${instance.id}`)
|
|
315
|
-
if (output.kind !== "instance") {
|
|
316
|
-
throw new Error("Expected instance node")
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
let sourceHash: string | undefined
|
|
320
|
-
if (isUnitModel(library.components[instance.type])) {
|
|
321
|
-
const resolvedUnits = await this.libraryBackend.getResolvedUnitSources([instance.type])
|
|
322
|
-
const resolvedUnit = resolvedUnits.find(unit => unit.unitType === instance.type)
|
|
323
|
-
|
|
324
|
-
if (resolvedUnit) {
|
|
325
|
-
sourceHash = resolvedUnit.sourceHash
|
|
326
|
-
} else {
|
|
327
|
-
this.logger.warn(`resolved unit source not found for type "%s"`, instance.type)
|
|
328
|
-
sourceHash = undefined
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
inputHashInputs.set(instance.id, {
|
|
333
|
-
instance,
|
|
334
|
-
component: library.components[instance.type],
|
|
335
|
-
resolvedInputs: output.resolvedInputs,
|
|
336
|
-
state: stateMap.get(instance.id),
|
|
337
|
-
sourceHash,
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
resolvedInputs[instance.id] = output.resolvedInputs
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const inputHashResolver = new InputHashResolver(inputHashInputs, this.logger)
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
inputHashInputs,
|
|
347
|
-
inputHashResolver,
|
|
348
|
-
library,
|
|
349
|
-
instances: filteredInstances,
|
|
350
|
-
stateMap,
|
|
351
|
-
resolvedInputs,
|
|
352
|
-
evaluatedInputHashes,
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private async watchLibraryChanges(): Promise<void> {
|
|
357
|
-
for await (const updates of this.libraryBackend.watchLibrary()) {
|
|
358
|
-
try {
|
|
359
|
-
await this.handleLibraryUpdates(updates)
|
|
360
|
-
} catch (error) {
|
|
361
|
-
this.logger.error({ error }, "failed to handle library updates")
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private async handleLibraryUpdates(updates: LibraryUpdate[]): Promise<void> {
|
|
367
|
-
const changedComponents = new Set<string>()
|
|
368
|
-
const library = await this.libraryBackend.loadLibrary()
|
|
369
|
-
|
|
370
|
-
// TODO: handle entity updates
|
|
371
|
-
|
|
372
|
-
for (const update of updates) {
|
|
373
|
-
switch (update.type) {
|
|
374
|
-
case "component-updated":
|
|
375
|
-
changedComponents.add(update.component.type)
|
|
376
|
-
break
|
|
377
|
-
case "component-removed":
|
|
378
|
-
changedComponents.add(update.componentType)
|
|
379
|
-
break
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (changedComponents.size === 0) {
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
this.logger.info(
|
|
388
|
-
{ changedComponents },
|
|
389
|
-
"library components changed, updating composite instances",
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
const projects = await this.projectBackend.getProjectIds()
|
|
393
|
-
for (const projectId of projects) {
|
|
394
|
-
const { instances } = await this.prepareResolvers(projectId)
|
|
395
|
-
|
|
396
|
-
const filteredInstances = instances.filter(
|
|
397
|
-
instance =>
|
|
398
|
-
changedComponents.has(instance.type) &&
|
|
399
|
-
library.components[instance.type] &&
|
|
400
|
-
!isUnitModel(library.components[instance.type]),
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
this.logger.info(
|
|
404
|
-
{ projectId, filteredInstanceIds: filteredInstances.map(instance => instance.id) },
|
|
405
|
-
"updating composite instances for project",
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
await this.evaluateChangedCompositeInstances(projectId)
|
|
410
|
-
} catch (error) {
|
|
411
|
-
this.logger.error({ error }, "failed to evaluate composite instances")
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
static create(
|
|
417
|
-
projectBackend: ProjectBackend,
|
|
418
|
-
stateBackend: StateBackend,
|
|
419
|
-
libraryBackend: LibraryBackend,
|
|
420
|
-
projectLockManager: ProjectLockManager,
|
|
421
|
-
stateManager: StateManager,
|
|
422
|
-
logger: Logger,
|
|
423
|
-
): ProjectManager {
|
|
424
|
-
return new ProjectManager(
|
|
425
|
-
projectBackend,
|
|
426
|
-
stateBackend,
|
|
427
|
-
libraryBackend,
|
|
428
|
-
projectLockManager,
|
|
429
|
-
stateManager,
|
|
430
|
-
logger.child({ service: "ProjectManager" }),
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
export class SecretAccessDeniedError extends Error {
|
|
2
|
-
constructor(projectId: string, key: string) {
|
|
3
|
-
super(`Access to the secrets of component "${projectId}.${key}" is denied.`)
|
|
4
|
-
}
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface SecretBackend {
|
|
8
|
-
/**
|
|
9
|
-
* Check if the state and secrets of the project are locked.
|
|
10
|
-
* The backend may not implement this method.
|
|
11
|
-
*
|
|
12
|
-
* @param projectId The ID of the project.
|
|
13
|
-
* @param signal The signal to abort the operation.
|
|
14
|
-
*
|
|
15
|
-
* @returns `true` if the project is locked, `false` if the project is unlocked.
|
|
16
|
-
*/
|
|
17
|
-
isLocked?(projectId: string, signal?: AbortSignal): Promise<boolean>
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Unlock the state and secrets of the project.
|
|
21
|
-
* The backend may not implement this method.
|
|
22
|
-
*
|
|
23
|
-
* @param projectId The ID of the project.
|
|
24
|
-
* @param password The password to unlock the secrets.
|
|
25
|
-
* @param signal The signal to abort the operation.
|
|
26
|
-
*
|
|
27
|
-
* @returns `true` if the project is unlocked, `false` if the password is incorrect.
|
|
28
|
-
*/
|
|
29
|
-
unlock?(projectId: string, password: string, signal?: AbortSignal): Promise<boolean>
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get the secrets of the component.
|
|
33
|
-
*
|
|
34
|
-
* @param projectId The ID of the project.
|
|
35
|
-
* @param instanceId The ID of the instance.
|
|
36
|
-
* @param signal The signal to abort the operation.
|
|
37
|
-
*
|
|
38
|
-
* @returns A record of secret key-value pairs or `null` if the secrets are not found.
|
|
39
|
-
* @throws {SecretAccessDeniedError} If access to the secrets is denied.
|
|
40
|
-
*/
|
|
41
|
-
get(projectId: string, instanceId: string, signal?: AbortSignal): Promise<Record<string, unknown>>
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Set the secrets of the component.
|
|
45
|
-
*
|
|
46
|
-
* @param projectId The ID of the project.
|
|
47
|
-
* @param instanceId The ID of the instance.
|
|
48
|
-
* @param values The record of secret key-value pairs.
|
|
49
|
-
* @param signal The signal to abort the operation.
|
|
50
|
-
*
|
|
51
|
-
* @throws {SecretAccessDeniedError} If access to the secrets is denied.
|
|
52
|
-
*/
|
|
53
|
-
set(
|
|
54
|
-
projectId: string,
|
|
55
|
-
instanceId: string,
|
|
56
|
-
values: Record<string, unknown>,
|
|
57
|
-
signal?: AbortSignal,
|
|
58
|
-
): Promise<void>
|
|
59
|
-
}
|
package/src/secret/factory.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { SecretBackend } from "./abstractions"
|
|
2
|
-
import type { LocalPulumiHost } from "../common"
|
|
3
|
-
import type { Logger } from "pino"
|
|
4
|
-
import { z } from "zod"
|
|
5
|
-
import { LocalSecretBackend, localSecretBackendConfig } from "./local"
|
|
6
|
-
|
|
7
|
-
export const secretBackendConfig = z.object({
|
|
8
|
-
HIGHSTATE_BACKEND_SECRET_TYPE: z.enum(["local"]).default("local"),
|
|
9
|
-
...localSecretBackendConfig.shape,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
export function createSecretBackend(
|
|
13
|
-
config: z.infer<typeof secretBackendConfig>,
|
|
14
|
-
localPulumiHost: LocalPulumiHost,
|
|
15
|
-
logger: Logger,
|
|
16
|
-
): Promise<SecretBackend> {
|
|
17
|
-
switch (config.HIGHSTATE_BACKEND_SECRET_TYPE) {
|
|
18
|
-
case "local": {
|
|
19
|
-
return LocalSecretBackend.create(config, localPulumiHost, logger)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
package/src/secret/local.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { SecretBackend } from "./abstractions"
|
|
2
|
-
import type { Logger } from "pino"
|
|
3
|
-
import { mapKeys, mapValues, pickBy } from "remeda"
|
|
4
|
-
import { z } from "zod"
|
|
5
|
-
import { LocalPulumiHost, resolveMainLocalProject, stringToValue, valueToString } from "../common"
|
|
6
|
-
|
|
7
|
-
export const localSecretBackendConfig = z.object({
|
|
8
|
-
HIGHSTATE_BACKEND_SECRET_PROJECT_PATH: z.string().optional(),
|
|
9
|
-
HIGHSTATE_BACKEND_SECRET_PROJECT_NAME: z.string().optional(),
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The backend for storing secrets in the local Pulumi project.
|
|
14
|
-
* It is the simplest backend and should be used for development and single-user projects.
|
|
15
|
-
*/
|
|
16
|
-
export class LocalSecretBackend implements SecretBackend {
|
|
17
|
-
private constructor(
|
|
18
|
-
private readonly projectPath: string,
|
|
19
|
-
private readonly projectName: string,
|
|
20
|
-
private readonly pulumiProjectHost: LocalPulumiHost,
|
|
21
|
-
private readonly logger: Logger,
|
|
22
|
-
) {
|
|
23
|
-
this.logger.debug({ msg: "initialized", projectPath, projectName })
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
isLocked(projectId: string): Promise<boolean> {
|
|
27
|
-
return Promise.resolve(!this.pulumiProjectHost.hasPassword(projectId))
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async unlock(projectId: string, password: string, signal?: AbortSignal): Promise<boolean> {
|
|
31
|
-
this.pulumiProjectHost.setPassword(projectId, password)
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
// try to run a command to check if the password is correct
|
|
35
|
-
await this.pulumiProjectHost.runLocal(
|
|
36
|
-
{
|
|
37
|
-
projectId,
|
|
38
|
-
pulumiProjectName: this.projectName,
|
|
39
|
-
pulumiStackName: projectId,
|
|
40
|
-
projectPath: this.projectPath,
|
|
41
|
-
},
|
|
42
|
-
async stack => {
|
|
43
|
-
this.logger.debug({ projectId }, "checking password")
|
|
44
|
-
|
|
45
|
-
await stack.info(true)
|
|
46
|
-
},
|
|
47
|
-
signal,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
return true
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (error instanceof Error) {
|
|
53
|
-
if (error.message.includes("incorrect passphrase")) {
|
|
54
|
-
this.logger.debug({ projectId, error }, "incorrect passphrase")
|
|
55
|
-
|
|
56
|
-
this.pulumiProjectHost.removePassword(projectId)
|
|
57
|
-
return false
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
throw error
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
get(
|
|
66
|
-
projectId: string,
|
|
67
|
-
instanceId: string,
|
|
68
|
-
signal?: AbortSignal,
|
|
69
|
-
): Promise<Record<string, unknown>> {
|
|
70
|
-
return this.pulumiProjectHost.runLocal(
|
|
71
|
-
{
|
|
72
|
-
projectId,
|
|
73
|
-
pulumiProjectName: this.projectName,
|
|
74
|
-
pulumiStackName: projectId,
|
|
75
|
-
projectPath: this.projectPath,
|
|
76
|
-
},
|
|
77
|
-
async stack => {
|
|
78
|
-
this.logger.debug({ projectId, instanceId }, "getting secrets")
|
|
79
|
-
|
|
80
|
-
const config = await stack.getAllConfig()
|
|
81
|
-
signal?.throwIfAborted()
|
|
82
|
-
|
|
83
|
-
const prefix = this.getPrefix(projectId, instanceId)
|
|
84
|
-
const secrets = pickBy(config, (_, key) => key.startsWith(prefix))
|
|
85
|
-
const trimmedSecrets = mapKeys(secrets, key => key.slice(prefix.length))
|
|
86
|
-
|
|
87
|
-
return mapValues(trimmedSecrets, value => stringToValue(value.value))
|
|
88
|
-
},
|
|
89
|
-
signal,
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
set(
|
|
94
|
-
projectId: string,
|
|
95
|
-
instanceId: string,
|
|
96
|
-
values: Record<string, unknown>,
|
|
97
|
-
signal?: AbortSignal,
|
|
98
|
-
): Promise<void> {
|
|
99
|
-
return this.pulumiProjectHost.runLocal(
|
|
100
|
-
{
|
|
101
|
-
projectId,
|
|
102
|
-
pulumiProjectName: this.projectName,
|
|
103
|
-
pulumiStackName: projectId,
|
|
104
|
-
projectPath: this.projectPath,
|
|
105
|
-
},
|
|
106
|
-
async stack => {
|
|
107
|
-
this.logger.debug({ projectId, instanceId }, "setting secrets")
|
|
108
|
-
|
|
109
|
-
const componentSecrets = mapValues(
|
|
110
|
-
mapKeys(values, key => `${this.getPrefix(projectId, instanceId)}${key}`),
|
|
111
|
-
value => ({
|
|
112
|
-
value: valueToString(value),
|
|
113
|
-
secret: true,
|
|
114
|
-
}),
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
const config = await stack.getAllConfig()
|
|
118
|
-
signal?.throwIfAborted()
|
|
119
|
-
|
|
120
|
-
Object.assign(config, componentSecrets)
|
|
121
|
-
|
|
122
|
-
await stack.setAllConfig(config)
|
|
123
|
-
},
|
|
124
|
-
signal,
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private getPrefix(projectId: string, instanceId: string) {
|
|
129
|
-
// replace all semicolons with dashes since extra semicolons are not allowed in Pulumi config keys
|
|
130
|
-
instanceId = instanceId.replace(/:/g, "_")
|
|
131
|
-
|
|
132
|
-
return `${this.projectName}:${projectId}/${instanceId}/`
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
static async create(
|
|
136
|
-
config: z.infer<typeof localSecretBackendConfig>,
|
|
137
|
-
pulumiProjectHost: LocalPulumiHost,
|
|
138
|
-
logger: Logger,
|
|
139
|
-
) {
|
|
140
|
-
const [projectPath, projectName] = await resolveMainLocalProject(
|
|
141
|
-
config.HIGHSTATE_BACKEND_SECRET_PROJECT_PATH,
|
|
142
|
-
config.HIGHSTATE_BACKEND_SECRET_PROJECT_NAME,
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
return new LocalSecretBackend(
|
|
146
|
-
projectPath,
|
|
147
|
-
projectName,
|
|
148
|
-
pulumiProjectHost,
|
|
149
|
-
logger.child({ backend: "SecretBackend", service: "LocalSecretBackend" }),
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
}
|