ADR 0122: a seat is data the controller owns; a rename is a database update #148
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-27
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
supersedes-in-part:
|
||||||
|
- 0110-a-seat-is-a-module-assignment-from-a-closed-set.md
|
||||||
|
- 0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 122. A seat is data the controller owns, and a rename is a database update
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) made the seats a closed set the
|
||||||
|
control plane defines, and [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||||
|
named them by scope. Both were right about *what* a seat is. Both left it defined the wrong *way*:
|
||||||
|
**the set is a hardcoded Go slice compiled into the controller, and everything references a seat by
|
||||||
|
its name as a string literal.** Renaming `the-packet-filter` to `node-packet-filter` this session
|
||||||
|
took, in one pass:
|
||||||
|
|
||||||
|
- an edit to the Go slice in `internal/catalogue/seats.go`, recompiled into a new controller image;
|
||||||
|
- an edit to a `const gitSeat = "git"` in *production* control-plane code (`source.go`), because a
|
||||||
|
seat's name was hardcoded where a repository's home is resolved;
|
||||||
|
- edits to every claiming manifest in the catalogue, each re-registered;
|
||||||
|
- a controller **rebuild and redeploy**, which — because the running controller then refused the
|
||||||
|
still-old-named claims in stored manifests — **froze composition** for the affected nodes until
|
||||||
|
each manifest was re-registered under its new name;
|
||||||
|
- the same coupling in the **build machine**, which embeds the same seat set and refused to build
|
||||||
|
anything claiming a name it did not yet know;
|
||||||
|
- a **deadlock** when the build machine's own seat was renamed, since the old builder could not
|
||||||
|
build the new builder whose manifest claimed a name it rejected.
|
||||||
|
|
||||||
|
None of that is what a rename should cost. A rename is the operator changing a label. It should be a
|
||||||
|
single write, and nothing should have to be rebuilt, refused, or unfrozen. The set being *closed*
|
||||||
|
(0110) and *named by scope* (0121) are good rules; **the set being code is the mistake.** When
|
||||||
|
adhering to the design means twenty steps and a `const` in the resolver, the design is what to fix.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The seat set is data the control plane owns, not code it is compiled from.** The seats live in a
|
||||||
|
table in the controller's store — one row per seat: a **stable id**, a `name`, a `scope`, what it
|
||||||
|
`delivers` (a provision, or nothing), and the record that decided it. The rows are seeded by a
|
||||||
|
migration (the closed set 0110 defines still ships with the mesh), and thereafter they are ordinary
|
||||||
|
data the control plane reads and writes.
|
||||||
|
|
||||||
|
**A seat is referenced by its stable id, never by its name.** A claim, a held-seat record, and any
|
||||||
|
control-plane code that must name a seat (the git-seat resolver, the artifact-store guard) hold the
|
||||||
|
**id**. The `name` is a label for people and for what a manifest writes; it is resolved to an id
|
||||||
|
once, when a claim is registered. So:
|
||||||
|
|
||||||
|
- **A rename is one `UPDATE seats set name = … where id = …`.** Nothing is recompiled, nothing is
|
||||||
|
re-registered, nothing is refused, nothing freezes. Held records and claims already point at the
|
||||||
|
id, so they follow the rename for free. The build machine is not involved, because the build
|
||||||
|
machine validates a claim against the set it reads from the mesh, not one baked into its image.
|
||||||
|
- **Adding or removing a seat is an `INSERT`/`DELETE`** (within the closed-set discipline: a change
|
||||||
|
to the set is still a decision with a record — the record is now a row's `decided` column and an
|
||||||
|
ADR, not a line of Go). No controller release is needed to change the roster of roles.
|
||||||
|
- **Production code stops hardcoding names.** `const gitSeat = "git"` becomes a lookup of the seat
|
||||||
|
that delivers the `git` provision (or a well-known id), so renaming its label cannot break the
|
||||||
|
code that finds a repository's forge.
|
||||||
|
|
||||||
|
**What does not change** (0110 and 0121 still hold): a seat is still a module assignment from a
|
||||||
|
closed set; there is still one holder per scope; a delivering seat is still the single answer for
|
||||||
|
its provision; system seats are still `mesh-*`/`node-*` and a module may still define its own. Only
|
||||||
|
their *storage and reference* change — from a compiled slice keyed by name to a table keyed by id.
|
||||||
|
|
||||||
|
**A manifest still claims by name, and that is fine.** A manifest is written by a person and names
|
||||||
|
the seat in words; the mesh resolves the name to an id at registration and stores the id. If a
|
||||||
|
seat's name changes, manifests written against the old name are updated in the catalogue like any
|
||||||
|
other edit (and the mesh can keep the old name as an alias row during a transition so nothing breaks
|
||||||
|
in the window) — but the *control plane* never has to change or redeploy for it, which is the whole
|
||||||
|
point. The heavy, mesh-wide, freeze-prone half of a rename disappears; only the ordinary catalogue
|
||||||
|
edit remains.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **A rename, and a set change, become operations, not releases.** The pain this session paid —
|
||||||
|
three freezes, a builder deadlock, hand-resolved manifests — is designed out. The seat migrations
|
||||||
|
still outstanding (the delivering registry seats, and the private network's scope change) should
|
||||||
|
wait for this: done as data, each is a write, not a coupled multi-repo deploy.
|
||||||
|
- **The controller gains a small table and a seed migration**, and its seat lookups change from
|
||||||
|
slice scans to id-keyed reads. `SeatNamed`, `SeatDelivering`, `claimProblems` read the table.
|
||||||
|
- **The build machine reads the set from the mesh** (it already talks to the control plane), rather
|
||||||
|
than embedding it — which removes the controller/builder seat coupling that made every breaking
|
||||||
|
seat change a two-sided deadlock (see [to-be 30](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md)).
|
||||||
|
- **The closed set is still closed.** Data being editable is not the set being open: changing it is
|
||||||
|
still a decision, still recorded. What changes is that recording it no longer means shipping a
|
||||||
|
binary.
|
||||||
|
- **This is a real refactor**, touching the store schema, the seat lookups, claim registration
|
||||||
|
(name→id resolution), and the held-seat records. It is worth its own build; until it lands, the
|
||||||
|
current compiled set stands and further renames are held rather than forced through the heavy path.
|
||||||
|
- **Config on a seat is still the module's** (the question that surfaced this): a seat row carries
|
||||||
|
the seat's own metadata (scope, delivers, protocol), not a module's configuration — that stays in
|
||||||
|
the holding module's manifest ([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)). Making
|
||||||
|
seats data does not make them a config store.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
|
||||||
|
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — the seat
|
||||||
|
rules this keeps, whose *storage* it changes
|
||||||
|
- [to-be 30](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md) — the controller/builder
|
||||||
|
seat coupling and the breaking-change freeze this removes for seat changes
|
||||||
|
- mesh-controller `internal/catalogue/seats.go` (the compiled slice this replaces),
|
||||||
|
`cmd/mesh-controller/source.go` (`const gitSeat`, the hardcoded name this removes)
|
||||||
Reference in New Issue
Block a user