The nats module, and every manifest's event names made local #115

Merged
jschoubben merged 8 commits from feat/nats-genesis into main 2026-09-27 17:31:04 +00:00
2 changed files with 30 additions and 14 deletions
Showing only changes of commit a093c88c32 - Show all commits
+22 -14
View File
@@ -3,9 +3,9 @@
# configuration.
#
# **Why this exists inside the module** (novox/hq design 25 §5). The controller composes every
# account and permission into one configuration file, and that file changes whenever a module is
# added, reassigned, or a person's access is granted or revoked — which is often, and on the one
# server everything else depends on. The host has no way to say "reload this container": a
# account and permission into one file, and that file changes whenever a module is added,
# reassigned, or a person's access is granted or revoked — which is often, and on the one server
# everything else depends on. The host has no way to say "reload this container": a
# container resource has `restart-on` and nothing else, and a container's `restart-on` means
# *recreate* — every connection dropped and every in-flight JetStream ack lost, mid-flight, for a
# permission change. `reload-on` is real but it is a *service* field, not a container's.
@@ -19,21 +19,29 @@
# here is declared `restart-on` or `reload-on`.
set -eu
# **Two files, and only one of them is the mesh's** (novox/hq design 25 §4, task 1.7). CONF is this
# module's own — ports, TLS, JetStream — declared in its manifest, because those are properties of
# the container this module raises. USERS is every account and permission, composed by the
# controller, and CONF includes it. So what is watched here is the mesh's half: the module's own
# does not change without a new declaration, and that recreates the container anyway.
CONF="${MESH_NATS_CONF:-/etc/nats/nats.conf}"
USERS="${MESH_NATS_USERS:-/etc/nats/accounts.conf}"
POLL="${MESH_NATS_CONF_POLL_SECONDS:-5}"
# The controller writes the configuration as part of the same declaration that creates this
# container, but the two are not ordered against each other. Waiting is correct and starting
# without one is not: nats-server would come up with its compiled-in defaults — no TLS, no
# accounts, every subject open to anyone who can reach the port — and then be reloaded into
# correctness a moment later. A bus that is briefly open to everything is not a bus that is
# briefly wrong; it is an open bus.
while [ ! -s "$CONF" ]; do
echo "[nats] waiting for the mesh to compose $CONF"
sleep 1
# Both are written as part of the same declaration that creates this container, but none of the
# three are ordered against each other. Waiting is correct and starting without them is not:
# nats-server given a configuration whose include is missing refuses to start, and one given no
# configuration at all comes up with its compiled-in defaults — no TLS, no accounts, every subject
# open to anyone who can reach the port. A bus that is briefly open to everything is not a bus that
# is briefly wrong; it is an open bus.
for needed in "$CONF" "$USERS"; do
while [ ! -s "$needed" ]; do
echo "[nats] waiting for the mesh to write $needed"
sleep 1
done
done
digest() { sha256sum "$CONF" 2>/dev/null | cut -d' ' -f1; }
digest() { sha256sum "$USERS" 2>/dev/null | cut -d' ' -f1; }
nats-server --config "$CONF" "$@" &
server=$!
@@ -52,7 +60,7 @@ while kill -0 "$server" 2>/dev/null; do
[ -n "$now" ] || continue
if [ "$now" != "$last" ]; then
last=$now
echo "[nats] configuration changed; reloading in place"
echo "[nats] the mesh's user list changed; reloading in place"
kill -HUP "$server" || true
fi
done
+8
View File
@@ -13,6 +13,7 @@
"scope": "mesh"
}
],
"bus-users": "/var/lib/nats-module/conf/accounts.conf",
"capabilities": [
"container-runtime"
],
@@ -42,6 +43,13 @@
"path": "/var/lib/nats-module/conf",
"mode": "0700"
},
{
"id": "server-conf",
"type": "file",
"path": "/var/lib/nats-module/conf/nats.conf",
"content": "# The nats module's own server settings. Declared by the module, because a port, a TLS path\n# and a store directory are properties of the container this module raises: they live in its\n# image and its mounts and change when it does.\n#\n# The mesh writes accounts.conf beside this one and nothing else. A controller that wrote the\n# whole file would have to be kept in step with a Dockerfile it never sees.\n\nport: 4222\nhttp: 127.0.0.1:8222\n\ntls {\n cert_file: \"/tls/tls.crt\"\n key_file: \"/tls/tls.key\"\n ca_file: \"/tls/ca.crt\"\n}\n\n# **No `verify`, deliberately, and it was `verify: true` until a probe ran this image.** That\n# setting makes the server demand a *client* certificate, and nothing in the mesh presents one: a\n# host pins this server's exact certificate and authenticates with the password the mesh minted\n# (novox/hq ADR 0004, design 25 \u00a74), and so does a module's runtime. With it on, every connection\n# in the mesh is refused at the TLS handshake, before any password is looked at \u2014 and the error is\n# \"client didn't provide a certificate\", which reads as a client fault.\n#\n# TLS is still required: a tls block is what makes it required, and verify only decides whether\n# client certificates are checked. What is given up is a second factor the mesh has no machinery\n# to issue or rotate \u2014 a certificate per module per node \u2014 and what is kept is stronger than a\n# name check in both directions: an exact pin outward, a per-user password inward.\n\njetstream {\n store_dir: \"/data\"\n}\n\n# Every user of the mesh, composed by the controller and rewritten whenever a module is\n# assigned, a node enrols or a person's access changes.\n#\n# **Relative, and in this same directory, because it has to be.** An absolute include path is\n# resolved relative to the including file's directory, not from the root: nats-server given\n# `include /etc/nats/accounts.conf` from /etc/nats-server/nats.conf looks for\n# /etc/nats-server/etc/nats/accounts.conf and refuses to start. Verified against the server.\ninclude accounts.conf\n",
"mode": "0644"
},
{
"id": "server",
"type": "container",