@highstate/backend 0.16.0 → 0.18.0
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-4JUMOKLV.js → chunk-JT4KWE3B.js} +10 -8
- package/dist/chunk-JT4KWE3B.js.map +1 -0
- package/dist/{chunk-VB4YL327.js → chunk-X2WG3WGL.js} +9 -2
- package/dist/chunk-X2WG3WGL.js.map +1 -0
- package/dist/highstate.manifest.json +4 -5
- package/dist/index.js +256 -287
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +1 -1
- package/dist/shared/index.js +1 -1
- package/package.json +9 -10
- package/prisma/backend/postgresql/main.prisma +0 -2
- package/prisma/backend/sqlite/main.prisma +0 -2
- package/prisma/project/instance.prisma +12 -0
- package/prisma/project/main.prisma +0 -1
- package/prisma/project/migrations/20260123000000_add_instance_state_self_hash/migration.sql +2 -0
- package/src/business/instance-state.test.ts +1 -0
- package/src/business/instance-state.ts +3 -0
- package/src/business/project.ts +1 -2
- package/src/common/utils.ts +9 -0
- package/src/database/_generated/backend/postgresql/browser.ts +54 -0
- package/src/database/_generated/backend/postgresql/client.ts +7 -8
- package/src/database/_generated/backend/postgresql/commonInputTypes.ts +3 -2
- package/src/database/_generated/backend/postgresql/enums.ts +3 -1
- package/src/database/_generated/backend/postgresql/internal/class.ts +24 -71
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +41 -43
- package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +191 -0
- package/src/database/_generated/backend/postgresql/models/BackendUnlockMethod.ts +12 -11
- package/src/database/_generated/backend/postgresql/models/Library.ts +29 -28
- package/src/database/_generated/backend/postgresql/models/Project.ts +69 -68
- package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +29 -28
- package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +26 -25
- package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +29 -28
- package/src/database/_generated/backend/postgresql/models/UserWorkspaceLayout.ts +12 -11
- package/src/database/_generated/backend/postgresql/models.ts +2 -1
- package/src/database/_generated/backend/postgresql/pjtg.ts +1 -0
- package/src/database/_generated/backend/sqlite/browser.ts +54 -0
- package/src/database/_generated/backend/sqlite/client.ts +7 -8
- package/src/database/_generated/backend/sqlite/commonInputTypes.ts +3 -2
- package/src/database/_generated/backend/sqlite/enums.ts +3 -1
- package/src/database/_generated/backend/sqlite/internal/class.ts +24 -71
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +41 -43
- package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +188 -0
- package/src/database/_generated/backend/sqlite/models/BackendUnlockMethod.ts +12 -11
- package/src/database/_generated/backend/sqlite/models/Library.ts +29 -28
- package/src/database/_generated/backend/sqlite/models/Project.ts +69 -68
- package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +29 -28
- package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +26 -25
- package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +29 -28
- package/src/database/_generated/backend/sqlite/models/UserWorkspaceLayout.ts +12 -11
- package/src/database/_generated/backend/sqlite/models.ts +2 -1
- package/src/database/_generated/backend/sqlite/pjtg.ts +1 -0
- package/src/database/_generated/project/browser.ts +1 -0
- package/src/database/_generated/project/client.ts +4 -5
- package/src/database/_generated/project/commonInputTypes.ts +1 -0
- package/src/database/_generated/project/enums.ts +1 -0
- package/src/database/_generated/project/internal/class.ts +21 -63
- package/src/database/_generated/project/internal/prismaNamespace.ts +41 -36
- package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +10 -6
- package/src/database/_generated/project/models/ApiKey.ts +1 -0
- package/src/database/_generated/project/models/Artifact.ts +1 -0
- package/src/database/_generated/project/models/HubModel.ts +1 -0
- package/src/database/_generated/project/models/InstanceCustomStatus.ts +1 -0
- package/src/database/_generated/project/models/InstanceEvaluationState.ts +1 -0
- package/src/database/_generated/project/models/InstanceLock.ts +1 -0
- package/src/database/_generated/project/models/InstanceModel.ts +1 -0
- package/src/database/_generated/project/models/InstanceOperationState.ts +1 -0
- package/src/database/_generated/project/models/InstanceState.ts +108 -1
- package/src/database/_generated/project/models/Operation.ts +1 -0
- package/src/database/_generated/project/models/OperationLog.ts +1 -0
- package/src/database/_generated/project/models/Page.ts +1 -0
- package/src/database/_generated/project/models/Secret.ts +1 -0
- package/src/database/_generated/project/models/ServiceAccount.ts +1 -0
- package/src/database/_generated/project/models/Terminal.ts +1 -0
- package/src/database/_generated/project/models/TerminalSession.ts +1 -0
- package/src/database/_generated/project/models/TerminalSessionLog.ts +1 -0
- package/src/database/_generated/project/models/Trigger.ts +1 -0
- package/src/database/_generated/project/models/UnlockMethod.ts +1 -0
- package/src/database/_generated/project/models/UserCompositeViewport.ts +1 -0
- package/src/database/_generated/project/models/UserProjectViewport.ts +1 -0
- package/src/database/_generated/project/models/Worker.ts +1 -0
- package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1 -0
- package/src/database/_generated/project/models/WorkerVersion.ts +1 -0
- package/src/database/_generated/project/models/WorkerVersionLog.ts +1 -0
- package/src/database/_generated/project/models.ts +1 -0
- package/src/database/abstractions.ts +1 -7
- package/src/database/index.ts +1 -0
- package/src/database/local/backend.ts +19 -30
- package/src/database/local/project.ts +4 -9
- package/src/database/manager.ts +28 -34
- package/src/database/migration.ts +126 -0
- package/src/orchestrator/operation.cancel.test.ts +112 -0
- package/src/orchestrator/operation.composite.test.ts +123 -0
- package/src/orchestrator/operation.destroy.test.ts +77 -0
- package/src/orchestrator/operation.preview.test.ts +95 -0
- package/src/orchestrator/operation.refresh.test.ts +77 -0
- package/src/orchestrator/operation.test-utils.ts +646 -0
- package/src/orchestrator/operation.ts +91 -3
- package/src/orchestrator/operation.update.failure.test.ts +88 -0
- package/src/orchestrator/operation.update.skip.test.ts +95 -0
- package/src/orchestrator/operation.update.test.ts +117 -0
- package/src/orchestrator/plan-test-builder.ts +1 -0
- package/src/runner/abstractions.ts +5 -0
- package/src/runner/local.ts +1 -0
- package/src/shared/resolvers/input-hash.ts +10 -6
- package/src/terminal/manager.ts +0 -3
- package/src/test-utils/database.ts +28 -14
- package/dist/chunk-4JUMOKLV.js.map +0 -1
- package/dist/chunk-VB4YL327.js.map +0 -1
- package/dist/database/local/prisma.config.js +0 -26
- package/dist/database/local/prisma.config.js.map +0 -1
- package/src/database/local/prisma.config.ts +0 -25
- package/src/database/migrate.ts +0 -35
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
parseInstanceId,
|
|
19
19
|
type TriggerInvocation,
|
|
20
20
|
type UnitConfig,
|
|
21
|
+
type UnitInputReference,
|
|
21
22
|
type VersionedName,
|
|
22
23
|
} from "@highstate/contract"
|
|
23
24
|
import { createId } from "@paralleldrive/cuid2"
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
isTransientInstanceOperationStatus,
|
|
29
30
|
type OperationPhase,
|
|
30
31
|
PromiseTracker,
|
|
32
|
+
type ResolvedInstanceInput,
|
|
31
33
|
waitAll,
|
|
32
34
|
} from "../shared"
|
|
33
35
|
import { OperationContext } from "./operation-context"
|
|
@@ -186,7 +188,7 @@ export class RuntimeOperation {
|
|
|
186
188
|
this.workset.markInstanceLocked(stateId)
|
|
187
189
|
}
|
|
188
190
|
},
|
|
189
|
-
true, // allow partial locks to allow independent
|
|
191
|
+
true, // allow partial locks to allow independent branches to run
|
|
190
192
|
this.workset.abortController.signal,
|
|
191
193
|
60_000, // wait up to 60 seconds for unlock events before retrying
|
|
192
194
|
this.unlockToken,
|
|
@@ -390,6 +392,47 @@ export class RuntimeOperation {
|
|
|
390
392
|
throw new AbortError("The operation is failing, aborting current branch (still not failed)")
|
|
391
393
|
}
|
|
392
394
|
|
|
395
|
+
// Runtime short-circuit:
|
|
396
|
+
// If this instance's own config has not changed and none of its dependencies outputs changed,
|
|
397
|
+
// we can skip execution even if the full inputHash changed due to upstream config-only changes.
|
|
398
|
+
//
|
|
399
|
+
// Do not short-circuit when the caller requested side effects that require a real engine run or the instance was explicitly requested.
|
|
400
|
+
if (
|
|
401
|
+
state.status === "deployed" &&
|
|
402
|
+
state.selfHash != null &&
|
|
403
|
+
state.dependencyOutputHash != null &&
|
|
404
|
+
// ignore explicitly requested updates
|
|
405
|
+
!this.operation.requestedInstanceIds.includes(instance.id) &&
|
|
406
|
+
// ignore when side effects are requested
|
|
407
|
+
!this.operation.options.refresh &&
|
|
408
|
+
!this.operation.options.deleteUnreachableResources &&
|
|
409
|
+
!this.operation.options.forceUpdateDependencies
|
|
410
|
+
) {
|
|
411
|
+
const expected = await this.context.getUpToDateInputHashOutput(instance)
|
|
412
|
+
|
|
413
|
+
if (
|
|
414
|
+
expected.selfHash === state.selfHash &&
|
|
415
|
+
expected.dependencyOutputHash === state.dependencyOutputHash
|
|
416
|
+
) {
|
|
417
|
+
logger.info("skipping unit update (short-circuit: no effective changes)")
|
|
418
|
+
|
|
419
|
+
const now = new Date()
|
|
420
|
+
|
|
421
|
+
await this.workset.updateState(instance.id, {
|
|
422
|
+
operationState: {
|
|
423
|
+
status: "skipped",
|
|
424
|
+
startedAt: now,
|
|
425
|
+
finishedAt: now,
|
|
426
|
+
},
|
|
427
|
+
instanceState: {
|
|
428
|
+
inputHash: expected.inputHash,
|
|
429
|
+
},
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
393
436
|
logger.info("updating unit")
|
|
394
437
|
|
|
395
438
|
await this.workset.updateState(instance.id, {
|
|
@@ -421,6 +464,7 @@ export class RuntimeOperation {
|
|
|
421
464
|
instanceName: instance.name,
|
|
422
465
|
config,
|
|
423
466
|
refresh: this.operation.options.refresh,
|
|
467
|
+
deleteUnreachable: this.operation.options.deleteUnreachableResources,
|
|
424
468
|
secrets,
|
|
425
469
|
artifacts,
|
|
426
470
|
signal,
|
|
@@ -503,6 +547,7 @@ export class RuntimeOperation {
|
|
|
503
547
|
instanceName: instance.name,
|
|
504
548
|
config: this.prepareUnitConfig(instance, secrets, invokedTriggers),
|
|
505
549
|
refresh: this.operation.options.refresh,
|
|
550
|
+
deleteUnreachable: this.operation.options.deleteUnreachableResources,
|
|
506
551
|
secrets,
|
|
507
552
|
signal,
|
|
508
553
|
forceSignal,
|
|
@@ -661,7 +706,9 @@ export class RuntimeOperation {
|
|
|
661
706
|
return {
|
|
662
707
|
instanceId: instance.id,
|
|
663
708
|
args: instance.args ?? {},
|
|
664
|
-
inputs: mapValues(resolvedInputs ?? {}, input
|
|
709
|
+
inputs: mapValues(resolvedInputs ?? {}, (input, inputName) =>
|
|
710
|
+
input.map(value => this.getUnitInputRef(inputName, value)),
|
|
711
|
+
),
|
|
665
712
|
invokedTriggers,
|
|
666
713
|
secretNames: Object.keys(secrets),
|
|
667
714
|
stateIdMap: this.context.getInstanceIdToStateIdMap(instance.id),
|
|
@@ -669,6 +716,45 @@ export class RuntimeOperation {
|
|
|
669
716
|
}
|
|
670
717
|
}
|
|
671
718
|
|
|
719
|
+
private getUnitInputRef(inputName: string, input: ResolvedInstanceInput): UnitInputReference {
|
|
720
|
+
const instance = this.context.getInstance(input.input.instanceId)
|
|
721
|
+
const component = this.context.library.components[instance.type]
|
|
722
|
+
|
|
723
|
+
const outputSpec = component.outputs[input.input.output]
|
|
724
|
+
if (!outputSpec) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`Output "${input.input.output}" is not defined on component "${instance.type}"`,
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const entity = this.context.library.entities[outputSpec.type]
|
|
731
|
+
if (!entity) {
|
|
732
|
+
throw new Error(`Entity type "${outputSpec.type}" is not defined in the library`)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// if output type matches input type or extends it, return simple reference, no transformation needed
|
|
736
|
+
if (input.type === outputSpec.type || entity.extensions?.includes(input.type)) {
|
|
737
|
+
return {
|
|
738
|
+
instanceId: input.input.instanceId,
|
|
739
|
+
output: input.input.output,
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// otherwise, find matching inclusion to perform transformation
|
|
744
|
+
const inclusion = entity.inclusions?.find(inc => inc.type === input.type)
|
|
745
|
+
if (!inclusion) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`Cannot use output "${input.input.output}" of type "${outputSpec.type}" from instance "${input.input.instanceId}" for input "${inputName}" of type "${input.type}": no matching inclusion found in entity "${entity.type}"`,
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
instanceId: input.input.instanceId,
|
|
753
|
+
output: input.input.output,
|
|
754
|
+
inclusion,
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
672
758
|
private async handleUnitStateUpdate(
|
|
673
759
|
update: UnitStateUpdate,
|
|
674
760
|
state: InstanceState,
|
|
@@ -753,9 +839,10 @@ export class RuntimeOperation {
|
|
|
753
839
|
state.outputHash = update.outputHash ?? null
|
|
754
840
|
|
|
755
841
|
// recalculate the input and output hashes for the instance
|
|
756
|
-
const { inputHash, dependencyOutputHash } =
|
|
842
|
+
const { selfHash, inputHash, dependencyOutputHash } =
|
|
757
843
|
await this.context.getUpToDateInputHashOutput(instance)
|
|
758
844
|
|
|
845
|
+
data.selfHash = selfHash
|
|
759
846
|
data.inputHash = inputHash
|
|
760
847
|
data.dependencyOutputHash = dependencyOutputHash
|
|
761
848
|
data.outputHash = update.outputHash
|
|
@@ -770,6 +857,7 @@ export class RuntimeOperation {
|
|
|
770
857
|
data.parentId = null
|
|
771
858
|
}
|
|
772
859
|
} else {
|
|
860
|
+
data.selfHash = null
|
|
773
861
|
data.inputHash = null
|
|
774
862
|
data.dependencyOutputHash = null
|
|
775
863
|
data.outputHash = null
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe } from "vitest"
|
|
2
|
+
import { RuntimeOperation } from "./operation"
|
|
3
|
+
import { operationTest } from "./operation.test-utils"
|
|
4
|
+
|
|
5
|
+
describe("Operation - Update Failure", () => {
|
|
6
|
+
operationTest(
|
|
7
|
+
"marks operation failed when runner emits error update",
|
|
8
|
+
async ({
|
|
9
|
+
project,
|
|
10
|
+
logger,
|
|
11
|
+
runnerBackend,
|
|
12
|
+
runner,
|
|
13
|
+
libraryBackend,
|
|
14
|
+
artifactService,
|
|
15
|
+
instanceLockService,
|
|
16
|
+
operationService,
|
|
17
|
+
secretService,
|
|
18
|
+
instanceStateService,
|
|
19
|
+
projectModelService,
|
|
20
|
+
unitExtraService,
|
|
21
|
+
createUnit,
|
|
22
|
+
createDeployedUnitState,
|
|
23
|
+
createOperation,
|
|
24
|
+
createContext,
|
|
25
|
+
setupPersistenceMocks,
|
|
26
|
+
setupImmediateLocking,
|
|
27
|
+
expect,
|
|
28
|
+
}) => {
|
|
29
|
+
// arrange
|
|
30
|
+
const unit = createUnit("A")
|
|
31
|
+
const state = createDeployedUnitState(unit)
|
|
32
|
+
|
|
33
|
+
await createContext({ instances: [unit], states: [state] })
|
|
34
|
+
setupImmediateLocking()
|
|
35
|
+
setupPersistenceMocks({ instances: [unit] })
|
|
36
|
+
|
|
37
|
+
runner.setAutoCompletion(false)
|
|
38
|
+
runner.emitError(unit.id, "boom")
|
|
39
|
+
|
|
40
|
+
const operation = createOperation({
|
|
41
|
+
type: "update",
|
|
42
|
+
requestedInstanceIds: [unit.id],
|
|
43
|
+
phases: [
|
|
44
|
+
{
|
|
45
|
+
type: "update",
|
|
46
|
+
instances: [{ id: unit.id, message: "requested", parentId: undefined }],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const runtimeOperation = new RuntimeOperation(
|
|
52
|
+
project,
|
|
53
|
+
operation,
|
|
54
|
+
runnerBackend,
|
|
55
|
+
libraryBackend,
|
|
56
|
+
artifactService,
|
|
57
|
+
instanceLockService,
|
|
58
|
+
operationService,
|
|
59
|
+
secretService,
|
|
60
|
+
instanceStateService,
|
|
61
|
+
projectModelService,
|
|
62
|
+
unitExtraService,
|
|
63
|
+
logger,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// act
|
|
67
|
+
await runtimeOperation.operateSafe()
|
|
68
|
+
|
|
69
|
+
// assert
|
|
70
|
+
expect(runnerBackend.update).toHaveBeenCalledTimes(1)
|
|
71
|
+
|
|
72
|
+
const updateOperationCalls = operationService.updateOperation.mock.calls
|
|
73
|
+
const wasMarkedFailed = updateOperationCalls.some(call => {
|
|
74
|
+
const patch = call[2] as unknown
|
|
75
|
+
if (!patch || typeof patch !== "object") {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const maybePatch = patch as { status?: unknown }
|
|
80
|
+
return maybePatch.status === "failed"
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(wasMarkedFailed).toBe(true)
|
|
84
|
+
expect(operationService.markOperationFinished).not.toHaveBeenCalled()
|
|
85
|
+
expect(operationService.appendLog).toHaveBeenCalled()
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe } from "vitest"
|
|
2
|
+
import { RuntimeOperation } from "./operation"
|
|
3
|
+
import { operationTest } from "./operation.test-utils"
|
|
4
|
+
|
|
5
|
+
describe("RuntimeOperation - Update Short-Circuit", () => {
|
|
6
|
+
operationTest(
|
|
7
|
+
"skips unit update when selfHash and dependencyOutputHash match",
|
|
8
|
+
async ({
|
|
9
|
+
project,
|
|
10
|
+
logger,
|
|
11
|
+
runnerBackend,
|
|
12
|
+
libraryBackend,
|
|
13
|
+
artifactService,
|
|
14
|
+
instanceLockService,
|
|
15
|
+
operationService,
|
|
16
|
+
secretService,
|
|
17
|
+
instanceStateService,
|
|
18
|
+
projectModelService,
|
|
19
|
+
unitExtraService,
|
|
20
|
+
createUnit,
|
|
21
|
+
createDeployedUnitState,
|
|
22
|
+
createContext,
|
|
23
|
+
createOperation,
|
|
24
|
+
setupImmediateLocking,
|
|
25
|
+
setupPersistenceMocks,
|
|
26
|
+
expect,
|
|
27
|
+
}) => {
|
|
28
|
+
// arrange
|
|
29
|
+
const unit = createUnit("A")
|
|
30
|
+
const state = createDeployedUnitState(unit)
|
|
31
|
+
|
|
32
|
+
const context = await createContext({ instances: [unit], states: [state] })
|
|
33
|
+
|
|
34
|
+
const expected = await context.getUpToDateInputHashOutput(unit)
|
|
35
|
+
state.selfHash = expected.selfHash
|
|
36
|
+
state.dependencyOutputHash = expected.dependencyOutputHash
|
|
37
|
+
instanceStateService.getInstanceStates.mockResolvedValue([state])
|
|
38
|
+
|
|
39
|
+
setupImmediateLocking()
|
|
40
|
+
setupPersistenceMocks({ instances: [unit] })
|
|
41
|
+
|
|
42
|
+
const operation = createOperation({
|
|
43
|
+
type: "update",
|
|
44
|
+
requestedInstanceIds: [],
|
|
45
|
+
phases: [
|
|
46
|
+
{
|
|
47
|
+
type: "update",
|
|
48
|
+
instances: [{ id: unit.id, message: "explicitly requested", parentId: undefined }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const runtimeOperation = new RuntimeOperation(
|
|
54
|
+
project,
|
|
55
|
+
operation,
|
|
56
|
+
runnerBackend,
|
|
57
|
+
libraryBackend,
|
|
58
|
+
artifactService,
|
|
59
|
+
instanceLockService,
|
|
60
|
+
operationService,
|
|
61
|
+
secretService,
|
|
62
|
+
instanceStateService,
|
|
63
|
+
projectModelService,
|
|
64
|
+
unitExtraService,
|
|
65
|
+
logger,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// act
|
|
69
|
+
await runtimeOperation.operateSafe()
|
|
70
|
+
|
|
71
|
+
// assert
|
|
72
|
+
expect(runnerBackend.update).not.toHaveBeenCalled()
|
|
73
|
+
|
|
74
|
+
const skipCall = instanceStateService.updateOperationState.mock.calls.find(
|
|
75
|
+
(_call): boolean => {
|
|
76
|
+
const options = _call[3]
|
|
77
|
+
return options.operationState?.status === "skipped"
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(skipCall).toBeDefined()
|
|
82
|
+
|
|
83
|
+
const skipOptions = skipCall?.[3]
|
|
84
|
+
expect(skipOptions?.instanceState).toEqual({
|
|
85
|
+
inputHash: expected.inputHash,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(operationService.markOperationFinished).toHaveBeenCalledWith(
|
|
89
|
+
project.id,
|
|
90
|
+
operation.id,
|
|
91
|
+
"completed",
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { InstanceModel } from "@highstate/contract"
|
|
2
|
+
import { describe } from "vitest"
|
|
3
|
+
import { RuntimeOperation } from "./operation"
|
|
4
|
+
import { createDeferred, operationTest } from "./operation.test-utils"
|
|
5
|
+
|
|
6
|
+
describe("Operation - Update", () => {
|
|
7
|
+
operationTest(
|
|
8
|
+
"waits for dependency update to complete before running dependent",
|
|
9
|
+
async ({
|
|
10
|
+
project,
|
|
11
|
+
logger,
|
|
12
|
+
runnerBackend,
|
|
13
|
+
runner,
|
|
14
|
+
libraryBackend,
|
|
15
|
+
artifactService,
|
|
16
|
+
instanceLockService,
|
|
17
|
+
operationService,
|
|
18
|
+
secretService,
|
|
19
|
+
instanceStateService,
|
|
20
|
+
projectModelService,
|
|
21
|
+
unitExtraService,
|
|
22
|
+
createUnit,
|
|
23
|
+
createDeployedUnitState,
|
|
24
|
+
createOperation,
|
|
25
|
+
createContext,
|
|
26
|
+
setupPersistenceMocks,
|
|
27
|
+
setupImmediateLocking,
|
|
28
|
+
expect,
|
|
29
|
+
}) => {
|
|
30
|
+
// arrange
|
|
31
|
+
const unitA = createUnit("A")
|
|
32
|
+
const unitB: InstanceModel = {
|
|
33
|
+
...createUnit("B"),
|
|
34
|
+
inputs: {
|
|
35
|
+
dependency: [{ instanceId: unitA.id, output: "value" }],
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const stateA = createDeployedUnitState(unitA)
|
|
40
|
+
const stateB = createDeployedUnitState(unitB)
|
|
41
|
+
|
|
42
|
+
await createContext({ instances: [unitA, unitB], states: [stateA, stateB] })
|
|
43
|
+
|
|
44
|
+
setupImmediateLocking()
|
|
45
|
+
setupPersistenceMocks({ instances: [unitA, unitB] })
|
|
46
|
+
|
|
47
|
+
const aStarted = createDeferred<void>()
|
|
48
|
+
const aDone = createDeferred<void>()
|
|
49
|
+
let canRunB = false
|
|
50
|
+
|
|
51
|
+
runner.setUpdateImpl(async input => {
|
|
52
|
+
if (input.instanceName === "A") {
|
|
53
|
+
aStarted.resolve(undefined)
|
|
54
|
+
await aDone.promise
|
|
55
|
+
canRunB = true
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (input.instanceName === "B") {
|
|
60
|
+
expect(canRunB).toBe(true)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error(`unexpected unit: ${input.instanceName}`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const operation = createOperation({
|
|
68
|
+
type: "update",
|
|
69
|
+
requestedInstanceIds: [unitB.id],
|
|
70
|
+
phases: [
|
|
71
|
+
{
|
|
72
|
+
type: "update",
|
|
73
|
+
instances: [
|
|
74
|
+
{ id: unitA.id, message: "dependency", parentId: undefined },
|
|
75
|
+
{ id: unitB.id, message: "requested", parentId: undefined },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const runtimeOperation = new RuntimeOperation(
|
|
82
|
+
project,
|
|
83
|
+
operation,
|
|
84
|
+
runnerBackend,
|
|
85
|
+
libraryBackend,
|
|
86
|
+
artifactService,
|
|
87
|
+
instanceLockService,
|
|
88
|
+
operationService,
|
|
89
|
+
secretService,
|
|
90
|
+
instanceStateService,
|
|
91
|
+
projectModelService,
|
|
92
|
+
unitExtraService,
|
|
93
|
+
logger,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// act
|
|
97
|
+
const operationPromise = runtimeOperation.operateSafe()
|
|
98
|
+
|
|
99
|
+
// assert
|
|
100
|
+
await aStarted.promise
|
|
101
|
+
expect(runnerBackend.update).toHaveBeenCalledTimes(1)
|
|
102
|
+
expect(runnerBackend.update.mock.calls[0]?.[0].instanceName).toBe("A")
|
|
103
|
+
|
|
104
|
+
// act
|
|
105
|
+
aDone.resolve(undefined)
|
|
106
|
+
await operationPromise
|
|
107
|
+
|
|
108
|
+
// assert
|
|
109
|
+
expect(runnerBackend.update).toHaveBeenCalledTimes(2)
|
|
110
|
+
expect(operationService.markOperationFinished).toHaveBeenCalledWith(
|
|
111
|
+
project.id,
|
|
112
|
+
operation.id,
|
|
113
|
+
"completed",
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
})
|
|
@@ -171,6 +171,11 @@ export type UnitUpdateOptions = UnitOptions & {
|
|
|
171
171
|
* Whether to refresh the state before updating.
|
|
172
172
|
*/
|
|
173
173
|
refresh?: boolean
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Whether to delete the unreachable resources (e.g. k8s resources in unreachable clusters).
|
|
177
|
+
*/
|
|
178
|
+
deleteUnreachable?: boolean
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
export type UnitDestroyOptions = UnitOptions & {
|
package/src/runner/local.ts
CHANGED
|
@@ -129,6 +129,7 @@ export class LocalRunnerBackend implements RunnerBackend {
|
|
|
129
129
|
const envVars: Record<string, string> = {
|
|
130
130
|
HIGHSTATE_CACHE_DIR: this.cacheDir,
|
|
131
131
|
HIGHSTATE_TEMP_PATH: unitTempPath,
|
|
132
|
+
PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : "",
|
|
132
133
|
...options.envVars,
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -16,6 +16,7 @@ export type InputHashNode = {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export type InputHashOutput = {
|
|
19
|
+
selfHash: number
|
|
19
20
|
inputHash: number
|
|
20
21
|
dependencyOutputHash: number
|
|
21
22
|
outputHash: number
|
|
@@ -44,27 +45,27 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
|
|
|
44
45
|
sourceHash,
|
|
45
46
|
state,
|
|
46
47
|
}: InputHashNode): InputHashOutput {
|
|
47
|
-
const
|
|
48
|
+
const selfHashSink: Uint8Array[] = []
|
|
48
49
|
|
|
49
50
|
// 0. include the instance id to reflect renames
|
|
50
|
-
|
|
51
|
+
selfHashSink.push(Buffer.from(instance.id))
|
|
51
52
|
|
|
52
53
|
// 1. include the component definition hash
|
|
53
|
-
|
|
54
|
+
selfHashSink.push(int32ToBytes(component.definitionHash))
|
|
54
55
|
|
|
55
56
|
// 2. include the input hash nonce if available
|
|
56
57
|
if (state?.inputHashNonce) {
|
|
57
|
-
|
|
58
|
+
selfHashSink.push(int32ToBytes(state.inputHashNonce))
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// 3. include instance args encoded as msgpack
|
|
61
62
|
if (instance.args) {
|
|
62
|
-
|
|
63
|
+
selfHashSink.push(encode(instance.args))
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// 4. include the source hash if available
|
|
66
67
|
if (sourceHash) {
|
|
67
|
-
|
|
68
|
+
selfHashSink.push(int32ToBytes(sourceHash))
|
|
68
69
|
} else if (isUnitModel(component)) {
|
|
69
70
|
this.logger.warn(
|
|
70
71
|
{ instanceId: instance.id },
|
|
@@ -72,6 +73,8 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
|
|
|
72
73
|
)
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
const inputHashSink = [...selfHashSink]
|
|
77
|
+
|
|
75
78
|
const sortedInputs = Object.entries(resolvedInputs)
|
|
76
79
|
//
|
|
77
80
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
@@ -126,6 +129,7 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
return {
|
|
132
|
+
selfHash: crc32(Buffer.concat(selfHashSink)),
|
|
129
133
|
inputHash: crc32(Buffer.concat(inputHashSink)),
|
|
130
134
|
dependencyOutputHash: crc32(Buffer.concat(dependencyOutputHashSink)),
|
|
131
135
|
outputHash: state?.outputHash ?? 0,
|
package/src/terminal/manager.ts
CHANGED
|
@@ -106,12 +106,9 @@ export class TerminalManager {
|
|
|
106
106
|
throw new Error(`Terminal "${terminalId}" not found`)
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const sessionId = uuidv7()
|
|
110
|
-
|
|
111
109
|
// create the terminal session in database
|
|
112
110
|
const session = await database.terminalSession.create({
|
|
113
111
|
data: {
|
|
114
|
-
id: sessionId,
|
|
115
112
|
terminalId,
|
|
116
113
|
},
|
|
117
114
|
})
|
|
@@ -4,12 +4,17 @@ import { constants } from "node:fs"
|
|
|
4
4
|
import { access, mkdtemp, rm } from "node:fs/promises"
|
|
5
5
|
import { hostname, tmpdir } from "node:os"
|
|
6
6
|
import { join } from "node:path"
|
|
7
|
-
import {
|
|
7
|
+
import { PrismaLibSql } from "@prisma/adapter-libsql"
|
|
8
8
|
import { generateIdentity, identityToRecipient } from "age-encryption"
|
|
9
|
-
import {
|
|
9
|
+
import { createProjectLogger } from "../common"
|
|
10
|
+
import {
|
|
11
|
+
type DatabaseManager,
|
|
12
|
+
ensureWellKnownEntitiesCreated,
|
|
13
|
+
migrateDatabase,
|
|
14
|
+
migrationPacks,
|
|
15
|
+
} from "../database"
|
|
10
16
|
import { PrismaClient as BackendPrismaClient } from "../database/_generated/backend/sqlite/client"
|
|
11
17
|
import { getInitialBackendUnlockMethodMeta } from "../database/local/backend"
|
|
12
|
-
import { migrateDatabase } from "../database/migrate"
|
|
13
18
|
import {
|
|
14
19
|
type BackendDatabase as BackendDatabaseClient,
|
|
15
20
|
ProjectDatabase as ProjectDatabaseClient,
|
|
@@ -58,12 +63,15 @@ export class TestDatabaseManager implements DatabaseManager {
|
|
|
58
63
|
async _forProject(projectId: string): Promise<ProjectDatabase> {
|
|
59
64
|
const tempPath = await this.createTempPath()
|
|
60
65
|
const projectUrl = `file:${join(tempPath, `${projectId}.db`)}`
|
|
66
|
+
const logger = createProjectLogger(this.logger, projectId)
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return new ProjectDatabaseClient({
|
|
65
|
-
adapter: new PrismaLibSQL({ url: projectUrl }),
|
|
68
|
+
const client = new ProjectDatabaseClient({
|
|
69
|
+
adapter: new PrismaLibSql({ url: projectUrl }),
|
|
66
70
|
})
|
|
71
|
+
|
|
72
|
+
await migrateDatabase(client, migrationPacks.project, 0, () => Promise.resolve(), logger)
|
|
73
|
+
|
|
74
|
+
return client
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
static async create(
|
|
@@ -75,20 +83,26 @@ export class TestDatabaseManager implements DatabaseManager {
|
|
|
75
83
|
const tempPath = await mkdtemp(join(tempRoot, "highstate"))
|
|
76
84
|
const backendUrl = `file:${join(tempPath, "backend.db")}`
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const backend = new BackendPrismaClient({
|
|
81
|
-
adapter: new PrismaLibSQL({ url: backendUrl }),
|
|
86
|
+
const backendClient = new BackendPrismaClient({
|
|
87
|
+
adapter: new PrismaLibSql({ url: backendUrl }),
|
|
82
88
|
}) as BackendDatabaseClient
|
|
83
89
|
|
|
84
|
-
await
|
|
90
|
+
await migrateDatabase(
|
|
91
|
+
backendClient,
|
|
92
|
+
migrationPacks["backend/sqlite"],
|
|
93
|
+
0,
|
|
94
|
+
() => Promise.resolve(),
|
|
95
|
+
logger,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
await ensureWellKnownEntitiesCreated(backendClient)
|
|
85
99
|
|
|
86
100
|
if (isEncryptionEnabled) {
|
|
87
101
|
const identity = await generateIdentity()
|
|
88
102
|
const recipient = await identityToRecipient(identity)
|
|
89
103
|
const meta = getInitialBackendUnlockMethodMeta(hostname())
|
|
90
104
|
|
|
91
|
-
await
|
|
105
|
+
await backendClient.backendUnlockMethod.create({
|
|
92
106
|
data: {
|
|
93
107
|
recipient,
|
|
94
108
|
meta,
|
|
@@ -96,7 +110,7 @@ export class TestDatabaseManager implements DatabaseManager {
|
|
|
96
110
|
})
|
|
97
111
|
}
|
|
98
112
|
|
|
99
|
-
return new TestDatabaseManager(
|
|
113
|
+
return new TestDatabaseManager(backendClient, logger, isEncryptionEnabled)
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
private async createTempPath(): Promise<string> {
|