@serfab/cadre-host 1.7.0 → 1.9.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 +31 -16
- package/dist/bin/host.js +41 -20
- package/dist/bin/host.js.map +1 -1
- package/dist/donation/donation-service.d.ts +27 -4
- package/dist/donation/donation-service.js +57 -6
- package/dist/donation/donation-service.js.map +1 -1
- package/dist/donation/donation-store.d.ts +3 -1
- package/dist/donation/donation-store.js +5 -1
- package/dist/donation/donation-store.js.map +1 -1
- package/dist/donation/grant-service.d.ts +16 -7
- package/dist/donation/grant-service.js +57 -10
- package/dist/donation/grant-service.js.map +1 -1
- package/dist/donation/index.d.ts +2 -2
- package/dist/donation/index.js +1 -1
- package/dist/donation/index.js.map +1 -1
- package/dist/donation/types.d.ts +29 -7
- package/dist/donation/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/nat/address-resolver.d.ts +3 -2
- package/dist/nat/address-resolver.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +2 -2
- package/dist/orchestrator/host-process-orchestrator.js +22 -12
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +1 -1
- package/dist/orchestrator/node-identity.js +1 -1
- package/dist/orchestrator/port-allocator.d.ts +6 -39
- package/dist/orchestrator/port-allocator.js +9 -89
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/server/events/types.d.ts +3 -0
- package/dist/server/index.d.ts +19 -18
- package/dist/server/index.js +13 -15
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/grants-admin.d.ts +15 -3
- package/dist/server/routes/grants-admin.js +33 -7
- package/dist/server/routes/grants-admin.js.map +1 -1
- package/dist/server/routes/nodes.d.ts +13 -5
- package/dist/server/routes/nodes.js +58 -39
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/status.d.ts +13 -0
- package/dist/server/routes/status.js +2 -0
- package/dist/server/routes/status.js.map +1 -1
- package/dist/ui/assets/index-C6nELNfC.js +23 -0
- package/dist/ui/assets/index-C6nELNfC.js.map +1 -0
- package/dist/ui/assets/index-TtIYSeZO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +4 -4
- package/src/__tests__/global-setup.ts +58 -56
- package/src/__tests__/orchestrator-env-scrub.test.ts +12 -11
- package/src/__tests__/orchestrator-handle-contract.ts +134 -0
- package/src/__tests__/orchestrator-node-identity.test.ts +6 -7
- package/src/__tests__/orchestrator-owner.test.ts +6 -13
- package/src/__tests__/orchestrator-pin-keys.test.ts +6 -7
- package/src/__tests__/orchestrator-ports.test.ts +5 -34
- package/src/__tests__/orchestrator-push.test.ts +6 -5
- package/src/__tests__/orchestrator-teardown.ts +65 -0
- package/src/__tests__/orchestrator.test.ts +23 -22
- package/src/auth/__tests__/trust-circle-integration.test.ts +1 -1
- package/src/bin/host.ts +56 -33
- package/src/donation/__tests__/donation-service.test.ts +38 -0
- package/src/donation/__tests__/fake-orchestrator.test.ts +19 -14
- package/src/donation/__tests__/fake-orchestrator.ts +21 -3
- package/src/donation/__tests__/grant-service.test.ts +2 -2
- package/src/donation/donation-service.ts +58 -6
- package/src/donation/donation-store.ts +176 -171
- package/src/donation/grant-service.ts +252 -200
- package/src/donation/index.ts +65 -64
- package/src/donation/types.ts +260 -244
- package/src/index.ts +2 -0
- package/src/nat/address-resolver.ts +3 -2
- package/src/orchestrator/host-process-orchestrator.ts +1260 -1251
- package/src/orchestrator/node-identity.ts +1 -1
- package/src/orchestrator/port-allocator.ts +16 -91
- package/src/server/__tests__/fakes.ts +44 -0
- package/src/server/__tests__/grants-admin-route.test.ts +171 -73
- package/src/server/__tests__/nodes-route.test.ts +49 -25
- package/src/server/__tests__/publishers.test.ts +5 -41
- package/src/server/__tests__/server.smoke.test.ts +7 -37
- package/src/server/__tests__/status-route.test.ts +8 -21
- package/src/server/__tests__/strands-route.test.ts +3 -2
- package/src/server/events/types.ts +24 -23
- package/src/server/index.ts +267 -267
- package/src/server/routes/grants-admin.ts +74 -41
- package/src/server/routes/nodes.ts +61 -43
- package/src/server/routes/status.ts +16 -0
- package/dist/donation/__tests__/fake-orchestrator.d.ts +0 -103
- package/dist/donation/__tests__/fake-orchestrator.js +0 -180
- package/dist/donation/__tests__/fake-orchestrator.js.map +0 -1
- package/dist/ui/assets/index-BVoqLjyX.css +0 -1
- package/dist/ui/assets/index-CEoNRwM_.js +0 -17
- package/dist/ui/assets/index-CEoNRwM_.js.map +0 -1
|
@@ -1,41 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Donation grant **admin** HTTP routes — thin adapters around
|
|
3
|
-
* `GrantAdminHandlers` (`createGrantAdminHandlers(service)`).
|
|
4
|
-
*
|
|
5
|
-
* Mount path: `/grants-admin` — the admin surface the `cadre-host grant` CLI
|
|
6
|
-
* targets. It is **loopback, no bearer**: same-machine admin, matching
|
|
7
|
-
* cadre-host's local-UI "no login" posture (see docs/cadre-host.md §
|
|
8
|
-
* Security posture). This is distinct from the grantee-facing `/grants`
|
|
9
|
-
* provisioning surface
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Donation grant **admin** HTTP routes — thin adapters around
|
|
3
|
+
* `GrantAdminHandlers` (`createGrantAdminHandlers(service, donations)`).
|
|
4
|
+
*
|
|
5
|
+
* Mount path: `/grants-admin` — the admin surface the `cadre-host grant` CLI
|
|
6
|
+
* targets. It is **loopback, no bearer**: same-machine admin, matching
|
|
7
|
+
* cadre-host's local-UI "no login" posture (see docs/cadre-host.md §
|
|
8
|
+
* Security posture). This is distinct from the grantee-facing `/grants`
|
|
9
|
+
* provisioning surface (`routes/grants.ts`), which carries the bearer gate.
|
|
10
|
+
*
|
|
11
|
+
* Clients: the `cadre-host grant` CLI and the local UI's Grants page. The list
|
|
12
|
+
* reports each grant's live-node count and the donations a revoke would end.
|
|
13
|
+
*
|
|
14
|
+
* Revoking a grant also terminates the nodes donated under it unless the
|
|
15
|
+
* caller passes `?keepNodes=true`; `DELETE /grants-admin/donations/:id` ends a
|
|
16
|
+
* single donated node. Those are the host's only teardown paths once a grant is
|
|
17
|
+
* revoked — the grantee's own `DELETE /grants/:id` is refused from then on.
|
|
18
|
+
*
|
|
19
|
+
* Each successful mutation publishes a `grants-changed` bus event, so an open
|
|
20
|
+
* Grants page refreshes when the CLI acts.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { FastifyInstance } from 'fastify';
|
|
24
|
+
|
|
25
|
+
import type { GrantAdminHandlers } from '../../donation/types.js';
|
|
26
|
+
import type { EventBus } from '../events/bus.js';
|
|
27
|
+
|
|
28
|
+
export interface GrantsAdminRoutesOptions {
|
|
29
|
+
handlers: GrantAdminHandlers;
|
|
30
|
+
events: EventBus;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerGrantsAdminRoutes(app: FastifyInstance, opts: GrantsAdminRoutesOptions): void {
|
|
34
|
+
const { handlers, events } = opts;
|
|
35
|
+
|
|
36
|
+
app.get('/grants-admin', async () => {
|
|
37
|
+
return handlers.listGrants();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
app.post('/grants-admin', async (request) => {
|
|
41
|
+
const body = (request.body ?? {}) as { label?: unknown; maxNodes?: unknown; ttlMs?: unknown };
|
|
42
|
+
const label = typeof body.label === 'string' ? body.label : '';
|
|
43
|
+
const args: { label: string; maxNodes?: number; ttlMs?: number } = { label };
|
|
44
|
+
if (typeof body.maxNodes === 'number') args.maxNodes = body.maxNodes;
|
|
45
|
+
if (typeof body.ttlMs === 'number') args.ttlMs = body.ttlMs;
|
|
46
|
+
const result = await handlers.postGrant(args);
|
|
47
|
+
events.publish({ type: 'grants-changed', kind: 'issued' });
|
|
48
|
+
return result;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.delete<{ Params: { token: string }; Querystring: { keepNodes?: string } }>(
|
|
52
|
+
'/grants-admin/:token',
|
|
53
|
+
async (request) => {
|
|
54
|
+
const keepNodes = isTrueFlag(request.query.keepNodes);
|
|
55
|
+
const { terminated } = await handlers.deleteGrant(request.params.token, { keepNodes });
|
|
56
|
+
events.publish({ type: 'grants-changed', kind: 'revoked' });
|
|
57
|
+
return { ok: true, terminated };
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Two path segments past `/grants-admin`, so it never collides with `/:token`.
|
|
62
|
+
const { terminateDonation } = handlers;
|
|
63
|
+
if (terminateDonation) {
|
|
64
|
+
app.delete<{ Params: { id: string } }>('/grants-admin/donations/:id', async (request) => {
|
|
65
|
+
await terminateDonation(request.params.id);
|
|
66
|
+
events.publish({ type: 'grants-changed', kind: 'terminated' });
|
|
67
|
+
return { ok: true };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isTrueFlag(value: string | undefined): boolean {
|
|
73
|
+
return value === 'true' || value === '1';
|
|
74
|
+
}
|
|
@@ -6,29 +6,38 @@
|
|
|
6
6
|
* `onStateChange` (forwarded by `createLocalUiServer.start`) — the route
|
|
7
7
|
* handlers don't re-publish.
|
|
8
8
|
*
|
|
9
|
-
* start/restart are real for the admin's owner node (
|
|
10
|
-
* persisted `OwnerSpawnConfig`).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* start/stop/restart are real for the admin's owner node only (start/restart
|
|
10
|
+
* re-spawn it from the persisted `OwnerSpawnConfig`). Every other node this
|
|
11
|
+
* route can see is a donated node, whose lifecycle belongs to the donation
|
|
12
|
+
* surface — this route no longer spawns generic nodes, and a stop here would
|
|
13
|
+
* be undone at once by the donation supervisor's respawn. So all three verbs
|
|
14
|
+
* answer a known non-owner node with 501, pointing at the donation teardown.
|
|
15
|
+
*
|
|
16
|
+
* Owner start/restart are founder-only: a donor-only host that once ran as a
|
|
17
|
+
* founder still holds the owner handle and saved config (so re-enabling
|
|
18
|
+
* `ownCadre` can resume it), but spawning it here would bring the owner node
|
|
19
|
+
* back without its trust circle, NAT or strand services — so those answer 409.
|
|
14
20
|
*/
|
|
15
21
|
|
|
16
22
|
import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
17
23
|
|
|
18
|
-
import type { FastifyInstance } from 'fastify';
|
|
24
|
+
import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
19
25
|
|
|
20
26
|
import type { HostProcessOrchestrator } from '../../orchestrator/index.js';
|
|
21
27
|
import { defaultLogPath } from '../../orchestrator/log-rotator.js';
|
|
22
28
|
|
|
29
|
+
import type { HostRole } from './status.js';
|
|
30
|
+
|
|
23
31
|
const DEFAULT_LOG_LINES = 200;
|
|
24
32
|
const MAX_LOG_LINES = 2000;
|
|
25
33
|
|
|
26
34
|
export interface NodesRoutesOptions {
|
|
27
35
|
orchestrator: HostProcessOrchestrator;
|
|
36
|
+
role: HostRole;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
export function registerNodesRoutes(app: FastifyInstance, opts: NodesRoutesOptions): void {
|
|
31
|
-
const { orchestrator } = opts;
|
|
40
|
+
const { orchestrator, role } = opts;
|
|
32
41
|
|
|
33
42
|
app.get('/api/nodes', async () => {
|
|
34
43
|
return { ok: true, data: { nodes: orchestrator.listNodes() } };
|
|
@@ -36,12 +45,7 @@ export function registerNodesRoutes(app: FastifyInstance, opts: NodesRoutesOptio
|
|
|
36
45
|
|
|
37
46
|
app.get<{ Params: { id: string } }>('/api/nodes/:id', async (request, reply) => {
|
|
38
47
|
const node = orchestrator.getNode(request.params.id);
|
|
39
|
-
if (!node)
|
|
40
|
-
return reply.code(404).send({
|
|
41
|
-
ok: false,
|
|
42
|
-
error: { code: 'not_found', message: `Unknown node: ${request.params.id}` },
|
|
43
|
-
});
|
|
44
|
-
}
|
|
48
|
+
if (!node) return notFound(reply, request.params.id);
|
|
45
49
|
let stats = null;
|
|
46
50
|
try {
|
|
47
51
|
stats = await orchestrator.getStats(node.dockerId);
|
|
@@ -55,12 +59,7 @@ export function registerNodesRoutes(app: FastifyInstance, opts: NodesRoutesOptio
|
|
|
55
59
|
'/api/nodes/:id/logs',
|
|
56
60
|
async (request, reply) => {
|
|
57
61
|
const node = orchestrator.getNode(request.params.id);
|
|
58
|
-
if (!node)
|
|
59
|
-
return reply.code(404).send({
|
|
60
|
-
ok: false,
|
|
61
|
-
error: { code: 'not_found', message: `Unknown node: ${request.params.id}` },
|
|
62
|
-
});
|
|
63
|
-
}
|
|
62
|
+
if (!node) return notFound(reply, request.params.id);
|
|
64
63
|
const requested = Number(request.query.lines ?? DEFAULT_LOG_LINES);
|
|
65
64
|
const tail = Number.isFinite(requested)
|
|
66
65
|
? Math.max(1, Math.min(MAX_LOG_LINES, Math.floor(requested)))
|
|
@@ -70,72 +69,91 @@ export function registerNodesRoutes(app: FastifyInstance, opts: NodesRoutesOptio
|
|
|
70
69
|
},
|
|
71
70
|
);
|
|
72
71
|
|
|
72
|
+
// start / stop / restart: real for the admin's owner node (started from the
|
|
73
|
+
// persisted OwnerSpawnConfig). Donated-node lifecycle belongs to the donation
|
|
74
|
+
// surface — those ids return a clear not_implemented that points there;
|
|
75
|
+
// unknown ids 404.
|
|
73
76
|
app.post<{ Params: { id: string } }>('/api/nodes/:id/stop', async (request, reply) => {
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
76
|
-
return reply
|
|
77
|
-
ok: false,
|
|
78
|
-
error: { code: 'not_found', message: `Unknown node: ${request.params.id}` },
|
|
79
|
-
});
|
|
77
|
+
const { id } = request.params;
|
|
78
|
+
if (!orchestrator.isOwnerNode(id)) {
|
|
79
|
+
return ownerOnlyFallback(reply, orchestrator, id, 'stop');
|
|
80
80
|
}
|
|
81
|
+
const dockerId = orchestrator.resolveDockerId(id);
|
|
82
|
+
if (!dockerId) return notFound(reply, id);
|
|
81
83
|
await orchestrator.stopContainer(dockerId);
|
|
82
84
|
// The orchestrator emits its own onStateChange — createLocalUiServer
|
|
83
85
|
// forwards that to the bus. Don't double-publish here.
|
|
84
86
|
return { ok: true };
|
|
85
87
|
});
|
|
86
88
|
|
|
87
|
-
// start / restart: real for the admin's owner node (spawned from the
|
|
88
|
-
// persisted OwnerSpawnConfig). Generic node lifecycle now belongs to the
|
|
89
|
-
// donation surface (/grants) — those ids return a clear not_implemented that
|
|
90
|
-
// points there; unknown ids 404.
|
|
91
89
|
app.post<{ Params: { id: string } }>('/api/nodes/:id/start', async (request, reply) => {
|
|
92
90
|
const { id } = request.params;
|
|
93
91
|
if (orchestrator.isOwnerNode(id)) {
|
|
92
|
+
if (role === 'donor') return ownCadreDisabled(reply, 'start');
|
|
94
93
|
if (!orchestrator.hasOwnerConfig()) {
|
|
95
94
|
return notImplemented(reply, `start ${id}: owner node has no saved spawn config.`);
|
|
96
95
|
}
|
|
97
96
|
const node = await orchestrator.ensureOwnerNode();
|
|
98
97
|
return { ok: true, data: { node } };
|
|
99
98
|
}
|
|
100
|
-
return
|
|
99
|
+
return ownerOnlyFallback(reply, orchestrator, id, 'start');
|
|
101
100
|
});
|
|
102
101
|
|
|
103
102
|
app.post<{ Params: { id: string } }>('/api/nodes/:id/restart', async (request, reply) => {
|
|
104
103
|
const { id } = request.params;
|
|
105
104
|
if (orchestrator.isOwnerNode(id)) {
|
|
105
|
+
if (role === 'donor') return ownCadreDisabled(reply, 'restart');
|
|
106
106
|
if (!orchestrator.hasOwnerConfig()) {
|
|
107
107
|
return notImplemented(reply, `restart ${id}: owner node has no saved spawn config.`);
|
|
108
108
|
}
|
|
109
109
|
const node = await orchestrator.restartOwnerNode();
|
|
110
110
|
return { ok: true, data: { node } };
|
|
111
111
|
}
|
|
112
|
-
return
|
|
112
|
+
return ownerOnlyFallback(reply, orchestrator, id, 'restart');
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
/**
|
|
117
|
+
* 404 for unknown ids; 501 not_implemented for known non-owner nodes — all of
|
|
118
|
+
* them donated. A donated node's containerId is its donation id, so the
|
|
119
|
+
* message names `node.id` rather than the route param, which may be the
|
|
120
|
+
* opaque dockerId — only the donation id is what the terminate command takes.
|
|
121
|
+
*/
|
|
122
|
+
function ownerOnlyFallback(
|
|
123
|
+
reply: FastifyReply,
|
|
119
124
|
orchestrator: HostProcessOrchestrator,
|
|
120
125
|
id: string,
|
|
121
|
-
verb:
|
|
126
|
+
verb: 'start' | 'stop' | 'restart',
|
|
122
127
|
) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
ok: false,
|
|
126
|
-
error: { code: 'not_found', message: `Unknown node: ${id}` },
|
|
127
|
-
});
|
|
128
|
-
}
|
|
128
|
+
const node = orchestrator.getNode(id);
|
|
129
|
+
if (!node) return notFound(reply, id);
|
|
129
130
|
return notImplemented(
|
|
130
131
|
reply,
|
|
131
|
-
`${verb} ${id}: only the owner node
|
|
132
|
+
`${verb} ${id}: only the owner node can be started, stopped or restarted here; a donated node's lifecycle is owned by the donation surface — end it with 'cadre-host grant terminate ${node.id}' (DELETE /grants-admin/donations/${node.id}).`,
|
|
132
133
|
);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
function
|
|
136
|
+
function notFound(reply: FastifyReply, id: string) {
|
|
137
|
+
return reply.code(404).send({
|
|
138
|
+
ok: false,
|
|
139
|
+
error: { code: 'not_found', message: `Unknown node: ${id}` },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function notImplemented(reply: FastifyReply, message: string) {
|
|
136
144
|
return reply.code(501).send({ ok: false, error: { code: 'not_implemented', message } });
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
function ownCadreDisabled(reply: FastifyReply, verb: 'start' | 'restart') {
|
|
148
|
+
return reply.code(409).send({
|
|
149
|
+
ok: false,
|
|
150
|
+
error: {
|
|
151
|
+
code: 'own_cadre_disabled',
|
|
152
|
+
message: `${verb} owner: this host's own cadre is turned off (donor-only mode). Set ownCadre.enabled in host.config.json and restart cadre-host to run it again.`,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
/**
|
|
140
158
|
* Read the last `n` lines from a log file by walking the tail in chunks.
|
|
141
159
|
* Returns [] for a missing file.
|
|
@@ -20,8 +20,15 @@ import type { UpdateService } from '../../update/index.js';
|
|
|
20
20
|
|
|
21
21
|
const PROCESS_STARTED_AT = process.hrtime.bigint();
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Which role the running process is in: `'founder'` also runs the host's own
|
|
25
|
+
* cadre (`ownCadre.enabled` at start), `'donor'` only donates nodes.
|
|
26
|
+
*/
|
|
27
|
+
export type HostRole = 'founder' | 'donor';
|
|
28
|
+
|
|
23
29
|
export interface StatusRouteOptions {
|
|
24
30
|
orchestrator: HostProcessOrchestrator;
|
|
31
|
+
role: HostRole;
|
|
25
32
|
/** Present only when the host runs its own personal cadre (`ownCadre.enabled`). */
|
|
26
33
|
trustCircle?: TrustCircleService;
|
|
27
34
|
/** Present only when the host runs its own personal cadre (`ownCadre.enabled`). */
|
|
@@ -31,11 +38,18 @@ export interface StatusRouteOptions {
|
|
|
31
38
|
|
|
32
39
|
export interface StatusResponse {
|
|
33
40
|
service: { name: 'cadre-host'; version: string; uptimeSeconds: number };
|
|
41
|
+
/**
|
|
42
|
+
* Decided at start from `ownCadre.enabled` — the running process, not the file
|
|
43
|
+
* on disk, which may have been edited since without a restart.
|
|
44
|
+
*/
|
|
45
|
+
role: HostRole;
|
|
34
46
|
nodes: Array<{
|
|
35
47
|
id: string;
|
|
36
48
|
partyId: string;
|
|
37
49
|
status: ManagedNodeInfo['status'];
|
|
38
50
|
profile: ManagedNodeInfo['profile'];
|
|
51
|
+
/** Present (true) only on the host's own owner node; every other node is donated. */
|
|
52
|
+
owner?: true;
|
|
39
53
|
}>;
|
|
40
54
|
/** Omitted in donor-only mode (no host-own trust circle). */
|
|
41
55
|
trustCircle?: { members: number; pending: number };
|
|
@@ -53,6 +67,7 @@ export function registerStatusRoute(app: FastifyInstance, opts: StatusRouteOptio
|
|
|
53
67
|
partyId: n.partyId,
|
|
54
68
|
status: n.status,
|
|
55
69
|
profile: n.profile,
|
|
70
|
+
...(n.owner ? { owner: true as const } : {}),
|
|
56
71
|
}));
|
|
57
72
|
|
|
58
73
|
const response: StatusResponse = {
|
|
@@ -61,6 +76,7 @@ export function registerStatusRoute(app: FastifyInstance, opts: StatusRouteOptio
|
|
|
61
76
|
version,
|
|
62
77
|
uptimeSeconds: secondsSince(PROCESS_STARTED_AT),
|
|
63
78
|
},
|
|
79
|
+
role: opts.role,
|
|
64
80
|
nodes,
|
|
65
81
|
};
|
|
66
82
|
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared fake orchestrator for the donation unit tests — no real child
|
|
3
|
-
* processes. Records every create / stop / remove, hands out deterministic
|
|
4
|
-
* unique spawn results, and tracks per-`dockerId` liveness so a test can crash
|
|
5
|
-
* one node and let the supervisor observe exactly that one as down.
|
|
6
|
-
*
|
|
7
|
-
* This file holds no tests of its own; its contract is pinned by the sibling
|
|
8
|
-
* `fake-orchestrator.test.ts`, which exists so a future agent cannot make a
|
|
9
|
-
* failing donation test go green by relaxing the fake.
|
|
10
|
-
*/
|
|
11
|
-
import type { Orchestrator, OrchestratorCreateRequest, OrchestratorCreateResult, OrchestratorStats } from '@serfab/cadre-provider';
|
|
12
|
-
import type { ManagedNodeInfo, NodeStateListener } from '../../orchestrator/types.js';
|
|
13
|
-
/**
|
|
14
|
-
* Mirrors the parts of `HostProcessOrchestrator`'s handle lifecycle the donation
|
|
15
|
-
* code depends on:
|
|
16
|
-
*
|
|
17
|
-
* - a **successful** `createContainer` drops every prior handle for the same
|
|
18
|
-
* `containerId` (the real `dropStaleHandle`), so an old `dockerId` stops
|
|
19
|
-
* resolving the moment its container re-spawns;
|
|
20
|
-
* - a **failed** `createContainer` leaves those handles exactly as it found them
|
|
21
|
-
* (the real `restoreDroppedHandles`);
|
|
22
|
-
* - `stopContainer` / `removeContainer` throw `Container not found: <dockerId>`
|
|
23
|
-
* for a handle this orchestrator no longer knows (the real `requireHandle`);
|
|
24
|
-
* - `reclaimWorkdir` refuses a containerId that still resolves to a live
|
|
25
|
-
* handle (the real guard — that directory belongs to `removeContainer`).
|
|
26
|
-
*
|
|
27
|
-
* Consequently `stopped` / `removed` mean **"handles this orchestrator actually
|
|
28
|
-
* acted on"**, not "calls attempted" — a stop aimed at a dropped handle is
|
|
29
|
-
* rejected and never appears. `onStop` still fires for those rejected calls, so
|
|
30
|
-
* a test can observe the attempt separately from its outcome.
|
|
31
|
-
*/
|
|
32
|
-
export declare class FakeOrchestrator implements Orchestrator {
|
|
33
|
-
createCalls: OrchestratorCreateRequest[];
|
|
34
|
-
stopped: string[];
|
|
35
|
-
removed: string[];
|
|
36
|
-
/**
|
|
37
|
-
* Container ids whose workdir this orchestrator actually removed. Same
|
|
38
|
-
* "acted on, not attempted" rule as `stopped` / `removed`: a refused reclaim
|
|
39
|
-
* leaves no entry.
|
|
40
|
-
*/
|
|
41
|
-
reclaimedWorkdirs: string[];
|
|
42
|
-
failCreate: boolean;
|
|
43
|
-
createDelayMs: number;
|
|
44
|
-
/**
|
|
45
|
-
* Observation hook — lets a test inspect the world as the stop happens. Fires
|
|
46
|
-
* even when the handle is unknown and the stop is then rejected.
|
|
47
|
-
*/
|
|
48
|
-
onStop?: (dockerId: string) => void;
|
|
49
|
-
/**
|
|
50
|
-
* Observation hook — fires before the spawn resolves or fails, modelling the
|
|
51
|
-
* real class's *pre-drop* `await` window (`ensureNodeIdentity`, `resolvePush`).
|
|
52
|
-
* Work driven from here sees the previous handle still alive. Contrast
|
|
53
|
-
* {@link onSpawned}.
|
|
54
|
-
*/
|
|
55
|
-
onCreate?: (request: OrchestratorCreateRequest) => void;
|
|
56
|
-
/**
|
|
57
|
-
* Observation hook — fires once the drop has landed and the new child is
|
|
58
|
-
* registered, immediately before `createContainer` resolves. Models the
|
|
59
|
-
* microtask gap between the real `createContainer` returning and its caller's
|
|
60
|
-
* `await` resuming: the real drop → launch → return runs synchronously, so
|
|
61
|
-
* this is the earliest point other code can observe the post-drop world.
|
|
62
|
-
* Contrast {@link onCreate}.
|
|
63
|
-
*/
|
|
64
|
-
onSpawned?: (dockerId: string) => void;
|
|
65
|
-
/** Observation hook — fires before the liveness answer is computed. */
|
|
66
|
-
onIsRunning?: (dockerId: string) => void;
|
|
67
|
-
private readonly children;
|
|
68
|
-
private readonly listeners;
|
|
69
|
-
private counter;
|
|
70
|
-
createContainer(request: OrchestratorCreateRequest): Promise<OrchestratorCreateResult>;
|
|
71
|
-
stopContainer(dockerId: string): Promise<void>;
|
|
72
|
-
removeContainer(dockerId: string): Promise<void>;
|
|
73
|
-
/**
|
|
74
|
-
* NOTE: this and {@link getLogs} take an unknown `dockerId` where the real
|
|
75
|
-
* class throws (`requireHandle`). No donation path calls either, so the gap is
|
|
76
|
-
* unobservable — route them through `requireChild` if one starts to.
|
|
77
|
-
*/
|
|
78
|
-
getStats(): Promise<OrchestratorStats>;
|
|
79
|
-
/** Unknown handles read as not running, matching the real orchestrator. */
|
|
80
|
-
isRunning(dockerId: string): Promise<boolean>;
|
|
81
|
-
/** Resolve a spawn's friendly containerId back to its dockerId. */
|
|
82
|
-
resolveDockerId(containerId: string): string | undefined;
|
|
83
|
-
/**
|
|
84
|
-
* Remove the workdir of a container no handle owns, mirroring the real
|
|
85
|
-
* `reclaimWorkdir`. Refuses (returns `false`, records nothing) while a child
|
|
86
|
-
* for that containerId still resolves — that directory belongs to
|
|
87
|
-
* `removeContainer`, which stops the child first.
|
|
88
|
-
*
|
|
89
|
-
* There is no directory here to check for existence, so a first reclaim of an
|
|
90
|
-
* unresolvable id always reports `true`; the real class returns `false` when
|
|
91
|
-
* the path is already gone. Nothing in the donation layer branches on the
|
|
92
|
-
* return value, so the gap is unobservable — model it if one starts to.
|
|
93
|
-
*/
|
|
94
|
-
reclaimWorkdir(containerId: string): boolean;
|
|
95
|
-
getLogs(): Promise<string>;
|
|
96
|
-
onStateChange(listener: NodeStateListener): () => void;
|
|
97
|
-
/** Kill a child out from under the host — the crash the supervisor exists for. */
|
|
98
|
-
crash(dockerId: string): void;
|
|
99
|
-
/** Emit an arbitrary state change (e.g. the owner node stopping). */
|
|
100
|
-
emit(info: ManagedNodeInfo): void;
|
|
101
|
-
/** Mirrors `HostProcessOrchestrator.requireHandle` — unknown handles throw. */
|
|
102
|
-
private requireChild;
|
|
103
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared fake orchestrator for the donation unit tests — no real child
|
|
3
|
-
* processes. Records every create / stop / remove, hands out deterministic
|
|
4
|
-
* unique spawn results, and tracks per-`dockerId` liveness so a test can crash
|
|
5
|
-
* one node and let the supervisor observe exactly that one as down.
|
|
6
|
-
*
|
|
7
|
-
* This file holds no tests of its own; its contract is pinned by the sibling
|
|
8
|
-
* `fake-orchestrator.test.ts`, which exists so a future agent cannot make a
|
|
9
|
-
* failing donation test go green by relaxing the fake.
|
|
10
|
-
*/
|
|
11
|
-
function sleep(ms) {
|
|
12
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Mirrors the parts of `HostProcessOrchestrator`'s handle lifecycle the donation
|
|
16
|
-
* code depends on:
|
|
17
|
-
*
|
|
18
|
-
* - a **successful** `createContainer` drops every prior handle for the same
|
|
19
|
-
* `containerId` (the real `dropStaleHandle`), so an old `dockerId` stops
|
|
20
|
-
* resolving the moment its container re-spawns;
|
|
21
|
-
* - a **failed** `createContainer` leaves those handles exactly as it found them
|
|
22
|
-
* (the real `restoreDroppedHandles`);
|
|
23
|
-
* - `stopContainer` / `removeContainer` throw `Container not found: <dockerId>`
|
|
24
|
-
* for a handle this orchestrator no longer knows (the real `requireHandle`);
|
|
25
|
-
* - `reclaimWorkdir` refuses a containerId that still resolves to a live
|
|
26
|
-
* handle (the real guard — that directory belongs to `removeContainer`).
|
|
27
|
-
*
|
|
28
|
-
* Consequently `stopped` / `removed` mean **"handles this orchestrator actually
|
|
29
|
-
* acted on"**, not "calls attempted" — a stop aimed at a dropped handle is
|
|
30
|
-
* rejected and never appears. `onStop` still fires for those rejected calls, so
|
|
31
|
-
* a test can observe the attempt separately from its outcome.
|
|
32
|
-
*/
|
|
33
|
-
export class FakeOrchestrator {
|
|
34
|
-
constructor() {
|
|
35
|
-
this.createCalls = [];
|
|
36
|
-
this.stopped = [];
|
|
37
|
-
this.removed = [];
|
|
38
|
-
/**
|
|
39
|
-
* Container ids whose workdir this orchestrator actually removed. Same
|
|
40
|
-
* "acted on, not attempted" rule as `stopped` / `removed`: a refused reclaim
|
|
41
|
-
* leaves no entry.
|
|
42
|
-
*/
|
|
43
|
-
this.reclaimedWorkdirs = [];
|
|
44
|
-
this.failCreate = false;
|
|
45
|
-
this.createDelayMs = 0;
|
|
46
|
-
this.children = new Map();
|
|
47
|
-
this.listeners = new Set();
|
|
48
|
-
this.counter = 0;
|
|
49
|
-
}
|
|
50
|
-
async createContainer(request) {
|
|
51
|
-
this.createCalls.push(request);
|
|
52
|
-
this.onCreate?.(request);
|
|
53
|
-
if (this.createDelayMs)
|
|
54
|
-
await sleep(this.createDelayMs);
|
|
55
|
-
if (this.failCreate)
|
|
56
|
-
throw new Error('spawn boom');
|
|
57
|
-
// Drop the prior handles for this container, mirroring `dropStaleHandle`.
|
|
58
|
-
// Success-only, and placed here on purpose: the real drop happens after the
|
|
59
|
-
// spawn's every `await` and cannot fail afterwards, and a create that throws
|
|
60
|
-
// puts the handles back (`restoreDroppedHandles`) — so a failed create must
|
|
61
|
-
// leave them untouched. `DonationSupervisor`'s give-up test depends on that.
|
|
62
|
-
for (const [id, child] of this.children) {
|
|
63
|
-
if (child.containerId === request.containerId)
|
|
64
|
-
this.children.delete(id);
|
|
65
|
-
}
|
|
66
|
-
const n = ++this.counter;
|
|
67
|
-
const dockerId = `dock_${n}`;
|
|
68
|
-
this.children.set(dockerId, {
|
|
69
|
-
containerId: request.containerId,
|
|
70
|
-
partyId: request.partyId,
|
|
71
|
-
profile: request.profile,
|
|
72
|
-
running: true,
|
|
73
|
-
});
|
|
74
|
-
this.onSpawned?.(dockerId);
|
|
75
|
-
return {
|
|
76
|
-
dockerId,
|
|
77
|
-
healthEndpoint: `http://127.0.0.1:${9000 + n}/health`,
|
|
78
|
-
metricsEndpoint: `http://127.0.0.1:${9000 + n}/metrics`,
|
|
79
|
-
seedEndpoint: `http://127.0.0.1:${9000 + n}/seed`,
|
|
80
|
-
seedToken: `seed-token-${n}`,
|
|
81
|
-
p2pPort: 4000 + n,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
async stopContainer(dockerId) {
|
|
85
|
-
// Before the check: a test may want to observe that a stop was *attempted*
|
|
86
|
-
// even when this orchestrator rejects it.
|
|
87
|
-
this.onStop?.(dockerId);
|
|
88
|
-
const child = this.requireChild(dockerId);
|
|
89
|
-
this.stopped.push(dockerId);
|
|
90
|
-
child.running = false;
|
|
91
|
-
}
|
|
92
|
-
async removeContainer(dockerId) {
|
|
93
|
-
// NOTE: the real `removeContainer` stops a still-live child first; this one
|
|
94
|
-
// only deletes, so a remove aimed at a running child records no `stopped`
|
|
95
|
-
// entry and emits no state change. Every donation caller stops before it
|
|
96
|
-
// reclaims, so nothing sees the difference today — if one stops doing that,
|
|
97
|
-
// stop the child here too.
|
|
98
|
-
this.requireChild(dockerId);
|
|
99
|
-
this.removed.push(dockerId);
|
|
100
|
-
this.children.delete(dockerId);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* NOTE: this and {@link getLogs} take an unknown `dockerId` where the real
|
|
104
|
-
* class throws (`requireHandle`). No donation path calls either, so the gap is
|
|
105
|
-
* unobservable — route them through `requireChild` if one starts to.
|
|
106
|
-
*/
|
|
107
|
-
async getStats() {
|
|
108
|
-
return { cpuPercent: 0, memoryBytes: 0, networkRxBytes: 0, networkTxBytes: 0 };
|
|
109
|
-
}
|
|
110
|
-
/** Unknown handles read as not running, matching the real orchestrator. */
|
|
111
|
-
async isRunning(dockerId) {
|
|
112
|
-
this.onIsRunning?.(dockerId);
|
|
113
|
-
return this.children.get(dockerId)?.running ?? false;
|
|
114
|
-
}
|
|
115
|
-
/** Resolve a spawn's friendly containerId back to its dockerId. */
|
|
116
|
-
resolveDockerId(containerId) {
|
|
117
|
-
for (const [dockerId, child] of this.children) {
|
|
118
|
-
if (child.containerId === containerId)
|
|
119
|
-
return dockerId;
|
|
120
|
-
}
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Remove the workdir of a container no handle owns, mirroring the real
|
|
125
|
-
* `reclaimWorkdir`. Refuses (returns `false`, records nothing) while a child
|
|
126
|
-
* for that containerId still resolves — that directory belongs to
|
|
127
|
-
* `removeContainer`, which stops the child first.
|
|
128
|
-
*
|
|
129
|
-
* There is no directory here to check for existence, so a first reclaim of an
|
|
130
|
-
* unresolvable id always reports `true`; the real class returns `false` when
|
|
131
|
-
* the path is already gone. Nothing in the donation layer branches on the
|
|
132
|
-
* return value, so the gap is unobservable — model it if one starts to.
|
|
133
|
-
*/
|
|
134
|
-
reclaimWorkdir(containerId) {
|
|
135
|
-
if (this.resolveDockerId(containerId))
|
|
136
|
-
return false;
|
|
137
|
-
this.reclaimedWorkdirs.push(containerId);
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
async getLogs() {
|
|
141
|
-
return '';
|
|
142
|
-
}
|
|
143
|
-
onStateChange(listener) {
|
|
144
|
-
this.listeners.add(listener);
|
|
145
|
-
return () => { this.listeners.delete(listener); };
|
|
146
|
-
}
|
|
147
|
-
/** Kill a child out from under the host — the crash the supervisor exists for. */
|
|
148
|
-
crash(dockerId) {
|
|
149
|
-
const child = this.children.get(dockerId);
|
|
150
|
-
if (!child?.running)
|
|
151
|
-
return;
|
|
152
|
-
child.running = false;
|
|
153
|
-
this.emit(toNodeInfo(dockerId, child));
|
|
154
|
-
}
|
|
155
|
-
/** Emit an arbitrary state change (e.g. the owner node stopping). */
|
|
156
|
-
emit(info) {
|
|
157
|
-
for (const listener of this.listeners)
|
|
158
|
-
listener(info);
|
|
159
|
-
}
|
|
160
|
-
/** Mirrors `HostProcessOrchestrator.requireHandle` — unknown handles throw. */
|
|
161
|
-
requireChild(dockerId) {
|
|
162
|
-
const child = this.children.get(dockerId);
|
|
163
|
-
if (!child)
|
|
164
|
-
throw new Error(`Container not found: ${dockerId}`);
|
|
165
|
-
return child;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function toNodeInfo(dockerId, child) {
|
|
169
|
-
return {
|
|
170
|
-
id: child.containerId,
|
|
171
|
-
dockerId,
|
|
172
|
-
partyId: child.partyId,
|
|
173
|
-
profile: child.profile,
|
|
174
|
-
status: child.running ? 'running' : 'stopped',
|
|
175
|
-
spawnedAt: new Date(0).toISOString(),
|
|
176
|
-
workdir: `/fake/${child.containerId}`,
|
|
177
|
-
ports: { health: 0, metrics: 0, p2p: 0, admin: 0, ws: 0 },
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
//# sourceMappingURL=fake-orchestrator.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fake-orchestrator.js","sourceRoot":"","sources":["../../../src/donation/__tests__/fake-orchestrator.ts"],"names":[],"mappings":"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"}
|