diff --git a/modules/nats/Dockerfile b/modules/nats/Dockerfile new file mode 100644 index 0000000..b362d7c --- /dev/null +++ b/modules/nats/Dockerfile @@ -0,0 +1,18 @@ +# nats's server image: the upstream server, plus an entrypoint that reloads it in place when the +# mesh rewrites its configuration. See entrypoint.sh for why that belongs here and not in the host. +# +# **Pinned to the multi-architecture index digest, not a platform's.** `docker manifest inspect` +# reports a platform manifest per architecture and the index that lists them; pinning a platform's +# digest builds on this workstation and fails on any node of another architecture, with an error +# that names a manifest rather than the mistake. This is the index — `docker pull` reports the same +# one, and `RepoDigests` confirms it. +# +# Unlike every other module's Dockerfile, this builds no TypeScript and uses no mesh base image: +# the module's code is the server, which upstream already built. There is no BUILD_BASE here on +# purpose — nothing is compiled. +FROM nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927 + +COPY entrypoint.sh /usr/local/bin/mesh-nats-entrypoint +RUN chmod 0755 /usr/local/bin/mesh-nats-entrypoint + +ENTRYPOINT ["/usr/local/bin/mesh-nats-entrypoint"] diff --git a/modules/nats/entrypoint.sh b/modules/nats/entrypoint.sh new file mode 100644 index 0000000..f117cc1 --- /dev/null +++ b/modules/nats/entrypoint.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# nats's entrypoint: run the server, and reload it in place when the mesh rewrites its +# 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 +# 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. +# +# nats-server already reloads its own configuration on SIGHUP — accounts, permissions, everything +# the mesh composes — without dropping a connection. That is the server's own documented +# capability, not something built for the mesh. So the configuration is mounted as a directory +# (a directory's contents are not digest-tracked the way a directly-mounted file's are, novox/hq +# issue 103), and this watches the one file inside it and signals the server itself. The host's +# only job is what it already does for any directory: keep the file's content current. Nothing +# here is declared `restart-on` or `reload-on`. +set -eu + +CONF="${MESH_NATS_CONF:-/etc/nats/nats.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 +done + +digest() { sha256sum "$CONF" 2>/dev/null | cut -d' ' -f1; } + +nats-server --config "$CONF" "$@" & +server=$! + +# Forward a stop to the server and let it drain, rather than dying and leaving it orphaned as +# PID 1's child. +stop() { kill -TERM "$server" 2>/dev/null || true; } +trap stop TERM INT + +last=$(digest) +while kill -0 "$server" 2>/dev/null; do + sleep "$POLL" + now=$(digest) + # An empty digest means the file is mid-write or briefly gone. Reloading on that would hand the + # server a truncated configuration; the next tick sees the finished one. + [ -n "$now" ] || continue + if [ "$now" != "$last" ]; then + last=$now + echo "[nats] configuration changed; reloading in place" + kill -HUP "$server" || true + fi +done + +# `wait` on an already-exited child still yields its status, which becomes this container's. +wait "$server" diff --git a/modules/nats/module.json b/modules/nats/module.json new file mode 100644 index 0000000..ed34306 --- /dev/null +++ b/modules/nats/module.json @@ -0,0 +1,71 @@ +{ + "module": "nats", + "version": "1", + "provides": [], + "claims": [ + { + "name": "mesh-broker", + "scope": "mesh" + } + ], + "capabilities": [ + "container-runtime" + ], + "emits": [], + "consumes": [], + "listens": [ + { + "port": 4222, + "protocol": "tcp", + "from": "mesh", + "why": "the mesh bus \u2014 every link the mesh has, over TLS, reached across the overlay" + } + ], + "guards": [ + 8222 + ], + "resources": [ + { + "id": "jetstream-data", + "type": "directory", + "path": "/var/lib/mesh-broker-nats", + "mode": "0700" + }, + { + "id": "conf-dir", + "type": "directory", + "path": "/var/lib/nats-module/conf", + "mode": "0700" + }, + { + "id": "server", + "type": "container", + "name": "mesh-broker-nats", + "ports": [ + "4222:4222", + "127.0.0.1:8222:8222" + ], + "volumes": [ + "/var/lib/mesh-broker-nats:/data", + "/var/lib/nats-module/conf:/etc/nats:ro", + "/var/lib/mesh-broker-nats-tls:/tls:ro" + ], + "artifact": "server" + } + ], + "accesses": [ + { + "path": "/var/lib/mesh-broker-nats-tls", + "mode": "read" + } + ], + "build": { + "artifacts": [ + { + "name": "server", + "kind": "image", + "from": "Dockerfile" + } + ] + } +}