@edgehero/pi-dispatch 1.10.3 → 2.1.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/.env.example +303 -150
- package/README.md +52 -0
- package/deploy/com.pi-dispatch.worker.plist +10 -4
- package/deploy/docker-compose.yml +49 -16
- package/deploy/egress-proxy.conf +32 -2
- package/deploy/nssm-install.cmd +12 -6
- package/deploy/pi-dispatch-egress-out.network +10 -0
- package/deploy/pi-dispatch-egress-proxy.container +50 -0
- package/deploy/pi-dispatch-netns-keeper.container +80 -0
- package/deploy/pi-dispatch-netns-keeper.network +18 -0
- package/deploy/pi-dispatch-valkey.container +51 -0
- package/deploy/pi-dispatch-valkey.network +16 -0
- package/deploy/receiver.service +6 -0
- package/deploy/worker-env-wrapper.cmd +12 -1
- package/deploy/worker-env-wrapper.sh +63 -37
- package/deploy/worker.service +18 -8
- package/package.json +15 -5
- package/src/azure-host.mjs +19 -0
- package/src/azure-identity.mjs +18 -2
- package/src/backend-conformance.mjs +71 -18
- package/src/backend-local.mjs +637 -21
- package/src/backend-podman.mjs +1168 -0
- package/src/backend-registry.mjs +86 -3
- package/src/backends.mjs +489 -37
- package/src/branch.mjs +7 -2
- package/src/cancel-cli.mjs +174 -0
- package/src/cancel-state.mjs +125 -0
- package/src/cli.mjs +188 -90
- package/src/config.mjs +503 -43
- package/src/connection.mjs +374 -8
- package/src/container-spec.mjs +102 -7
- package/src/daemon-facts.mjs +167 -0
- package/src/deployment-venue.mjs +158 -0
- package/src/docker-run.mjs +146 -15
- package/src/doctor.mjs +4756 -394
- package/src/egress-conf-copy.mjs +166 -0
- package/src/egress-proxy-state.mjs +151 -0
- package/src/egress.mjs +456 -25
- package/src/entry.mjs +27 -0
- package/src/env-allowlist.mjs +245 -40
- package/src/env-file.mjs +1869 -33
- package/src/exit-code.mjs +15 -0
- package/src/flow-gate.mjs +5 -3
- package/src/forgejo-host.mjs +19 -0
- package/src/forgejo-identity.mjs +21 -2
- package/src/get-token.mjs +67 -18
- package/src/git-dirty.mjs +9 -1
- package/src/git-hardening.mjs +33 -0
- package/src/github-app-setup.mjs +29 -12
- package/src/github-prompt.mjs +4 -1
- package/src/gitlab-host.mjs +19 -0
- package/src/gitlab-identity.mjs +19 -2
- package/src/host-pi.mjs +19 -3
- package/src/host-registry.mjs +29 -2
- package/src/identity.mjs +29 -4
- package/src/image-preflight.mjs +46 -11
- package/src/image-ref.mjs +21 -0
- package/src/index.mjs +363 -13
- package/src/init.mjs +197 -38
- package/src/job-user.mjs +252 -0
- package/src/json-duplicates.mjs +204 -0
- package/src/live-probes.mjs +1020 -0
- package/src/materialize.mjs +4 -11
- package/src/netns-keeper.mjs +264 -0
- package/src/on-failure.mjs +119 -0
- package/src/outbox.mjs +7 -0
- package/src/packages.mjs +2 -2
- package/src/podman-stack.mjs +1304 -0
- package/src/prepare-github.mjs +6 -6
- package/src/prepare-local.mjs +51 -17
- package/src/prepare.mjs +27 -6
- package/src/pricing.mjs +9 -5
- package/src/processor.mjs +506 -26
- package/src/provider-key.mjs +66 -0
- package/src/provider-steering.mjs +185 -0
- package/src/queue.mjs +35 -8
- package/src/redact.mjs +84 -0
- package/src/reserved-env.mjs +7 -3
- package/src/retention-sweep.mjs +178 -0
- package/src/run-container.mjs +181 -14
- package/src/run-history.mjs +105 -16
- package/src/runtime-observations.mjs +1152 -0
- package/src/runtime-settings.mjs +13 -8
- package/src/sandbox-cli.mjs +100 -95
- package/src/sandbox-store.mjs +612 -45
- package/src/sandbox.mjs +1459 -37
- package/src/schedules.mjs +16 -3
- package/src/secret-profiles.mjs +2 -1
- package/src/secrets.mjs +24 -6
- package/src/service-env.mjs +247 -0
- package/src/service.mjs +618 -28
- package/src/session-store.mjs +678 -53
- package/src/start.mjs +1348 -326
- package/src/subscriptions.mjs +7 -3
- package/src/transient.mjs +240 -0
- package/src/triggers-file.mjs +71 -15
- package/src/triggers.mjs +179 -19
- package/src/up.mjs +1399 -85
- package/src/valkey-auth.mjs +529 -0
- package/src/valkey-endpoint.mjs +367 -0
- package/src/watch-closer.mjs +158 -0
package/src/backend-registry.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// The one import this module takes, and it is an import-free leaf for that reason (issue #339).
|
|
2
|
+
import { scrubCredentials } from "./redact.mjs";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Reap several backends and combine the tri-state CONSERVATIVELY.
|
|
3
6
|
*
|
|
@@ -29,13 +32,80 @@ export async function reapAll(reaps = [], { log = () => {} } = {}) {
|
|
|
29
32
|
// catch reached the caller before this function existed, and the caller logged it. Swallowing it
|
|
30
33
|
// here without a word would delete an operator-visible signal about a venue that could not be
|
|
31
34
|
// swept, so the log seam is threaded through rather than assumed to be somebody else's job.
|
|
32
|
-
log("reaper_skipped", { reason: err?.message });
|
|
35
|
+
log("reaper_skipped", { reason: scrubCredentials(err?.message) });
|
|
33
36
|
reaped = false;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
return { reaped };
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The `code` on the error the registry throws for a venue it does not hold, and ONLY that error.
|
|
44
|
+
*
|
|
45
|
+
* A CODE, not a message match: the processor catches this one refusal at pickup (a venue this worker never
|
|
46
|
+
* built is refused and recorded rather than retried) and must let every OTHER throw through -- a registered
|
|
47
|
+
* venue's own `containerName` failing is a broken adapter, and swallowing that would run the job under a
|
|
48
|
+
* null container name that nothing could stop by name.
|
|
49
|
+
*/
|
|
50
|
+
export const BACKEND_NOT_REGISTERED = "BACKEND_NOT_REGISTERED";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The per-venue preflights a bundle MAY carry (issue #354), each with what a venue that carries none answers.
|
|
54
|
+
*
|
|
55
|
+
* OPTIONAL, not in the required list, because requiring them would break every adapter written against the
|
|
56
|
+
* five-function contract `docs/backends.md` publishes, and because the absent answer is not a guess: it is what
|
|
57
|
+
* every non-local venue got before this, when both were `start.mjs` closures that returned exactly these for
|
|
58
|
+
* any venue but `local`. `observationPreflight` admits (nothing about this host is observed for a venue with no
|
|
59
|
+
* `observedBy`), and `jobUserPreflight` runs the image's own USER. Kept equal to the processor's own defaults for
|
|
60
|
+
* those two deps, so a wiring that omits the registry and one whose venue omits the member agree.
|
|
61
|
+
*
|
|
62
|
+
* What absence cannot be is SAFE for a venue whose table entry is observation-gated: that venue's words would
|
|
63
|
+
* then hold with nothing observing them. `start.mjs` refuses such a bundle at boot, because this module cannot
|
|
64
|
+
* read the table (it imports only the redactor).
|
|
65
|
+
*/
|
|
66
|
+
export const OPTIONAL_PREFLIGHT_DEFAULTS = Object.freeze({
|
|
67
|
+
observationPreflight: () => ({ ok: true }),
|
|
68
|
+
jobUserPreflight: () => ({ user: null, home: null }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** The venue's own member when it carries one, else the absent answer, as a promise either way. */
|
|
72
|
+
async function optionalPreflight(bundle, fn, ...args) {
|
|
73
|
+
return typeof bundle[fn] === "function" ? bundle[fn](...args) : OPTIONAL_PREFLIGHT_DEFAULTS[fn]();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The NAME of the venue a job resolves to: its own `run.backend`, else the deployment default.
|
|
78
|
+
*
|
|
79
|
+
* ONE DERIVATION FOR DISPATCH AND FOR EVERY STORE THAT RECORDS A VENUE (issue #277). The registry dispatches
|
|
80
|
+
* on it and the run record writes it down. A copy of the expression per store would be one more place a
|
|
81
|
+
* later change could land in one and not the others, and the whole value of a recorded venue is that it IS
|
|
82
|
+
* what dispatched: a record naming a venue the registry did not choose is an audit trail that lies.
|
|
83
|
+
*
|
|
84
|
+
* ABSENT MEANS THE KEY IS ABSENT, `undefined` and nothing else. The loader writes no `backend` key for a
|
|
85
|
+
* trigger that names no venue, and the processor's blessed gate tests `!== undefined`, so an explicit `null`
|
|
86
|
+
* is a NAME the gate refuses rather than a request for the default. Resolving it with `??` would record the
|
|
87
|
+
* default on a job the processor refused for naming something else. No producer writes a null today; this
|
|
88
|
+
* keeps the three readers agreeing if one ever does.
|
|
89
|
+
*
|
|
90
|
+
* TAKES JOB DATA, NEVER THE BULLMQ WRAPPER. A wrapper's own keys are `id` and `data`, so `wrapper.backend`
|
|
91
|
+
* is always undefined and every job would resolve to the default in silence -- the defect `index.mjs`
|
|
92
|
+
* records beside `containerName`, which shipped once. Callers holding a wrapper pass `wrapper.data`.
|
|
93
|
+
*
|
|
94
|
+
* `null` WHEN NEITHER IS KNOWN, never a guessed `local`. Only a dependency-injection seam reaches that (every
|
|
95
|
+
* wired caller is handed `config.defaultBackend`), and a consumer must fail closed on it: the record stores
|
|
96
|
+
* the null, and the registry refuses a job it cannot name. A default here would hide a dropped wire today and
|
|
97
|
+
* become a wrong answer the day `PI_BACKENDS` names a remote venue first.
|
|
98
|
+
*
|
|
99
|
+
* HERE rather than in `backends.mjs`, whose `backendFor(name)` falls back to the TABLE's default -- a
|
|
100
|
+
* different question, and putting the two side by side invites answering one with the other. And a free
|
|
101
|
+
* function rather than a registry method, because a store constructed in `startWorker` before any bundle
|
|
102
|
+
* exists could not reach one -- the temporal dead zone `reapAll` above already records.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveBackendName(data, defaultName) {
|
|
105
|
+
const named = data?.backend;
|
|
106
|
+
return named !== undefined ? named : (defaultName ?? null);
|
|
107
|
+
}
|
|
108
|
+
|
|
39
109
|
/**
|
|
40
110
|
* WHICH backend runs THIS job, and the one place that decides (issue #227).
|
|
41
111
|
*
|
|
@@ -83,6 +153,12 @@ export function makeBackendRegistry({ bundles = [], defaultName, blessed = null,
|
|
|
83
153
|
// exactly the case the list exists to refund.
|
|
84
154
|
throw new Error(`backend registry: ${JSON.stringify(bundle.name)} must declare neverStartedExits as an array of integers ([] if it normalises to container-never-started itself)`);
|
|
85
155
|
}
|
|
156
|
+
// OPTIONAL, but a present one must be callable (issue #354). Absent is a real answer, today's non-local
|
|
157
|
+
// behaviour (see `OPTIONAL_PREFLIGHT_DEFAULTS`); a truthy non-function is a typo'd adapter, and admitting it would
|
|
158
|
+
// land as a TypeError on the pre-spend path at the first pickup, the failure the required list above exists for.
|
|
159
|
+
for (const fn of Object.keys(OPTIONAL_PREFLIGHT_DEFAULTS)) {
|
|
160
|
+
if (bundle[fn] !== undefined && typeof bundle[fn] !== "function") throw new Error(`backend registry: ${JSON.stringify(bundle.name)} has a ${fn} that is not a function`);
|
|
161
|
+
}
|
|
86
162
|
byName.set(bundle.name, bundle);
|
|
87
163
|
}
|
|
88
164
|
if (byName.size === 0) throw new Error("backend registry: at least one backend must be registered");
|
|
@@ -117,10 +193,12 @@ export function makeBackendRegistry({ bundles = [], defaultName, blessed = null,
|
|
|
117
193
|
|
|
118
194
|
/** The bundle this job runs in. Throws for a name no gate should have let through. */
|
|
119
195
|
function backendFor(job) {
|
|
120
|
-
const name = job
|
|
196
|
+
const name = resolveBackendName(job, defaultName);
|
|
121
197
|
const bundle = byName.get(name);
|
|
122
198
|
if (!bundle) {
|
|
123
|
-
|
|
199
|
+
const err = new Error(`backend registry: no backend named ${JSON.stringify(name)} is registered (have: ${[...byName.keys()].join(", ")})`);
|
|
200
|
+
err.code = BACKEND_NOT_REGISTERED;
|
|
201
|
+
throw err;
|
|
124
202
|
}
|
|
125
203
|
return bundle;
|
|
126
204
|
}
|
|
@@ -140,6 +218,11 @@ export function makeBackendRegistry({ bundles = [], defaultName, blessed = null,
|
|
|
140
218
|
runContainer: (args) => backendFor(args?.job).runContainer(args),
|
|
141
219
|
imagePreflight: (job) => backendFor(job).imagePreflight(job),
|
|
142
220
|
egressPreflight: (job) => backendFor(job).egressPreflight(job),
|
|
221
|
+
// The two OPTIONAL per-venue preflights (issue #354), dispatched through the same resolution as every
|
|
222
|
+
// required one, so an unregistered name still throws BACKEND_NOT_REGISTERED rather than being waved
|
|
223
|
+
// through by a default. A venue that carries none gets the answer the processor's own defaults give.
|
|
224
|
+
observationPreflight: (job) => optionalPreflight(backendFor(job), "observationPreflight", job),
|
|
225
|
+
jobUserPreflight: (job, opts) => optionalPreflight(backendFor(job), "jobUserPreflight", job, opts),
|
|
143
226
|
stopContainer: (name, job) => backendFor(job).stopContainer(name, job),
|
|
144
227
|
// ON THE SURFACE, not reached for through `backendFor` by a call site. Both are per-job backend
|
|
145
228
|
// FACTS rather than functions, and an earlier draft left them off: the wiring rebuilt
|