The enumeration behind the first set read manifests in two repositories and missed a claim made in the control plane's own code: the private-network module it ships claims the-private-network at node scope. A closed set without it would refuse the control plane's own module. Thirteen claims in use, naming twelve seats.
158 lines
10 KiB
Markdown
158 lines
10 KiB
Markdown
---
|
|
topic: what runs on it
|
|
status: accepted
|
|
date: 2026-09-25
|
|
deciders: jochen
|
|
reconstructed: false
|
|
extends: 0009-modules-and-the-graph.md
|
|
---
|
|
|
|
# 110. A seat is a module assignment from a closed set, and it may deliver a provision
|
|
|
|
## Context
|
|
|
|
[ADR 0009](0009-modules-and-the-graph.md) introduced claims: a module declares something
|
|
singular, at a scope, and a second holder is refused. [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)
|
|
named the foundation's three after their servers. That mechanism is enforced and works. What it
|
|
means has drifted, and three things are now true of it that no record says.
|
|
|
|
**Any well-formed name becomes a seat by being claimed.** The control plane's manifest check
|
|
refuses a claim only for a malformed name or an unknown scope. Nothing says which seats a mesh has.
|
|
The names in use were each invented by the module that claims them: `the-showcase`,
|
|
`the-build-machine`, `the-intrusion-prevention`.
|
|
|
|
**Nothing can say what a mesh has, or who fills it.** There is no seat table and no command that
|
|
lists seats. Holdings are assembled while planning, one node at a time, and discarded afterwards.
|
|
The only way to answer "which seats does this mesh have, and which module holds each" is to read
|
|
every manifest in two repositories, because the core modules' manifests moved into the control
|
|
plane's own repository ([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), and then the
|
|
control plane's code, because one module it ships has its manifest composed there. While this
|
|
record was being prepared, that enumeration was done by hand, and it missed both of the last two
|
|
sources: eleven claims were reported where there are thirteen.
|
|
|
|
**Some seats are the mesh's one of something that others consume, and nothing uses that fact.**
|
|
Of the thirteen claims, four are held by a module that provides something consumers require:
|
|
`mesh-store` (`postgres-database`, eleven consumers), `mesh-broker` (`amqp`), `the-artifact-store`
|
|
(`artifact-store`) and `the-dns-port`. The rest deliver nothing to anybody, and are still
|
|
meaningful: they say which module is this mesh's packet filter, or resolver configuration.
|
|
Meanwhile a requirement for a mesh-scoped provision with more than one provider is refused until a
|
|
person pins, **per consumer node**, which provider to use. [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)
|
|
anticipates exactly that case — gitea and verdaccio both answering npm — and under today's
|
|
resolution it would mean a pin on every machine that builds anything.
|
|
|
|
## Considered Options
|
|
|
|
**1. Leave seats as free-form exclusion, and keep provisions as the only thing that delivers.**
|
|
Rejected. The overview stays unanswerable, and a second provider of anything costs a pin per
|
|
consumer node — the decision "gitea is our npm registry" made again on every machine.
|
|
|
|
**2. Two concepts: seats for exclusion, and a new word for the mesh's one consumable thing.**
|
|
Rejected. Both mean "this mesh's one X". Every existing claim would first have to be classified
|
|
into one or the other, and the overview a person wants is one list, not two.
|
|
|
|
**3. A seat is a module assignment from a closed set, and occupying it may deliver a provision.**
|
|
Chosen.
|
|
|
|
## Decision
|
|
|
|
**The mesh defines a closed set of seats.** Each entry has a name, a scope, what occupying it
|
|
delivers (if anything), and the decision that made it a seat. A claim naming a seat outside the set
|
|
is refused, and so is a claim at a scope other than the seat's. Adding a seat is a decision, for the
|
|
same reason adding a shape to the host's vocabulary is one: the set is what a person reads to learn
|
|
what a mesh can have, and a name added without an argument is a name nobody can explain later.
|
|
|
|
**A seat is held by a module assignment.** What the mesh knows about a seat's holder is what it knows
|
|
about that assignment: its node, the node's settings for it, and what it serves. Holdings are not
|
|
stored separately. The seat points at an assignment, and a second record of the same fact would be a
|
|
second thing to disagree with the first.
|
|
|
|
**A seat may deliver a provision, and then its holder answers for it.** A seat that delivers a
|
|
provision can only be held by a module that provides it, at the seat's scope, and a claim that does
|
|
not is refused. When several providers answer a requirement for that provision, resolution takes, in
|
|
order:
|
|
|
|
1. a provider the consumer's node was **pinned** to — a consumer coupled to one provider's
|
|
contents, as [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md) already allows;
|
|
2. **the holder of the seat** that delivers it;
|
|
3. the **only** provider, when there is one;
|
|
4. otherwise, refused with the candidates named, as now.
|
|
|
|
This keeps [ADR 0009](0009-modules-and-the-graph.md)'s rule that a requirement with several answers is
|
|
never guessed. The seat is not a guess. It is the choice made once, mesh-wide, by assigning the holder,
|
|
instead of once per consumer by pinning. A second provider may run beside the holder, and whatever
|
|
requires the provision still resolves to the holder without anybody naming it.
|
|
|
|
**Seats are also informational.** The control plane lists every seat in the set, what it delivers,
|
|
and which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh
|
|
has no X", not an error.
|
|
|
|
**The first set is the twelve seats already claimed, plus one.** Thirteen claims are in use, and
|
|
they name twelve seats because two alternative modules claim `the-resolver-configuration`. This
|
|
record admits every seat the catalogue and the control plane claim today, so no module is refused
|
|
by it:
|
|
|
|
| seat | scope | delivers | held today by | made a seat by |
|
|
|---|---|---|---|---|
|
|
| `mesh-controller` | mesh | — | `mesh-controller` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
|
| `mesh-store` | mesh | `postgres-database` | `postgres` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
|
| `mesh-broker` | mesh | `amqp` | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
|
| `the-artifact-store` | mesh | `artifact-store` | `distribution` | [0075](0075-two-stores-and-which-provides-what.md) |
|
|
| `the-catalogue` | mesh | — | `mesh-catalog` | this record |
|
|
| `npm-package-registry` | mesh | `npm-package-registry` | `gitea` | [0109](0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
|
| `the-build-machine` | node | — | `builder` | this record |
|
|
| `the-dns-port` | node | — | `dnsmasq` | this record |
|
|
| `the-intrusion-prevention` | node | — | `fail2ban` | this record |
|
|
| `the-packet-filter` | node | — | `nftables` | this record |
|
|
| `the-private-network` | node | — | the control plane's private-network module | this record |
|
|
| `the-resolver-configuration` | node | — | `resolv-conf` or `resolved-split-dns` | this record |
|
|
| `the-showcase` | node | — | `showcase` | this record |
|
|
|
|
`npm-package-registry` is the one addition. It is [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)'s
|
|
seat, named after the provision it delivers, as 0079 named the foundation's seats after what they are.
|
|
gitea claims it. verdaccio provides the same provision and claims nothing, so it is the second
|
|
provider this record exists to make harmless.
|
|
|
|
`the-dns-port` is listed as delivering nothing, although `dnsmasq` provides `wildcard-resolution`.
|
|
That provision is node-scoped and answered on the machine, so no preference between providers
|
|
arises. Whether the seat should say it delivers it is left for when a second resolver makes the
|
|
question real.
|
|
|
|
## Consequences
|
|
|
|
- The control plane carries the set in code. A test asserts its size, and that every entry names the
|
|
record that made it a seat, so changing the set means finding the argument rather than a number.
|
|
This is the pattern the host's vocabulary test already follows.
|
|
- Manifest validation refuses an unknown seat, a seat claimed at the wrong scope, and a
|
|
provision-delivering seat claimed by a module that does not provide the provision. The three
|
|
refusals name the seat and the set.
|
|
- Resolution prefers the seat's holder among several providers, after a pin. A provider record
|
|
gains the module it came from, because two modules on one node could otherwise not be told apart
|
|
as holder and non-holder.
|
|
- A `seats` command lists the set with each seat's holders, derived from assignments.
|
|
- gitea claims `npm-package-registry`. The catalogue's `package-registry` becomes
|
|
`npm-package-registry` throughout, per [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md).
|
|
- **What got harder:** a module wanting a new singular role can no longer invent a name. It needs a
|
|
record. That is the point, and it costs one record per seat.
|
|
- **Not changed:** `${seat:<seat>:<port>}` stays as it is. It exists so the control plane can reach a
|
|
foundation it made before any module existed, and it cannot be a consumer. A module that needs
|
|
something from a seat's holder requires the provision the seat delivers, and receives it the way
|
|
any provision is received: through a grant.
|
|
|
|
## How it is checked
|
|
|
|
| Rule | Checked by |
|
|
|---|---|
|
|
| The set is closed, and every entry names its decision | A control-plane unit test asserts the set's size and a non-empty decision for every entry. |
|
|
| A claim outside the set is refused | Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat whose claimant does not provide. |
|
|
| Every module in use claims a seat in the set | A control-plane test parses every catalogue manifest and fails on any refused claim. The lab's beds read the same manifests ([ADR 0089](0089-a-bed-reads-the-catalogue-it-proves.md)). |
|
|
| The holder answers among several providers | Resolution tests: two providers with the seat held, two with a pin overriding the seat, two with the seat unheld (refused). |
|
|
|
|
## References
|
|
|
|
- [ADR 0009](0009-modules-and-the-graph.md): claims, scopes, and "refused, never guessed"
|
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md): a seat named after what it is
|
|
- [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md): the per-ecosystem registry seats
|
|
- [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md): pins, co-location and refusal
|
|
- `mesh-controller internal/catalogue/resolve.go` (`checkClaims`, the brokered branch of `Resolve`),
|
|
`internal/catalogue/manifest.go` (claim validation), `cmd/mesh-controller/plan.go` (holdings)
|