@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
|
@@ -1,114 +1,204 @@
|
|
|
1
1
|
import type { LibraryBackend } from "../library"
|
|
2
|
-
import type {
|
|
3
|
-
import type { ProjectBackend
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import type { StateManager } from "../state"
|
|
3
|
+
import type { ProjectBackend } from "../project"
|
|
4
|
+
import type {
|
|
5
|
+
RunnerArtifact,
|
|
6
|
+
RunnerBackend,
|
|
7
|
+
TypedUnitStateUpdate,
|
|
8
|
+
UnitStateUpdate,
|
|
9
|
+
} from "../runner"
|
|
8
10
|
import type { Logger } from "pino"
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
+
import type {
|
|
12
|
+
InstanceLockService,
|
|
13
|
+
InstanceStateService,
|
|
14
|
+
OperationService,
|
|
15
|
+
SecretService,
|
|
16
|
+
WorkerService,
|
|
17
|
+
} from "../business"
|
|
18
|
+
import type { PubSubManager } from "../pubsub"
|
|
19
|
+
import { randomBytes } from "node:crypto"
|
|
20
|
+
import { v7 as uuidv7 } from "uuid"
|
|
21
|
+
import {
|
|
22
|
+
isUnitModel,
|
|
23
|
+
parseInstanceId,
|
|
24
|
+
type ComponentModel,
|
|
25
|
+
type InstanceModel,
|
|
26
|
+
type UnitSecretModel,
|
|
27
|
+
} from "@highstate/contract"
|
|
28
|
+
import { mapValues, unique } from "remeda"
|
|
29
|
+
import { ArtifactService } from "../artifact"
|
|
11
30
|
import {
|
|
12
31
|
type InstanceState,
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
type UnitPage,
|
|
33
|
+
type Operation,
|
|
34
|
+
type Page,
|
|
35
|
+
type Trigger,
|
|
36
|
+
type UnitTrigger,
|
|
37
|
+
type UnitTerminal,
|
|
38
|
+
type Terminal,
|
|
39
|
+
type TerminalSpec,
|
|
40
|
+
type TriggerInvocation,
|
|
41
|
+
PromiseTracker,
|
|
42
|
+
type InstanceStatePatch,
|
|
43
|
+
type PageBlock,
|
|
44
|
+
type Project,
|
|
18
45
|
} from "../shared"
|
|
19
46
|
import {
|
|
47
|
+
AbortError,
|
|
20
48
|
errorToString,
|
|
21
|
-
isAbortError,
|
|
22
49
|
isAbortErrorLike,
|
|
23
|
-
|
|
50
|
+
PerformanceLogger,
|
|
51
|
+
stringToValue,
|
|
24
52
|
valueToString,
|
|
53
|
+
waitForAbort,
|
|
25
54
|
} from "../common"
|
|
26
55
|
import { OperationWorkset, type OperationPhase } from "./operation-workset"
|
|
27
56
|
|
|
28
57
|
export class RuntimeOperation {
|
|
29
58
|
private readonly abortController = new AbortController()
|
|
59
|
+
private readonly forceAbortController = new AbortController()
|
|
60
|
+
|
|
61
|
+
private readonly instanceAbortControllers = new Map<string, AbortController>()
|
|
62
|
+
private readonly instanceForceAbortControllers = new Map<string, AbortController>()
|
|
63
|
+
|
|
30
64
|
private readonly instancePromiseMap = new Map<string, Promise<void>>()
|
|
31
|
-
private
|
|
65
|
+
private readonly promiseTracker = new PromiseTracker()
|
|
32
66
|
|
|
33
|
-
private
|
|
67
|
+
private workset!: OperationWorkset
|
|
34
68
|
|
|
35
69
|
constructor(
|
|
36
|
-
private readonly
|
|
70
|
+
private readonly project: Project,
|
|
71
|
+
private readonly operation: Operation,
|
|
37
72
|
private readonly runnerBackend: RunnerBackend,
|
|
38
|
-
private readonly stateBackend: StateBackend,
|
|
39
73
|
private readonly libraryBackend: LibraryBackend,
|
|
40
74
|
private readonly projectBackend: ProjectBackend,
|
|
41
|
-
private readonly
|
|
42
|
-
private readonly projectLock: ProjectLock,
|
|
75
|
+
private readonly artifactService: ArtifactService,
|
|
43
76
|
private readonly stateManager: StateManager,
|
|
44
|
-
private readonly
|
|
45
|
-
private readonly
|
|
77
|
+
private readonly instanceLockService: InstanceLockService,
|
|
78
|
+
private readonly operationService: OperationService,
|
|
79
|
+
private readonly secretService: SecretService,
|
|
80
|
+
private readonly instanceStateService: InstanceStateService,
|
|
81
|
+
private readonly pubsubManager: PubSubManager,
|
|
82
|
+
private readonly workerService: WorkerService,
|
|
46
83
|
private readonly logger: Logger,
|
|
47
84
|
) {}
|
|
48
85
|
|
|
49
86
|
async operateSafe(): Promise<void> {
|
|
50
87
|
try {
|
|
51
88
|
await this.operate()
|
|
89
|
+
|
|
90
|
+
// ensure that all promises are resolved
|
|
91
|
+
await this.promiseTracker.waitForAll()
|
|
52
92
|
} catch (error) {
|
|
53
|
-
if (
|
|
93
|
+
if (isAbortErrorLike(error)) {
|
|
54
94
|
this.logger.info("the operation was cancelled")
|
|
55
95
|
this.operation.status = "cancelled"
|
|
96
|
+
this.operation.error = null
|
|
56
97
|
|
|
57
|
-
await this.updateOperation()
|
|
98
|
+
await this.operationService.updateOperation(this.project.id, this.operation)
|
|
58
99
|
return
|
|
59
100
|
}
|
|
60
101
|
|
|
61
|
-
this.logger.error({
|
|
102
|
+
this.logger.error({ error }, "an error occurred while running the operation")
|
|
62
103
|
|
|
63
104
|
this.operation.status = "failed"
|
|
64
105
|
this.operation.error = errorToString(error)
|
|
65
106
|
|
|
66
|
-
await this.updateOperation()
|
|
107
|
+
await this.operationService.updateOperation(this.project.id, this.operation)
|
|
67
108
|
} finally {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
try {
|
|
110
|
+
// ensure that all promises are resolved even if the operation failed
|
|
111
|
+
await this.promiseTracker.waitForAll()
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.error(
|
|
114
|
+
{ error },
|
|
115
|
+
"one of the tracked promises failed after the operation failed",
|
|
116
|
+
)
|
|
117
|
+
}
|
|
75
118
|
}
|
|
76
119
|
}
|
|
77
120
|
|
|
78
121
|
private async operate(): Promise<void> {
|
|
79
122
|
this.logger.info("starting operation")
|
|
80
|
-
let lockInstanceIds: string[]
|
|
81
|
-
|
|
82
|
-
// keep recalculating the workset until we can acquire the locks and actually start the operation
|
|
83
|
-
while (true) {
|
|
84
|
-
this.workset = await OperationWorkset.load(
|
|
85
|
-
this.operation,
|
|
86
|
-
this.projectBackend,
|
|
87
|
-
this.libraryBackend,
|
|
88
|
-
this.stateBackend,
|
|
89
|
-
this.stateManager,
|
|
90
|
-
this.logger,
|
|
91
|
-
this.abortController.signal,
|
|
92
|
-
)
|
|
93
123
|
|
|
94
|
-
|
|
124
|
+
// create the workset for the operation
|
|
125
|
+
this.workset = await OperationWorkset.load(
|
|
126
|
+
this.project,
|
|
127
|
+
this.operation,
|
|
128
|
+
this.projectBackend,
|
|
129
|
+
this.libraryBackend,
|
|
130
|
+
this.stateManager,
|
|
131
|
+
this.instanceLockService,
|
|
132
|
+
this.instanceStateService,
|
|
133
|
+
this.logger,
|
|
134
|
+
this.abortController.signal,
|
|
135
|
+
)
|
|
95
136
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
137
|
+
// start the process to lock more instances when they become available
|
|
138
|
+
const lockAbortSignal = new AbortController()
|
|
139
|
+
this.promiseTracker.track(this.tryLockMore(lockAbortSignal.signal))
|
|
140
|
+
|
|
141
|
+
// try to lock as many instances as possible
|
|
142
|
+
await this.workset.tryLock()
|
|
99
143
|
|
|
100
|
-
|
|
144
|
+
if (this.workset.instanceIdsToLockIds.size === 0) {
|
|
145
|
+
this.logger.debug("successfully locked all instances for the operation before starting")
|
|
146
|
+
lockAbortSignal.abort()
|
|
147
|
+
}
|
|
101
148
|
|
|
102
|
-
|
|
149
|
+
// setup abort controllers for the instances
|
|
150
|
+
for (const instanceId of this.workset.getAllAffectedInstanceIds()) {
|
|
151
|
+
this.setupInstanceAbortControllers(instanceId)
|
|
103
152
|
}
|
|
104
153
|
|
|
105
154
|
try {
|
|
106
|
-
//
|
|
107
|
-
await this.
|
|
155
|
+
// run the operation
|
|
156
|
+
await this.processOperation()
|
|
108
157
|
} finally {
|
|
158
|
+
lockAbortSignal.abort()
|
|
159
|
+
|
|
109
160
|
if (this.operation.type === "preview") {
|
|
110
161
|
// stream initial states for preview operations
|
|
111
|
-
this.workset.
|
|
162
|
+
await this.workset.restoreAffectedInitialStates()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private setupInstanceAbortControllers(instanceId: string): void {
|
|
168
|
+
const abortController = new AbortController()
|
|
169
|
+
this.abortController.signal.addEventListener("abort", () => abortController.abort())
|
|
170
|
+
this.instanceAbortControllers.set(instanceId, abortController)
|
|
171
|
+
|
|
172
|
+
const forceAbortController = new AbortController()
|
|
173
|
+
this.forceAbortController.signal.addEventListener("abort", () => forceAbortController.abort())
|
|
174
|
+
this.instanceForceAbortControllers.set(instanceId, forceAbortController)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async tryLockMore(signal: AbortSignal): Promise<void> {
|
|
178
|
+
const eventStream = this.pubsubManager.subscribe(["instance-lock", this.project.id], signal)
|
|
179
|
+
|
|
180
|
+
for await (const event of eventStream) {
|
|
181
|
+
if (event.type !== "unlocked") {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const instanceIdsToLock = event.instanceIds.filter(
|
|
186
|
+
//
|
|
187
|
+
instanceId => this.workset.instanceIdsToLockIds.has(instanceId),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if (instanceIdsToLock.length === 0) {
|
|
191
|
+
try {
|
|
192
|
+
await this.workset.tryLock(instanceIdsToLock)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.logger.error({ error }, "failed to lock more instances during operation")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (this.workset.instanceIdsToLockIds.size === 0) {
|
|
199
|
+
// no more instances to lock, stop listening for events
|
|
200
|
+
this.logger.debug("successfully locked all instances for the operation")
|
|
201
|
+
break
|
|
112
202
|
}
|
|
113
203
|
}
|
|
114
204
|
}
|
|
@@ -118,22 +208,22 @@ export class RuntimeOperation {
|
|
|
118
208
|
this.operation.instanceIdsToDestroy = this.workset.operation.instanceIdsToDestroy
|
|
119
209
|
|
|
120
210
|
this.logger.info(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
},
|
|
125
|
-
"operation started",
|
|
211
|
+
"operation started with %s instances to update and %s to destroy",
|
|
212
|
+
this.operation.instanceIdsToUpdate.length,
|
|
213
|
+
this.operation.instanceIdsToDestroy.length,
|
|
126
214
|
)
|
|
127
215
|
|
|
128
216
|
const phases = this.getOperationPhases()
|
|
217
|
+
const errors: string[] = []
|
|
218
|
+
let hasAbortError = false
|
|
129
219
|
|
|
130
220
|
for (const phase of phases) {
|
|
131
|
-
this.currentPhase = phase
|
|
221
|
+
this.workset.currentPhase = phase
|
|
132
222
|
|
|
133
223
|
const promises: Promise<void>[] = []
|
|
134
|
-
for (const instanceId of this.workset.getAffectedInstanceIds(
|
|
135
|
-
const parentId = this.workset.getParentId(instanceId
|
|
136
|
-
if (parentId && this.workset.isAffected(parentId
|
|
224
|
+
for (const instanceId of this.workset.getAffectedInstanceIds()) {
|
|
225
|
+
const parentId = this.workset.getParentId(instanceId)
|
|
226
|
+
if (parentId && this.workset.isAffected(parentId)) {
|
|
137
227
|
// do not call the operation for child instances of affected composites,
|
|
138
228
|
// they will be called by their parent instance
|
|
139
229
|
continue
|
|
@@ -144,23 +234,94 @@ export class RuntimeOperation {
|
|
|
144
234
|
|
|
145
235
|
this.logger.info(`all operations for phase "%s" started`, phase)
|
|
146
236
|
this.operation.status = "running"
|
|
147
|
-
await this.updateOperation()
|
|
237
|
+
await this.operationService.updateOperation(this.project.id, this.operation)
|
|
148
238
|
|
|
149
|
-
await Promise.
|
|
239
|
+
const phaseResults = await Promise.allSettled(promises)
|
|
240
|
+
for (const result of phaseResults) {
|
|
241
|
+
if (result.status !== "rejected") {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isAbortErrorLike(result.reason)) {
|
|
246
|
+
hasAbortError = true
|
|
247
|
+
} else {
|
|
248
|
+
errors.push(errorToString(result.reason))
|
|
249
|
+
}
|
|
250
|
+
}
|
|
150
251
|
|
|
151
252
|
this.logger.info(`all operations for phase "%s" completed`, phase)
|
|
152
253
|
}
|
|
153
254
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
255
|
+
if (errors.length > 0) {
|
|
256
|
+
this.operation.status = "failed"
|
|
257
|
+
// TODO: map errors to instances inside operation
|
|
258
|
+
this.operation.error = errors.join("\n\n")
|
|
259
|
+
} else if (hasAbortError) {
|
|
260
|
+
this.operation.status = "cancelled"
|
|
261
|
+
this.operation.error = null
|
|
262
|
+
} else {
|
|
263
|
+
this.operation.status = "completed"
|
|
264
|
+
this.operation.error = null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await this.operationService.updateOperation(this.project.id, this.operation)
|
|
157
268
|
|
|
158
269
|
this.logger.info("operation completed")
|
|
159
270
|
}
|
|
160
271
|
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
|
|
272
|
+
cancelInstance(instanceId: string): void {
|
|
273
|
+
const abortController = this.instanceAbortControllers.get(instanceId)
|
|
274
|
+
if (!abortController) {
|
|
275
|
+
throw new Error(`No abort controller found for instance "${instanceId}"`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!abortController.signal.aborted) {
|
|
279
|
+
this.logger.info(`cancelling operation for instance "${instanceId}"`)
|
|
280
|
+
abortController.abort()
|
|
281
|
+
|
|
282
|
+
// just the UX feature to indicate that we are trying to cancel the operation
|
|
283
|
+
this.promiseTracker.track(
|
|
284
|
+
this.workset.patchState({
|
|
285
|
+
id: instanceId,
|
|
286
|
+
operationStatus: {
|
|
287
|
+
status: "cancelling",
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// then try to force cancel the operation
|
|
296
|
+
const forceAbortController = this.instanceForceAbortControllers.get(instanceId)
|
|
297
|
+
if (!forceAbortController) {
|
|
298
|
+
throw new Error(`No force abort controller found for instance "${instanceId}"`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!forceAbortController.signal.aborted) {
|
|
302
|
+
this.logger.info(`force cancelling operation for instance "${instanceId}"`)
|
|
303
|
+
forceAbortController.abort()
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.logger.warn(`operation for instance "${instanceId}" is already force cancelled`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
cancel(): void {
|
|
311
|
+
if (!this.abortController.signal.aborted) {
|
|
312
|
+
this.logger.info("cancelling the operation")
|
|
313
|
+
this.abortController.abort()
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// then try to force cancel the operation
|
|
318
|
+
if (!this.forceAbortController.signal.aborted) {
|
|
319
|
+
this.logger.info("force cancelling the operation")
|
|
320
|
+
this.forceAbortController.abort()
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.logger.warn("the operation is already cancelled or force cancelled")
|
|
164
325
|
}
|
|
165
326
|
|
|
166
327
|
private getInstancePromiseForOperation(instanceId: string): Promise<void> {
|
|
@@ -195,7 +356,7 @@ export class RuntimeOperation {
|
|
|
195
356
|
}
|
|
196
357
|
|
|
197
358
|
private async getUnitPromise(instanceId: string): Promise<void> {
|
|
198
|
-
switch (this.currentPhase) {
|
|
359
|
+
switch (this.workset.currentPhase) {
|
|
199
360
|
case "update": {
|
|
200
361
|
return this.updateUnit(instanceId)
|
|
201
362
|
}
|
|
@@ -208,22 +369,13 @@ export class RuntimeOperation {
|
|
|
208
369
|
}
|
|
209
370
|
}
|
|
210
371
|
|
|
211
|
-
private
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return this.getInstancePromise(instanceId, async () => {
|
|
215
|
-
const state = this.workset.getState(instanceId) ?? createInstanceState(instanceId)
|
|
216
|
-
const instance = this.workset.getInstanceOrUndefined(instanceId)
|
|
372
|
+
private getCompositePromise(instanceId: string): Promise<void> {
|
|
373
|
+
return this.getInstancePromise(instanceId, async logger => {
|
|
374
|
+
const instance = this.workset.getInstance(instanceId)
|
|
217
375
|
|
|
218
|
-
this.
|
|
219
|
-
...state,
|
|
220
|
-
parentId: instance?.parentId,
|
|
221
|
-
latestOperationId: this.operation.id,
|
|
222
|
-
status: "pending",
|
|
223
|
-
error: null,
|
|
224
|
-
})
|
|
376
|
+
await this.setInitialOperationStatus(instance)
|
|
225
377
|
|
|
226
|
-
const children = this.workset.getAffectedCompositeChildren(instanceId
|
|
378
|
+
const children = this.workset.getAffectedCompositeChildren(instanceId)
|
|
227
379
|
const childPromises: Promise<void>[] = []
|
|
228
380
|
|
|
229
381
|
if (children.length) {
|
|
@@ -233,15 +385,12 @@ export class RuntimeOperation {
|
|
|
233
385
|
}
|
|
234
386
|
|
|
235
387
|
for (const child of children) {
|
|
236
|
-
if (
|
|
237
|
-
!this.operation.options.forceUpdateChildren &&
|
|
238
|
-
!this.workset.isAffected(child.id, this.currentPhase)
|
|
239
|
-
) {
|
|
388
|
+
if (!this.operation.options.forceUpdateChildren && !this.workset.isAffected(child.id)) {
|
|
240
389
|
// skip children that are not affected by the operation
|
|
241
390
|
continue
|
|
242
391
|
}
|
|
243
392
|
|
|
244
|
-
logger.debug(`waiting for child
|
|
393
|
+
logger.debug(`waiting for child "%s"`, child.id)
|
|
245
394
|
childPromises.push(this.getInstancePromiseForOperation(child.id))
|
|
246
395
|
}
|
|
247
396
|
|
|
@@ -249,105 +398,130 @@ export class RuntimeOperation {
|
|
|
249
398
|
await Promise.all(childPromises)
|
|
250
399
|
|
|
251
400
|
if (children.length > 0) {
|
|
252
|
-
logger.
|
|
401
|
+
logger.debug("all children completed")
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (this.operation.type === "destroy") {
|
|
405
|
+
await this.clearOperationState(instanceId)
|
|
406
|
+
return
|
|
253
407
|
}
|
|
254
408
|
|
|
255
|
-
|
|
409
|
+
const { inputHash, dependencyOutputHash } =
|
|
410
|
+
await this.workset.getUpToDateInputHashOutput(instance)
|
|
411
|
+
|
|
412
|
+
await this.workset.patchState({
|
|
256
413
|
id: instanceId,
|
|
257
|
-
|
|
258
|
-
|
|
414
|
+
operationStatus: {
|
|
415
|
+
status: "created",
|
|
416
|
+
inputHash,
|
|
417
|
+
dependencyOutputHash,
|
|
418
|
+
},
|
|
259
419
|
})
|
|
260
420
|
} catch (error) {
|
|
261
421
|
if (isAbortErrorLike(error)) {
|
|
262
|
-
this.workset.
|
|
422
|
+
await this.workset.restoreInitialState(instanceId)
|
|
263
423
|
return
|
|
264
424
|
}
|
|
265
425
|
|
|
266
|
-
this.
|
|
426
|
+
await this.workset.patchState({
|
|
267
427
|
id: instanceId,
|
|
268
|
-
|
|
269
|
-
|
|
428
|
+
operationStatus: {
|
|
429
|
+
status: "error",
|
|
430
|
+
message: errorToString(error),
|
|
431
|
+
},
|
|
270
432
|
})
|
|
271
433
|
}
|
|
272
434
|
})
|
|
273
435
|
}
|
|
274
436
|
|
|
275
437
|
private updateUnit(instanceId: string): Promise<void> {
|
|
276
|
-
return this.getInstancePromise(instanceId, async logger => {
|
|
438
|
+
return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
|
|
277
439
|
const instance = this.workset.getInstance(instanceId)
|
|
440
|
+
const component = this.workset.library.components[instance.type]
|
|
441
|
+
const perfLogger = new PerformanceLogger(logger)
|
|
442
|
+
perfLogger.log("starting update promise for instance")
|
|
278
443
|
|
|
279
|
-
this.
|
|
280
|
-
|
|
281
|
-
parentId: instance.parentId,
|
|
282
|
-
latestOperationId: this.operation.id,
|
|
283
|
-
status: "pending",
|
|
284
|
-
error: null,
|
|
285
|
-
currentResourceCount: 0,
|
|
286
|
-
})
|
|
444
|
+
await this.setInitialOperationStatus(instance)
|
|
445
|
+
perfLogger.log("initial operation status set")
|
|
287
446
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
447
|
+
await Promise.race([
|
|
448
|
+
this.updateUnitDependencies(instance.id, logger),
|
|
449
|
+
|
|
450
|
+
// to immediately abort the operation if requested
|
|
451
|
+
waitForAbort(signal),
|
|
452
|
+
])
|
|
453
|
+
|
|
454
|
+
if (!signal.aborted) {
|
|
455
|
+
perfLogger.log("dependencies updated")
|
|
295
456
|
}
|
|
296
457
|
|
|
458
|
+
signal.throwIfAborted()
|
|
459
|
+
|
|
297
460
|
logger.info("updating unit")
|
|
298
461
|
|
|
299
|
-
|
|
300
|
-
|
|
462
|
+
await this.workset.patchState({
|
|
463
|
+
id: instance.id,
|
|
464
|
+
operationStatus: {
|
|
465
|
+
status: "updating",
|
|
466
|
+
currentResourceCount: 0,
|
|
467
|
+
totalResourceCount: 0,
|
|
468
|
+
},
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
perfLogger.log("set 'updating' operation status")
|
|
472
|
+
signal.throwIfAborted()
|
|
473
|
+
|
|
474
|
+
const secrets = await this.secretService.getInstanceSecretValues(this.project, instance.id)
|
|
301
475
|
|
|
302
|
-
|
|
476
|
+
perfLogger.log("secrets loaded")
|
|
477
|
+
signal.throwIfAborted()
|
|
478
|
+
|
|
479
|
+
const config = this.prepareUnitConfig(instance, component)
|
|
480
|
+
perfLogger.log("unit config prepared")
|
|
481
|
+
|
|
482
|
+
// extract artifact hashes from dependencies based on instance inputs
|
|
483
|
+
const artifactHashes = this.collectArtifactIdsForInstance(instance)
|
|
484
|
+
const artifacts = await this.stateManager
|
|
485
|
+
.getArtifactHashIndexRepository(this.project.id)
|
|
486
|
+
.getManyItems(artifactHashes)
|
|
487
|
+
|
|
488
|
+
logger.debug({ count: artifactHashes.length }, "artifact hashes collected from dependencies")
|
|
489
|
+
perfLogger.log("artifact hashes collected")
|
|
303
490
|
|
|
304
491
|
await this.runnerBackend[this.operation.type === "preview" ? "preview" : "update"]({
|
|
305
|
-
projectId: this.
|
|
492
|
+
projectId: this.project.id,
|
|
493
|
+
libraryId: this.project.libraryId,
|
|
306
494
|
instanceType: instance.type,
|
|
307
495
|
instanceName: instance.name,
|
|
308
|
-
config
|
|
496
|
+
config,
|
|
309
497
|
refresh: this.operation.options.refresh,
|
|
310
498
|
secrets: mapValues(secrets, value => valueToString(value)),
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
logger.debug("unit update requested")
|
|
315
|
-
|
|
316
|
-
const stream = this.runnerBackend.watch({
|
|
317
|
-
projectId: this.operation.projectId,
|
|
318
|
-
instanceType: instance.type,
|
|
319
|
-
instanceName: instance.name,
|
|
320
|
-
finalStatuses: ["created", "error"],
|
|
499
|
+
artifacts,
|
|
500
|
+
signal,
|
|
501
|
+
forceSignal,
|
|
321
502
|
})
|
|
322
503
|
|
|
323
|
-
|
|
504
|
+
perfLogger.log("unit update requested")
|
|
324
505
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.updateInstanceState({
|
|
328
|
-
id: instance.id,
|
|
329
|
-
inputHash,
|
|
330
|
-
dependencyIds,
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
logger.debug("input hash after update", { inputHash })
|
|
506
|
+
await this.watchStateStream(instance.type, instance.name, logger)
|
|
507
|
+
perfLogger.log("unit update completed")
|
|
334
508
|
logger.info("unit updated")
|
|
335
509
|
})
|
|
336
510
|
}
|
|
337
511
|
|
|
338
|
-
private async updateUnitDependencies(
|
|
512
|
+
private async updateUnitDependencies(instanceId: string, logger: Logger): Promise<void> {
|
|
339
513
|
try {
|
|
340
|
-
const dependencies = this.
|
|
514
|
+
const dependencies = this.getInstanceDependencyIds(instanceId)
|
|
341
515
|
const dependencyPromises: Promise<void>[] = []
|
|
342
516
|
|
|
343
|
-
for (const
|
|
344
|
-
if (!this.operation.instanceIdsToUpdate.includes(
|
|
517
|
+
for (const dependencyId of dependencies) {
|
|
518
|
+
if (!this.operation.instanceIdsToUpdate.includes(dependencyId)) {
|
|
345
519
|
// skip dependencies that are not affected by the operation
|
|
346
520
|
continue
|
|
347
521
|
}
|
|
348
522
|
|
|
349
|
-
logger.debug(`waiting for dependency
|
|
350
|
-
dependencyPromises.push(this.getInstancePromiseForOperation(
|
|
523
|
+
logger.debug(`waiting for dependency "${dependencyId}"`)
|
|
524
|
+
dependencyPromises.push(this.getInstancePromiseForOperation(dependencyId))
|
|
351
525
|
}
|
|
352
526
|
|
|
353
527
|
await Promise.all(dependencyPromises)
|
|
@@ -355,71 +529,90 @@ export class RuntimeOperation {
|
|
|
355
529
|
if (dependencies.length > 0) {
|
|
356
530
|
logger.info("all dependencies completed")
|
|
357
531
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// restore the initial status of the instance if one of the dependencies failed
|
|
362
|
-
this.workset.restoreInitialStatus(instance.id)
|
|
363
|
-
throw error
|
|
532
|
+
} catch {
|
|
533
|
+
// abort the instance if any dependency fails
|
|
534
|
+
throw new AbortError()
|
|
364
535
|
}
|
|
365
536
|
}
|
|
366
537
|
|
|
367
|
-
private async processBeforeDestroyTriggers(
|
|
538
|
+
private async processBeforeDestroyTriggers(
|
|
539
|
+
state: InstanceState,
|
|
540
|
+
logger: Logger,
|
|
541
|
+
signal: AbortSignal,
|
|
542
|
+
forceSignal: AbortSignal,
|
|
543
|
+
): Promise<void> {
|
|
368
544
|
if (!this.operation.options.invokeDestroyTriggers) {
|
|
369
545
|
logger.debug("destroy triggers are disabled for the operation")
|
|
370
546
|
return
|
|
371
547
|
}
|
|
372
548
|
|
|
373
549
|
const instance = this.workset.getInstance(state.id)
|
|
550
|
+
const component = this.workset.library.components[instance.type]
|
|
374
551
|
|
|
375
|
-
|
|
376
|
-
|
|
552
|
+
// fetch triggers from state backend by their IDs
|
|
553
|
+
const triggerIds = Object.values(state.extra?.triggerIds ?? {})
|
|
554
|
+
|
|
555
|
+
const allTriggers = await this.stateManager
|
|
556
|
+
.getTriggerRepository(this.project.id)
|
|
557
|
+
.getManyItems(triggerIds)
|
|
558
|
+
|
|
559
|
+
const beforeDestroyTriggers = allTriggers.filter(
|
|
560
|
+
trigger => trigger.spec.type === "before-destroy",
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if (beforeDestroyTriggers.length === 0) {
|
|
377
564
|
return
|
|
378
565
|
}
|
|
379
566
|
|
|
380
|
-
|
|
567
|
+
// get trigger names from state mapping by finding the key for each trigger ID
|
|
568
|
+
const invokedTriggers = beforeDestroyTriggers.map(trigger => {
|
|
569
|
+
const triggerName = Object.keys(state?.extra?.triggerIds ?? {}).find(
|
|
570
|
+
name => state?.extra?.triggerIds?.[name] === trigger.id,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
return { name: triggerName || "unknown" }
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
await this.workset.patchState({
|
|
577
|
+
id: instance.id,
|
|
578
|
+
operationStatus: {
|
|
579
|
+
status: "processing-triggers",
|
|
580
|
+
},
|
|
581
|
+
})
|
|
381
582
|
|
|
382
583
|
logger.info("updating unit to process before-destroy triggers...")
|
|
383
584
|
|
|
384
|
-
const secrets = await this.
|
|
385
|
-
this.operation.projectId,
|
|
386
|
-
instance.id,
|
|
387
|
-
this.abortController.signal,
|
|
388
|
-
)
|
|
585
|
+
const secrets = await this.secretService.getInstanceSecretValues(this.project, instance.id)
|
|
389
586
|
|
|
390
587
|
await this.runnerBackend.update({
|
|
391
|
-
projectId: this.
|
|
588
|
+
projectId: this.project.id,
|
|
589
|
+
libraryId: this.project.libraryId,
|
|
392
590
|
instanceType: instance.type,
|
|
393
591
|
instanceName: instance.name,
|
|
394
|
-
config: this.prepareUnitConfig(instance, invokedTriggers),
|
|
592
|
+
config: this.prepareUnitConfig(instance, component, invokedTriggers),
|
|
395
593
|
refresh: this.operation.options.refresh,
|
|
396
594
|
secrets: mapValues(secrets, value => valueToString(value)),
|
|
397
|
-
signal
|
|
595
|
+
signal,
|
|
596
|
+
forceSignal,
|
|
398
597
|
})
|
|
399
598
|
|
|
400
599
|
logger.debug("unit update requested")
|
|
401
600
|
|
|
402
|
-
|
|
403
|
-
projectId: this.operation.projectId,
|
|
404
|
-
instanceType: instance.type,
|
|
405
|
-
instanceName: instance.name,
|
|
406
|
-
finalStatuses: ["created", "error"],
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
await this.watchStateStream(stream)
|
|
410
|
-
|
|
601
|
+
await this.watchStateStream(instance.type, instance.name, logger)
|
|
411
602
|
logger.debug("before-destroy triggers processed")
|
|
412
603
|
}
|
|
413
604
|
|
|
414
605
|
private async destroyUnit(instanceId: string): Promise<void> {
|
|
415
|
-
return this.getInstancePromise(instanceId, async logger => {
|
|
416
|
-
this.
|
|
606
|
+
return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
|
|
607
|
+
await this.workset.patchState({
|
|
417
608
|
id: instanceId,
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
609
|
+
operationStatus: {
|
|
610
|
+
status: "pending",
|
|
611
|
+
},
|
|
421
612
|
})
|
|
422
613
|
|
|
614
|
+
signal.throwIfAborted()
|
|
615
|
+
|
|
423
616
|
const state = this.workset.getState(instanceId)
|
|
424
617
|
if (!state) {
|
|
425
618
|
logger.warn("state not found for unit, but destroy was requested")
|
|
@@ -442,48 +635,65 @@ export class RuntimeOperation {
|
|
|
442
635
|
}
|
|
443
636
|
|
|
444
637
|
await Promise.all(dependentPromises)
|
|
445
|
-
|
|
638
|
+
signal.throwIfAborted()
|
|
446
639
|
|
|
447
|
-
await this.processBeforeDestroyTriggers(state, logger)
|
|
640
|
+
await this.processBeforeDestroyTriggers(state, logger, signal, forceSignal)
|
|
641
|
+
signal.throwIfAborted()
|
|
448
642
|
|
|
449
643
|
logger.info("destroying unit...")
|
|
450
644
|
|
|
645
|
+
await this.workset.patchState({
|
|
646
|
+
id: instanceId,
|
|
647
|
+
operationStatus: {
|
|
648
|
+
status: "destroying",
|
|
649
|
+
},
|
|
650
|
+
})
|
|
651
|
+
|
|
451
652
|
const [type, name] = parseInstanceId(instanceId)
|
|
452
653
|
|
|
453
654
|
await this.runnerBackend.destroy({
|
|
454
|
-
projectId: this.
|
|
655
|
+
projectId: this.project.id,
|
|
656
|
+
libraryId: this.project.libraryId,
|
|
455
657
|
instanceType: type,
|
|
456
658
|
instanceName: name,
|
|
457
659
|
refresh: this.operation.options.refresh,
|
|
458
|
-
signal
|
|
660
|
+
signal,
|
|
661
|
+
forceSignal,
|
|
459
662
|
deleteUnreachable: this.operation.options.deleteUnreachableResources,
|
|
460
663
|
forceDeleteState: this.operation.options.forceDeleteState,
|
|
461
664
|
})
|
|
462
665
|
|
|
463
|
-
|
|
666
|
+
logger.debug("destroy request sent")
|
|
464
667
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
668
|
+
await this.watchStateStream(type, name, logger)
|
|
669
|
+
|
|
670
|
+
// clean up artifacts after successful destruction
|
|
671
|
+
try {
|
|
672
|
+
const artifactIds = unique(Object.values(state.extra?.exportedArtifactIds ?? {}).flat())
|
|
673
|
+
|
|
674
|
+
await this.artifactService.removeUsages(
|
|
675
|
+
//
|
|
676
|
+
this.project.id,
|
|
677
|
+
artifactIds,
|
|
678
|
+
[{ type: "instance", instanceId: state.id }],
|
|
679
|
+
)
|
|
680
|
+
} catch (error) {
|
|
681
|
+
logger.warn({ error }, "failed to cleanup artifacts for destroyed instance")
|
|
682
|
+
}
|
|
471
683
|
|
|
472
|
-
|
|
473
|
-
this.logger.info("unit destroyed")
|
|
684
|
+
logger.info("unit destroyed")
|
|
474
685
|
})
|
|
475
686
|
}
|
|
476
687
|
|
|
477
688
|
private async refreshUnit(instanceId: string) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
return this.getInstancePromise(instanceId, async () => {
|
|
481
|
-
this.updateInstanceState({
|
|
689
|
+
return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
|
|
690
|
+
await this.workset.patchState({
|
|
482
691
|
id: instanceId,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
692
|
+
operationStatus: {
|
|
693
|
+
status: "refreshing",
|
|
694
|
+
currentResourceCount: 0,
|
|
695
|
+
totalResourceCount: 0,
|
|
696
|
+
},
|
|
487
697
|
})
|
|
488
698
|
|
|
489
699
|
logger.info("refreshing unit...")
|
|
@@ -491,62 +701,84 @@ export class RuntimeOperation {
|
|
|
491
701
|
const [type, name] = parseInstanceId(instanceId)
|
|
492
702
|
|
|
493
703
|
await this.runnerBackend.refresh({
|
|
494
|
-
projectId: this.
|
|
704
|
+
projectId: this.project.id,
|
|
705
|
+
libraryId: this.project.libraryId,
|
|
495
706
|
instanceType: type,
|
|
496
707
|
instanceName: name,
|
|
497
|
-
signal
|
|
708
|
+
signal,
|
|
709
|
+
forceSignal,
|
|
498
710
|
})
|
|
499
711
|
|
|
500
712
|
logger.debug("unit refresh requested")
|
|
501
713
|
|
|
502
|
-
|
|
503
|
-
projectId: this.operation.projectId,
|
|
504
|
-
instanceType: type,
|
|
505
|
-
instanceName: name,
|
|
506
|
-
finalStatuses: ["created", "error"],
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
await this.watchStateStream(stream)
|
|
714
|
+
await this.watchStateStream(type, name, logger)
|
|
510
715
|
logger.info("unit refreshed")
|
|
511
716
|
})
|
|
512
717
|
}
|
|
513
718
|
|
|
514
|
-
private async watchStateStream(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
719
|
+
private async watchStateStream(
|
|
720
|
+
instanceType: string,
|
|
721
|
+
instanceName: string,
|
|
722
|
+
logger: Logger,
|
|
723
|
+
): Promise<void> {
|
|
724
|
+
const stream = this.runnerBackend.watch({
|
|
725
|
+
projectId: this.project.id,
|
|
726
|
+
libraryId: this.project.libraryId,
|
|
727
|
+
instanceType,
|
|
728
|
+
instanceName,
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
let update: UnitStateUpdate | undefined
|
|
732
|
+
|
|
733
|
+
for await (update of stream) {
|
|
734
|
+
try {
|
|
735
|
+
await this.handleUnitStateUpdate(update)
|
|
736
|
+
} catch (error) {
|
|
737
|
+
logger.error({ error }, "failed to handle unit state update")
|
|
520
738
|
}
|
|
521
739
|
|
|
522
|
-
|
|
523
|
-
|
|
740
|
+
if (update.type === "error") {
|
|
741
|
+
if (isAbortErrorLike(update.message)) {
|
|
742
|
+
// abort the unit if the returned error contains some abort-like pattern
|
|
743
|
+
// generally, this might not be safe, but for now, we assume that
|
|
744
|
+
throw new AbortError()
|
|
745
|
+
}
|
|
524
746
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
747
|
+
// rethrow the error to stop the execution of dependent units
|
|
748
|
+
throw new Error(`An error occurred while processing the unit "${update.unitId}"`, {
|
|
749
|
+
cause: update.message,
|
|
750
|
+
})
|
|
751
|
+
}
|
|
528
752
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
)
|
|
753
|
+
if (update.type === "completion") {
|
|
754
|
+
return
|
|
755
|
+
}
|
|
533
756
|
}
|
|
757
|
+
|
|
758
|
+
throw new Error(
|
|
759
|
+
"The unit state stream was closed without a completion update or it was not handled properly.",
|
|
760
|
+
)
|
|
534
761
|
}
|
|
535
762
|
|
|
536
763
|
private prepareUnitConfig(
|
|
537
764
|
instance: InstanceModel,
|
|
538
|
-
|
|
765
|
+
component: ComponentModel,
|
|
766
|
+
invokedTriggers: TriggerInvocation[] = [],
|
|
539
767
|
): Record<string, string> {
|
|
540
768
|
const config: Record<string, string> = {}
|
|
541
769
|
|
|
542
|
-
for (const
|
|
543
|
-
|
|
770
|
+
for (const key of Object.keys(component.args)) {
|
|
771
|
+
// explicitly set empty values to remove old values in the config
|
|
772
|
+
const value = instance.args?.[key]
|
|
773
|
+
config[key] = value ? valueToString(value) : ""
|
|
544
774
|
}
|
|
545
775
|
|
|
546
776
|
const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
|
|
547
777
|
|
|
548
|
-
for (const
|
|
549
|
-
|
|
778
|
+
for (const key of Object.keys(component.inputs)) {
|
|
779
|
+
const inputs = instanceInputs[key] ?? []
|
|
780
|
+
|
|
781
|
+
config[`input.${key}`] = JSON.stringify(inputs.map(input => input.input))
|
|
550
782
|
}
|
|
551
783
|
|
|
552
784
|
config["$invokedTriggers"] = JSON.stringify(invokedTriggers)
|
|
@@ -554,49 +786,181 @@ export class RuntimeOperation {
|
|
|
554
786
|
return config
|
|
555
787
|
}
|
|
556
788
|
|
|
557
|
-
private async
|
|
558
|
-
|
|
559
|
-
|
|
789
|
+
private async handleUnitStateUpdate(update: UnitStateUpdate): Promise<void> {
|
|
790
|
+
switch (update.type) {
|
|
791
|
+
case "message":
|
|
792
|
+
this.handleUnitMessage(update)
|
|
793
|
+
return
|
|
794
|
+
case "progress":
|
|
795
|
+
await this.handleUnitProgress(update)
|
|
796
|
+
return
|
|
797
|
+
case "error":
|
|
798
|
+
await this.handleUnitError(update)
|
|
799
|
+
return
|
|
800
|
+
case "completion":
|
|
801
|
+
await this.handleUnitCompletion(update)
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private handleUnitMessage(update: TypedUnitStateUpdate<"message">): void {
|
|
807
|
+
// collect all instances in the hierarchy that should receive this log
|
|
808
|
+
const instanceIdsInHierarchy: string[] = []
|
|
809
|
+
let instance: InstanceModel | null = this.workset.getInstance(update.unitId)
|
|
810
|
+
|
|
811
|
+
for (;;) {
|
|
812
|
+
instanceIdsInHierarchy.push(instance.id)
|
|
813
|
+
|
|
814
|
+
if (!instance.parentId) {
|
|
815
|
+
break
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
instance = this.workset.getInstance(instance.parentId)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// persist the log for all instances in the hierarchy
|
|
822
|
+
// TODO: batch
|
|
823
|
+
this.promiseTracker.track(
|
|
824
|
+
this.operationService.appendLogs(this.project.id, this.operation.id, instanceIdsInHierarchy, [
|
|
825
|
+
update.message,
|
|
826
|
+
]),
|
|
827
|
+
)
|
|
828
|
+
return
|
|
560
829
|
}
|
|
561
830
|
|
|
562
|
-
private
|
|
563
|
-
|
|
564
|
-
|
|
831
|
+
private async handleUnitProgress(update: TypedUnitStateUpdate<"progress">): Promise<void> {
|
|
832
|
+
const patch: InstanceStatePatch = { id: update.unitId }
|
|
833
|
+
patch.operationStatus = {}
|
|
834
|
+
|
|
835
|
+
if (update.currentResourceCount !== undefined) {
|
|
836
|
+
patch.operationStatus.currentResourceCount = update.currentResourceCount
|
|
565
837
|
}
|
|
566
838
|
|
|
567
|
-
if (
|
|
568
|
-
|
|
839
|
+
if (update.totalResourceCount !== undefined) {
|
|
840
|
+
patch.operationStatus.totalResourceCount = update.totalResourceCount
|
|
841
|
+
}
|
|
569
842
|
|
|
570
|
-
|
|
843
|
+
await this.workset.patchState(patch)
|
|
844
|
+
}
|
|
571
845
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
846
|
+
private async handleUnitError(update: TypedUnitStateUpdate<"error">): Promise<void> {
|
|
847
|
+
if (isAbortErrorLike(update.message)) {
|
|
848
|
+
return
|
|
849
|
+
}
|
|
575
850
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
851
|
+
const patch: InstanceStatePatch = {
|
|
852
|
+
id: update.unitId,
|
|
853
|
+
operationStatus: {
|
|
854
|
+
status: "error",
|
|
855
|
+
message: update.message,
|
|
856
|
+
},
|
|
857
|
+
}
|
|
579
858
|
|
|
580
|
-
|
|
581
|
-
|
|
859
|
+
await this.workset.patchState(patch)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private async handleUnitCompletion(update: TypedUnitStateUpdate<"completion">): Promise<void> {
|
|
863
|
+
if (this.workset.currentPhase === "destroy") {
|
|
864
|
+
// if the destroy operation was completed, clear the state from all operation-related fields
|
|
865
|
+
await this.clearOperationState(update.unitId)
|
|
582
866
|
return
|
|
583
867
|
}
|
|
584
868
|
|
|
585
|
-
const state = this.workset.
|
|
869
|
+
const state = this.workset.getState(update.unitId)
|
|
870
|
+
if (!state) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
`Cannot handle completion for unit "${update.unitId}" because its state is not found.`,
|
|
873
|
+
)
|
|
874
|
+
}
|
|
586
875
|
|
|
587
|
-
|
|
588
|
-
if (this.operation.type !== "preview") {
|
|
589
|
-
this.persistStates.call(state)
|
|
876
|
+
const instance = this.workset.getInstance(update.unitId)
|
|
590
877
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
878
|
+
// patch the state with output hash first to recalculate the input hash
|
|
879
|
+
if (update.outputHash) {
|
|
880
|
+
await this.workset.patchState({
|
|
881
|
+
id: update.unitId,
|
|
882
|
+
operationStatus: { outputHash: update.outputHash },
|
|
883
|
+
})
|
|
594
884
|
}
|
|
885
|
+
|
|
886
|
+
const { inputHash, dependencyOutputHash } =
|
|
887
|
+
await this.workset.getUpToDateInputHashOutput(instance)
|
|
888
|
+
|
|
889
|
+
const patch: InstanceStatePatch = {
|
|
890
|
+
id: update.unitId,
|
|
891
|
+
operationStatus: {
|
|
892
|
+
status: "created",
|
|
893
|
+
message:
|
|
894
|
+
update.message ?? "The operation on this instance has been completed successfully.",
|
|
895
|
+
outputHash: update.outputHash ?? undefined,
|
|
896
|
+
inputHash,
|
|
897
|
+
dependencyOutputHash,
|
|
898
|
+
},
|
|
899
|
+
extra: {
|
|
900
|
+
statusFields: update.statusFields,
|
|
901
|
+
|
|
902
|
+
terminalIds: update.terminals
|
|
903
|
+
? this.processTerminals(update.unitId, update.terminals, state.extra?.terminalIds)
|
|
904
|
+
: null,
|
|
905
|
+
|
|
906
|
+
pageIds: update.pages
|
|
907
|
+
? this.processPages(update.unitId, update.pages, state.extra?.pageIds)
|
|
908
|
+
: null,
|
|
909
|
+
|
|
910
|
+
triggerIds: update.triggers
|
|
911
|
+
? this.processTriggers(update.unitId, update.triggers, state.extra?.triggerIds)
|
|
912
|
+
: null,
|
|
913
|
+
|
|
914
|
+
workerRegistrationIds: update.workers
|
|
915
|
+
? await this.workerService.updateUnitRegistrations(
|
|
916
|
+
this.project.id,
|
|
917
|
+
update.unitId,
|
|
918
|
+
state.extra?.workerRegistrationIds ?? {},
|
|
919
|
+
update.workers,
|
|
920
|
+
)
|
|
921
|
+
: null,
|
|
922
|
+
|
|
923
|
+
exportedArtifactIds: update.exportedArtifacts
|
|
924
|
+
? this.processArtifacts(
|
|
925
|
+
update.unitId,
|
|
926
|
+
update.exportedArtifacts,
|
|
927
|
+
state.extra?.exportedArtifactIds,
|
|
928
|
+
)
|
|
929
|
+
: null,
|
|
930
|
+
|
|
931
|
+
exportedSecretIds: update.exportedSecrets
|
|
932
|
+
? this.processExportedSecrets(update.unitId, update.exportedSecrets)
|
|
933
|
+
: null,
|
|
934
|
+
},
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (update.secrets) {
|
|
938
|
+
const instance = this.workset.getInstance(update.unitId)
|
|
939
|
+
|
|
940
|
+
this.promiseTracker.track(
|
|
941
|
+
this.updateInstanceSecrets(
|
|
942
|
+
instance,
|
|
943
|
+
mapValues(update.secrets, value => stringToValue(value)),
|
|
944
|
+
),
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
await this.workset.patchState(patch)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private async clearOperationState(instanceId: string): Promise<void> {
|
|
952
|
+
const patch: InstanceStatePatch = {
|
|
953
|
+
id: instanceId,
|
|
954
|
+
operationStatus: null,
|
|
955
|
+
extra: null,
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
await this.workset.patchState(patch)
|
|
595
959
|
}
|
|
596
960
|
|
|
597
961
|
private getInstancePromise(
|
|
598
962
|
instanceId: string,
|
|
599
|
-
fn: (logger: Logger) => Promise<void>,
|
|
963
|
+
fn: (logger: Logger, signal: AbortSignal, forceSignal: AbortSignal) => Promise<void>,
|
|
600
964
|
): Promise<void> {
|
|
601
965
|
let instancePromise = this.instancePromiseMap.get(instanceId)
|
|
602
966
|
if (instancePromise) {
|
|
@@ -604,56 +968,430 @@ export class RuntimeOperation {
|
|
|
604
968
|
}
|
|
605
969
|
|
|
606
970
|
const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
|
|
971
|
+
const abortController = this.instanceAbortControllers.get(instanceId)
|
|
972
|
+
const forceAbortController = this.instanceForceAbortControllers.get(instanceId)
|
|
973
|
+
|
|
974
|
+
if (!abortController || !forceAbortController) {
|
|
975
|
+
throw new Error(`Abort controllers for instance "${instanceId}" are not initialized`)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
instancePromise = this.workset
|
|
979
|
+
.ensureInstanceLocked(instanceId, abortController.signal)
|
|
980
|
+
.then(() => fn(logger, abortController.signal, forceAbortController.signal))
|
|
981
|
+
.catch(error => {
|
|
982
|
+
if (isAbortErrorLike(error)) {
|
|
983
|
+
// if the operation was aborted, restore the initial state of the instance
|
|
984
|
+
this.promiseTracker.track(this.workset.restoreInitialState(instanceId))
|
|
985
|
+
|
|
986
|
+
throw error
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (this.operation.status !== "failing") {
|
|
990
|
+
// report the failing status of the operation
|
|
991
|
+
this.operation.status = "failing"
|
|
992
|
+
this.promiseTracker.track(
|
|
993
|
+
this.operationService.updateOperation(this.project.id, this.operation),
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// rethrow the error
|
|
998
|
+
throw error
|
|
999
|
+
})
|
|
1000
|
+
.finally(() => {
|
|
1001
|
+
this.instancePromiseMap.delete(instanceId)
|
|
1002
|
+
|
|
1003
|
+
// TODO: ideally we should defer unlocking until all direct dependents are completed,
|
|
1004
|
+
// to ensure that they are received expected inputs from this instance
|
|
1005
|
+
this.promiseTracker.track(
|
|
1006
|
+
this.instanceLockService.unlockInstancesUnconditionally(this.project.id, [instanceId]),
|
|
1007
|
+
)
|
|
1008
|
+
})
|
|
607
1009
|
|
|
608
|
-
instancePromise = fn(logger).finally(() => this.instancePromiseMap.delete(instanceId))
|
|
609
1010
|
this.instancePromiseMap.set(instanceId, instancePromise)
|
|
610
1011
|
|
|
611
1012
|
return instancePromise
|
|
612
1013
|
}
|
|
613
1014
|
|
|
614
|
-
private
|
|
615
|
-
const
|
|
1015
|
+
private async setInitialOperationStatus(instance: InstanceModel): Promise<void> {
|
|
1016
|
+
const state = this.workset.getState(instance.id)
|
|
1017
|
+
|
|
1018
|
+
await this.workset.patchState({
|
|
1019
|
+
id: instance.id,
|
|
1020
|
+
parentId: instance.parentId,
|
|
1021
|
+
operationStatus: {
|
|
1022
|
+
status: "pending",
|
|
1023
|
+
message: "",
|
|
1024
|
+
dependencyIds: this.getInstanceDependencyIds(instance.id),
|
|
1025
|
+
latestOperationId: this.operation.id,
|
|
1026
|
+
inputHashNonce: state?.operationStatus?.inputHashNonce ?? randomBytes(4).readInt32LE(),
|
|
1027
|
+
args: instance.args,
|
|
1028
|
+
},
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private getInstanceDependencyIds(instanceId: string): string[] {
|
|
1033
|
+
const dependencies = new Set<string>()
|
|
1034
|
+
const instanceInputs = this.workset.resolvedInstanceInputs.get(instanceId) ?? {}
|
|
1035
|
+
|
|
1036
|
+
for (const inputs of Object.values(instanceInputs)) {
|
|
1037
|
+
for (const input of inputs) {
|
|
1038
|
+
dependencies.add(input.input.instanceId)
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return Array.from(dependencies)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Collects artifact IDs from dependencies based on the direct connections
|
|
1047
|
+
* from instance inputs to dependency outputs.
|
|
1048
|
+
*/
|
|
1049
|
+
private collectArtifactIdsForInstance(instance: InstanceModel): string[] {
|
|
1050
|
+
const artifactIds = new Set<string>()
|
|
616
1051
|
const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
|
|
617
1052
|
|
|
618
1053
|
for (const inputs of Object.values(instanceInputs)) {
|
|
619
1054
|
for (const input of inputs) {
|
|
620
|
-
const
|
|
1055
|
+
const dependencyState = this.workset.getState(input.input.instanceId)
|
|
1056
|
+
if (!dependencyState?.extra?.exportedArtifactIds) {
|
|
1057
|
+
continue
|
|
1058
|
+
}
|
|
621
1059
|
|
|
622
|
-
|
|
1060
|
+
const outputKey = input.input.output
|
|
1061
|
+
const outputArtifacts = dependencyState.extra.exportedArtifactIds[outputKey]
|
|
1062
|
+
if (!outputArtifacts) {
|
|
1063
|
+
continue
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
for (const hash of outputArtifacts) {
|
|
1067
|
+
artifactIds.add(hash)
|
|
1068
|
+
}
|
|
623
1069
|
}
|
|
624
1070
|
}
|
|
625
1071
|
|
|
626
|
-
return
|
|
1072
|
+
return Array.from(artifactIds)
|
|
627
1073
|
}
|
|
628
1074
|
|
|
629
|
-
|
|
630
|
-
|
|
1075
|
+
/**
|
|
1076
|
+
* Collects secret IDs from dependencies based on the direct connections
|
|
1077
|
+
* from instance inputs to dependency outputs.
|
|
1078
|
+
*/
|
|
1079
|
+
private collectSecretIdsForInstance(instance: InstanceModel): Set<string> {
|
|
1080
|
+
const secretIds = new Set<string>()
|
|
1081
|
+
const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
|
|
1082
|
+
|
|
1083
|
+
for (const inputs of Object.values(instanceInputs)) {
|
|
1084
|
+
for (const input of inputs) {
|
|
1085
|
+
const dependencyState = this.workset.getState(input.input.instanceId)
|
|
1086
|
+
if (!dependencyState?.extra?.exportedSecretIds) {
|
|
1087
|
+
continue
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const outputKey = input.input.output
|
|
1091
|
+
const outputSecrets = dependencyState.extra.exportedSecretIds[outputKey]
|
|
1092
|
+
if (!outputSecrets) {
|
|
1093
|
+
continue
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
for (const secretId of outputSecrets) {
|
|
1097
|
+
secretIds.add(secretId)
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
631
1101
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1102
|
+
return secretIds
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Updates the secrets for a specific instance.
|
|
1107
|
+
* Processes terminals from instance state update.
|
|
1108
|
+
*
|
|
1109
|
+
* Converts unit terminals to instance terminals and handles cleanup.
|
|
1110
|
+
*/
|
|
1111
|
+
private processTerminals(
|
|
1112
|
+
instanceId: string,
|
|
1113
|
+
unitTerminals: UnitTerminal[],
|
|
1114
|
+
currentTerminalIds: Record<string, string> | undefined,
|
|
1115
|
+
): Record<string, string> {
|
|
1116
|
+
const terminals: Terminal[] = unitTerminals.map(unitTerminal => ({
|
|
1117
|
+
id: currentTerminalIds?.[unitTerminal.name] ?? uuidv7(),
|
|
1118
|
+
instanceId,
|
|
1119
|
+
meta: unitTerminal.meta ?? {},
|
|
1120
|
+
}))
|
|
1121
|
+
|
|
1122
|
+
const terminalSpecEntries: [string, TerminalSpec][] = unitTerminals
|
|
1123
|
+
//
|
|
1124
|
+
.map((unitTerminal, index) => [terminals[index].id, unitTerminal.spec])
|
|
1125
|
+
|
|
1126
|
+
// create mapping from local name to UUID
|
|
1127
|
+
const newTerminalIds: Record<string, string> = {}
|
|
1128
|
+
for (let i = 0; i < unitTerminals.length; i++) {
|
|
1129
|
+
const unitTerminal = unitTerminals[i]
|
|
1130
|
+
const instanceTerminal = terminals[i]
|
|
1131
|
+
newTerminalIds[unitTerminal.name] = instanceTerminal.id
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// find terminals that need to be deleted (old terminals not in new list)
|
|
1135
|
+
const terminalsToDelete = Object.values(currentTerminalIds ?? {}).filter(
|
|
1136
|
+
id => !Object.values(newTerminalIds).includes(id),
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
if (terminals.length > 0) {
|
|
1140
|
+
this.promiseTracker.track(this.putTerminals(terminals, terminalSpecEntries))
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (terminalsToDelete.length > 0) {
|
|
1144
|
+
this.promiseTracker.track(this.deleteTerminals(terminalsToDelete))
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return newTerminalIds
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private async putTerminals(
|
|
1151
|
+
terminals: Terminal[],
|
|
1152
|
+
terminalSpecEntries: [string, TerminalSpec][],
|
|
1153
|
+
): Promise<void> {
|
|
1154
|
+
if (this.operation.type === "preview") {
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
await using batch = this.stateManager.batch()
|
|
1159
|
+
|
|
1160
|
+
await Promise.all([
|
|
1161
|
+
this.stateManager.getTerminalRepository(this.project.id).putManyItems(terminals, batch),
|
|
1162
|
+
|
|
1163
|
+
this.stateManager
|
|
1164
|
+
.getTerminalSpecRepository(this.project.id)
|
|
1165
|
+
.putMany(terminalSpecEntries, batch),
|
|
1166
|
+
])
|
|
1167
|
+
|
|
1168
|
+
await batch.write()
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
private async deleteTerminals(terminalIds: string[]): Promise<void> {
|
|
1172
|
+
if (this.operation.type === "preview") {
|
|
1173
|
+
return
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
await using batch = this.stateManager.batch()
|
|
1177
|
+
|
|
1178
|
+
await Promise.all([
|
|
1179
|
+
this.stateManager.getTerminalRepository(this.project.id).deleteMany(terminalIds, batch),
|
|
1180
|
+
this.stateManager.getTerminalSpecRepository(this.project.id).deleteMany(terminalIds, batch),
|
|
1181
|
+
])
|
|
1182
|
+
|
|
1183
|
+
await batch.write()
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Processes pages from instance state update.
|
|
1188
|
+
*
|
|
1189
|
+
* Converts unit pages to instance pages and handles cleanup.
|
|
1190
|
+
*/
|
|
1191
|
+
private processPages(
|
|
1192
|
+
instanceId: string,
|
|
1193
|
+
unitPages: UnitPage[],
|
|
1194
|
+
currentPageIds: Record<string, string> | undefined,
|
|
1195
|
+
): Record<string, string> {
|
|
1196
|
+
const pages: Page[] = unitPages.map(unitPage => ({
|
|
1197
|
+
id: currentPageIds?.[unitPage.name] ?? uuidv7(),
|
|
1198
|
+
instanceId,
|
|
1199
|
+
meta: unitPage.meta,
|
|
1200
|
+
}))
|
|
1201
|
+
|
|
1202
|
+
const pageContentEntries: [string, PageBlock[]][] = unitPages
|
|
1203
|
+
//
|
|
1204
|
+
.map((unitPage, index) => [pages[index].id, unitPage.content])
|
|
1205
|
+
|
|
1206
|
+
// create mapping from local name to UUID
|
|
1207
|
+
const newPageIds: Record<string, string> = {}
|
|
1208
|
+
for (let i = 0; i < unitPages.length; i++) {
|
|
1209
|
+
const unitPage = unitPages[i]
|
|
1210
|
+
const instancePage = pages[i]
|
|
1211
|
+
newPageIds[unitPage.name] = instancePage.id
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// find pages that need to be deleted (old pages not in new list)
|
|
1215
|
+
const pagesToDelete = Object.values(currentPageIds ?? {}).filter(
|
|
1216
|
+
id => !Object.values(newPageIds).includes(id),
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
if (pages.length > 0) {
|
|
1220
|
+
this.promiseTracker.track(this.putPages(pages, pageContentEntries))
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (pagesToDelete.length > 0) {
|
|
1224
|
+
this.promiseTracker.track(this.deletePages(pagesToDelete))
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return newPageIds
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
private async putPages(
|
|
1231
|
+
pages: Page[],
|
|
1232
|
+
pageContentEntries: [string, PageBlock[]][],
|
|
1233
|
+
): Promise<void> {
|
|
1234
|
+
if (this.operation.type === "preview") {
|
|
1235
|
+
return
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const batch = this.stateManager.batch()
|
|
1239
|
+
|
|
1240
|
+
await Promise.all([
|
|
1241
|
+
this.stateManager.getPageRepository(this.project.id).putManyItems(pages, batch),
|
|
1242
|
+
this.stateManager
|
|
1243
|
+
.getPageContentRepository(this.project.id)
|
|
1244
|
+
.putMany(pageContentEntries, batch),
|
|
1245
|
+
])
|
|
1246
|
+
|
|
1247
|
+
await batch.write()
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private async deletePages(pageIds: string[]): Promise<void> {
|
|
1251
|
+
if (this.operation.type === "preview") {
|
|
1252
|
+
return
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const batch = this.stateManager.batch()
|
|
1256
|
+
|
|
1257
|
+
await Promise.all([
|
|
1258
|
+
this.stateManager.getPageRepository(this.project.id).deleteMany(pageIds, batch),
|
|
1259
|
+
this.stateManager.getPageContentRepository(this.project.id).deleteMany(pageIds, batch),
|
|
1260
|
+
])
|
|
1261
|
+
|
|
1262
|
+
await batch.write()
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Processes triggers from instance state update.
|
|
1267
|
+
*
|
|
1268
|
+
* Converts unit triggers to instance triggers and handles cleanup.
|
|
1269
|
+
*/
|
|
1270
|
+
private processTriggers(
|
|
1271
|
+
instanceId: string,
|
|
1272
|
+
unitTriggers: UnitTrigger[],
|
|
1273
|
+
currentTriggerIds: Record<string, string> | undefined,
|
|
1274
|
+
): Record<string, string> {
|
|
1275
|
+
const triggers: Trigger[] = unitTriggers.map(unitTrigger => ({
|
|
1276
|
+
id: currentTriggerIds?.[unitTrigger.name] ?? uuidv7(),
|
|
1277
|
+
instanceId,
|
|
1278
|
+
meta: unitTrigger.meta ?? {},
|
|
1279
|
+
spec: unitTrigger.spec,
|
|
1280
|
+
}))
|
|
1281
|
+
|
|
1282
|
+
// create mapping from local name to UUID
|
|
1283
|
+
const newTriggerIds: Record<string, string> = {}
|
|
1284
|
+
for (let i = 0; i < unitTriggers.length; i++) {
|
|
1285
|
+
const unitTrigger = unitTriggers[i]
|
|
1286
|
+
newTriggerIds[unitTrigger.name] = triggers[i].id
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// find triggers that need to be deleted (old triggers not in new list)
|
|
1290
|
+
const triggersToDelete = Object.values(currentTriggerIds ?? {}).filter(
|
|
1291
|
+
id => !Object.values(newTriggerIds).includes(id),
|
|
636
1292
|
)
|
|
637
|
-
})
|
|
638
1293
|
|
|
639
|
-
|
|
640
|
-
|
|
1294
|
+
if (triggers.length > 0) {
|
|
1295
|
+
this.promiseTracker.track(this.putTriggers(triggers))
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (triggersToDelete.length > 0) {
|
|
1299
|
+
this.promiseTracker.track(this.deleteTriggers(triggersToDelete))
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return newTriggerIds
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
private async putTriggers(triggers: Trigger[]): Promise<void> {
|
|
1306
|
+
if (this.operation.type === "preview") {
|
|
1307
|
+
return
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
await this.stateManager.getTriggerRepository(this.project.id).putManyItems(triggers)
|
|
1311
|
+
}
|
|
641
1312
|
|
|
642
|
-
|
|
643
|
-
|
|
1313
|
+
private async deleteTriggers(triggerIds: string[]): Promise<void> {
|
|
1314
|
+
if (this.operation.type === "preview") {
|
|
1315
|
+
return
|
|
1316
|
+
}
|
|
644
1317
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
this.logger.debug({ msg: "persisting secrets", count: entries.length })
|
|
1318
|
+
await this.stateManager.getTriggerRepository(this.project.id).deleteMany(triggerIds)
|
|
1319
|
+
}
|
|
648
1320
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1321
|
+
/**
|
|
1322
|
+
* Processes artifacts from instance state update.
|
|
1323
|
+
*/
|
|
1324
|
+
private processArtifacts(
|
|
1325
|
+
instanceId: string,
|
|
1326
|
+
newOwnedArtifacts: Record<string, RunnerArtifact[]>,
|
|
1327
|
+
oldOwnedArtifactIds: Record<string, string[]> | undefined,
|
|
1328
|
+
): Record<string, string[]> {
|
|
1329
|
+
if (this.operation.type !== "preview") {
|
|
1330
|
+
// persist the new owned artifacts if this is not a preview operation
|
|
1331
|
+
this.promiseTracker.track(
|
|
1332
|
+
this.artifactService.updateUsage(
|
|
1333
|
+
this.project.id,
|
|
1334
|
+
{ type: "instance", instanceId },
|
|
1335
|
+
unique(Object.values(oldOwnedArtifactIds ?? {}).flat()),
|
|
1336
|
+
unique(
|
|
1337
|
+
Object.values(newOwnedArtifacts).flatMap(artifacts =>
|
|
1338
|
+
artifacts.map(artifact => artifact.id),
|
|
1339
|
+
),
|
|
1340
|
+
),
|
|
1341
|
+
),
|
|
1342
|
+
)
|
|
1343
|
+
}
|
|
652
1344
|
|
|
653
|
-
|
|
1345
|
+
return mapValues(newOwnedArtifacts, artifacts => artifacts.map(artifact => artifact.id))
|
|
1346
|
+
}
|
|
654
1347
|
|
|
655
|
-
|
|
1348
|
+
/**
|
|
1349
|
+
* Processes exported secrets from instance state update.
|
|
1350
|
+
*
|
|
1351
|
+
* Validates that all secret IDs are either present in the instance secrets or are in "exportedSecrets" of the direct dependencies
|
|
1352
|
+
* from connected inputs.
|
|
1353
|
+
*
|
|
1354
|
+
* Returns a mapping of output key to array of secret IDs exported via this output.
|
|
1355
|
+
*/
|
|
1356
|
+
private processExportedSecrets(
|
|
1357
|
+
instanceId: string,
|
|
1358
|
+
exportedSecrets: Record<string, Omit<UnitSecretModel, "value">[]>,
|
|
1359
|
+
): Record<string, string[]> {
|
|
1360
|
+
const instance = this.workset.getInstance(instanceId)
|
|
1361
|
+
const allowedSecretIds = this.collectSecretIdsForInstance(instance)
|
|
1362
|
+
|
|
1363
|
+
return mapValues(exportedSecrets, secrets => {
|
|
1364
|
+
const exportedSecretIds = secrets.map(secret => secret.id)
|
|
1365
|
+
for (const exportedSecretId of exportedSecretIds) {
|
|
1366
|
+
if (!allowedSecretIds.has(exportedSecretId)) {
|
|
1367
|
+
throw new Error(
|
|
1368
|
+
`The secret "${exportedSecretId}" is not allowed to be exported since it is not present in the instance secrets or in the exported secrets of connected dependencies.`,
|
|
1369
|
+
)
|
|
1370
|
+
}
|
|
656
1371
|
}
|
|
657
|
-
|
|
658
|
-
|
|
1372
|
+
|
|
1373
|
+
return exportedSecretIds
|
|
1374
|
+
})
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Updates the secrets for a specific instance.
|
|
1379
|
+
*/
|
|
1380
|
+
private async updateInstanceSecrets(
|
|
1381
|
+
instance: InstanceModel,
|
|
1382
|
+
secrets: Record<string, unknown>,
|
|
1383
|
+
): Promise<void> {
|
|
1384
|
+
if (this.operation.type === "preview") {
|
|
1385
|
+
// do not update secrets in preview mode
|
|
1386
|
+
return
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
await this.secretService.updateInstanceSecrets(
|
|
1390
|
+
this.project,
|
|
1391
|
+
instance.id,
|
|
1392
|
+
secrets,
|
|
1393
|
+
[], // no secrets to delete in this context
|
|
1394
|
+
false, // do not invalidate state even if the secrets were updated
|
|
1395
|
+
)
|
|
1396
|
+
}
|
|
659
1397
|
}
|