Compare commits

...
Author SHA1 Message Date
jschoubben f8ca36aacf fail2ban: declare where it logs, so the recidive jail has a file to read
The recidive jail reads /var/log/fail2ban.log and this module ships the
logrotate file for it, but nothing ever told fail2ban to write there. Where
the package default stands, fail2ban logs to the journal, the recidive jail
finds no log file, and the whole service refuses to start -- taking the sshd
jail with it. Two machines assigned this module today came up failed; the two
where it worked had /etc/fail2ban/fail2ban.conf edited by hand, which a
package upgrade would have undone.

Declared in fail2ban.local, because fail2ban.conf belongs to the package.
2026-09-28 20:41:09 +02:00
mesh-admin 812355bf31 Merge pull request 'The catalogue hears what it missed' (#131) from feat/the-catalogue-hears-what-it-missed into main 2026-09-28 14:08:48 +00:00
jschoubben 016ddb2b3a The catalogue hears what it missed
It asks what it missed on every start and the answer never arrived: the control plane replayed each
build it held as a module's event from a module called "control-plane", which does not exist, so its
own account refused the publish and the graph kept the gap. The control plane now states those under
the seat it holds (novox/hq ADR 0134, mesh-controller #129), so this consumes that too — one handler,
because what a build means for the graph is the same whether the build machine says it as it happens
or the mesh says what it already held.
2026-09-28 16:08:46 +02:00
mesh-admin ea17bf46d2 Merge pull request 'The catalogue prepares its own schema instead of migrating at start' (#130) from feat/the-catalogue-prepares-its-own-schema into main 2026-09-28 13:40:30 +00:00
jschoubben 4258f01614 The catalogue prepares its own schema instead of migrating at start
It brought its schema up inside its runtime, on every start. That made a schema it could not reach a
crash loop rather than a stop, with the module graph keeping a gap and nothing saying so — which is
how a whole morning's builds went unrecorded. The mesh now prepares this module's state before it
starts this version and does not start it if that failed (novox/hq ADR 0135): the work moves to an
entrypoint the image names in MESH_PREPARE, beside the entrypoints it already names.

The reason it was at start — that a step blocking the apply would block the very apply bringing the
overlay up — stopped being true when a step's failure became its module's business rather than the
machine's (ADR 0136).
2026-09-28 15:40:28 +02:00
6 changed files with 55 additions and 9 deletions
+7
View File
@@ -28,6 +28,13 @@
"path": "/etc/fail2ban/action.d",
"mode": "0755"
},
{
"id": "fail2ban-local",
"type": "file",
"path": "/etc/fail2ban/fail2ban.local",
"mode": "0644",
"content": "[Definition]\n\n# Where fail2ban writes its own log, declared rather than assumed. The recidive jail reads\n# this file to ban whoever keeps coming back, and the logrotate file this module ships\n# rotates it -- but nothing told fail2ban to write there. Where the package default stands,\n# fail2ban logs to the journal, the recidive jail finds no log file, and the whole service\n# refuses to start, taking the sshd jail with it.\n#\n# In .local, not in fail2ban.conf: that file belongs to the package.\nlogtarget = /var/log/fail2ban.log\n"
},
{
"id": "jail-local",
"type": "file",
+6 -1
View File
@@ -22,7 +22,7 @@ COPY . .
# The compiler is invoked by its real path rather than through node_modules/.bin, whose entries are
# symlinks to a launcher that requires its library relatively — resolved away when the base image
# was assembled.
RUN node /app/node_modules/typescript/bin/tsc pg.d.ts store.ts index.ts tools/index.ts \
RUN node /app/node_modules/typescript/bin/tsc pg.d.ts store.ts index.ts tools/index.ts prepare/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
# **A module may need something the base image does not carry.** The base holds what every module
@@ -48,3 +48,8 @@ COPY --from=build /deps/node_modules /app/modules/mesh-catalog/node_modules
# to listen for what the builder announces. Serve binds the broker first, then imports these, so
# `on()` has something to subscribe to.
ENV MESH_TOOL_MODULES=/app/modules/mesh-catalog/dist/index.js,/app/modules/mesh-catalog/dist/tools/index.js
# And what prepares this module's state, for the runtime's `prepare` mode (novox/hq ADR 0135). Named
# here, beside the entrypoints above, because the module knows which of its files prepares its state
# and nothing else could: the mesh asks one word and this says what answers it.
ENV MESH_PREPARE=/app/modules/mesh-catalog/dist/prepare/index.js
+20 -6
View File
@@ -14,10 +14,12 @@ import { Graph, type Made } from "./store.js";
const graph = Graph.fromEnv();
// Before subscribing, and idempotent. The runtime is restarted until its store is reachable, which
// is the same arrangement model-usage uses: a schema step that had to reach the provider over the
// overlay would block the very apply that brings the overlay up.
await graph.migrate();
// The schema is not brought up here. The mesh prepares this module's state before it starts this
// version, and does not start it if that failed (novox/hq ADR 0135) — see prepare/index.ts. Doing it
// at start made a schema that could not be reached a crash loop instead of a stop, with the graph
// keeping a gap and nothing saying so. The reason it used to be here — that a step blocking the apply
// would block the very apply that brings the overlay up — stopped being true when a step's failure
// became this module's business and not the machine's (ADR 0136).
/** What the builder says when it has built something. */
interface Built {
@@ -47,7 +49,15 @@ interface Built {
replay?: boolean;
}
await on("mesh-build-machine.built", async (event) => {
/**
* What a build means for the graph, wherever it came from.
*
* Two emitters say the same thing and neither is a mistake: the build machine says it as it happens,
* and the control plane says what it already held when this module asks what it missed
* (novox/hq ADR 0134). A replay is marked as one in its body, so nothing acts on a module that moved
* months ago — see `replay` above.
*/
const placeTheBuild = async (event: { body: unknown }): Promise<void> => {
const body = event.body as Built;
if (!body.module || !body.commit) {
// Said rather than dropped: a build that announced itself without saying what it built is a
@@ -89,7 +99,11 @@ await on("mesh-build-machine.built", async (event) => {
because: next.because,
});
}
});
};
// As it happens, and what the mesh already held when this module asked what it missed.
await on("mesh-build-machine.built", placeTheBuild);
await on("mesh-controller.built-before", placeTheBuild);
// **And ask for what was built before this catalogue existed** (novox/hq 04-ISSUES/050).
//
+3 -1
View File
@@ -29,7 +29,8 @@
"broker": "/var/lib/mesh/mesh-catalog/broker"
},
"consumes": [
"mesh-build-machine.built"
"mesh-build-machine.built",
"mesh-controller.built-before"
],
"emits": [
"registered",
@@ -37,6 +38,7 @@
"rebuild-needed",
"catching-up"
],
"prepares": true,
"resources": [
{
"id": "mesh-state",
+17
View File
@@ -0,0 +1,17 @@
// The catalogue's state, brought to the shape this version needs (novox/hq ADR 0135).
//
// **The mesh runs this before the version that needs it, and does not start that version if it
// fails** — and the refusal reaches this module and nothing else on the machine
// (novox/hq ADR 0136). That is the whole difference from where this used to happen: at start, inside
// the runtime, a schema that could not be brought up was a crash loop, the graph kept a gap, and
// nothing anywhere said so.
//
// Nothing here connects to the broker. Preparation runs before the version that would use it, so
// there is nothing yet to talk to; the runtime's `prepare` mode imports this and awaits it, and this
// process exiting non-zero is how the host knows not to start the runtime.
import { Graph } from "../store.js";
const graph = Graph.fromEnv();
await graph.migrate();
console.log("[mesh-catalog] the module graph's schema is what this version needs");
await graph.close();
+2 -1
View File
@@ -12,6 +12,7 @@
"pg.d.ts",
"store.ts",
"index.ts",
"tools/index.ts"
"tools/index.ts",
"prepare/index.ts"
]
}