# 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](https://git.novox.be/novox/hq), which decides it, and [issue 093](https://git.novox.be/novox/hq), 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`](../route-proxy/README.md) — 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-.yml`, in the predecessor's dynamic directory. For a contribution naming `git.example` on port 2999, on this machine: ```yaml # 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 ``` **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 1. 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. 2. `assign route-adapter`, and set `dynamic` for the node if the predecessor keeps its directory anywhere but the default. 3. The predecessor's proxy must be able to reach the machine — `host.docker.internal` resolving to the host gateway, as it already does for every service of the predecessor's that runs beside it. 4. Assign and then **`take `** 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 one `Host` rule and which one answers is not something to rely on. 5. At the end, `converge `: the predecessor's proxy retires, `route-proxy` takes the ports and already knows every route, because by then every name belongs to a module that contributes it. 6. **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](../cloudflare-dns/Dockerfile) and [mosquitto](../mosquitto/Dockerfile) 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.