To-be 27 (proposed): a module requires, the mesh resolves — with ADRs 0109–0114, research 016 and issue 119 #113
+11
-4
@@ -44,15 +44,22 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
||||
|
||||
## How modules relate to the mesh
|
||||
|
||||
- **seat** — a named position at a scope (node / site / mesh) with a **capacity**. A capacity-1 seat
|
||||
is exclusive (one holder); a higher-capacity seat is a **bench** (several holders coexist).
|
||||
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a
|
||||
**closed set** the mesh defines: a claim naming a seat outside the set is refused. A seat may
|
||||
**deliver a provision**, and its holder is then the mesh's answer for it when several modules
|
||||
provide it ([ADR 0110](../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)).
|
||||
The set, with who holds each seat, is the overview of what a mesh has
|
||||
([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)). A seat has a **capacity**: a
|
||||
capacity-1 seat is exclusive (one holder); a higher-capacity seat is a **bench** (several holders
|
||||
coexist).
|
||||
- **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A
|
||||
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
|
||||
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
|
||||
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
|
||||
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
|
||||
and wires the two with an endpoint and a credential. This is separate from seats: a provision is
|
||||
a service you offer, a seat is a slot you occupy.
|
||||
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat
|
||||
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
||||
what makes a module *the* provider of it.
|
||||
|
||||
## How this page is kept
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
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)). While this record
|
||||
was being prepared, that enumeration was done once by hand, and it missed the control plane's own
|
||||
manifest: eleven claims were reported where there are twelve.
|
||||
|
||||
**Some seats are the mesh's one of something that others consume, and nothing uses that fact.**
|
||||
Of the twelve 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 eleven seats already claimed, plus one.** Twelve claims are in use, and they
|
||||
name eleven 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-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)
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-09-25
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 0069-a-module-is-a-repository-and-a-path.md
|
||||
---
|
||||
|
||||
# 111. A build source is on the mesh's git seat, or it is an external repository
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0069](0069-a-module-is-a-repository-and-a-path.md) made a module a repository, a path and a
|
||||
ref, and the control plane records all three against the module so it can rebuild it and say when
|
||||
its source has moved ahead. **The repository is recorded exactly as a person typed it.** `build
|
||||
<repository>` hands the string to a build machine, which runs `git clone` on it, and the same string
|
||||
becomes the module's recorded source.
|
||||
|
||||
**So a self-hosted forge's address is written into every module built from it.** The mesh runs its
|
||||
own forge, and most of what it builds lives there. Every one of those modules carries the forge's
|
||||
scheme, host and port in its recorded source. Move the forge to another machine, or change the port
|
||||
it is published on, and every recorded source is stale at once. Nothing notices until a rebuild fails
|
||||
to clone.
|
||||
|
||||
**And nothing names the mesh's git at all.** gitea serves git over HTTP and over SSH, and the mesh's
|
||||
vocabulary contains neither. No provision, no `serves`, no seat, as the forge survey
|
||||
([research 013](../01-RESEARCH/013-the-forge-and-the-registries/the-survey.md)) found. The only trace
|
||||
is a label on its public route, which the mesh is explicitly not meant to interpret.
|
||||
|
||||
**External repositories are ordinary, and must stay so.** An application the mesh hosts may live on a
|
||||
public forge. Building it from its URL works today and must keep working unchanged.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Keep recording literal URLs.** Rejected. It is the problem: the forge's address copied into
|
||||
every module built from it.
|
||||
|
||||
**2. Recognise a self-hosted source by matching its URL against the forge's current address.**
|
||||
Rejected. It infers the kind of source from the shape of a string, and the inference fails in the
|
||||
one case it exists for: after the forge moves, old URLs no longer match anything.
|
||||
|
||||
**3. Two explicit forms: a repository on the holder of the `git` seat, or an external URL.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The mesh has a `git` seat.** It is mesh-scoped and delivers the `git` provision, per
|
||||
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md). Its holder provides `git`,
|
||||
serving how a repository on it is cloned: the scheme and the port. gitea claims it.
|
||||
|
||||
**A source is on the git seat, or it is external, and the mesh records which.**
|
||||
|
||||
- `build --self <owner>/<repository>` builds from a repository on the seat's holder. The recorded
|
||||
source is the repository's path on that holder, and the seat it is on. **It never contains an
|
||||
address.** At the moment of building, the control plane composes the clone URL from where the
|
||||
holder runs and what it serves for `git`, so a moved forge changes nothing recorded.
|
||||
- `build <url>` is unchanged: an external repository, recorded and cloned exactly as given. GitHub
|
||||
and GitLab are the ordinary cases.
|
||||
|
||||
**An unheld seat refuses self-hosted builds and nothing else.** With nobody holding `git`, `build
|
||||
--self` is refused, naming the seat and saying what would hold it. External builds are unaffected. A
|
||||
mesh without a forge of its own builds from external repositories only, and says so rather than
|
||||
failing to clone.
|
||||
|
||||
**The build machine is not told the difference.** It receives a URL either way. Composing the URL is
|
||||
the control plane's job, because only the control plane knows where the seat's holder runs.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The control plane's inventory gains a column saying which seat a source is on. It is empty for
|
||||
every module recorded before this, which is correct: they were all recorded as literal URLs.
|
||||
- `build`, `build --behind` and `build --dry-run` resolve a seat source before asking a builder. The
|
||||
recorded source keeps the seat form; the build log keeps the URL that was actually cloned, because
|
||||
that is what happened.
|
||||
- gitea claims `git` and provides it, serving HTTP clone on its web port.
|
||||
- **Not decided: credentials for private repositories.** The mesh's own repositories are public, and
|
||||
clone without one. A private repository still works only if the build machine's own git
|
||||
configuration authenticates, exactly as before. Delivering a clone credential through the `git`
|
||||
provision's grant is the obvious next step, and it is its own decision.
|
||||
- **Not changed:** modules already recorded from the forge keep their literal URLs until they are
|
||||
rebuilt with `--self`. Rewriting them in place would be the URL-matching this record rejects.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A seat source records no address | A control-plane test resolves a seat source and asserts the recorded repository is the path alone. |
|
||||
| The URL comes from the holder | A test composes the clone URL from a holder's node address and served `git` scheme and port, and a second where the port was moved on the node. |
|
||||
| An unheld seat refuses self-hosted builds only | A test with no holder: `--self` is refused naming the seat; an external URL passes through unchanged. |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0069](0069-a-module-is-a-repository-and-a-path.md): a module is a repository, a path and a ref
|
||||
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): seats, and a seat delivering a provision
|
||||
- [Research 013](../01-RESEARCH/013-the-forge-and-the-registries/the-survey.md): git is served and declared nowhere
|
||||
- `mesh-controller cmd/mesh-controller/build.go`, `internal/builder/builder.go`
|
||||
@@ -156,6 +156,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md)
|
||||
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
|
||||
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
|
||||
- **0110** — [A seat is a module assignment from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
@@ -175,6 +176,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0096** — [An upstream image is copied between registries, never through a machine's image store](0096-an-upstream-image-is-copied-between-registries.md)
|
||||
- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md)
|
||||
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
||||
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
||||
|
||||
### How it is checked
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ code:
|
||||
- mesh-controller cmd/mesh-builder
|
||||
- mesh-controller internal/builder
|
||||
- mesh-catalog modules/builder
|
||||
updated: 2026-09-21
|
||||
updated: 2026-09-25
|
||||
decisions:
|
||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||
- 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md
|
||||
- 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
|
||||
- 02-DECISIONS/0091-a-mount-is-declared-three-ways.md
|
||||
@@ -38,7 +39,7 @@ controller's again. The builder's whole responsibility is the middle.
|
||||
|
||||
| term | is |
|
||||
|---|---|
|
||||
| **source** | a repository, a path within it, and a ref — resolved to one commit ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)) |
|
||||
| **source** | a repository, a path within it, and a ref — resolved to one commit ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)). The repository is either on the forge holding the `git` seat, recorded by its path there and cloned from wherever that forge runs at build time, or external, recorded and cloned exactly as given ([ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md)) |
|
||||
| **recipe** | how *one* artifact is produced from that source |
|
||||
| **toolchain** | what a recipe runs inside — a compiler, a runtime, the SDK |
|
||||
| **artifact** | what a recipe produced, named by the digest of its content |
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-09-20
|
||||
updated: 2026-09-25
|
||||
decisions:
|
||||
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
||||
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
|
||||
---
|
||||
|
||||
# 23 — Choosing a provider
|
||||
@@ -50,9 +51,16 @@ provider on a different node. That coupling is exactly what may not be guessed,
|
||||
names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is
|
||||
to that provider and not to whichever one is nearest.
|
||||
|
||||
**A seat names the mesh's one provider of a kind.** Where a seat delivers the provision, its holder
|
||||
answers for it when several providers exist and the consumer named none. That is not picking: the
|
||||
choice was made once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
|
||||
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
|
||||
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
|
||||
coupled to particular contents has said so.
|
||||
|
||||
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
|
||||
named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates
|
||||
shown — the same stance
|
||||
named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused
|
||||
with the candidates shown — the same stance
|
||||
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took
|
||||
against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
|
||||
delivered quietly costs more than a refusal.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-controller internal/catalogue/seats.go
|
||||
- mesh-controller internal/catalogue/resolve.go
|
||||
- mesh-controller cmd/mesh-controller/seats.go
|
||||
- mesh-controller cmd/mesh-controller/build.go
|
||||
- mesh-catalog modules/gitea/module.json
|
||||
updated: 2026-09-25
|
||||
decisions:
|
||||
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
|
||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
|
||||
---
|
||||
|
||||
# 26 — The seats
|
||||
|
||||
**What a mesh can have one of, and who fills each.** A seat is a named role at a scope, taken by a
|
||||
module assignment. The mesh defines which seats exist. Occupying one may deliver a provision, and the
|
||||
list of seats with their holders is the quickest answer to "what is in this mesh".
|
||||
|
||||
## What a seat is
|
||||
|
||||
A seat has four properties, fixed by the mesh rather than by any module:
|
||||
|
||||
| property | is |
|
||||
|---|---|
|
||||
| name | what a manifest claims, and what a person reads in the list |
|
||||
| scope | node, site or mesh: where there may be only one holder |
|
||||
| delivers | the provision its holder answers for, or nothing |
|
||||
| decision | the record that made it a seat |
|
||||
|
||||
**A module assignment holds a seat by claiming it.** The claim is the manifest's `claims`, and it is
|
||||
satisfied by assigning the module somewhere. The seat is not a second record beside the assignment.
|
||||
It points at the assignment, and everything the mesh knows about the holder is what it knows about
|
||||
that assignment: the node, the node's settings for the module, and what the module serves.
|
||||
|
||||
**The set is closed.** A claim naming a seat the mesh does not define is refused, and so is a claim at
|
||||
the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the host's
|
||||
vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry nobody
|
||||
argued for is an entry nobody can explain.
|
||||
|
||||
## The set
|
||||
|
||||
| seat | scope | delivers | typically held by |
|
||||
|---|---|---|---|
|
||||
| `mesh-controller` | mesh | — | the controller |
|
||||
| `mesh-store` | mesh | `postgres-database` | the store |
|
||||
| `mesh-broker` | mesh | `amqp` | the broker |
|
||||
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
||||
| `the-catalogue` | mesh | — | the catalogue |
|
||||
| `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||
| `git` | mesh | `git` | the forge |
|
||||
| `the-build-machine` | node | — | a builder |
|
||||
| `the-dns-port` | node | — | the local resolver |
|
||||
| `the-intrusion-prevention` | node | — | an intrusion-prevention service |
|
||||
| `the-packet-filter` | node | — | the packet filter |
|
||||
| `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen |
|
||||
| `the-showcase` | node | — | the showcase module |
|
||||
|
||||
The control plane holds this set in code, and a test asserts both its size and that every entry names
|
||||
the record that made it a seat. This document follows the code, not the reverse. If the two disagree,
|
||||
the test has been changed without this table, and the table is what is wrong.
|
||||
|
||||
## A seat that delivers a provision
|
||||
|
||||
A seat that delivers a provision may only be held by a module that provides it, at the seat's scope.
|
||||
A mesh seat delivers a mesh-scoped provision.
|
||||
|
||||
**Its holder answers for that provision.** When a requirement for it has more than one provider in the
|
||||
mesh, the control plane takes, in order:
|
||||
|
||||
1. the provider the consumer's node was pinned to, because a consumer coupled to one provider's
|
||||
contents has said so ([23 — Choosing a provider](23-choosing-a-provider.md));
|
||||
2. the holder of the seat;
|
||||
3. the only provider, when there is one;
|
||||
4. otherwise nothing, and the requirement is refused with the candidates named.
|
||||
|
||||
So a second provider can run beside the holder and harm nothing. The forge holds
|
||||
`npm-package-registry`. An npm proxy may provide the same provision on another machine, and a module
|
||||
requiring an npm registry is still served by the forge, without anybody pinning it. Moving the role
|
||||
to the proxy is moving the seat: unassign the claim from one, assign it to the other, and every
|
||||
consumer follows.
|
||||
|
||||
**What a consumer receives is a grant**, the same as for any provision: where the provider answers,
|
||||
what it serves, and a credential where one is minted. A consumer never reads the seat directly. The
|
||||
one exception is the control plane itself, which reaches the store and the broker through a narrow
|
||||
seat placeholder because it made them before any module existed and cannot be their consumer.
|
||||
|
||||
## A seat that delivers nothing
|
||||
|
||||
Most node seats deliver nothing. They say which module is this machine's packet filter, or which of
|
||||
two alternative resolver configurations it runs, and a second holder is refused. That is the whole of
|
||||
their job, and it is a real one: it is the mesh saying what a machine is, in words a person can read.
|
||||
|
||||
## The overview
|
||||
|
||||
The control plane lists every seat in the set with its scope, what it delivers, and each holder as a
|
||||
node and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no
|
||||
forge", and not a fault.
|
||||
|
||||
Holdings are derived from assignments whenever they are asked for, never stored. The list is always
|
||||
what the mesh is running, because it is computed from the same thing that decides what the mesh runs.
|
||||
|
||||
## The git seat, and where a build comes from
|
||||
|
||||
A module is built from a repository, a path and a ref. The repository is one of two things, and the
|
||||
mesh records which:
|
||||
|
||||
| form | means | recorded as |
|
||||
|---|---|---|
|
||||
| on the `git` seat | a repository on the forge that holds the seat | its path on the forge, and the seat |
|
||||
| external | a repository anywhere else, a public forge for instance | its URL, exactly as given |
|
||||
|
||||
For a repository on the seat, the control plane composes the clone URL at the moment of building,
|
||||
from where the holder runs and the scheme and port it serves for `git`. The recorded source never
|
||||
contains an address, so moving the forge changes nothing that was recorded. The build machine is not
|
||||
told the difference: it receives a URL either way.
|
||||
|
||||
With the seat unheld, a build from the seat is refused and says why. External builds carry on.
|
||||
|
||||
**Not yet designed:** a credential for cloning a private repository. The mesh's own repositories are
|
||||
public. The natural place for a clone credential is the `git` provision's grant, and that is a
|
||||
decision still to take.
|
||||
@@ -34,6 +34,7 @@ document is written and this one's status becomes `implemented`.
|
||||
| [`22-the-work-ahead.md`](22-the-work-ahead.md) | Everything decided and not yet built, in dependency order, each phase ending at a run | [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md), [ADR 0075](../../02-DECISIONS/0075-two-stores-and-which-provides-what.md), [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md) |
|
||||
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
|
||||
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
|
||||
| [`26-the-seats.md`](26-the-seats.md) | What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
||||
|
||||
## Not yet written
|
||||
|
||||
|
||||
Reference in New Issue
Block a user