Merge pull request 'ADR 0121: a system seat is named for its scope; a module may define its own' (#145) from design/system-seats-are-named-by-scope into main
This commit was merged in pull request #145.
This commit is contained in:
+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