@serfab/cadre-host 1.13.0 → 1.15.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/README.md +347 -423
- package/dist/bin/host.d.ts +6 -3
- package/dist/bin/host.js +332 -524
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/join-output.d.ts +56 -0
- package/dist/bin/join-output.js +92 -0
- package/dist/bin/join-output.js.map +1 -0
- package/dist/bin/nat-output.d.ts +54 -0
- package/dist/bin/nat-output.js +138 -0
- package/dist/bin/nat-output.js.map +1 -0
- package/dist/bin/ui-access.d.ts +22 -0
- package/dist/bin/ui-access.js +44 -0
- package/dist/bin/ui-access.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +66 -0
- package/dist/hosted/claim-details.js +60 -0
- package/dist/hosted/claim-details.js.map +1 -0
- package/dist/hosted/hosted-node-service.d.ts +325 -0
- package/dist/hosted/hosted-node-service.js +678 -0
- package/dist/hosted/hosted-node-service.js.map +1 -0
- package/dist/hosted/hosted-node-store.d.ts +40 -0
- package/dist/hosted/hosted-node-store.js +126 -0
- package/dist/hosted/hosted-node-store.js.map +1 -0
- package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
- package/dist/hosted/hosted-node-supervisor.js +396 -0
- package/dist/hosted/hosted-node-supervisor.js.map +1 -0
- package/dist/hosted/hosted-node-watcher.d.ts +95 -0
- package/dist/hosted/hosted-node-watcher.js +260 -0
- package/dist/hosted/hosted-node-watcher.js.map +1 -0
- package/dist/hosted/index.d.ts +16 -0
- package/dist/hosted/index.js +13 -0
- package/dist/hosted/index.js.map +1 -0
- package/dist/hosted/node-status.d.ts +51 -0
- package/dist/hosted/node-status.js +77 -0
- package/dist/hosted/node-status.js.map +1 -0
- package/dist/hosted/types.d.ts +141 -0
- package/dist/hosted/types.js +22 -0
- package/dist/hosted/types.js.map +1 -0
- package/dist/index.d.ts +12 -24
- package/dist/index.js +9 -19
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +27 -34
- package/dist/installer/config.js +46 -15
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +9 -19
- package/dist/installer/index.js +21 -85
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/paths.d.ts +0 -2
- package/dist/installer/paths.js +0 -4
- package/dist/installer/paths.js.map +1 -1
- package/dist/installer/wizard.d.ts +12 -9
- package/dist/installer/wizard.js +15 -14
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/address-resolver.d.ts +18 -17
- package/dist/nat/address-resolver.js +79 -25
- package/dist/nat/address-resolver.js.map +1 -1
- package/dist/nat/address-watch.d.ts +37 -0
- package/dist/nat/address-watch.js +84 -0
- package/dist/nat/address-watch.js.map +1 -0
- package/dist/nat/ddns/duckdns.d.ts +2 -1
- package/dist/nat/ddns/duckdns.js +5 -1
- package/dist/nat/ddns/duckdns.js.map +1 -1
- package/dist/nat/ddns/index.d.ts +6 -0
- package/dist/nat/ddns/index.js.map +1 -1
- package/dist/nat/index.d.ts +13 -9
- package/dist/nat/index.js +7 -5
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +220 -77
- package/dist/nat/nat-service.js +653 -239
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.d.ts +12 -5
- package/dist/nat/nat-store.js +124 -29
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/network-mode.d.ts +19 -0
- package/dist/nat/network-mode.js +32 -0
- package/dist/nat/network-mode.js.map +1 -0
- package/dist/nat/port-mapper.d.ts +83 -79
- package/dist/nat/port-mapper.js +148 -189
- package/dist/nat/port-mapper.js.map +1 -1
- package/dist/nat/reachability.d.ts +32 -15
- package/dist/nat/reachability.js +110 -22
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +98 -27
- package/dist/nat/types.js +8 -5
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +32 -74
- package/dist/orchestrator/host-process-orchestrator.js +84 -233
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
- package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
- package/dist/orchestrator/identity-file.js.map +1 -0
- package/dist/orchestrator/index.d.ts +2 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +6 -6
- package/dist/orchestrator/node-identity.js +7 -7
- package/dist/orchestrator/node-identity.js.map +1 -1
- package/dist/orchestrator/port-allocator.d.ts +5 -6
- package/dist/orchestrator/port-allocator.js +9 -11
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +3 -5
- package/dist/orchestrator/state-store.js +2 -10
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +33 -22
- package/dist/orchestrator/types.js.map +1 -1
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +4 -0
- package/dist/package-version.js.map +1 -0
- package/dist/server/error-handler.d.ts +5 -25
- package/dist/server/error-handler.js +11 -74
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +7 -11
- package/dist/server/events/types.js +2 -1
- package/dist/server/events/types.js.map +1 -1
- package/dist/server/index.d.ts +11 -37
- package/dist/server/index.js +30 -48
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/hosted-nodes.d.ts +28 -0
- package/dist/server/routes/hosted-nodes.js +68 -0
- package/dist/server/routes/hosted-nodes.js.map +1 -0
- package/dist/server/routes/nat.d.ts +4 -1
- package/dist/server/routes/nat.js +13 -2
- package/dist/server/routes/nat.js.map +1 -1
- package/dist/server/routes/nodes.d.ts +6 -20
- package/dist/server/routes/nodes.js +7 -86
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +3 -7
- package/dist/server/routes/settings.js +18 -8
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +2 -25
- package/dist/server/routes/status.js +3 -43
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/server.js +8 -8
- package/dist/server/settings-store.d.ts +2 -2
- package/dist/server/settings-store.js +2 -2
- package/dist/server/static.js +1 -2
- package/dist/server/static.js.map +1 -1
- package/dist/ui/assets/index-BHd8CWZA.css +1 -0
- package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
- package/dist/ui/assets/index-Ccr2GjPT.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/store.d.ts +1 -1
- package/dist/update/store.js +1 -1
- package/dist/update/types.d.ts +1 -1
- package/dist/update/types.js +1 -1
- package/package.json +5 -7
- package/src/__tests__/cli-nat.smoke.test.ts +219 -212
- package/src/__tests__/cli.smoke.test.ts +2 -2
- package/src/__tests__/global-setup.ts +2 -3
- package/src/__tests__/orchestrator-claim-secret.test.ts +111 -0
- package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
- package/src/__tests__/orchestrator-handle-contract.ts +134 -134
- package/src/__tests__/orchestrator-node-identity.test.ts +177 -203
- package/src/__tests__/orchestrator-ports.test.ts +124 -143
- package/src/__tests__/orchestrator-push.test.ts +138 -146
- package/src/__tests__/orchestrator.test.ts +1017 -1063
- package/src/__tests__/ui-access.test.ts +30 -0
- package/src/bin/host.ts +397 -572
- package/src/bin/join-output.ts +120 -0
- package/src/bin/nat-output.ts +188 -0
- package/src/bin/ui-access.ts +50 -0
- package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +172 -173
- package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +43 -23
- package/src/hosted/__tests__/hosted-node-service.test.ts +708 -0
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -0
- package/src/hosted/__tests__/test-invitation.ts +33 -0
- package/src/hosted/claim-details.ts +103 -0
- package/src/hosted/hosted-node-service.ts +814 -0
- package/src/hosted/hosted-node-store.ts +144 -0
- package/src/hosted/hosted-node-supervisor.ts +439 -0
- package/src/hosted/hosted-node-watcher.ts +288 -0
- package/src/hosted/index.ts +53 -0
- package/src/hosted/node-status.ts +97 -0
- package/src/hosted/types.ts +160 -0
- package/src/index.ts +72 -91
- package/src/installer/__tests__/config.test.ts +15 -40
- package/src/installer/__tests__/installer.smoke.test.ts +184 -263
- package/src/installer/__tests__/wizard.test.ts +25 -13
- package/src/installer/config.ts +201 -175
- package/src/installer/index.ts +252 -327
- package/src/installer/paths.ts +104 -109
- package/src/installer/wizard.ts +24 -26
- package/src/nat/__tests__/address-resolver.test.ts +107 -82
- package/src/nat/__tests__/nat-service.test.ts +608 -515
- package/src/nat/__tests__/nat-store.test.ts +49 -12
- package/src/nat/__tests__/network-mode.test.ts +38 -0
- package/src/nat/__tests__/port-mapper.test.ts +30 -191
- package/src/nat/__tests__/reachability.test.ts +96 -13
- package/src/nat/address-resolver.ts +94 -52
- package/src/nat/address-watch.ts +92 -0
- package/src/nat/ddns/duckdns.ts +5 -1
- package/src/nat/ddns/index.ts +6 -0
- package/src/nat/index.ts +33 -13
- package/src/nat/nat-service.ts +942 -501
- package/src/nat/nat-store.ts +126 -31
- package/src/nat/network-mode.ts +36 -0
- package/src/nat/port-mapper.ts +241 -329
- package/src/nat/reachability.ts +155 -34
- package/src/nat/types.ts +110 -37
- package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +1089 -1260
- package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
- package/src/orchestrator/index.ts +9 -10
- package/src/orchestrator/node-identity.ts +41 -41
- package/src/orchestrator/port-allocator.ts +71 -73
- package/src/orchestrator/state-store.ts +76 -90
- package/src/orchestrator/types.ts +34 -23
- package/src/package-version.ts +4 -0
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +81 -118
- package/src/server/__tests__/fakes.ts +15 -18
- package/src/server/__tests__/hosted-nodes-route.test.ts +161 -0
- package/src/server/__tests__/nodes-route.test.ts +114 -283
- package/src/server/__tests__/publishers.test.ts +193 -180
- package/src/server/__tests__/server.smoke.test.ts +134 -223
- package/src/server/__tests__/settings-route.test.ts +210 -195
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/status-route.test.ts +82 -121
- package/src/server/error-handler.ts +112 -178
- package/src/server/events/types.ts +20 -24
- package/src/server/index.ts +223 -267
- package/src/server/routes/hosted-nodes.ts +83 -0
- package/src/server/routes/nat.ts +75 -63
- package/src/server/routes/nodes.ts +96 -188
- package/src/server/routes/settings.ts +20 -15
- package/src/server/routes/status.ts +5 -67
- package/src/server/server.ts +8 -8
- package/src/server/settings-store.ts +2 -2
- package/src/server/static.ts +1 -2
- package/src/update/index.ts +2 -2
- package/src/update/store.ts +1 -1
- package/src/update/types.ts +1 -1
- package/dist/auth/duration.d.ts +0 -1
- package/dist/auth/duration.js +0 -32
- package/dist/auth/duration.js.map +0 -1
- package/dist/auth/index.d.ts +0 -11
- package/dist/auth/index.js +0 -9
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/self-label.d.ts +0 -36
- package/dist/auth/self-label.js +0 -57
- package/dist/auth/self-label.js.map +0 -1
- package/dist/auth/trust-circle-store.d.ts +0 -26
- package/dist/auth/trust-circle-store.js +0 -129
- package/dist/auth/trust-circle-store.js.map +0 -1
- package/dist/auth/trust-circle.d.ts +0 -156
- package/dist/auth/trust-circle.js +0 -307
- package/dist/auth/trust-circle.js.map +0 -1
- package/dist/auth/types.d.ts +0 -84
- package/dist/auth/types.js +0 -24
- package/dist/auth/types.js.map +0 -1
- package/dist/donation/donation-service.d.ts +0 -389
- package/dist/donation/donation-service.js +0 -828
- package/dist/donation/donation-service.js.map +0 -1
- package/dist/donation/donation-store.d.ts +0 -51
- package/dist/donation/donation-store.js +0 -152
- package/dist/donation/donation-store.js.map +0 -1
- package/dist/donation/donation-supervisor.js +0 -364
- package/dist/donation/donation-supervisor.js.map +0 -1
- package/dist/donation/grant-service.d.ts +0 -80
- package/dist/donation/grant-service.js +0 -210
- package/dist/donation/grant-service.js.map +0 -1
- package/dist/donation/grant-store.d.ts +0 -38
- package/dist/donation/grant-store.js +0 -135
- package/dist/donation/grant-store.js.map +0 -1
- package/dist/donation/index.d.ts +0 -20
- package/dist/donation/index.js +0 -17
- package/dist/donation/index.js.map +0 -1
- package/dist/donation/types.d.ts +0 -224
- package/dist/donation/types.js +0 -40
- package/dist/donation/types.js.map +0 -1
- package/dist/installer/identity.js.map +0 -1
- package/dist/owner/index.d.ts +0 -11
- package/dist/owner/index.js +0 -11
- package/dist/owner/index.js.map +0 -1
- package/dist/owner/owner-node-client.d.ts +0 -110
- package/dist/owner/owner-node-client.js +0 -184
- package/dist/owner/owner-node-client.js.map +0 -1
- package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
- package/dist/server/routes/bootstrap-node-validation.js +0 -197
- package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
- package/dist/server/routes/grants-admin.d.ts +0 -29
- package/dist/server/routes/grants-admin.js +0 -58
- package/dist/server/routes/grants-admin.js.map +0 -1
- package/dist/server/routes/grants.d.ts +0 -28
- package/dist/server/routes/grants.js +0 -165
- package/dist/server/routes/grants.js.map +0 -1
- package/dist/server/routes/provision-request-validation.d.ts +0 -45
- package/dist/server/routes/provision-request-validation.js +0 -97
- package/dist/server/routes/provision-request-validation.js.map +0 -1
- package/dist/server/routes/strands.d.ts +0 -20
- package/dist/server/routes/strands.js +0 -38
- package/dist/server/routes/strands.js.map +0 -1
- package/dist/server/routes/trust-circle.d.ts +0 -17
- package/dist/server/routes/trust-circle.js +0 -35
- package/dist/server/routes/trust-circle.js.map +0 -1
- package/dist/strands/index.d.ts +0 -7
- package/dist/strands/index.js +0 -6
- package/dist/strands/index.js.map +0 -1
- package/dist/strands/strand-service.d.ts +0 -50
- package/dist/strands/strand-service.js +0 -103
- package/dist/strands/strand-service.js.map +0 -1
- package/dist/strands/types.d.ts +0 -93
- package/dist/strands/types.js +0 -23
- package/dist/strands/types.js.map +0 -1
- package/dist/ui/assets/index-C6nELNfC.js +0 -23
- package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
- package/dist/ui/assets/index-TtIYSeZO.css +0 -1
- package/src/__tests__/cli-invite.smoke.test.ts +0 -192
- package/src/__tests__/host-owner.smoke.test.ts +0 -123
- package/src/__tests__/orchestrator-owner.test.ts +0 -234
- package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
- package/src/auth/__tests__/duration.test.ts +0 -28
- package/src/auth/__tests__/self-label.test.ts +0 -85
- package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
- package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
- package/src/auth/__tests__/trust-circle.test.ts +0 -445
- package/src/auth/duration.ts +0 -33
- package/src/auth/index.ts +0 -21
- package/src/auth/self-label.ts +0 -74
- package/src/auth/trust-circle-store.ts +0 -155
- package/src/auth/trust-circle.ts +0 -395
- package/src/auth/types.ts +0 -99
- package/src/donation/__tests__/donation-service.test.ts +0 -1193
- package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
- package/src/donation/__tests__/grant-service.test.ts +0 -213
- package/src/donation/__tests__/grant-store.test.ts +0 -113
- package/src/donation/donation-service.ts +0 -1033
- package/src/donation/donation-store.ts +0 -176
- package/src/donation/donation-supervisor.ts +0 -411
- package/src/donation/grant-service.ts +0 -252
- package/src/donation/grant-store.ts +0 -152
- package/src/donation/index.ts +0 -65
- package/src/donation/types.ts +0 -260
- package/src/owner/__tests__/owner-node-client.test.ts +0 -194
- package/src/owner/index.ts +0 -12
- package/src/owner/owner-node-client.ts +0 -259
- package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
- package/src/server/__tests__/grants-admin-route.test.ts +0 -171
- package/src/server/__tests__/grants-route.test.ts +0 -394
- package/src/server/__tests__/strands-route.test.ts +0 -234
- package/src/server/routes/bootstrap-node-validation.ts +0 -206
- package/src/server/routes/grants-admin.ts +0 -74
- package/src/server/routes/grants.ts +0 -193
- package/src/server/routes/provision-request-validation.ts +0 -105
- package/src/server/routes/strands.ts +0 -54
- package/src/server/routes/trust-circle.ts +0 -49
- package/src/strands/__tests__/strand-service.test.ts +0 -165
- package/src/strands/index.ts +0 -14
- package/src/strands/strand-service.ts +0 -135
- package/src/strands/types.ts +0 -102
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The handle rules `HostProcessOrchestrator` and its test stand-in
|
|
3
|
-
* `FakeOrchestrator` (`
|
|
4
|
-
* written once and run against each: `orchestrator.test.ts` registers them over
|
|
5
|
-
* the real class, `fake-orchestrator.test.ts` over the fake. A rule one of them
|
|
6
|
-
* stops obeying fails in that file's run.
|
|
7
|
-
*
|
|
8
|
-
* Not a `.test.ts` file, so vitest does not collect it on its own.
|
|
9
|
-
*
|
|
10
|
-
* Only what both classes can satisfy belongs here — no ports, no files on disk,
|
|
11
|
-
* none of the fake's recording arrays. Those stay in the two calling files.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, expect, it } from 'vitest';
|
|
15
|
-
|
|
16
|
-
import type { OrchestratorCreateRequest, OrchestratorCreateResult } from '@serfab/cadre-provider';
|
|
17
|
-
|
|
18
|
-
import type { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
|
|
19
|
-
|
|
20
|
-
/** What the contract needs from either orchestrator. */
|
|
21
|
-
export type ContractOrchestrator = Pick<
|
|
22
|
-
HostProcessOrchestrator,
|
|
23
|
-
'createContainer' | 'stopContainer' | 'removeContainer' | 'resolveDockerId'
|
|
24
|
-
>;
|
|
25
|
-
|
|
26
|
-
export interface HandleContractHarness<O extends ContractOrchestrator> {
|
|
27
|
-
/** A fresh orchestrator. Teardown is the calling file's own afterEach. */
|
|
28
|
-
make(): O;
|
|
29
|
-
request(containerId: string): OrchestratorCreateRequest;
|
|
30
|
-
/** Resolve once the spawn named by `dockerId` reads as running. */
|
|
31
|
-
started(orch: O, dockerId: string): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Make `createContainer` for `containerId` throw from here on. Only called
|
|
34
|
-
* after that container has been spawned once and stopped, and no case creates
|
|
35
|
-
* again afterwards.
|
|
36
|
-
*/
|
|
37
|
-
failNextCreate(orch: O, containerId: string): void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const CONTAINER = '
|
|
41
|
-
const OTHER_CONTAINER = '
|
|
42
|
-
|
|
43
|
-
export function describeHandleContract<O extends ContractOrchestrator>(
|
|
44
|
-
label: string,
|
|
45
|
-
harness: HandleContractHarness<O>,
|
|
46
|
-
): void {
|
|
47
|
-
async function spawn(orch: O, containerId: string): Promise<OrchestratorCreateResult> {
|
|
48
|
-
const result = await orch.createContainer(harness.request(containerId));
|
|
49
|
-
await harness.started(orch, result.dockerId);
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Stops first: a container whose previous child is still running is refused.
|
|
54
|
-
async function respawn(orch: O, containerId: string, previousDockerId: string): Promise<OrchestratorCreateResult> {
|
|
55
|
-
await orch.stopContainer(previousDockerId);
|
|
56
|
-
return spawn(orch, containerId);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
describe(`${label} handle contract`, () => {
|
|
60
|
-
it('rejects a dockerId it never issued', async () => {
|
|
61
|
-
const orch = harness.make();
|
|
62
|
-
|
|
63
|
-
await expectNotFound(orch.stopContainer('never-issued'), 'never-issued');
|
|
64
|
-
await expectNotFound(orch.removeContainer('never-issued'), 'never-issued');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('rejects a dockerId once its container has been removed', async () => {
|
|
68
|
-
const orch = harness.make();
|
|
69
|
-
const { dockerId } = await spawn(orch, CONTAINER);
|
|
70
|
-
await orch.stopContainer(dockerId);
|
|
71
|
-
|
|
72
|
-
await orch.removeContainer(dockerId);
|
|
73
|
-
|
|
74
|
-
await expectNotFound(orch.stopContainer(dockerId), dockerId);
|
|
75
|
-
await expectNotFound(orch.removeContainer(dockerId), dockerId);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('replaces the first spawn\'s dockerId when the container re-spawns', async () => {
|
|
79
|
-
const orch = harness.make();
|
|
80
|
-
const first = await spawn(orch, CONTAINER);
|
|
81
|
-
|
|
82
|
-
const second = await respawn(orch, CONTAINER, first.dockerId);
|
|
83
|
-
|
|
84
|
-
await expectNotFound(orch.stopContainer(first.dockerId), first.dockerId);
|
|
85
|
-
expect(orch.resolveDockerId(CONTAINER)).toBe(second.dockerId);
|
|
86
|
-
await expect(orch.stopContainer(second.dockerId)).resolves.toBeUndefined();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Launching over a live child hands its ports to a second child that then
|
|
90
|
-
// dies on them, while the caller records the new dockerId
|
|
91
|
-
it('refuses to re-spawn a container whose previous child is still running', async () => {
|
|
92
|
-
const orch = harness.make();
|
|
93
|
-
const first = await spawn(orch, CONTAINER);
|
|
94
|
-
|
|
95
|
-
await expect(orch.createContainer(harness.request(CONTAINER)))
|
|
96
|
-
.rejects.toHaveProperty('message', `container ${CONTAINER} is still running`);
|
|
97
|
-
|
|
98
|
-
expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
|
|
99
|
-
await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// The drop on re-spawn filters on containerId; an orchestrator that cleared
|
|
103
|
-
// every handle would still pass the same-container case above.
|
|
104
|
-
it('leaves another container\'s dockerId alone when one container re-spawns', async () => {
|
|
105
|
-
const orch = harness.make();
|
|
106
|
-
const other = await spawn(orch, OTHER_CONTAINER);
|
|
107
|
-
const first = await spawn(orch, CONTAINER);
|
|
108
|
-
|
|
109
|
-
await respawn(orch, CONTAINER, first.dockerId);
|
|
110
|
-
|
|
111
|
-
expect(orch.resolveDockerId(OTHER_CONTAINER)).toBe(other.dockerId);
|
|
112
|
-
await expect(orch.stopContainer(other.dockerId)).resolves.toBeUndefined();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// What lets a caller still clean up the node it knows about after a re-spawn
|
|
116
|
-
// that did not happen.
|
|
117
|
-
it('keeps the first spawn\'s dockerId when the re-spawn fails', async () => {
|
|
118
|
-
const orch = harness.make();
|
|
119
|
-
const first = await spawn(orch, CONTAINER);
|
|
120
|
-
await orch.stopContainer(first.dockerId);
|
|
121
|
-
|
|
122
|
-
harness.failNextCreate(orch, CONTAINER);
|
|
123
|
-
await expect(orch.createContainer(harness.request(CONTAINER))).rejects.toThrow();
|
|
124
|
-
|
|
125
|
-
expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
|
|
126
|
-
await expect(orch.removeContainer(first.dockerId)).resolves.toBeUndefined();
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** The whole message, not a substring, so the two classes cannot come to word it differently. */
|
|
132
|
-
async function expectNotFound(call: Promise<void>, dockerId: string): Promise<void> {
|
|
133
|
-
await expect(call).rejects.toHaveProperty('message', `Container not found: ${dockerId}`);
|
|
134
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The handle rules `HostProcessOrchestrator` and its test stand-in
|
|
3
|
+
* `FakeOrchestrator` (`hosted/__tests__/fake-orchestrator.ts`) must both obey,
|
|
4
|
+
* written once and run against each: `orchestrator.test.ts` registers them over
|
|
5
|
+
* the real class, `fake-orchestrator.test.ts` over the fake. A rule one of them
|
|
6
|
+
* stops obeying fails in that file's run.
|
|
7
|
+
*
|
|
8
|
+
* Not a `.test.ts` file, so vitest does not collect it on its own.
|
|
9
|
+
*
|
|
10
|
+
* Only what both classes can satisfy belongs here — no ports, no files on disk,
|
|
11
|
+
* none of the fake's recording arrays. Those stay in the two calling files.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
|
|
16
|
+
import type { OrchestratorCreateRequest, OrchestratorCreateResult } from '@serfab/cadre-provider';
|
|
17
|
+
|
|
18
|
+
import type { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
|
|
19
|
+
|
|
20
|
+
/** What the contract needs from either orchestrator. */
|
|
21
|
+
export type ContractOrchestrator = Pick<
|
|
22
|
+
HostProcessOrchestrator,
|
|
23
|
+
'createContainer' | 'stopContainer' | 'removeContainer' | 'resolveDockerId'
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export interface HandleContractHarness<O extends ContractOrchestrator> {
|
|
27
|
+
/** A fresh orchestrator. Teardown is the calling file's own afterEach. */
|
|
28
|
+
make(): O;
|
|
29
|
+
request(containerId: string): OrchestratorCreateRequest;
|
|
30
|
+
/** Resolve once the spawn named by `dockerId` reads as running. */
|
|
31
|
+
started(orch: O, dockerId: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Make `createContainer` for `containerId` throw from here on. Only called
|
|
34
|
+
* after that container has been spawned once and stopped, and no case creates
|
|
35
|
+
* again afterwards.
|
|
36
|
+
*/
|
|
37
|
+
failNextCreate(orch: O, containerId: string): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CONTAINER = 'hn_contract_a';
|
|
41
|
+
const OTHER_CONTAINER = 'hn_contract_b';
|
|
42
|
+
|
|
43
|
+
export function describeHandleContract<O extends ContractOrchestrator>(
|
|
44
|
+
label: string,
|
|
45
|
+
harness: HandleContractHarness<O>,
|
|
46
|
+
): void {
|
|
47
|
+
async function spawn(orch: O, containerId: string): Promise<OrchestratorCreateResult> {
|
|
48
|
+
const result = await orch.createContainer(harness.request(containerId));
|
|
49
|
+
await harness.started(orch, result.dockerId);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Stops first: a container whose previous child is still running is refused.
|
|
54
|
+
async function respawn(orch: O, containerId: string, previousDockerId: string): Promise<OrchestratorCreateResult> {
|
|
55
|
+
await orch.stopContainer(previousDockerId);
|
|
56
|
+
return spawn(orch, containerId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe(`${label} handle contract`, () => {
|
|
60
|
+
it('rejects a dockerId it never issued', async () => {
|
|
61
|
+
const orch = harness.make();
|
|
62
|
+
|
|
63
|
+
await expectNotFound(orch.stopContainer('never-issued'), 'never-issued');
|
|
64
|
+
await expectNotFound(orch.removeContainer('never-issued'), 'never-issued');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects a dockerId once its container has been removed', async () => {
|
|
68
|
+
const orch = harness.make();
|
|
69
|
+
const { dockerId } = await spawn(orch, CONTAINER);
|
|
70
|
+
await orch.stopContainer(dockerId);
|
|
71
|
+
|
|
72
|
+
await orch.removeContainer(dockerId);
|
|
73
|
+
|
|
74
|
+
await expectNotFound(orch.stopContainer(dockerId), dockerId);
|
|
75
|
+
await expectNotFound(orch.removeContainer(dockerId), dockerId);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('replaces the first spawn\'s dockerId when the container re-spawns', async () => {
|
|
79
|
+
const orch = harness.make();
|
|
80
|
+
const first = await spawn(orch, CONTAINER);
|
|
81
|
+
|
|
82
|
+
const second = await respawn(orch, CONTAINER, first.dockerId);
|
|
83
|
+
|
|
84
|
+
await expectNotFound(orch.stopContainer(first.dockerId), first.dockerId);
|
|
85
|
+
expect(orch.resolveDockerId(CONTAINER)).toBe(second.dockerId);
|
|
86
|
+
await expect(orch.stopContainer(second.dockerId)).resolves.toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Launching over a live child hands its ports to a second child that then
|
|
90
|
+
// dies on them, while the caller records the new dockerId.
|
|
91
|
+
it('refuses to re-spawn a container whose previous child is still running', async () => {
|
|
92
|
+
const orch = harness.make();
|
|
93
|
+
const first = await spawn(orch, CONTAINER);
|
|
94
|
+
|
|
95
|
+
await expect(orch.createContainer(harness.request(CONTAINER)))
|
|
96
|
+
.rejects.toHaveProperty('message', `container ${CONTAINER} is still running`);
|
|
97
|
+
|
|
98
|
+
expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
|
|
99
|
+
await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// The drop on re-spawn filters on containerId; an orchestrator that cleared
|
|
103
|
+
// every handle would still pass the same-container case above.
|
|
104
|
+
it('leaves another container\'s dockerId alone when one container re-spawns', async () => {
|
|
105
|
+
const orch = harness.make();
|
|
106
|
+
const other = await spawn(orch, OTHER_CONTAINER);
|
|
107
|
+
const first = await spawn(orch, CONTAINER);
|
|
108
|
+
|
|
109
|
+
await respawn(orch, CONTAINER, first.dockerId);
|
|
110
|
+
|
|
111
|
+
expect(orch.resolveDockerId(OTHER_CONTAINER)).toBe(other.dockerId);
|
|
112
|
+
await expect(orch.stopContainer(other.dockerId)).resolves.toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// What lets a caller still clean up the node it knows about after a re-spawn
|
|
116
|
+
// that did not happen.
|
|
117
|
+
it('keeps the first spawn\'s dockerId when the re-spawn fails', async () => {
|
|
118
|
+
const orch = harness.make();
|
|
119
|
+
const first = await spawn(orch, CONTAINER);
|
|
120
|
+
await orch.stopContainer(first.dockerId);
|
|
121
|
+
|
|
122
|
+
harness.failNextCreate(orch, CONTAINER);
|
|
123
|
+
await expect(orch.createContainer(harness.request(CONTAINER))).rejects.toThrow();
|
|
124
|
+
|
|
125
|
+
expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
|
|
126
|
+
await expect(orch.removeContainer(first.dockerId)).resolves.toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The whole message, not a substring, so the two classes cannot come to word it differently. */
|
|
132
|
+
async function expectNotFound(call: Promise<void>, dockerId: string): Promise<void> {
|
|
133
|
+
await expect(call).rejects.toHaveProperty('message', `Container not found: ${dockerId}`);
|
|
134
|
+
}
|
|
@@ -1,203 +1,177 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HostProcessOrchestrator per-node identity tests.
|
|
3
|
-
*
|
|
4
|
-
* A
|
|
5
|
-
* and needs a durable on-disk home for its node-local stores (`cadre-cli start`
|
|
6
|
-
* opens the file-backed bootstrap-peer / trusted-owner stores only when a
|
|
7
|
-
* protobuf identity key file is configured). The orchestrator therefore writes
|
|
8
|
-
* `<workdir>/identity.key` once per container and passes it as
|
|
9
|
-
* `--identity-file`.
|
|
10
|
-
*
|
|
11
|
-
* A fake CLI records the `--identity-file` argument it was launched with, so
|
|
12
|
-
* the spawn wiring is observable without a real cadre node.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
-
import { tmpdir } from 'node:os';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
|
|
20
|
-
import { HostProcessOrchestrator
|
|
21
|
-
import { loadIdentity } from '../
|
|
22
|
-
import { removeAllNodes } from './orchestrator-teardown.js';
|
|
23
|
-
|
|
24
|
-
// Writes the --identity-file argument (as seen on its own command line) next
|
|
25
|
-
// to the startup token, then behaves like a minimal long-lived node.
|
|
26
|
-
const FAKE_CLI = `
|
|
27
|
-
import fs from 'node:fs';
|
|
28
|
-
import path from 'node:path';
|
|
29
|
-
const args = process.argv.slice(2);
|
|
30
|
-
const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
|
|
31
|
-
const tokenPath = get('--startup-token-file');
|
|
32
|
-
const token = process.env.CADRE_STARTUP_TOKEN ?? '';
|
|
33
|
-
if (tokenPath) {
|
|
34
|
-
const dir = path.dirname(tokenPath);
|
|
35
|
-
try { fs.writeFileSync(path.join(dir, 'identity-arg.txt'), get('--identity-file') ?? '', 'utf8'); } catch (e) { console.error(e); }
|
|
36
|
-
if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
|
|
37
|
-
}
|
|
38
|
-
process.on('SIGTERM', () => process.exit(0));
|
|
39
|
-
setInterval(() => {}, 1 << 30);
|
|
40
|
-
`;
|
|
41
|
-
|
|
42
|
-
let tmpRoot: string;
|
|
43
|
-
let scriptPath: string;
|
|
44
|
-
const orchestrators: HostProcessOrchestrator[] = [];
|
|
45
|
-
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nodeid-'));
|
|
48
|
-
scriptPath = join(tmpRoot, 'fake-cli.mjs');
|
|
49
|
-
writeFileSync(scriptPath, FAKE_CLI, 'utf8');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
afterEach(async () => {
|
|
53
|
-
try {
|
|
54
|
-
await removeAllNodes(orchestrators);
|
|
55
|
-
} finally {
|
|
56
|
-
await sleep(50);
|
|
57
|
-
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
function makeOrchestrator(
|
|
62
|
-
rootDir: string,
|
|
63
|
-
portRange: { start: number; end: number } = { start: 17500, end: 17999 },
|
|
64
|
-
): HostProcessOrchestrator {
|
|
65
|
-
mkdirSync(rootDir, { recursive: true });
|
|
66
|
-
const orch = new HostProcessOrchestrator({
|
|
67
|
-
rootDir,
|
|
68
|
-
portRange,
|
|
69
|
-
stopTimeoutMs: 1500,
|
|
70
|
-
spawn: { entrypoint: scriptPath },
|
|
71
|
-
});
|
|
72
|
-
orchestrators.push(orch);
|
|
73
|
-
return orch;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
|
|
77
|
-
const deadline = Date.now() + timeoutMs;
|
|
78
|
-
while (Date.now() < deadline) {
|
|
79
|
-
if (existsSync(path)) return readFileSync(path, 'utf8');
|
|
80
|
-
await sleep(50);
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`file never appeared: ${path}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function sleep(ms: number): Promise<void> {
|
|
86
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
describe('HostProcessOrchestrator node identity', () => {
|
|
90
|
-
it('passes --identity-file pointing at a real key inside the node workdir', async () => {
|
|
91
|
-
const rootDir = join(tmpRoot, 'a');
|
|
92
|
-
const orch = makeOrchestrator(rootDir);
|
|
93
|
-
await orch.init();
|
|
94
|
-
|
|
95
|
-
await orch.createContainer({
|
|
96
|
-
containerId: '
|
|
97
|
-
partyId: '
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
expect(arg).
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await orch.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
profile: 'storage' as const,
|
|
179
|
-
pinnedOwnerKeys: ['key-a'],
|
|
180
|
-
};
|
|
181
|
-
await expect(orch.createContainer({ ...request, containerId: 'donated-bad' })).rejects.toThrow();
|
|
182
|
-
|
|
183
|
-
// The whole range is still free, so a healthy container still gets its five.
|
|
184
|
-
const ok = await orch.createContainer({ ...request, containerId: 'donated-good' });
|
|
185
|
-
expect(ok.p2pPort).toBeGreaterThanOrEqual(18100);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// The owner node carries the HOST's installer identity from <dataDir>, not a
|
|
189
|
-
// per-workdir key — a second key in its workdir would mean the host's own
|
|
190
|
-
// cadre node quietly changed peer id.
|
|
191
|
-
it('leaves the owner node on its own installer identity', async () => {
|
|
192
|
-
const rootDir = join(tmpRoot, 'e');
|
|
193
|
-
const orch = makeOrchestrator(rootDir);
|
|
194
|
-
await orch.init();
|
|
195
|
-
|
|
196
|
-
const hostIdentity = join(tmpRoot, 'host-identity.key');
|
|
197
|
-
await orch.ensureOwnerNode({ identityPath: hostIdentity, partyId: 'host-P', libp2pPort: 4101 });
|
|
198
|
-
|
|
199
|
-
const ownerWorkdir = join(rootDir, OWNER_CONTAINER_ID);
|
|
200
|
-
expect(await waitForFile(join(ownerWorkdir, 'identity-arg.txt'))).toBe(hostIdentity);
|
|
201
|
-
expect(existsSync(join(ownerWorkdir, 'identity.key'))).toBe(false);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* HostProcessOrchestrator per-node identity tests.
|
|
3
|
+
*
|
|
4
|
+
* A hosted node must keep the peer id the claim named across restarts,
|
|
5
|
+
* and needs a durable on-disk home for its node-local stores (`cadre-cli start`
|
|
6
|
+
* opens the file-backed bootstrap-peer / trusted-owner stores only when a
|
|
7
|
+
* protobuf identity key file is configured). The orchestrator therefore writes
|
|
8
|
+
* `<workdir>/identity.key` once per container and passes it as
|
|
9
|
+
* `--identity-file`.
|
|
10
|
+
*
|
|
11
|
+
* A fake CLI records the `--identity-file` argument it was launched with, so
|
|
12
|
+
* the spawn wiring is observable without a real cadre node.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
|
|
21
|
+
import { loadIdentity } from '../orchestrator/identity-file.js';
|
|
22
|
+
import { removeAllNodes } from './orchestrator-teardown.js';
|
|
23
|
+
|
|
24
|
+
// Writes the --identity-file argument (as seen on its own command line) next
|
|
25
|
+
// to the startup token, then behaves like a minimal long-lived node.
|
|
26
|
+
const FAKE_CLI = `
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
|
|
31
|
+
const tokenPath = get('--startup-token-file');
|
|
32
|
+
const token = process.env.CADRE_STARTUP_TOKEN ?? '';
|
|
33
|
+
if (tokenPath) {
|
|
34
|
+
const dir = path.dirname(tokenPath);
|
|
35
|
+
try { fs.writeFileSync(path.join(dir, 'identity-arg.txt'), get('--identity-file') ?? '', 'utf8'); } catch (e) { console.error(e); }
|
|
36
|
+
if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
|
|
37
|
+
}
|
|
38
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
39
|
+
setInterval(() => {}, 1 << 30);
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
let tmpRoot: string;
|
|
43
|
+
let scriptPath: string;
|
|
44
|
+
const orchestrators: HostProcessOrchestrator[] = [];
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nodeid-'));
|
|
48
|
+
scriptPath = join(tmpRoot, 'fake-cli.mjs');
|
|
49
|
+
writeFileSync(scriptPath, FAKE_CLI, 'utf8');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
try {
|
|
54
|
+
await removeAllNodes(orchestrators);
|
|
55
|
+
} finally {
|
|
56
|
+
await sleep(50);
|
|
57
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function makeOrchestrator(
|
|
62
|
+
rootDir: string,
|
|
63
|
+
portRange: { start: number; end: number } = { start: 17500, end: 17999 },
|
|
64
|
+
): HostProcessOrchestrator {
|
|
65
|
+
mkdirSync(rootDir, { recursive: true });
|
|
66
|
+
const orch = new HostProcessOrchestrator({
|
|
67
|
+
rootDir,
|
|
68
|
+
portRange,
|
|
69
|
+
stopTimeoutMs: 1500,
|
|
70
|
+
spawn: { entrypoint: scriptPath },
|
|
71
|
+
});
|
|
72
|
+
orchestrators.push(orch);
|
|
73
|
+
return orch;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
|
|
77
|
+
const deadline = Date.now() + timeoutMs;
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
if (existsSync(path)) return readFileSync(path, 'utf8');
|
|
80
|
+
await sleep(50);
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`file never appeared: ${path}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sleep(ms: number): Promise<void> {
|
|
86
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('HostProcessOrchestrator node identity', () => {
|
|
90
|
+
it('passes --identity-file pointing at a real key inside the node workdir', async () => {
|
|
91
|
+
const rootDir = join(tmpRoot, 'a');
|
|
92
|
+
const orch = makeOrchestrator(rootDir);
|
|
93
|
+
await orch.init();
|
|
94
|
+
|
|
95
|
+
await orch.createContainer({
|
|
96
|
+
containerId: 'hosted-1',
|
|
97
|
+
partyId: 'party-P',
|
|
98
|
+
profile: 'storage',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const workdir = join(rootDir, 'hosted-1');
|
|
102
|
+
const arg = await waitForFile(join(workdir, 'identity-arg.txt'));
|
|
103
|
+
expect(arg).toBe(join(workdir, 'identity.key'));
|
|
104
|
+
// Decodes as a libp2p protobuf private key → the child gets a usable identity.
|
|
105
|
+
expect(loadIdentity(arg).peerId).toMatch(/^12D3Koo/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('reuses the same identity when the same containerId is spawned again', async () => {
|
|
109
|
+
const rootDir = join(tmpRoot, 'b');
|
|
110
|
+
const orch = makeOrchestrator(rootDir);
|
|
111
|
+
await orch.init();
|
|
112
|
+
|
|
113
|
+
const first = await orch.createContainer({
|
|
114
|
+
containerId: 'hosted-2',
|
|
115
|
+
partyId: 'party-P',
|
|
116
|
+
profile: 'storage',
|
|
117
|
+
});
|
|
118
|
+
const workdir = join(rootDir, 'hosted-2');
|
|
119
|
+
await waitForFile(join(workdir, 'identity-arg.txt'));
|
|
120
|
+
const before = loadIdentity(join(workdir, 'identity.key')).peerId;
|
|
121
|
+
|
|
122
|
+
// Stop (not remove — removal deletes the workdir) and re-spawn, as a restart
|
|
123
|
+
// of a live hosted node would.
|
|
124
|
+
await orch.stopContainer(first.dockerId);
|
|
125
|
+
await orch.createContainer({
|
|
126
|
+
containerId: 'hosted-2',
|
|
127
|
+
partyId: 'party-P',
|
|
128
|
+
profile: 'storage',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const after = loadIdentity(join(workdir, 'identity.key')).peerId;
|
|
132
|
+
expect(after).toBe(before);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('destroys the identity with the workdir when the node is removed', async () => {
|
|
136
|
+
const rootDir = join(tmpRoot, 'c');
|
|
137
|
+
const orch = makeOrchestrator(rootDir);
|
|
138
|
+
await orch.init();
|
|
139
|
+
|
|
140
|
+
const created = await orch.createContainer({
|
|
141
|
+
containerId: 'hosted-3',
|
|
142
|
+
partyId: 'party-P',
|
|
143
|
+
profile: 'storage',
|
|
144
|
+
});
|
|
145
|
+
const identityPath = join(rootDir, 'hosted-3', 'identity.key');
|
|
146
|
+
await waitForFile(identityPath);
|
|
147
|
+
|
|
148
|
+
await orch.removeContainer(created.dockerId);
|
|
149
|
+
expect(existsSync(identityPath)).toBe(false);
|
|
150
|
+
expect(existsSync(join(rootDir, 'hosted-3'))).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// The failure the identity step can actually produce is a damaged key file,
|
|
154
|
+
// and `HostedNodeService.join` turns every such throw into an `error`
|
|
155
|
+
// record and lets the admin retry. If the spawn path reserved its ports
|
|
156
|
+
// before the step that throws, each retry would burn four more ports out of a
|
|
157
|
+
// bounded range until provisioning stopped working altogether.
|
|
158
|
+
it('reserves no ports when the identity step fails', async () => {
|
|
159
|
+
const rootDir = join(tmpRoot, 'd');
|
|
160
|
+
// Exactly one node's worth of ports (health, metrics, p2p, ws).
|
|
161
|
+
const orch = makeOrchestrator(rootDir, { start: 18100, end: 18103 });
|
|
162
|
+
await orch.init();
|
|
163
|
+
|
|
164
|
+
mkdirSync(join(rootDir, 'hosted-bad'), { recursive: true });
|
|
165
|
+
writeFileSync(join(rootDir, 'hosted-bad', 'identity.key'), 'not a protobuf private key', 'utf8');
|
|
166
|
+
|
|
167
|
+
const request = {
|
|
168
|
+
partyId: 'party-P',
|
|
169
|
+
profile: 'storage' as const,
|
|
170
|
+
};
|
|
171
|
+
await expect(orch.createContainer({ ...request, containerId: 'hosted-bad' })).rejects.toThrow();
|
|
172
|
+
|
|
173
|
+
// The whole range is still free, so a healthy container still gets its four.
|
|
174
|
+
const ok = await orch.createContainer({ ...request, containerId: 'hosted-good' });
|
|
175
|
+
expect(ok.p2pPort).toBeGreaterThanOrEqual(18100);
|
|
176
|
+
});
|
|
177
|
+
});
|