@highstate/backend 0.9.15 → 0.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-NAAIDR4U.js +8499 -0
- package/dist/chunk-NAAIDR4U.js.map +1 -0
- package/dist/chunk-OU5OQBLB.js +74 -0
- package/dist/chunk-OU5OQBLB.js.map +1 -0
- package/dist/chunk-Y7DXREVO.js +1745 -0
- package/dist/chunk-Y7DXREVO.js.map +1 -0
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +7227 -2501
- package/dist/index.js.map +1 -1
- package/dist/library/package-resolution-worker.js +7 -5
- package/dist/library/package-resolution-worker.js.map +1 -1
- package/dist/library/worker/main.js +76 -185
- package/dist/library/worker/main.js.map +1 -1
- package/dist/magic-string.es-5ABAC4JN.js +1292 -0
- package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
- package/dist/shared/index.js +3 -98
- package/dist/shared/index.js.map +1 -1
- package/package.json +31 -10
- package/src/artifact/abstractions.ts +46 -0
- package/src/artifact/encryption.ts +109 -0
- package/src/artifact/factory.ts +36 -0
- package/src/artifact/index.ts +3 -0
- package/src/artifact/local.ts +138 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
- package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
- package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
- package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
- package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
- package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
- package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
- package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
- package/src/business/api-key.ts +65 -0
- package/src/business/artifact.ts +289 -0
- package/src/business/backend-unlock.ts +10 -0
- package/src/business/index.ts +10 -0
- package/src/business/instance-lock.ts +125 -0
- package/src/business/instance-state.ts +434 -0
- package/src/business/operation.ts +251 -0
- package/src/business/project-unlock.ts +260 -0
- package/src/business/project.ts +299 -0
- package/src/business/secret.test.ts +178 -0
- package/src/business/secret.ts +281 -0
- package/src/business/worker.test.ts +614 -0
- package/src/business/worker.ts +398 -0
- package/src/common/clock.ts +18 -0
- package/src/common/index.ts +5 -1
- package/src/common/performance.ts +44 -0
- package/src/common/random.ts +68 -0
- package/src/common/test/index.ts +2 -0
- package/src/common/test/render.ts +98 -0
- package/src/common/test/tracer.ts +359 -0
- package/src/common/tree.ts +33 -0
- package/src/common/utils.ts +40 -1
- package/src/config.ts +19 -11
- package/src/hotstate/abstractions.ts +48 -0
- package/src/hotstate/factory.ts +17 -0
- package/src/{secret → hotstate}/index.ts +1 -0
- package/src/hotstate/manager.ts +192 -0
- package/src/hotstate/memory.ts +100 -0
- package/src/hotstate/validation.ts +100 -0
- package/src/index.ts +2 -1
- package/src/library/abstractions.ts +24 -28
- package/src/library/factory.ts +2 -2
- package/src/library/local.ts +91 -111
- package/src/library/worker/evaluator.ts +36 -73
- package/src/library/worker/loader.lite.ts +54 -0
- package/src/library/worker/main.ts +15 -66
- package/src/library/worker/protocol.ts +6 -33
- package/src/lock/abstractions.ts +6 -0
- package/src/lock/factory.ts +15 -0
- package/src/lock/index.ts +4 -0
- package/src/lock/manager.ts +97 -0
- package/src/lock/memory.ts +19 -0
- package/src/lock/test.ts +108 -0
- package/src/orchestrator/manager.ts +118 -90
- package/src/orchestrator/operation-workset.ts +181 -93
- package/src/orchestrator/operation.ts +1021 -283
- package/src/project/abstractions.ts +27 -38
- package/src/project/evaluation.ts +248 -0
- package/src/project/factory.ts +1 -1
- package/src/project/index.ts +1 -2
- package/src/project/local.ts +107 -103
- package/src/pubsub/abstractions.ts +13 -0
- package/src/pubsub/factory.ts +19 -0
- package/src/{workspace → pubsub}/index.ts +1 -0
- package/src/pubsub/local.ts +36 -0
- package/src/pubsub/manager.ts +108 -0
- package/src/pubsub/validation.ts +33 -0
- package/src/runner/abstractions.ts +155 -68
- package/src/runner/artifact-env.ts +160 -0
- package/src/runner/factory.ts +20 -5
- package/src/runner/force-abort.ts +117 -0
- package/src/runner/local.ts +292 -372
- package/src/{common → runner}/pulumi.ts +89 -37
- package/src/services.ts +251 -40
- package/src/shared/index.ts +3 -11
- package/src/shared/models/backend/index.ts +3 -0
- package/src/shared/{library.ts → models/backend/library.ts} +4 -4
- package/src/shared/models/backend/project.ts +82 -0
- package/src/shared/models/backend/unlock-method.ts +20 -0
- package/src/shared/models/base.ts +68 -0
- package/src/shared/models/errors.ts +5 -0
- package/src/shared/models/index.ts +4 -0
- package/src/shared/models/project/api-key.ts +65 -0
- package/src/shared/models/project/artifact.ts +83 -0
- package/src/shared/models/project/index.ts +13 -0
- package/src/shared/models/project/lock.ts +91 -0
- package/src/shared/models/project/model.ts +14 -0
- package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
- package/src/shared/models/project/page.ts +57 -0
- package/src/shared/models/project/secret.ts +98 -0
- package/src/shared/models/project/service-account.ts +22 -0
- package/src/shared/models/project/state.ts +449 -0
- package/src/shared/models/project/terminal.ts +98 -0
- package/src/shared/models/project/trigger.ts +56 -0
- package/src/shared/models/project/unlock-method.ts +38 -0
- package/src/shared/models/project/worker.ts +107 -0
- package/src/shared/resolvers/graph-resolver.ts +61 -18
- package/src/shared/resolvers/index.ts +5 -0
- package/src/shared/resolvers/input-hash.ts +53 -15
- package/src/shared/resolvers/input.ts +47 -13
- package/src/shared/resolvers/registry.ts +3 -2
- package/src/shared/resolvers/state.ts +2 -2
- package/src/shared/resolvers/validation.ts +82 -25
- package/src/shared/utils/args.ts +25 -0
- package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
- package/src/shared/utils/hash.ts +6 -0
- package/src/shared/utils/index.ts +4 -0
- package/src/shared/utils/promise-tracker.ts +23 -0
- package/src/state/abstractions.ts +199 -131
- package/src/state/encryption.ts +98 -0
- package/src/state/factory.ts +3 -5
- package/src/state/index.ts +4 -0
- package/src/state/keyring.ts +22 -0
- package/src/state/local/backend.ts +106 -0
- package/src/state/local/collection.ts +361 -0
- package/src/state/local/index.ts +2 -0
- package/src/state/manager.ts +875 -18
- package/src/state/memory/backend.ts +70 -0
- package/src/state/memory/collection.ts +270 -0
- package/src/state/memory/index.ts +2 -0
- package/src/state/repository/index.ts +2 -0
- package/src/state/repository/repository.index.ts +193 -0
- package/src/state/repository/repository.ts +507 -0
- package/src/state/test.ts +457 -0
- package/src/terminal/{shared.ts → abstractions.ts} +3 -3
- package/src/terminal/docker.ts +18 -14
- package/src/terminal/factory.ts +3 -3
- package/src/terminal/index.ts +1 -1
- package/src/terminal/manager.ts +131 -79
- package/src/terminal/run.sh.ts +21 -11
- package/src/unlock/abstractions.ts +49 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +32 -0
- package/src/worker/abstractions.ts +42 -0
- package/src/worker/docker.ts +83 -0
- package/src/worker/factory.ts +20 -0
- package/src/worker/index.ts +3 -0
- package/src/worker/manager.ts +167 -0
- package/dist/chunk-KTGKNSKM.js +0 -979
- package/dist/chunk-KTGKNSKM.js.map +0 -1
- package/dist/chunk-WXDYCRTT.js +0 -234
- package/dist/chunk-WXDYCRTT.js.map +0 -1
- package/src/library/worker/loader.ts +0 -114
- package/src/preferences/shared.ts +0 -1
- package/src/project/lock.ts +0 -39
- package/src/project/manager.ts +0 -433
- package/src/secret/abstractions.ts +0 -59
- package/src/secret/factory.ts +0 -22
- package/src/secret/local.ts +0 -152
- package/src/shared/project.ts +0 -62
- package/src/shared/state.ts +0 -247
- package/src/shared/terminal.ts +0 -14
- package/src/state/local.ts +0 -612
- package/src/workspace/abstractions.ts +0 -41
- package/src/workspace/factory.ts +0 -14
- package/src/workspace/local.ts +0 -54
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import type { LockManager } from "../lock"
|
|
3
|
+
import type { PubSubManager } from "../pubsub"
|
|
4
|
+
import type { StateManager } from "../state"
|
|
5
|
+
import type { InstanceId, ObjectMeta } from "@highstate/contract"
|
|
6
|
+
import { compareInstanceLockSpecs, type InstanceLockSpec } from "../shared"
|
|
7
|
+
|
|
8
|
+
export class InstanceLockService {
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly stateManager: StateManager,
|
|
11
|
+
private readonly lockManager: LockManager,
|
|
12
|
+
private readonly pubsubManager: PubSubManager,
|
|
13
|
+
private readonly logger: Logger,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tries to lock instances in the project.
|
|
18
|
+
* Returns true if the lock was acquired successfully, false otherwise.
|
|
19
|
+
*
|
|
20
|
+
* @param projectId The project ID to lock instances in.
|
|
21
|
+
* @param instanceIds The instance IDs to lock.
|
|
22
|
+
* @param lockMeta The metadata for the lock.
|
|
23
|
+
* @param spec The lock specification containing the lock type and other parameters.
|
|
24
|
+
* @param allowPartialLock Whether to allow partial locking of instances.
|
|
25
|
+
* If true, it will lock as many instances as possible even if some are already locked.
|
|
26
|
+
*
|
|
27
|
+
* @return A tuple containing a boolean indicating if the lock was acquired,
|
|
28
|
+
* and an array of instance IDs that were successfully locked.
|
|
29
|
+
* If `allowPartialLock` is false and some instances are already locked,
|
|
30
|
+
* it will return false and an empty array.
|
|
31
|
+
*/
|
|
32
|
+
async tryLockInstances(
|
|
33
|
+
projectId: string,
|
|
34
|
+
instanceIds: InstanceId[],
|
|
35
|
+
lockMeta: ObjectMeta,
|
|
36
|
+
spec: InstanceLockSpec,
|
|
37
|
+
allowPartialLock = false,
|
|
38
|
+
): Promise<[locked: boolean, lockedInstanceIds: string[]]> {
|
|
39
|
+
const lockKeys = instanceIds.map(id => ["instance-lock", projectId, id] as const)
|
|
40
|
+
|
|
41
|
+
// acquire the hard lock to ensure no other process can manage the lock entries
|
|
42
|
+
return await this.lockManager.acquire(lockKeys, async () => {
|
|
43
|
+
// check if the instances are already locked
|
|
44
|
+
const existingLocks = await this.stateManager
|
|
45
|
+
.getInstanceLockRepository(projectId)
|
|
46
|
+
.getManyItems(instanceIds)
|
|
47
|
+
|
|
48
|
+
// only the locks with another spec are considered as the conflicting locks
|
|
49
|
+
const conflictingLocks = existingLocks.filter(
|
|
50
|
+
lock => !compareInstanceLockSpecs(lock.spec, spec),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (conflictingLocks.length > 0) {
|
|
54
|
+
this.logger.debug(
|
|
55
|
+
{
|
|
56
|
+
projectId,
|
|
57
|
+
requestedInstanceIds: instanceIds,
|
|
58
|
+
lockedInstanceIds: conflictingLocks.map(lock => lock.id),
|
|
59
|
+
},
|
|
60
|
+
`failed to lock %s of %s instances in project "%s"`,
|
|
61
|
+
conflictingLocks.length,
|
|
62
|
+
instanceIds.length,
|
|
63
|
+
projectId,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (!allowPartialLock) {
|
|
67
|
+
// if partial locking is not allowed, return false
|
|
68
|
+
return [false, []]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// if partial locking is allowed, filter out the already locked instances and proceed with the rest
|
|
72
|
+
instanceIds = instanceIds.filter(id => !conflictingLocks.some(lock => lock.id === id))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newLocks = instanceIds.map(id => ({ id, projectId, meta: lockMeta, spec }))
|
|
76
|
+
|
|
77
|
+
// persist the new locks
|
|
78
|
+
await this.stateManager.getInstanceLockRepository(projectId).putManyItems(newLocks)
|
|
79
|
+
|
|
80
|
+
this.logger.debug(
|
|
81
|
+
{
|
|
82
|
+
projectId,
|
|
83
|
+
instanceIds: newLocks.map(lock => lock.id),
|
|
84
|
+
spec,
|
|
85
|
+
},
|
|
86
|
+
`locked %s instances in project "%s"`,
|
|
87
|
+
newLocks.length,
|
|
88
|
+
projectId,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// publish the lock event
|
|
92
|
+
await this.pubsubManager.publish(["instance-lock", projectId], {
|
|
93
|
+
type: "locked",
|
|
94
|
+
locks: newLocks,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return [true, newLocks.map(lock => lock.id)]
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Unlocks instances in the project unconditionally.
|
|
103
|
+
* This will remove the locks regardless of their current state.
|
|
104
|
+
*
|
|
105
|
+
* @param projectId The project ID to unlock instances in.
|
|
106
|
+
* @param instanceIds The instance IDs to unlock.
|
|
107
|
+
*/
|
|
108
|
+
async unlockInstancesUnconditionally(projectId: string, instanceIds: string[]): Promise<void> {
|
|
109
|
+
this.logger.debug(
|
|
110
|
+
{ projectId, instanceIds },
|
|
111
|
+
`unconditionally unlocking %s instances in project "%s"`,
|
|
112
|
+
instanceIds.length,
|
|
113
|
+
projectId,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// remove the locks from the state
|
|
117
|
+
await this.stateManager.getInstanceLockRepository(projectId).deleteMany(instanceIds)
|
|
118
|
+
|
|
119
|
+
// publish the unlock event
|
|
120
|
+
await this.pubsubManager.publish(["instance-lock", projectId], {
|
|
121
|
+
type: "unlocked",
|
|
122
|
+
instanceIds,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import type { HotStateManager } from "../hotstate"
|
|
2
|
+
import type { LockManager } from "../lock"
|
|
3
|
+
import type { PubSubManager } from "../pubsub"
|
|
4
|
+
import type { StateManager } from "../state"
|
|
5
|
+
import type { Logger } from "pino"
|
|
6
|
+
import type { RunnerBackend } from "../runner"
|
|
7
|
+
import type { ArtifactService } from "../artifact"
|
|
8
|
+
import { randomBytes } from "node:crypto"
|
|
9
|
+
import { parseInstanceId } from "@highstate/contract"
|
|
10
|
+
import {
|
|
11
|
+
applyStatePatch,
|
|
12
|
+
isStableOperationStatus,
|
|
13
|
+
isStateEmpty,
|
|
14
|
+
type InstanceCustomStatus,
|
|
15
|
+
type InstanceState,
|
|
16
|
+
type InstanceStatePatch,
|
|
17
|
+
type Operation,
|
|
18
|
+
} from "../shared"
|
|
19
|
+
|
|
20
|
+
export class InstanceStateService {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly stateManager: StateManager,
|
|
23
|
+
private readonly hotStateManager: HotStateManager,
|
|
24
|
+
private readonly pubsubManager: PubSubManager,
|
|
25
|
+
private readonly lockManager: LockManager,
|
|
26
|
+
private readonly runnerBackend: RunnerBackend,
|
|
27
|
+
private readonly artifactManager: ArtifactService,
|
|
28
|
+
private readonly logger: Logger,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gets the current states of all instances in a project.
|
|
33
|
+
*
|
|
34
|
+
* @param projectId The ID of the project for which to retrieve instance states.
|
|
35
|
+
* @param checkLoaded Whether to check if instance states are loaded in the hot state.
|
|
36
|
+
* Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
|
|
37
|
+
*/
|
|
38
|
+
async getInstanceStates(projectId: string, checkLoaded = true): Promise<InstanceState[]> {
|
|
39
|
+
return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
|
|
40
|
+
const entries = await this.hotStateManager.hgetall(["instance-states", projectId])
|
|
41
|
+
|
|
42
|
+
return entries.map(([, value]) => value)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets the state of a specific instance in a project.
|
|
48
|
+
*
|
|
49
|
+
* @param projectId The ID of the project to which the instance belongs.
|
|
50
|
+
* @param instanceId The ID of the instance to retrieve the state for.
|
|
51
|
+
* @param checkLoaded Whether to check if instance states are loaded in the hot state.
|
|
52
|
+
* Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
|
|
53
|
+
*/
|
|
54
|
+
async getInstanceState(
|
|
55
|
+
projectId: string,
|
|
56
|
+
instanceId: string,
|
|
57
|
+
checkLoaded = true,
|
|
58
|
+
): Promise<InstanceState | null> {
|
|
59
|
+
return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
|
|
60
|
+
return await this.hotStateManager.hget(["instance-states", projectId], instanceId)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Updates the states of multiple instances in a project.
|
|
66
|
+
*
|
|
67
|
+
* @param projectId The ID of the project to which the instances belong.
|
|
68
|
+
* @param states The array of instance states to update.
|
|
69
|
+
* @param persistent Whether to persist the changes in the state backend.
|
|
70
|
+
* @param checkLoaded Whether to check if instance states are loaded in the hot state.
|
|
71
|
+
* Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
|
|
72
|
+
*/
|
|
73
|
+
async updateInstanceStates(
|
|
74
|
+
projectId: string,
|
|
75
|
+
states: InstanceState[],
|
|
76
|
+
persistent = false,
|
|
77
|
+
checkLoaded = true,
|
|
78
|
+
): Promise<InstanceState[]> {
|
|
79
|
+
return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
|
|
80
|
+
const batch = this.stateManager.batch()
|
|
81
|
+
const promises: Promise<void>[] = []
|
|
82
|
+
|
|
83
|
+
for (const state of states) {
|
|
84
|
+
promises.push(
|
|
85
|
+
this.hotStateManager.hset(["instance-states", projectId], state.id, state),
|
|
86
|
+
this.pubsubManager.publish(["instance-state", projectId], { type: "updated", state }),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (persistent) {
|
|
90
|
+
promises.push(
|
|
91
|
+
this.stateManager.getInstanceStateRepository(projectId).putItem(state, batch),
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await Promise.all(promises)
|
|
97
|
+
await batch.write()
|
|
98
|
+
|
|
99
|
+
this.logger.debug(
|
|
100
|
+
{ projectId, states },
|
|
101
|
+
`instance states updated for project "%s"`,
|
|
102
|
+
projectId,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return states
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Patches multiple instance states in a project.
|
|
111
|
+
*
|
|
112
|
+
* @param projectId The ID of the project to which the instances belong.
|
|
113
|
+
* @param patches The array of instance state patches to apply.
|
|
114
|
+
* @param checkLoaded Whether to check if instance states are loaded in the hot state.
|
|
115
|
+
* Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
|
|
116
|
+
*/
|
|
117
|
+
async patchInstanceStates(
|
|
118
|
+
projectId: string,
|
|
119
|
+
patches: InstanceStatePatch[],
|
|
120
|
+
checkLoaded = true,
|
|
121
|
+
): Promise<InstanceState[]> {
|
|
122
|
+
return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
|
|
123
|
+
const batch = this.stateManager.batch()
|
|
124
|
+
const promises: Promise<InstanceState>[] = []
|
|
125
|
+
|
|
126
|
+
for (const patch of patches) {
|
|
127
|
+
promises.push(
|
|
128
|
+
this.hotStateManager.hget(["instance-states", projectId], patch.id).then(state => {
|
|
129
|
+
return this.applyStatePatch(projectId, state, patch, true)
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const updatedStates = await Promise.all(promises)
|
|
135
|
+
|
|
136
|
+
await batch.write()
|
|
137
|
+
|
|
138
|
+
this.logger.debug(
|
|
139
|
+
{ projectId, patches },
|
|
140
|
+
`instance states patched for project "%s"`,
|
|
141
|
+
projectId,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return updatedStates
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Patches an instance state in a project.
|
|
150
|
+
* TODO: delete
|
|
151
|
+
*
|
|
152
|
+
* @param projectId The ID of the project to which the instance belongs.
|
|
153
|
+
* @param patch The patch to apply to the instance state.
|
|
154
|
+
* @param checkLoaded Whether to check if instance states are loaded in the hot state.
|
|
155
|
+
* Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
|
|
156
|
+
*/
|
|
157
|
+
async patchInstanceState(
|
|
158
|
+
projectId: string,
|
|
159
|
+
patch: InstanceStatePatch,
|
|
160
|
+
checkLoaded = true,
|
|
161
|
+
): Promise<InstanceState> {
|
|
162
|
+
return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
|
|
163
|
+
return await this.lockManager.acquire(["instance-state", projectId, patch.id], async () => {
|
|
164
|
+
const state = await this.hotStateManager.hget(["instance-states", projectId], patch.id)
|
|
165
|
+
|
|
166
|
+
const patchedState = await this.applyStatePatch(
|
|
167
|
+
projectId,
|
|
168
|
+
state,
|
|
169
|
+
patch,
|
|
170
|
+
patch.operationStatus !== undefined,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
this.logger.debug(
|
|
174
|
+
{ projectId, instanceId: patch.id, patch },
|
|
175
|
+
`instance state updated for instance "%s" in project "%s"`,
|
|
176
|
+
patch.id,
|
|
177
|
+
projectId,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return patchedState
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Patches an instance state within an operation.
|
|
187
|
+
*
|
|
188
|
+
* @param projectId The ID of the project to which the instance belongs.
|
|
189
|
+
* @param operation The operation context for which the instance state is being patched.
|
|
190
|
+
* @param patch The patch to apply to the instance state.
|
|
191
|
+
*/
|
|
192
|
+
async patchOperationInstanceState(
|
|
193
|
+
projectId: string,
|
|
194
|
+
operation: Operation,
|
|
195
|
+
patch: InstanceStatePatch,
|
|
196
|
+
): Promise<InstanceState> {
|
|
197
|
+
return await this.lockManager.acquire(["instance-state", projectId, patch.id], async () => {
|
|
198
|
+
const state = await this.hotStateManager.hget(["instance-states", projectId], patch.id)
|
|
199
|
+
|
|
200
|
+
const patchedState = await this.applyStatePatch(
|
|
201
|
+
projectId,
|
|
202
|
+
state,
|
|
203
|
+
patch,
|
|
204
|
+
operation.type !== "preview",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
this.logger.debug(
|
|
208
|
+
{ projectId, instanceId: patch.id, patch },
|
|
209
|
+
`instance state updated for instance "%s" in project "%s"`,
|
|
210
|
+
patch.id,
|
|
211
|
+
projectId,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return patchedState
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Updates the secret names of an instance state.
|
|
220
|
+
*
|
|
221
|
+
* @param projectId The ID of the project to which the instance belongs.
|
|
222
|
+
* @param instanceId The ID of the instance whose state is to be updated.
|
|
223
|
+
* @param secretNamesToAdd The list of secret names to add to the instance state.
|
|
224
|
+
* @param secretNamesToRemove The list of secret names to remove from the instance state.
|
|
225
|
+
* @param invalidateState Whether to invalidate the state after updating secret names.
|
|
226
|
+
* @param checkLoaded Whether to check if instance states are loaded in the hot state.
|
|
227
|
+
*/
|
|
228
|
+
async updateStateSecretNames(
|
|
229
|
+
projectId: string,
|
|
230
|
+
instanceId: string,
|
|
231
|
+
secretNamesToAdd: string[],
|
|
232
|
+
secretNamesToRemove: string[],
|
|
233
|
+
invalidateState = true,
|
|
234
|
+
checkLoaded = true,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
|
|
237
|
+
await this.lockManager.acquire(["instance-state", projectId, instanceId], async () => {
|
|
238
|
+
const state = await this.hotStateManager.hget(["instance-states", projectId], instanceId)
|
|
239
|
+
|
|
240
|
+
const secretNameSet = new Set(state?.secretNames ?? [])
|
|
241
|
+
for (const name of secretNamesToAdd) {
|
|
242
|
+
secretNameSet.add(name)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const name of secretNamesToRemove) {
|
|
246
|
+
secretNameSet.delete(name)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const patch: InstanceStatePatch = {
|
|
250
|
+
id: instanceId,
|
|
251
|
+
secretNames: Array.from(secretNameSet),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (state?.operationStatus && invalidateState) {
|
|
255
|
+
// invalidate the input hash nonce if the operation status is present
|
|
256
|
+
// if not, the invalidation makes no sense
|
|
257
|
+
patch.operationStatus = {
|
|
258
|
+
inputHashNonce: randomBytes(4).readInt32LE(),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await this.applyStatePatch(projectId, state, patch, true)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Completely deletes the instance state from the system including all related resources.
|
|
269
|
+
*
|
|
270
|
+
* @param projectId The ID of the project from which to delete the instance state.
|
|
271
|
+
* @param instanceId The ID of the instance whose state is to be deleted.
|
|
272
|
+
*/
|
|
273
|
+
async deleteInstanceState(projectId: string, instanceId: string): Promise<void> {
|
|
274
|
+
await this.lockManager.acquire(["instance-state", projectId, instanceId], async () => {
|
|
275
|
+
const [instanceType, instanceName] = parseInstanceId(instanceId)
|
|
276
|
+
const state = await this.getInstanceState(projectId, instanceId)
|
|
277
|
+
|
|
278
|
+
const batch = this.stateManager.batch()
|
|
279
|
+
|
|
280
|
+
await Promise.allSettled([
|
|
281
|
+
// remove from the state
|
|
282
|
+
this.stateManager.getInstanceStateRepository(projectId).delete(instanceId, batch),
|
|
283
|
+
|
|
284
|
+
// clear related resources
|
|
285
|
+
this.stateManager.getInstanceLockRepository(projectId).delete(instanceId, batch),
|
|
286
|
+
|
|
287
|
+
this.stateManager
|
|
288
|
+
.getPageRepository(projectId)
|
|
289
|
+
.deleteMany(Object.values(state?.extra?.pageIds ?? {}), batch),
|
|
290
|
+
|
|
291
|
+
this.stateManager
|
|
292
|
+
.getTriggerRepository(projectId)
|
|
293
|
+
.deleteMany(Object.values(state?.extra?.triggerIds ?? {}), batch),
|
|
294
|
+
|
|
295
|
+
this.stateManager
|
|
296
|
+
.getTerminalRepository(projectId)
|
|
297
|
+
.deleteMany(Object.values(state?.extra?.terminalIds ?? {}), batch),
|
|
298
|
+
])
|
|
299
|
+
|
|
300
|
+
await Promise.allSettled([
|
|
301
|
+
// commit the state batch
|
|
302
|
+
batch.write(),
|
|
303
|
+
|
|
304
|
+
// update hotstate
|
|
305
|
+
this.hotStateManager.hdel(["instance-states", projectId], instanceId),
|
|
306
|
+
|
|
307
|
+
// publish the instance state deletion event
|
|
308
|
+
this.pubsubManager.publish(["instance-state", projectId], { type: "deleted", instanceId }),
|
|
309
|
+
|
|
310
|
+
// remove all artifact references
|
|
311
|
+
this.artifactManager.removeUsages(
|
|
312
|
+
projectId,
|
|
313
|
+
Object.values(state?.extra?.exportedArtifactIds ?? {}).flat(),
|
|
314
|
+
[{ type: "instance", instanceId }],
|
|
315
|
+
),
|
|
316
|
+
|
|
317
|
+
// clear pulumi state
|
|
318
|
+
this.runnerBackend.deleteState({ projectId, instanceName, instanceType }),
|
|
319
|
+
])
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async updateCustomStatus(
|
|
324
|
+
projectId: string,
|
|
325
|
+
instanceId: string,
|
|
326
|
+
status: InstanceCustomStatus,
|
|
327
|
+
): Promise<void> {
|
|
328
|
+
await this.lockManager.acquire(["instance-state", projectId, instanceId], async () => {
|
|
329
|
+
const state = await this.hotStateManager.hget(["instance-states", projectId], instanceId)
|
|
330
|
+
if (!state) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Instance state with ID "${instanceId}" not found in project "${projectId}"`,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const existingStatus = state.extra?.customStatuses?.find(s => s.name === status.name)
|
|
337
|
+
if (existingStatus) {
|
|
338
|
+
Object.assign(existingStatus, status)
|
|
339
|
+
} else {
|
|
340
|
+
state.extra ??= {}
|
|
341
|
+
state.extra.customStatuses ??= []
|
|
342
|
+
state.extra.customStatuses?.push(status)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await this.applyStatePatch(projectId, state, { id: instanceId, extra: state.extra })
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private async withLoadedInstanceStates<T>(
|
|
350
|
+
projectId: string,
|
|
351
|
+
checkLoaded: boolean,
|
|
352
|
+
action: () => Promise<T>,
|
|
353
|
+
): Promise<T> {
|
|
354
|
+
if (!checkLoaded) {
|
|
355
|
+
return await action()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return await this.lockManager.acquire(["project-instance-states", projectId], async () => {
|
|
359
|
+
const instanceStatesLoaded = await this.hotStateManager.get([
|
|
360
|
+
"instance-states-loaded",
|
|
361
|
+
projectId,
|
|
362
|
+
])
|
|
363
|
+
|
|
364
|
+
if (instanceStatesLoaded) {
|
|
365
|
+
this.logger.debug(`instance states already loaded for project "%s"`, projectId)
|
|
366
|
+
return await action()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// load instance states from the storage
|
|
370
|
+
const states = await this.stateManager.getInstanceStateRepository(projectId).getAllItems()
|
|
371
|
+
// store them in the hot state
|
|
372
|
+
await this.hotStateManager.hmset(
|
|
373
|
+
["instance-states", projectId],
|
|
374
|
+
states.map(state => [state.id, state]),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
this.logger.info(
|
|
378
|
+
{ projectId, count: states.length },
|
|
379
|
+
`loaded %d instance states`,
|
|
380
|
+
states.length,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
// mark instance states as loaded
|
|
384
|
+
await this.hotStateManager.set(["instance-states-loaded", projectId], true)
|
|
385
|
+
|
|
386
|
+
return await action()
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async applyStatePatch(
|
|
391
|
+
projectId: string,
|
|
392
|
+
state: InstanceState | null,
|
|
393
|
+
patch: InstanceStatePatch,
|
|
394
|
+
persistent = true,
|
|
395
|
+
): Promise<InstanceState> {
|
|
396
|
+
const patchedState = applyStatePatch(state, patch)
|
|
397
|
+
|
|
398
|
+
const promises: Promise<void>[] = [
|
|
399
|
+
this.hotStateManager.hset(["instance-states", projectId], patchedState.id, patchedState),
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
const shouldPersist =
|
|
403
|
+
persistent &&
|
|
404
|
+
(!patchedState.operationStatus ||
|
|
405
|
+
isStableOperationStatus(patchedState.operationStatus?.status))
|
|
406
|
+
|
|
407
|
+
const repo = this.stateManager.getInstanceStateRepository(projectId)
|
|
408
|
+
|
|
409
|
+
if (isStateEmpty(patchedState)) {
|
|
410
|
+
promises.push(
|
|
411
|
+
this.pubsubManager.publish(["instance-state", projectId], {
|
|
412
|
+
type: "deleted",
|
|
413
|
+
instanceId: patchedState.id,
|
|
414
|
+
}),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if (shouldPersist) {
|
|
418
|
+
promises.push(repo.delete(patchedState.id))
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
promises.push(
|
|
422
|
+
this.pubsubManager.publish(["instance-state", projectId], { type: "patched", patch }),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if (shouldPersist) {
|
|
426
|
+
promises.push(repo.putItem(patchedState))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await Promise.allSettled(promises)
|
|
431
|
+
|
|
432
|
+
return patchedState
|
|
433
|
+
}
|
|
434
|
+
}
|