The adapter skips what its one file shape cannot say. A body limit is the exception: the predecessor has a buffering middleware and served its own registry name with exactly it, so this is written rather than skipped, named after the router so the two halves cannot drift. A limit that is not a whole positive number of bytes takes the route with it. Written without the limit, the predecessor would carry what the module said not to carry and this module would report success. Silence stays silence — no middleware, the predecessor's default.
route-adapter — route, answered by writing into the predecessor's proxy
This is migration scaffolding. It exists so that a node being adopted can migrate one web module at a time, and it is deleted when that migration ends. novox/hq ADR 0104, which decides it, and issue 093, which found the fault.
What it is
Every module reachable by name requires a route. The mesh has one provider of it — its own
proxy, route-proxy — and that proxy binds the two public ports on the
machine's own network. On a node that is adopted (novox/hq ADR 0100) the predecessor's proxy holds
those ports and serves every public name there, so the two cannot run at once. That is a circle: no
web module can be taken until the mesh's proxy runs, the mesh's proxy cannot run until the
predecessor's stops, and when it stops every name the predecessor served goes dark.
This module answers the same provision without binding anything. It provides route, receives
exactly the contributions file the proxy receives, and turns each contribution into one route file
in the directory the predecessor's file provider reads. The predecessor keeps serving every name it
already serves and keeps renewing its certificates; a name whose module has migrated is served by
the same proxy, pointing at the mesh's container instead of the predecessor's.
| field | value | why |
|---|---|---|
provides |
route (scope mesh) |
exactly what route-proxy provides, so the controller resolves route from this unchanged — a module that publishes through it cannot tell which one answered |
serves.route |
{} |
a route hands back a name, not a credential |
receives.route |
…/routes/mesh.json |
the same contributions file, in the same shape |
listens |
nothing | the point. It opens no port, so it stands beside the predecessor rather than replacing it |
requires |
nothing | in particular not acme-ca: the predecessor holds the certificates and this must not ask for one |
Assign either this or route-proxy to a node, never both — two providers of one mesh-scoped
provision is an ambiguity the resolver is right to refuse.
What it writes
One file per contribution, named mesh-<name>.yml, in the predecessor's dynamic directory. For a
contribution naming git.example on port 2999, on this machine:
# written by the novox mesh route-adapter (novox/hq ADR 0104)
# gitea contributed this route. It is removed when that contribution goes.
http:
routers:
mesh-git-example:
entryPoints: [websecure]
rule: Host(`git.example`)
service: mesh-git-example
tls:
certResolver: le
domains:
- main: git.example
services:
mesh-git-example:
loadBalancer:
servers:
- url: http://host.docker.internal:2999
- The certificate resolver is the predecessor's own, by its own name. The predecessor already holds a certificate for every public name it serves, so naming its resolver means a migrated name is served from the certificate that exists. A resolver of the mesh's would ask a public authority for one in the same window the module is cut over — the risk ADR 0104 exists to remove.
- The port is the contributor's, straight out of the contribution: the mesh assigned the machine-side number (novox/hq ADR 0038) and carries it there. Nothing here guesses it.
- The address is where the mesh says that machine is. Empty means this one, reached from inside
the predecessor's container at
host.docker.internal— not loopback, which from inside that container is the container. A contributor on another node carries its overlay address and the predecessor is sent straight there.
It never touches a file it did not write. Two tests, not one: the name must match mesh-*.yml,
and the file must start with the marker line above. A file somebody else happened to call
mesh-something is not this module's — it is left alone, and the route that wanted that name is
reported as unserved rather than written over. The directory belongs to the predecessor; the mesh
is a guest in it.
It removes a file it wrote when that contribution goes — including when nothing is contributed at all, which is what unassigning the contributing module has to mean.
How it runs again
It is a step, not a daemon: one run-once container that reads two files, reconciles the
directory and exits. It names both files under restart-on, so the host runs it again the moment
either changes — the mechanism already in the catalogue for a fact arrived, do it again
(novox/hq ADR 0099).
restart-on |
what changed |
|---|---|
received-route |
a module contributing a route arrived or left, or its port moved |
config |
this node's settings for the adapter changed |
A watcher of its own would be a second way to notice the same event, and it would keep running after the module was unassigned.
The node setting
Where the predecessor keeps the directory its file provider reads is a fact about one machine,
so it is a setting laid over the module's default rather than a constant in the catalogue —
novox/hq ADR 0100's ports is the precedent, and cloudflare-dns's config.json is the mechanism:
a merge: "json" file the mesh composes from the module's defaults and the node's layer, mounted
into the container.
| key | default | meaning |
|---|---|---|
dynamic |
/services/traefik/dynamic |
the directory the predecessor's file provider reads, on this machine |
entrypoint |
websecure |
the entry point it serves public HTTPS on |
certificate-resolver |
le |
the predecessor's resolver, by its own name |
machine |
host.docker.internal |
how the predecessor's proxy reaches this machine |
mesh-controller settings set route-adapter settings.json --node <node>
One thing does not follow the setting yet, and it fails loudly rather than quietly. The
container has to have that directory bind-mounted, and a bind mount's host side is written in the
manifest — the control plane can settle a file's content and a container's published ports, but not
a volume. So the mount is the default path. A node whose predecessor keeps its directory elsewhere
needs the manifest's mount changed with the setting; until then the module refuses on its first
pass and says exactly that, rather than writing route files somewhere nothing reads and reporting
success. The general fix — a per-node path reaching a container's volumes, as ports reaches its
published ports — is a decision for novox/hq, not for a module that is scheduled for deletion.
Using it on an adopted node
- The node is adopted and the predecessor's control is stopped (novox/hq ADR 0100). This module is refused on a converged node: it writes into a directory the predecessor owns, and that is only safe while nothing else is writing there.
assign <node> route-adapter, and setdynamicfor the node if the predecessor keeps its directory anywhere but the default.- The predecessor's proxy must be able to reach the machine —
host.docker.internalresolving to the host gateway, as it already does for every service of the predecessor's that runs beside it. - Assign and then
take <node> <module>for each web module, one at a time, when its data has moved. On the take, the module's container starts on the machine port the mesh gave it and the adapter writes its route file; remove the predecessor's own route file for that name in the same step, or two routers claim oneHostrule and which one answers is not something to rely on. - At the end,
converge <node>: the predecessor's proxy retires,route-proxytakes the ports and already knows every route, because by then every name belongs to a module that contributes it. - Then delete this module. It has a stated end; something has to delete it, and that something is the operator.
How it ships
The tool runtime carrying this module's compiled code, as
cloudflare-dns and mosquitto do — built
from this directory and nothing else. It connects to no broker: reconciling files on the machine it
runs on is an offline operation, and mesh-tools run gives it exactly that.
| env | default | meaning |
|---|---|---|
MESH_RECEIVES |
/var/lib/route-adapter/routes/mesh.json |
the contributions file (receives.route) |
MESH_ROUTE_ADAPTER_CONFIG |
/run/config/config.json |
the settled settings file |
Tests
cd modules/route-adapter && npm test
They hold it to what ADR 0104 says holds it: one file per contribution, a file removed when its contribution goes, every file it did not write left alone — and the two facts a route file has to get right, the port the contributor publishes and the address of the machine it is on.
A body limit
A contribution may say max-request-body, in bytes, and the adapter writes it as the predecessor's
own buffering middleware, named after the router so the two halves cannot drift. A route that says
nothing gets no middleware and the predecessor's default stands.
This is the one thing the file shape can say that a policy cannot, which is why it is written rather than skipped: the predecessor already served its own registry name this way. A limit that is not a whole positive number of bytes takes the route with it — written without the limit, the predecessor would carry exactly what the module said not to carry, and this module would report success doing it.