ADR 0121: a system seat is named for its scope; a module may define its own #145
+128
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-27
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 0110-a-seat-is-a-module-assignment-from-a-closed-set.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 121. A system seat is named for its scope, and a module may define its own
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) made seats a closed set the
|
||||||
|
control plane defines: a well-formed name no longer becomes a seat by being claimed, so a person can
|
||||||
|
read what a mesh can have and who fills each role. It left two things unsettled that the growing set
|
||||||
|
now exposes:
|
||||||
|
|
||||||
|
- **The names carry no rule.** `mesh-controller`, `mesh-store`, `mesh-broker` are named for the mesh;
|
||||||
|
beside them sit `the-artifact-store`, `the-build-machine`, `the-dns-port`, `the-showcase`,
|
||||||
|
`the-uplink` — a second naming style with no principle behind it. A reader cannot tell a seat's
|
||||||
|
scope from its name, and the mesh's own roles do not look like the mesh's.
|
||||||
|
- **The set is the *only* place a seat may be defined.** A module claiming any name not in the
|
||||||
|
control plane's set is refused. That is right for *system* roles — one broker, one packet filter
|
||||||
|
per node — but it means a module can never define a role of its own: a demo module's
|
||||||
|
`the-showcase`, a future application's coordination role, must be smuggled into the control plane's
|
||||||
|
set or not exist. The control plane ends up holding roles that are not the mesh's to define.
|
||||||
|
|
||||||
|
Reviewing the set against these also found seats whose *scope* or *membership* is wrong, not just
|
||||||
|
their name — the review is the occasion to fix those too.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**A system seat — one the control plane defines — is named for its scope:**
|
||||||
|
|
||||||
|
- **`mesh-*`** for a mesh-scoped seat: one holder in the whole mesh, a role the mesh has once
|
||||||
|
(`mesh-controller`, `mesh-store`, `mesh-broker`, `mesh-git`, …). A `mesh-*` seat is always held by
|
||||||
|
a module **on a named node** — `mesh-git` is gitea *on novox*, not "gitea"; another node running
|
||||||
|
gitea does not hold `mesh-git` unless it is the holder. The seat is the mesh's single answer for
|
||||||
|
the role, and which node answers is part of what the seat records.
|
||||||
|
- **`node-*`** for a node-scoped seat: one holder per node, a role each machine has at most once
|
||||||
|
(`node-packet-filter`, `node-intrusion-prevention`, `node-uplink`, …).
|
||||||
|
|
||||||
|
The three already-`mesh-*` seats keep their names; the rest are renamed by this rule. The scope a
|
||||||
|
name declares must match the seat's actual scope — a `mesh-*` seat at node scope, or the reverse, is
|
||||||
|
a contradiction the reader is entitled to trust is impossible.
|
||||||
|
|
||||||
|
**The control plane defines only system seats. A module may define its own.** A seat named `mesh-*`
|
||||||
|
or `node-*` is the control plane's, and claiming one the control plane does not define is refused as
|
||||||
|
before. Any *other* name is a **module-defined seat**: valid when the module declaring the claim also
|
||||||
|
declares the seat (its name, scope, and — if any — the protocol its holder speaks). The control plane
|
||||||
|
enforces one-holder-per-scope for it exactly as for its own, but does not otherwise know what it
|
||||||
|
means. So an application can coordinate its own instances through a seat of its own, and the mesh's
|
||||||
|
closed set stays what its name says it is: the *system's* roles, not everyone's.
|
||||||
|
|
||||||
|
**Specific seats this settles:**
|
||||||
|
|
||||||
|
- **`the-build-machine` → `mesh-build-machine`, and its scope becomes mesh.** There is one build
|
||||||
|
machine in the mesh (the builder on novox), not one per node. Node scope said the opposite. It
|
||||||
|
delivers no provision; it is the mesh's single build machine.
|
||||||
|
- **`the-private-network` → `mesh-private-network`, held by the network *server* on one node.** Today
|
||||||
|
it is node-scoped and held on every node, with a stated (untested) story that a different VPN could
|
||||||
|
hold it per machine — which would force every provider module to independently implement receiving
|
||||||
|
and applying the controller-composed configuration. The mesh does not work that way and should not
|
||||||
|
pretend to: **one mesh decides one private network.** The seat is mesh-scoped, held by the server
|
||||||
|
module (WireGuard on the hub, novox). A machine that joins is given a **client module** that
|
||||||
|
receives the composed configuration and applies it; when a node joins, the mesh emits each node's
|
||||||
|
configuration so all of them know each other at once. This drops per-node VPN choice deliberately —
|
||||||
|
the private network is nox-mesh's own, and it defines the nodes' configuration rather than being
|
||||||
|
assembled from each node's opinion. (Implementation: the overlay generator's per-node computation
|
||||||
|
is unchanged; what changes is the seat's scope and the server/client split of the module.)
|
||||||
|
- **`the-showcase` → removed from the set; it becomes a module-defined seat.** It is a demo module's
|
||||||
|
own coordination role, claimed by nothing else and held nowhere. It is the first module-defined
|
||||||
|
seat, and the reason the rule above is needed rather than hypothetical.
|
||||||
|
- **`the-dns-port` → `node-dns-resolver`** (the daemon that binds `:53`), kept distinct from
|
||||||
|
**`the-resolver-configuration` → `node-resolver-config`** (what writes `resolv.conf`). Two roles,
|
||||||
|
two seats; the rename must not blur them.
|
||||||
|
- **`the-packet-filter` → `node-packet-filter`** and **`the-intrusion-prevention` →
|
||||||
|
`node-intrusion-prevention`** — names kept as-is but for the prefix. "Packet filter" stays distinct
|
||||||
|
from "firewall", which would swallow intrusion-prevention too.
|
||||||
|
- **`the-uplink` → `node-uplink`** ([ADR 0117](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
||||||
|
renames with no migration.
|
||||||
|
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
||||||
|
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but gated.** They are
|
||||||
|
entangled with consolidating every registry onto gitea (below), so their final shape is settled
|
||||||
|
when that lands, not renamed in isolation first.
|
||||||
|
|
||||||
|
**The registry consolidates onto gitea.** The mesh should have **one** registry: gitea serving the
|
||||||
|
container/OCI images, the npm packages, crates, and trivial-tarball artifacts. `distribution` (the
|
||||||
|
standalone OCI registry) and `verdaccio` (a second npm registry) are retired once gitea serves what
|
||||||
|
each did. This is recorded here because it reshapes the registry seats; it is **not** a rename and
|
||||||
|
**not** surgical — see Consequences.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **A reader learns a seat's scope from its name.** `mesh-*` is mesh-wide and one; `node-*` is
|
||||||
|
per-machine. The mesh's own roles finally look like the mesh's.
|
||||||
|
- **Applications get their own seats** without the control plane learning their meaning. The closed
|
||||||
|
set shrinks to what it should be — the system's roles — and stops being where unrelated roles hide.
|
||||||
|
- **The renames are a coordinated migration, not a rename.** A held seat's name lives in three places
|
||||||
|
that must move together: the control plane's set (`seats.go`), every claiming manifest, and what
|
||||||
|
each node reports it holds (re-derived by re-registering the manifest and re-pushing). A seat
|
||||||
|
renamed in one place and not the others stops resolving to its holder — and for a *delivering* seat
|
||||||
|
(`mesh-store`→postgres, `mesh-broker`→amqp, the registry seats) that is a mesh-wide provision
|
||||||
|
outage, the same failure mode as a schema change hitting an old manifest. So: the non-delivering
|
||||||
|
`node-*` seats and `mesh-build-machine` migrate as one tested controller+catalogue change;
|
||||||
|
`node-uplink` is free (unheld); the delivering registry seats wait for the gitea consolidation.
|
||||||
|
- **Retiring `distribution` is blocked until gitea serves images, and is the mesh's highest-risk
|
||||||
|
operation.** Every image — the control plane's own, the builder's, every module's — is
|
||||||
|
`artifact-store://…@sha256` served by `distribution`. gitea today provides only `npm-package-registry`
|
||||||
|
and `git`; it has no OCI registry. Removing `distribution` before gitea serves images strands every
|
||||||
|
image: nothing pulls, nothing reconciles, and the control plane cannot recover itself. The order is
|
||||||
|
fixed: stand up gitea's container registry → gitea `provides artifact-store` and holds the seat →
|
||||||
|
repoint the builder to push there → migrate or re-push existing images → **only then** retire
|
||||||
|
`distribution` and `verdaccio`. Recorded here so the sequence is not skipped.
|
||||||
|
- **The private network stops pretending to be swappable per node.** The gain is a coherent
|
||||||
|
server/client model matching how the controller already composes configuration; the cost is that
|
||||||
|
choosing a different VPN is now a mesh-wide change, not a per-node one — accepted.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) — the closed set this refines
|
||||||
|
- [ADR 0117](0117-a-machines-uplink-is-a-seat.md) — `the-uplink`, renamed here to `node-uplink`
|
||||||
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — the original `mesh-*` seats
|
||||||
|
whose naming this generalises
|
||||||
|
- [to-be 26](../03-DESIGN/01-to-be/26-the-seats.md) — the seat table, updated by this
|
||||||
|
- mesh-controller `internal/catalogue/seats.go` (the set and claim validation),
|
||||||
|
`internal/overlay/generator.go` (the private network as server + client)
|
||||||
Reference in New Issue
Block a user