Decided with the author: - A seat is held by one assignment, not claimed by a definition. A definition says which seats a module can hold; an assignment says which it does. The store module can run on every node and one assignment holds mesh-store; moving a role changes an assignment, never a definition. The foundation's seats name what the mesh itself uses and route no consumer — database and amqp consumers use co-location, the holder included. This replaces the wrong rationale that the foundation's store is "provider to nobody", which contradicted ADR 0078 and to-be 21. 0079's one-postgres rule becomes one mesh-store holder. - A module is assigned at most once to a node. The instance identity in 0112 and 27 is withdrawn, and the login-length problem with it. Review fixes to 0113: - The bottom of the stack: the vault is installed as soon as the shared runtime base exists, and genesis generates everything needed until then — including the permanent controller's, the control-node agent's, the builder's and the broker provisioner's bus accounts, and the controller's store login. Genesis creates those accounts until the broker's provisioner runs and adopts them. - Genesis's values are delivered recorded as the mesh's own, so 0092's never-replace rule for operator values does not make them unrotatable. - Backend-issued secrets (a forge's once-only API token) enter through the vault. Non-module parties (the controller's logins, node agents' accounts) are answered the same way, the controller asking on their behalf; an enrolment token reaches the controller only as what verifies it. - A secret with no provisioner to apply it is marked not rotatable by the mesh and refused, instead of a restart reported as done. Unused password generators in six provider clients are removed, and a catalogue scan checks no module mints. - Rotation's lock-out cases (offline reader, bus account owner, restarted provisioner) are recorded as open, with overlap and re-confirm-with-safeguards as the two answers, to be chosen before acceptance. 0110, 0111 and 26 are marked proposed: they changed in meaning and are under review, and an accepted record must not rest on proposed ones. To-be 23 and the glossary are restored to main; they change when these records are accepted.
346 lines
23 KiB
Markdown
346 lines
23 KiB
Markdown
---
|
|
layer: to-be
|
|
status: proposed
|
|
code: []
|
|
updated: 2026-09-25
|
|
decisions:
|
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
|
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
|
- 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/0084-which-provider-serves-a-consumer.md
|
|
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
|
|
- 02-DECISIONS/0038-the-mesh-assigns-the-port.md
|
|
---
|
|
|
|
# 27 — A module requires, the mesh resolves
|
|
|
|
**One concept for everything a module needs.** A module definition states what it requires. Each
|
|
requirement has a contract and a kind of provider. Installing the module on a node resolves every
|
|
requirement, or refuses and says why. Nothing else reaches a module: no path it chose, no setting
|
|
beside the model, no literal it carries.
|
|
|
|
This replaces six mechanisms that grew separately: provisions read through bindings, settings,
|
|
assigned ports, machine facts, minted or accepted secrets, and literals in the definition. Each
|
|
resolved, validated and failed in its own way ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
|
[issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
|
|
|
|
## A requirement
|
|
|
|
A requirement has three parts:
|
|
|
|
| part | is |
|
|
|---|---|
|
|
| name | what the module calls it, unique within the module |
|
|
| contract | the fields the module may read, and what each promises: a type, whether it is secret, and anything the provider must honour |
|
|
| provider kind | which of the four kinds of provider answers it |
|
|
|
|
**A contract is shared, not per module.** A database's contract is the database's, whoever requires
|
|
it. **The controller holds every contract**, one per provision name, declared where the provision is
|
|
defined in the catalogue. Today contracts are implicit in each provider's served fields; the first
|
|
phase below makes them explicit, because nothing can be checked against a contract that is not
|
|
written down. A provider is checked against the contract it claims to answer. A module's own
|
|
specification may narrow a contract (a password of at least this length, a directory owned by this
|
|
user) and never widen it.
|
|
|
|
## The four kinds of provider
|
|
|
|
The set is closed, like the seats. A fifth kind is a decision, because each kind is a place an answer
|
|
can come from and a reviewer has to know every one.
|
|
|
|
| provider | answers | resolved by | replaces |
|
|
|---|---|---|---|
|
|
| **a module** | a database, a bucket, a vhost, a route, a secret | the rule below | provisions and bindings |
|
|
| **the node's host** | a directory, a port, a fact about the machine | always the module's own node | resource paths, assigned ports, machine placeholders, facts |
|
|
| **the mesh** | the module's identity and names, and the delivery of every answer | the controller | derived logins and generated names; the controller's delivery |
|
|
| **the operator** | a value a person chooses | the assignment, else the requirement's default | settings, carried literals |
|
|
|
|
### A module provider
|
|
|
|
Which module answers, in order:
|
|
|
|
1. **a pin**: the assignment names a provider, because this consumer is coupled to that provider's
|
|
contents ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md));
|
|
2. **the holder of a seat** that delivers the provision, where one does. Co-location does not apply
|
|
to these: the seat is the mesh's one answer for everyone ([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
|
|
[26 — The seats](26-the-seats.md));
|
|
3. for a provision no seat delivers, **the provider on the consumer's own node**;
|
|
4. for a provision no seat delivers, **the only provider** in the mesh;
|
|
5. otherwise **refused**: naming the unheld seat, for a provision a seat delivers, even when exactly
|
|
one provider exists; naming the candidates otherwise.
|
|
|
|
### Secrets: provisioning all the way down
|
|
|
|
**Two kinds of secret, one rule each** ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)):
|
|
|
|
- **a shared secret**, a value more than one party must hold (a password, a token, an API key), is
|
|
made by the vault, and by nothing else;
|
|
- **a private key**, such as a node's sealing key, the operator's key or the mesh's certificate
|
|
authority, is made where it is used and never leaves. A private key anyone else held would no
|
|
longer be private.
|
|
|
|
**Every shared secret is a `secret` requirement, and only the vault provides `secret`.** The vault
|
|
holds the `mesh-vault` seat, and that provision is reserved to it: no other module may provide it, and
|
|
no pin can choose another provider ([26 — The seats](26-the-seats.md)).
|
|
|
|
**A provider that needs a secret for a consumer requires one, like any consumer.** A provision's
|
|
contract declares it: *for each consumer, one secret*. Resolution expands that into one requirement
|
|
per consumer, named for that consumer:
|
|
|
|
1. gitea requires `postgres-database`;
|
|
2. the database provider, to serve gitea, requires a `secret` named for gitea;
|
|
3. the vault makes it and hands it to the mesh;
|
|
4. the mesh delivers it to both of its **recipients**, each sealed to its own node: the database's
|
|
machine, which *applies* it by creating the login, and gitea's, which *presents* it;
|
|
5. the database provider creates the login, exactly as it does today, and gitea connects.
|
|
|
|
Every other shared secret takes the same path:
|
|
- a module's own secret;
|
|
- every broker account's password on the mesh's bus, where the broker's own provisioner creates the
|
|
account;
|
|
- an enrolment token, which the operator receives and the controller can only verify;
|
|
- a secret operator value, which the operator delivers to the vault;
|
|
- a secret a backend issues itself, such as a forge's API token, which the module that received it
|
|
delivers to the vault.
|
|
|
|
**Parties that are not modules take the same path too.** The controller's own store login and bus
|
|
account, and each node agent's bus account, have no definition to require them, because the controller
|
|
and a node agent are the mesh itself. The controller asks the vault on its own behalf or a node's, and
|
|
the answer is made, sealed and carried exactly as for a module.
|
|
|
|
**A provider makes resources and data.** Beyond secrets, a provider's adapter may answer with its
|
|
contract's non-secret fields: an analytics site id, a registered public name. The mesh carries them
|
|
back to the consumer as resolved values.
|
|
|
|
### The node's host
|
|
|
|
The host answers what only a machine can: where a directory is, which port is free, what the machine
|
|
is. It is always the module's own node, because none of these means anything elsewhere.
|
|
|
|
**A directory.** The contract is an owner and a mode, and the owner the image expects where it has
|
|
one. There is no persistence flag. A directory is kept while it holds anything, and data that may be
|
|
lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md),
|
|
[ADR 0107](../../02-DECISIONS/0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)).
|
|
|
|
*Where* a directory is on the machine is the assignment's:
|
|
|
|
- **a node's default layout**, a root per node with one directory per assignment beneath it, used when
|
|
the assignment says nothing;
|
|
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
|
|
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
|
|
|
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
|
|
never created, owned or removed by the mesh. The module requires read or read-write access. Where
|
|
the data is, is an operator value on the assignment.
|
|
|
|
**A port** is what [ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md) already decided: the
|
|
module says which port its software uses, and the host answers with where the machine put it.
|
|
|
|
**A fact** is something the machine knows: its name on the private network, the names of the mesh's
|
|
machines. Each fact has a contract like anything else.
|
|
|
|
### The mesh
|
|
|
|
The mesh answers who the module is: its login, its broker account, the names it is known by. These
|
|
are derived by the mesh so every party agrees by construction, and no provider may make them
|
|
([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
|
|
|
|
### The operator
|
|
|
|
A value a person chooses: a public name for an endpoint, a greeting, how many workers to run.
|
|
|
|
**It must stay cheap.** An operator requirement's contract is a type and, optionally, a default. It
|
|
needs no provider module, no grant and no credential. If asking a person for a value took more than
|
|
that, module authors would route around it, and the literals this replaces would come back.
|
|
|
|
**A secret operator value**, such as an external API key, follows the one rule for secrets: the
|
|
vault provides it. The operator hands the value to the vault, once
|
|
([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)), and the module requires
|
|
a `secret` like any other. The only difference is that rotation never replaces it: the vault cannot
|
|
make a new external key, so rotating one means an operator handing over a new value.
|
|
|
|
**An endpoint** is an operator value inside a route requirement: the public name is chosen on the
|
|
assignment, and the route provider answers. A public name already held by another assignment is
|
|
refused, like any other singular thing.
|
|
|
|
## How a definition reads what was resolved
|
|
|
|
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
|
host in an environment variable, the directory's location on the host side of a mount, or the public
|
|
name in a configuration file writes the same thing: the requirement's name and the field. The
|
|
controller fills it at resolution.
|
|
|
|
This one form replaces the placeholders that exist today, one per mechanism: bound values, secrets,
|
|
ports and machine facts.
|
|
|
|
**The seat placeholder stays, for the controller alone.** The controller composes its own
|
|
declaration and reaches the store and broker it made before any module existed, so it cannot be
|
|
their consumer. One module reads the placeholder today: the store module, to find its own server's
|
|
port. That is its own port, so it becomes a host port requirement in phase 3, and after that no
|
|
module uses the seat placeholder.
|
|
|
|
**A secret field reaches a process as a file**, as [ADR 0086](../../02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md)
|
|
decided. The one exception 0086 allows is a declared env-file with its reason; a secret field as a
|
|
value in a container's environment is refused when the definition is parsed, with no exception.
|
|
|
|
## An assignment
|
|
|
|
**A module is assigned at most once to a node**, and that pair is the assignment's identity
|
|
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). Its directories,
|
|
containers, login, broker account and settings are keyed by it, as today, and a login still fits the
|
|
tightest backend ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
|
|
|
|
**A module may run on many nodes, and one assignment may hold a seat**
|
|
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). The definition
|
|
says which seats the module can hold; the assignment says which it does. So the store module can run
|
|
on every node, one of those assignments holds `mesh-store`, and moving that role changes an
|
|
assignment, not a definition.
|
|
|
|
What must stay singular stays so: by a seat, or by an operator value colliding, as with a public name.
|
|
|
|
## Genesis
|
|
|
|
**Genesis delivers, and the vault adopts.** The vault cannot run first: it is built on the shared
|
|
runtime base, which the installation makes only after the store, the broker and the controller exist
|
|
([21 — The installation in full](21-the-installation-in-full.md)), and it learns what to answer from
|
|
the controller over the bus. So the vault is installed **as soon as that base exists**, before any other
|
|
module built on it, and genesis generates what is needed until then:
|
|
- the store's superuser, and the broker's admin in the hashed form the broker needs;
|
|
- the bus accounts of the temporary and permanent controller, the control-node's agent, the builder,
|
|
the broker's own provisioner and the vault;
|
|
- the controller's store login, and the first enrolment token.
|
|
|
|
Until the broker's provisioner runs, genesis creates those bus accounts with the broker's admin, as the
|
|
controller does today; the provisioner adopts them when it starts. Genesis seals everything to the
|
|
operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)), and when the
|
|
vault is installed it **delivers the values to it, recorded as the mesh's own**. That distinction keeps
|
|
them rotatable: an operator's value is never replaced, and these are, because the vault can make their
|
|
replacements.
|
|
|
|
That is the one time anything but the vault generates a shared secret, and it ends by handing them
|
|
over. It is also the answer to the objection ADR 0085 had to the vault being the only maker: the
|
|
vault cannot make what exists before it, so what exists before it is delivered to it.
|
|
|
|
## Rotation
|
|
|
|
Rotating a secret is asked of the vault, by an operator or by the vault's own policy, such as a
|
|
maximum age in the secret's contract ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)).
|
|
An operator's external key is not rotated by the vault, which cannot make its replacement: an
|
|
operator delivers a new one.
|
|
|
|
**A secret's contract says how each recipient takes a new value.** A recipient either *applies* it,
|
|
through a provisioner (a provider creating the login, the broker's provisioner updating an account,
|
|
the store's own provisioner changing its superuser), or *reads it at start*. A secret a service reads
|
|
only when it first initialises is marked applied, because a restart would change nothing.
|
|
|
|
1. **The vault makes the new value.**
|
|
2. **It goes first to the recipients that apply it.** Each applies it, verifies that the new value
|
|
authenticates and the old one no longer does, and confirms. It repeats that confirmation on every
|
|
reconcile pass until the vault acknowledges it, so a lost message costs one pass.
|
|
3. **Only then is it released to the recipients that read it at start**, such as gitea.
|
|
4. **The host restarts every such recipient**, and recreates a container whose env-file carries the
|
|
secret. It knows which, because a definition reads a secret only through its requirement, so no
|
|
definition declares a restart for a secret. An applying recipient is never restarted for it.
|
|
This needs [issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
|
|
fixed, or a container fed by an env-file keeps the old value.
|
|
5. **It is confirmed.** The rotation shows as unconfirmed until every applier has confirmed, and every
|
|
recipient that reads at start has restarted and passed its health check, where its definition
|
|
declares one. Delivered and working are shown as different things.
|
|
|
|
**Open: keeping readers from being locked out.** Steps 2 and 3 leave a reader locked out when its
|
|
machine is offline after an applier applied, when the secret is a bus account whose owner loses the bus
|
|
it would hear the new value on, or when a restarted provisioner can no longer check the old value.
|
|
[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) records the two answers, overlapping
|
|
old and new credentials or re-confirming with safeguards, and one is chosen before it is accepted.
|
|
Neither changes a consumer module.
|
|
|
|
A secret some service reads only when it first initialises cannot be rotated by restarting it. It is
|
|
applied by a provisioner, or, where none exists, marked not rotatable by the mesh, and a rotation is
|
|
refused rather than reported done.
|
|
|
|
## Refusing
|
|
|
|
Installation refuses when any requirement is unresolved, and **says everything at once**. For each
|
|
requirement it names what is missing and what would answer it:
|
|
|
|
- an unheld seat, and which modules could hold it;
|
|
- no provider, and which modules could provide it;
|
|
- an operator value with no default, and that the assignment must give it;
|
|
- a provider, or the vault, that has not answered yet, and which one.
|
|
|
|
The last one is a state, not a failure. A consumer waiting for its provider or for the vault is shown
|
|
as waiting, and nothing is delivered until the answer arrives.
|
|
|
|
## What this retires
|
|
|
|
| mechanism | becomes |
|
|
|---|---|
|
|
| provisions read through bindings | a module requirement; its answer is the contract's fields |
|
|
| settings on an assignment ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)) | operator requirements on an assignment |
|
|
| a port the mesh assigns | a host requirement |
|
|
| machine facts and machine placeholders | host requirements |
|
|
| every secret the controller mints: provider credentials, own secrets, broker passwords, enrolment tokens | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)) |
|
|
| root secrets genesis mints and keeps apart | made by genesis once, then delivered to the vault, which holds and rotates them |
|
|
| a separate command issuing a broker account | a requirement resolved on assignment |
|
|
| `restart-on` naming a secret's file | a restart the host derives |
|
|
| paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment |
|
|
| literals carried in a definition | operator requirements with defaults |
|
|
|
|
Each is retired only once nothing uses it. Until then both are accepted, and a catalogue test lists
|
|
the definitions still using the old form. That list shrinks to empty, and then the old form is
|
|
removed from the parser.
|
|
|
|
## Phases
|
|
|
|
Each phase ends at a check that holds, so none of them leaves a mechanism half-replaced.
|
|
|
|
1. **Contracts, resolution and the new form.** Every provision's contract is written down and held by
|
|
the controller. The controller resolves requirements from the four providers, refuses as above,
|
|
and fills the one form. Old mechanisms keep working beside it. *Ends when* a definition written
|
|
entirely in the new form installs on a lab machine.
|
|
2. **The vault makes every shared secret, and providers answer.** The vault holds its seat and its
|
|
reserved provision; resolution expands per-consumer secret requirements; genesis delivers the
|
|
foundation's first secrets to the vault; the broker's provisioner creates every bus account; the
|
|
mesh carries providers' data back.
|
|
[Issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
|
|
is fixed first. *Ends when* nothing outside the vault generates a shared secret after genesis, a lab
|
|
consumer of analytics receives its site id, and a database credential rotates applier-first, with
|
|
the consumer restarted by derivation and the rotation confirmed.
|
|
3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted
|
|
and running assignments placed where their data already is, and each claim becomes a seat the
|
|
module can hold, held by the assignment that holds it today. *Ends when* the list of definitions
|
|
using an old form is empty, the old forms are removed, and the store module runs on two lab
|
|
machines with one holding `mesh-store`.
|
|
|
|
## How it is checked
|
|
|
|
| Rule | Checked by |
|
|
|---|---|
|
|
| Every requirement has one of the four provider kinds | The parser refuses any other. |
|
|
| A definition names no host path, node or mesh | The catalogue tests of [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md). |
|
|
| A module provider is chosen by pin, seat, co-location, only one, refusal | Resolution tests for each step; for a second provider on a consumer's own machine when a seat delivers the provision; and for an unheld seat with exactly one provider, refused. |
|
|
| A provider answers within its contract | A controller test: an answer carrying a field its contract does not name, or missing one it does, is refused and not delivered. |
|
|
| A module narrows a contract and never widens it | The parser refuses a module specification that loosens a contract's field. |
|
|
| A host requirement is answered on its own node | A resolution test placing one elsewhere: refused. |
|
|
| An operator value needs no provider module | A resolution test: a requirement with a default resolves with no module assigned anywhere. |
|
|
| A secret field reaches a process as a file | The parser refuses a secret field as a container environment value, and accepts it in a file or a declared env-file with its reason. |
|
|
| A public name already held is refused | A resolution test: a second assignment asking for a public name another holds is refused, naming the holder. |
|
|
| A module is assigned at most once to a node | A resolution test: assigning a module to a node that already runs it is refused. |
|
|
| A seat is held by an assignment, not a module | A resolution test: the store module on two nodes, one holding `mesh-store`; a second assignment asking to hold it is refused. |
|
|
| A consumer waits for its provider | A resolution test with a provider that has not answered: shown as waiting, and nothing delivered. |
|
|
| Only the vault generates a shared secret after genesis | A controller test: no code path generates one. An installer test: genesis generates exactly the foundation's first secrets and delivers them to the vault. |
|
|
| Only the vault provides `secret` | The parser refuses another provider of it, and resolution refuses a pin on a `secret` requirement. |
|
|
| A provider's per-consumer secret comes from the vault | A resolution test: requiring a database expands to a secret requirement named for the consumer, answered by the vault and delivered to both recipients. |
|
|
| Rotation is applier-first, derived and confirmed | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): readers wait for every applier's repeated confirmation; an applied secret restarts nothing, and one read at start restarts its reader without a declared restart; the rotation shows unconfirmed until the new value authenticates, the old does not, and readers are healthy. |
|
|
| Refusal names everything at once | A resolution test with three unresolved requirements of different kinds: one refusal naming all three. |
|
|
| The old forms retire | The catalogue test listing definitions still using one. It must be empty before a form is removed. |
|
|
|
|
## Not settled here
|
|
|
|
- The exact spelling of the one form. It must name a requirement and a field and nothing else.
|
|
- The layout a node's default root uses beneath it, beyond one directory per assignment.
|
|
- Whether a module provider's answer can change without the provider being asked, for example a
|
|
provider moving. The rule so far is that it cannot, and moving is re-resolving.
|
|
- **How rotation keeps a recipient from being locked out.** Under review: see
|
|
[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), rotation.
|