@celilo/cli 2.2.1 → 3.0.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/CELILO_CORE_MODULES.md +2 -1
- package/CELILO_SUBSYSTEMS.md +1 -1
- package/README.md +1 -1
- package/drizzle/0032_module_jail_policies.sql +28 -0
- package/drizzle/0033_build_bus_hook_runs.sql +37 -0
- package/drizzle/meta/_journal.json +15 -1
- package/package.json +3 -3
- package/src/__integration__/container-services-cli.integration.test.ts +1 -1
- package/src/api/serve.ts +13 -1
- package/src/api/sessions.test.ts +3 -3
- package/src/api-clients/proxmox.ts +10 -7
- package/src/cli/backup-rename.test.ts +3 -3
- package/src/cli/cli.test.ts +1 -1
- package/src/cli/commands/events.test.ts +2 -2
- package/src/cli/commands/firewall-interface-list.test.ts +2 -2
- package/src/cli/commands/machine-list.test.ts +57 -0
- package/src/cli/commands/machine-list.ts +35 -4
- package/src/cli/commands/module-health.test.ts +35 -0
- package/src/cli/commands/module-health.ts +12 -3
- package/src/cli/commands/module-import-registry.test.ts +1 -1
- package/src/cli/commands/module-jail.test.ts +242 -0
- package/src/cli/commands/module-jail.ts +227 -0
- package/src/cli/commands/module-list-jail.test.ts +136 -0
- package/src/cli/commands/module-list.ts +30 -3
- package/src/cli/commands/module-publish.test.ts +28 -6
- package/src/cli/commands/module-publish.ts +14 -12
- package/src/cli/commands/module-update.test.ts +22 -7
- package/src/cli/commands/module-update.ts +73 -18
- package/src/cli/commands/module-upgrade-gate.test.ts +154 -0
- package/src/cli/commands/module-upgrade.test.ts +115 -1
- package/src/cli/commands/module-upgrade.ts +66 -4
- package/src/cli/commands/module-verify.test.ts +1 -1
- package/src/cli/commands/publish/execute.ts +4 -1
- package/src/cli/commands/publish/helpers.ts +4 -3
- package/src/cli/commands/publish/index.ts +23 -1
- package/src/cli/commands/publish/module-registry.test.ts +24 -1
- package/src/cli/commands/publish/module-registry.ts +23 -4
- package/src/cli/commands/publish/plan.test.ts +64 -0
- package/src/cli/commands/publish/plan.ts +52 -19
- package/src/cli/commands/publish/types.ts +20 -0
- package/src/cli/commands/registry-owner.test.ts +1 -1
- package/src/cli/commands/registry-token.test.ts +1 -1
- package/src/cli/commands/subscribers-install-daemon.test.ts +44 -0
- package/src/cli/commands/subscribers-install-daemon.ts +107 -0
- package/src/cli/commands/subscribers-serve.test.ts +22 -0
- package/src/cli/commands/subscribers-serve.ts +22 -4
- package/src/cli/commands/system-audit.ts +34 -11
- package/src/cli/commands/system-doctor-remediation-gate.test.ts +295 -0
- package/src/cli/commands/system-doctor.test.ts +76 -10
- package/src/cli/commands/system-doctor.ts +34 -3
- package/src/cli/commands/system-init-deprecation.test.ts +1 -1
- package/src/cli/commands/system-update.ts +16 -6
- package/src/cli/completion.ts +18 -3
- package/src/cli/flag-surface-gate.test.ts +279 -0
- package/src/cli/fuel-gauge.ts +12 -4
- package/src/cli/index.ts +80 -2
- package/src/cli/json-output.test.ts +81 -0
- package/src/cli/parser.test.ts +37 -1
- package/src/cli/restore-command.test.ts +3 -3
- package/src/cli/restore-migration-failure.test.ts +2 -2
- package/src/cli/tui/audit-state.ts +2 -0
- package/src/cli/types.ts +9 -0
- package/src/config/paths.test.ts +19 -19
- package/src/db/client.test.ts +46 -1
- package/src/db/client.ts +26 -0
- package/src/db/schema.ts +63 -0
- package/src/hooks/broker.test.ts +9 -3
- package/src/hooks/capability-loader-firewall.test.ts +9 -1
- package/src/hooks/capability-loader.ts +8 -0
- package/src/hooks/executor.test.ts +91 -14
- package/src/hooks/executor.ts +31 -1
- package/src/hooks/hook-jail-toolchain-reach.test.ts +8 -3
- package/src/hooks/hook-jail-unreachability.test.ts +27 -20
- package/src/hooks/hook-trespass.test.ts +22 -6
- package/src/hooks/jail.test.ts +105 -17
- package/src/hooks/jail.ts +47 -10
- package/src/hooks/run-named-hook.ts +19 -16
- package/src/hooks/test-fixtures/artifact-writing-hook.ts +0 -1
- package/src/hooks/test-fixtures/capability-calling-hook.ts +20 -13
- package/src/hooks/test-fixtures/jail-probe-hook.ts +9 -1
- package/src/hooks/test-fixtures/jail-toolchain-hook.ts +7 -2
- package/src/hooks/test-fixtures/runaway-hook.ts +0 -1
- package/src/hooks/test-fixtures/sigterm-ignoring-hook.ts +0 -1
- package/src/hooks/test-fixtures/silent-hook.ts +0 -1
- package/src/hooks/test-fixtures/store-writing-hook.ts +20 -11
- package/src/hooks/test-fixtures/success-hook.ts +4 -4
- package/src/manifest/contracts/v1.ts +32 -14
- package/src/manifest/json-schema-roundtrip.test.ts +1 -1
- package/src/manifest/schema.ts +66 -16
- package/src/manifest/validate.test.ts +47 -0
- package/src/policy/capability-shape-baseline.ts +63 -21
- package/src/policy/capability-shape-drift.test.ts +53 -1
- package/src/policy/capability-shape.test.ts +105 -0
- package/src/policy/capability-shape.ts +283 -2
- package/src/policy/module-script-scan.ts +52 -68
- package/src/policy/no-hand-built-ssh.test.ts +5 -1
- package/src/policy/no-swallowed-refusal.test.ts +14 -14
- package/src/registry/client.test.ts +67 -2
- package/src/registry/client.ts +7 -7
- package/src/secrets/storage.test.ts +70 -4
- package/src/secrets/storage.ts +71 -1
- package/src/services/alerting/keys.test.ts +4 -0
- package/src/services/alerting/keys.ts +12 -2
- package/src/services/audit/health.test.ts +155 -2
- package/src/services/audit/health.ts +76 -2
- package/src/services/audit/index.test.ts +23 -1
- package/src/services/audit/index.ts +8 -2
- package/src/services/audit/interface-classification.test.ts +16 -5
- package/src/services/audit/interface-classification.ts +25 -2
- package/src/services/audit/jail-exemptions.test.ts +42 -0
- package/src/services/audit/jail-exemptions.ts +44 -0
- package/src/services/audit/module-integrity.test.ts +23 -1
- package/src/services/audit/module-integrity.ts +7 -2
- package/src/services/audit/module-versions.ts +5 -1
- package/src/services/audit/public-dns.test.ts +20 -0
- package/src/services/audit/public-dns.ts +7 -2
- package/src/services/audit/recurrence-gate.test.ts +225 -0
- package/src/services/audit/trusted-sources.test.ts +17 -0
- package/src/services/audit/trusted-sources.ts +21 -0
- package/src/services/audit/types.ts +2 -1
- package/src/services/backup-create.ts +9 -0
- package/src/services/backup-envelope-roundtrip.test.ts +1 -1
- package/src/services/backup-in-flight-refusal.test.ts +1 -1
- package/src/services/build-bus/hook-dispatch-executor.test.ts +269 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +105 -105
- package/src/services/build-bus/hook-dispatch-path.test.ts +95 -0
- package/src/services/build-bus/hook-dispatch.test.ts +86 -116
- package/src/services/build-bus/hook-dispatch.ts +99 -121
- package/src/services/build-bus/hook-dispatcher.ts +143 -17
- package/src/services/build-bus/receiver-daemon.test.ts +189 -0
- package/src/services/build-bus/receiver-daemon.ts +355 -0
- package/src/services/build-bus/self-update.ts +156 -0
- package/src/services/bus-ensure-flow.test.ts +1 -1
- package/src/services/bus-interview-park.test.ts +2 -2
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-secret-flow.test.ts +1 -1
- package/src/services/capability-compat.test.ts +90 -0
- package/src/services/capability-compat.ts +128 -0
- package/src/services/celilo-events.test.ts +1 -1
- package/src/services/celilo-mgmt-hooks.test.ts +23 -5
- package/src/services/container-service.test.ts +1 -1
- package/src/services/cross-module-read.test.ts +2 -2
- package/src/services/deploy-preflight.ts +7 -0
- package/src/services/deploy-terraform.ts +38 -1
- package/src/services/deployed-systems.ts +1 -1
- package/src/services/events-daemon.test.ts +57 -0
- package/src/services/events-daemon.ts +76 -0
- package/src/services/firewall-reach.ts +21 -8
- package/src/services/fleet-checks.test.ts +159 -4
- package/src/services/fleet-checks.ts +206 -3
- package/src/services/health-runner.test.ts +87 -2
- package/src/services/health-runner.ts +83 -16
- package/src/services/infrastructure-selector.test.ts +1 -1
- package/src/services/jail-exemptions.test.ts +125 -0
- package/src/services/jail-exemptions.ts +81 -0
- package/src/services/machine-pool.test.ts +1 -1
- package/src/services/module-deploy-prune.test.ts +89 -0
- package/src/services/module-deploy.ts +84 -129
- package/src/services/module-subscriptions.test.ts +2 -2
- package/src/services/module-types-drift.test.ts +1 -1
- package/src/services/module-validator/capability-versions.test.ts +13 -2
- package/src/services/network-discovery.test.ts +64 -1
- package/src/services/network-discovery.ts +30 -5
- package/src/services/responder-probe.test.ts +1 -1
- package/src/services/restore-from-file.test.ts +4 -4
- package/src/services/restore-preflight.test.ts +1 -1
- package/src/services/ssh-key-manager.test.ts +2 -2
- package/src/services/system-state-stage.test.ts +2 -2
- package/src/services/terraform-safety.test.ts +83 -0
- package/src/services/terraform-safety.ts +53 -0
- package/src/services/update/orchestrator.test.ts +3 -1
- package/src/test-utils/bus-responder.ts +1 -1
- package/tsconfig.json +2 -13
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build-bus hook dispatcher service.
|
|
3
3
|
*
|
|
4
|
-
* Stateful wrapper around `planHookDispatch` + `
|
|
4
|
+
* Stateful wrapper around `planHookDispatch` + `runUpstreamHook`:
|
|
5
5
|
* loads installed-module manifests from the celilo DB, dispatches
|
|
6
6
|
* matching `on_upstream_publish` hooks when a verified PublishEvent
|
|
7
|
-
* arrives,
|
|
7
|
+
* arrives, and records every run durably in the `build_bus_hook_runs`
|
|
8
|
+
* ledger (celilo#1304) — without which a failed run exists only as
|
|
9
|
+
* daemon console output, which no surface reads. Since
|
|
10
|
+
* module-orchestrator-primitives slice 7 it also performs the FRAMEWORK
|
|
11
|
+
* self-update on the same events (tasks.md 7.2): celilo replacing its
|
|
12
|
+
* own binaries on the management host is class H work that stopped
|
|
13
|
+
* being a module hook (design D6).
|
|
8
14
|
*
|
|
9
15
|
* Started by the `celilo subscribers serve` daemon (in-process,
|
|
10
16
|
* sharing the same lifetime); the receiver-server calls
|
|
@@ -18,62 +24,167 @@
|
|
|
18
24
|
import { readFileSync } from 'node:fs';
|
|
19
25
|
import { join } from 'node:path';
|
|
20
26
|
import type { PublishEvent } from '@celilo/event-bus/build-bus';
|
|
27
|
+
import { eq } from 'drizzle-orm';
|
|
21
28
|
import { parse as parseYaml } from 'yaml';
|
|
22
29
|
import { getDb } from '../../db/client';
|
|
23
|
-
import { modules } from '../../db/schema';
|
|
30
|
+
import { buildBusHookRuns, modules } from '../../db/schema';
|
|
31
|
+
import { CONTROL_PLANE_MODULE_ID } from '../deployed-systems';
|
|
24
32
|
import {
|
|
25
|
-
type
|
|
33
|
+
type HookDispatchPlan,
|
|
26
34
|
type ModuleHookContext,
|
|
27
35
|
type UpstreamHookEntry,
|
|
36
|
+
type UpstreamHookResult,
|
|
37
|
+
defaultUpstreamHookDeps,
|
|
28
38
|
planHookDispatch,
|
|
29
|
-
|
|
39
|
+
runUpstreamHook,
|
|
30
40
|
} from './hook-dispatch';
|
|
41
|
+
import { type SelfUpdatePlan, planSelfUpdate, runSelfUpdate } from './self-update';
|
|
31
42
|
|
|
32
43
|
export interface HookDispatcher {
|
|
33
44
|
/** Run the matching hooks for a single event. Returns the per-hook results. */
|
|
34
|
-
handleEvent(event: PublishEvent): Promise<
|
|
45
|
+
handleEvent(event: PublishEvent): Promise<UpstreamHookResult[]>;
|
|
35
46
|
/** Clean up (currently a no-op; included for symmetry with the receiver). */
|
|
36
47
|
stop(): Promise<void>;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
export interface HookDispatcherOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Database for the default executor hop. Production omits it and the
|
|
53
|
+
* process database opens lazily; tests inject an isolated client.
|
|
54
|
+
*/
|
|
55
|
+
db?: Parameters<typeof defaultUpstreamHookDeps>[0];
|
|
40
56
|
/**
|
|
41
57
|
* Override the module-loading step. Tests inject a fixture; in
|
|
42
58
|
* production we omit and the dispatcher reads from the celilo
|
|
43
59
|
* modules table.
|
|
44
60
|
*/
|
|
45
61
|
loadModules?: () => ModuleHookContext[];
|
|
62
|
+
/**
|
|
63
|
+
* Override the executor hop for one matched upstream-publish entry.
|
|
64
|
+
* Production omits it; `runUpstreamHook` over the process database runs.
|
|
65
|
+
*/
|
|
66
|
+
runUpstreamHook?: (plan: HookDispatchPlan, event: PublishEvent) => Promise<UpstreamHookResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Override the durable outcome record. The production default writes one
|
|
69
|
+
* `build_bus_hook_runs` row per run — without it a failed hook run or
|
|
70
|
+
* self-update exists only as daemon console output, which no surface
|
|
71
|
+
* reads (celilo#1304). Tests inject a capture to stay DB-free. The plan
|
|
72
|
+
* is passed because the jailed executor result does not carry the
|
|
73
|
+
* script path; the ledger's `script_path` column is NOT NULL.
|
|
74
|
+
*/
|
|
75
|
+
recordRun?: (event: PublishEvent, plan: HookDispatchPlan, result: UpstreamHookResult) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Override "is the control-plane module installed", the gate for the
|
|
78
|
+
* framework self-update. Production reads the modules table; tests
|
|
79
|
+
* inject the answer.
|
|
80
|
+
*/
|
|
81
|
+
controlPlaneInstalled?: () => boolean;
|
|
82
|
+
/** Override the self-update runner. Production: `runSelfUpdate`. */
|
|
83
|
+
selfUpdate?: (plan: SelfUpdatePlan) => ReturnType<typeof runSelfUpdate>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Bound on error tails so one chatty hook cannot bloat the ledger. */
|
|
87
|
+
const TAIL_CHARS = 2000;
|
|
88
|
+
|
|
89
|
+
function tail(s: string): string {
|
|
90
|
+
return s.length > TAIL_CHARS ? s.slice(-TAIL_CHARS) : s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The production outcome record: one `build_bus_hook_runs` row per run.
|
|
95
|
+
* Throwing here would 500 nothing (the webhook already succeeded) but
|
|
96
|
+
* would kill dispatch of later plans, so a record failure is logged and
|
|
97
|
+
* swallowed — the console outcome lines below remain the fallback signal.
|
|
98
|
+
*
|
|
99
|
+
* The jailed executor path (slice 7) does not expose exit codes or
|
|
100
|
+
* captured stdio, so `exit_code` is always null — the column's "the
|
|
101
|
+
* script never reached an exit" semantics now hold of the path itself —
|
|
102
|
+
* and the executor's error text lands in `stderr_tail`.
|
|
103
|
+
*/
|
|
104
|
+
function recordHookRun(
|
|
105
|
+
event: PublishEvent,
|
|
106
|
+
plan: HookDispatchPlan,
|
|
107
|
+
result: UpstreamHookResult,
|
|
108
|
+
): void {
|
|
109
|
+
try {
|
|
110
|
+
getDb()
|
|
111
|
+
.insert(buildBusHookRuns)
|
|
112
|
+
.values({
|
|
113
|
+
eventId: event.eventId,
|
|
114
|
+
packageName: event.package.name,
|
|
115
|
+
packageVersion: event.package.version,
|
|
116
|
+
tag: event.tag,
|
|
117
|
+
moduleId: result.module,
|
|
118
|
+
hookName: result.hookName,
|
|
119
|
+
scriptPath: plan.hook.script,
|
|
120
|
+
exitCode: null,
|
|
121
|
+
timedOut: false,
|
|
122
|
+
durationMs: result.durationMs,
|
|
123
|
+
stdoutTail: null,
|
|
124
|
+
stderrTail: result.error ? tail(result.error) : null,
|
|
125
|
+
})
|
|
126
|
+
.run();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.warn(
|
|
129
|
+
`[build-bus] could not record hook run outcome: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
46
132
|
}
|
|
47
133
|
|
|
48
134
|
export async function startHookDispatcher(
|
|
49
135
|
opts: HookDispatcherOptions = {},
|
|
50
136
|
): Promise<HookDispatcher> {
|
|
51
137
|
const loadModules = opts.loadModules ?? loadInstalledModulesWithHooks;
|
|
138
|
+
const runEntry =
|
|
139
|
+
opts.runUpstreamHook ??
|
|
140
|
+
((plan, event) => runUpstreamHook(plan, event, defaultUpstreamHookDeps(opts.db)));
|
|
141
|
+
const recordRun = opts.recordRun ?? recordHookRun;
|
|
142
|
+
const isControlPlaneInstalled = opts.controlPlaneInstalled ?? defaultControlPlaneInstalled;
|
|
143
|
+
const selfUpdateRunner = opts.selfUpdate ?? ((plan) => runSelfUpdate(plan));
|
|
52
144
|
|
|
53
145
|
return {
|
|
54
146
|
async handleEvent(event) {
|
|
147
|
+
// Framework self-update first (design D6): celilo replacing celilo's
|
|
148
|
+
// own binaries is not a module hook, and the dispatcher is the
|
|
149
|
+
// component that already receives the publish event.
|
|
150
|
+
const update = planSelfUpdate(event);
|
|
151
|
+
if (update && isControlPlaneInstalled()) {
|
|
152
|
+
console.log(
|
|
153
|
+
`[build-bus] self-update: ${update.packageName}@${update.version} (@${event.tag})`,
|
|
154
|
+
);
|
|
155
|
+
const result = selfUpdateRunner(update);
|
|
156
|
+
if (result.updated && result.verified) {
|
|
157
|
+
console.log(
|
|
158
|
+
` ✓ ${update.binary} reports ${result.version} (was ${result.beforeVersion ?? 'not installed'})`,
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
console.warn(` ✗ self-update did not complete: ${result.error ?? 'unknown error'}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
55
165
|
const moduleContexts = loadModules();
|
|
56
166
|
const plans = planHookDispatch(event, moduleContexts);
|
|
57
167
|
if (plans.length === 0) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
168
|
+
if (!update) {
|
|
169
|
+
console.log(
|
|
170
|
+
`[build-bus] ${event.package.name}@${event.package.version} (${event.tag}) — no module hooks match`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
61
173
|
return [];
|
|
62
174
|
}
|
|
63
|
-
const results:
|
|
175
|
+
const results: UpstreamHookResult[] = [];
|
|
64
176
|
for (const plan of plans) {
|
|
65
177
|
console.log(
|
|
66
178
|
`[build-bus] ${event.package.name}@${event.package.version} → ${plan.module.moduleId} (${plan.hook.name ?? plan.hook.script})`,
|
|
67
179
|
);
|
|
68
|
-
const result = await
|
|
180
|
+
const result = await runEntry(plan, event);
|
|
181
|
+
recordRun(event, plan, result);
|
|
69
182
|
results.push(result);
|
|
70
|
-
if (result.
|
|
71
|
-
console.log(` ✓
|
|
72
|
-
} else if (result.timedOut) {
|
|
73
|
-
console.warn(` ✗ timed out after ${result.durationMs}ms`);
|
|
183
|
+
if (result.success) {
|
|
184
|
+
console.log(` ✓ success in ${result.durationMs}ms`);
|
|
74
185
|
} else {
|
|
75
|
-
console.warn(` ✗
|
|
76
|
-
if (result.
|
|
186
|
+
console.warn(` ✗ failed in ${result.durationMs}ms`);
|
|
187
|
+
if (result.error) console.warn(` ${result.error.trim().slice(0, 500)}`);
|
|
77
188
|
}
|
|
78
189
|
}
|
|
79
190
|
return results;
|
|
@@ -85,6 +196,21 @@ export async function startHookDispatcher(
|
|
|
85
196
|
};
|
|
86
197
|
}
|
|
87
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The framework self-update runs only where the control-plane module is
|
|
201
|
+
* deployed — the same population celilo-mgmt's hook covered, since the
|
|
202
|
+
* module is installed on the management host and nowhere else.
|
|
203
|
+
*/
|
|
204
|
+
function defaultControlPlaneInstalled(): boolean {
|
|
205
|
+
const db = getDb();
|
|
206
|
+
const row = db
|
|
207
|
+
.select({ id: modules.id })
|
|
208
|
+
.from(modules)
|
|
209
|
+
.where(eq(modules.id, CONTROL_PLANE_MODULE_ID))
|
|
210
|
+
.get();
|
|
211
|
+
return row != null;
|
|
212
|
+
}
|
|
213
|
+
|
|
88
214
|
/**
|
|
89
215
|
* Load every installed module's `on_upstream_publish` hooks from the
|
|
90
216
|
* celilo DB + each module's manifest.yml. Modules without the hook
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
getReceiverUnitPath,
|
|
7
|
+
installReceiverDaemon,
|
|
8
|
+
planReceiverInstall,
|
|
9
|
+
renderReceiverLaunchdPlist,
|
|
10
|
+
renderReceiverSystemdUnit,
|
|
11
|
+
uninstallReceiverDaemon,
|
|
12
|
+
} from './receiver-daemon';
|
|
13
|
+
|
|
14
|
+
const SECRET = 'a1b2c3d4e5f6secret';
|
|
15
|
+
|
|
16
|
+
// resolveCeliloPath existence-checks explicit overrides, so plan tests need
|
|
17
|
+
// a path that exists — a fake binary in a temp dir.
|
|
18
|
+
const FAKE_CELOLILO = join(mkdtempSync(join(tmpdir(), 'celilo-fake-bin-')), 'celilo');
|
|
19
|
+
writeFileSync(FAKE_CELOLILO, '#!/bin/sh\n', { mode: 0o755 });
|
|
20
|
+
|
|
21
|
+
describe('renderReceiverSystemdUnit', () => {
|
|
22
|
+
it('runs `subscribers serve` with --no-dispatch and the secret as an Environment line', () => {
|
|
23
|
+
const out = renderReceiverSystemdUnit({
|
|
24
|
+
celiloPath: FAKE_CELOLILO,
|
|
25
|
+
port: 8123,
|
|
26
|
+
secret: SECRET,
|
|
27
|
+
scope: 'user',
|
|
28
|
+
dispatch: false,
|
|
29
|
+
});
|
|
30
|
+
expect(out).toContain('subscribers serve --port 8123 --no-dispatch');
|
|
31
|
+
expect(out).toContain(`Environment=CELILO_BUS_SECRET=${SECRET}`);
|
|
32
|
+
expect(out).toContain('Restart=on-failure');
|
|
33
|
+
expect(out).toContain('WantedBy=default.target');
|
|
34
|
+
expect(out).not.toContain('User=');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('honors the port and a --dispatch render for boxes without the events daemon', () => {
|
|
38
|
+
const out = renderReceiverSystemdUnit({
|
|
39
|
+
celiloPath: '/c',
|
|
40
|
+
port: 9999,
|
|
41
|
+
secret: SECRET,
|
|
42
|
+
scope: 'user',
|
|
43
|
+
dispatch: true,
|
|
44
|
+
});
|
|
45
|
+
expect(out).toContain('ExecStart=/c subscribers serve --port 9999');
|
|
46
|
+
expect(out).not.toContain('--no-dispatch');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('system scope sets an explicit User= and multi-user.target', () => {
|
|
50
|
+
const out = renderReceiverSystemdUnit({
|
|
51
|
+
celiloPath: FAKE_CELOLILO,
|
|
52
|
+
port: 8123,
|
|
53
|
+
secret: SECRET,
|
|
54
|
+
scope: 'system',
|
|
55
|
+
runAsUser: 'celilo',
|
|
56
|
+
dispatch: false,
|
|
57
|
+
});
|
|
58
|
+
expect(out).toContain('User=celilo');
|
|
59
|
+
expect(out).toContain('WantedBy=multi-user.target');
|
|
60
|
+
expect(out).toContain('journalctl -u celilo-build-bus-receiver.service');
|
|
61
|
+
expect(out).not.toContain('journalctl --user');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('renderReceiverLaunchdPlist', () => {
|
|
66
|
+
it('carries the secret under EnvironmentVariables and --no-dispatch by default', () => {
|
|
67
|
+
const out = renderReceiverLaunchdPlist({
|
|
68
|
+
celiloPath: FAKE_CELOLILO,
|
|
69
|
+
port: 8123,
|
|
70
|
+
secret: SECRET,
|
|
71
|
+
scope: 'system',
|
|
72
|
+
home: '/root',
|
|
73
|
+
dispatch: false,
|
|
74
|
+
});
|
|
75
|
+
expect(out).toContain('<key>Label</key>');
|
|
76
|
+
expect(out).toContain('com.celilo.build-bus-receiver');
|
|
77
|
+
expect(out).toContain('<key>CELILO_BUS_SECRET</key>');
|
|
78
|
+
expect(out).toContain(`<string>${SECRET}</string>`);
|
|
79
|
+
expect(out).toContain('<string>--no-dispatch</string>');
|
|
80
|
+
expect(out).toContain('<string>subscribers</string>');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getReceiverUnitPath', () => {
|
|
85
|
+
it('names the unit distinctly from the dispatcher unit in every platform/scope', () => {
|
|
86
|
+
expect(getReceiverUnitPath('linux', '/home/op', 'user')).toBe(
|
|
87
|
+
join('/home/op/.config/systemd/user', 'celilo-build-bus-receiver.service'),
|
|
88
|
+
);
|
|
89
|
+
expect(getReceiverUnitPath('linux', '/root', 'system')).toBe(
|
|
90
|
+
'/etc/systemd/system/celilo-build-bus-receiver.service',
|
|
91
|
+
);
|
|
92
|
+
expect(getReceiverUnitPath('darwin', '/root', 'system')).toBe(
|
|
93
|
+
'/Library/LaunchDaemons/com.celilo.build-bus-receiver.plist',
|
|
94
|
+
);
|
|
95
|
+
expect(getReceiverUnitPath('darwin', '/home/op', 'user')).toBe(
|
|
96
|
+
'/home/op/Library/LaunchAgents/com.celilo.build-bus-receiver.plist',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('planReceiverInstall', () => {
|
|
102
|
+
it('requires a secret and refuses to invent one', () => {
|
|
103
|
+
expect(() => planReceiverInstall({ secret: undefined, home: '/h' })).toThrow(/secret/i);
|
|
104
|
+
expect(() => planReceiverInstall({ secret: '', home: '/h' })).toThrow(/secret/i);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('plans a user-scope unit that runs the receiver receiver-only by default', () => {
|
|
108
|
+
const plan = planReceiverInstall({
|
|
109
|
+
secret: SECRET,
|
|
110
|
+
home: '/home/op',
|
|
111
|
+
platform: 'linux',
|
|
112
|
+
celiloPath: FAKE_CELOLILO,
|
|
113
|
+
});
|
|
114
|
+
expect(plan.platform).toBe('linux');
|
|
115
|
+
expect(plan.scope).toBe('user');
|
|
116
|
+
expect(plan.unitContent).toContain('--no-dispatch');
|
|
117
|
+
expect(plan.unitContent).toContain(`CELILO_BUS_SECRET=${SECRET}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('plans a system-scope unit with an explicit run-as user', () => {
|
|
121
|
+
const plan = planReceiverInstall({
|
|
122
|
+
secret: SECRET,
|
|
123
|
+
platform: 'linux',
|
|
124
|
+
scope: 'system',
|
|
125
|
+
home: '/root',
|
|
126
|
+
runAsUser: 'celilo',
|
|
127
|
+
celiloPath: FAKE_CELOLILO,
|
|
128
|
+
});
|
|
129
|
+
expect(plan.unitContent).toContain('User=celilo');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('system scope falls back to the bus-dir owner when no run-as user is given', () => {
|
|
133
|
+
// events-daemon's resolveRunAsUser is the shared seam; here we only pin
|
|
134
|
+
// that the receiver unit passes a runAsUser through rather than dropping it.
|
|
135
|
+
const plan = planReceiverInstall({
|
|
136
|
+
secret: SECRET,
|
|
137
|
+
platform: 'linux',
|
|
138
|
+
scope: 'system',
|
|
139
|
+
home: '/root',
|
|
140
|
+
runAsUser: 'celilo',
|
|
141
|
+
celiloPath: FAKE_CELOLILO,
|
|
142
|
+
});
|
|
143
|
+
expect(plan.runAsUser).toBe('celilo');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('installReceiverDaemon / uninstallReceiverDaemon', () => {
|
|
148
|
+
const home = mkdtempSync(join(tmpdir(), 'celilo-receiver-daemon-'));
|
|
149
|
+
|
|
150
|
+
it('writes the unit file mode 0600 (it carries a secret) and idempotently rewrites', () => {
|
|
151
|
+
// celiloPath is passed explicitly: the unit under test must not
|
|
152
|
+
// consult the ambient PATH (resolveCeliloPath throws where no global
|
|
153
|
+
// celilo is installed — forge runners). Every planReceiverInstall test
|
|
154
|
+
// above injects FAKE_CELOLILO; the install path resolves the same way.
|
|
155
|
+
const first = installReceiverDaemon({
|
|
156
|
+
secret: SECRET,
|
|
157
|
+
platform: 'linux',
|
|
158
|
+
scope: 'user',
|
|
159
|
+
home,
|
|
160
|
+
celiloPath: FAKE_CELOLILO,
|
|
161
|
+
});
|
|
162
|
+
expect(existsSync(first.unitPath)).toBe(true);
|
|
163
|
+
expect(statSync(first.unitPath).mode & 0o777).toBe(0o600);
|
|
164
|
+
const content = readFileSync(first.unitPath, 'utf-8');
|
|
165
|
+
expect(content).toContain('--no-dispatch');
|
|
166
|
+
expect(content).toContain(`CELILO_BUS_SECRET=${SECRET}`);
|
|
167
|
+
|
|
168
|
+
// Idempotent second install.
|
|
169
|
+
const second = installReceiverDaemon({
|
|
170
|
+
secret: SECRET,
|
|
171
|
+
platform: 'linux',
|
|
172
|
+
scope: 'user',
|
|
173
|
+
home,
|
|
174
|
+
celiloPath: FAKE_CELOLILO,
|
|
175
|
+
});
|
|
176
|
+
expect(second.unitPath).toBe(first.unitPath);
|
|
177
|
+
|
|
178
|
+
const removed = uninstallReceiverDaemon({ platform: 'linux', scope: 'user', home });
|
|
179
|
+
expect(removed.removed).toBe(true);
|
|
180
|
+
expect(existsSync(first.unitPath)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('uninstall of a missing unit reports removed=false', () => {
|
|
184
|
+
const empty = mkdtempSync(join(tmpdir(), 'celilo-receiver-daemon-'));
|
|
185
|
+
const result = uninstallReceiverDaemon({ platform: 'linux', scope: 'user', home: empty });
|
|
186
|
+
expect(result.removed).toBe(false);
|
|
187
|
+
rmSync(empty, { recursive: true, force: true });
|
|
188
|
+
});
|
|
189
|
+
});
|