Decided with the author. A credential is never changed in place: each consumer has two logins, both derived by the mesh, and uses one at a time. An applier adds the new login beside the old through the adapter's existing create, and confirms both work; only then are readers released to the new one and restarted by derivation; only when every reader has confirmed is the old login retired through the existing remove. It closes the three cases review found in applier-first rotation: an offline reader keeps working on the old login until it returns; a bus account's owner keeps its bus until it has moved; a provisioner restarted mid-rotation is still delivered both values. Nobody is ever without a credential that works, which replaces to-be 13's all-or-nothing rule with a stronger one. No consumer module changes. The alternation is the provider loop's. A provider's adapter gains one duty, giving both logins the same rights over the consumer's data — in postgres, membership of one role that owns it. The mesh derives two logins per consumer, both within ADR 0049's limit, which 0113 now names among what it amends. Every rule has a check: overlap, offline reader, bus account, restarted provisioner, equal rights, login length, and confirmation only once the old login is gone.
350 lines
23 KiB
Markdown
350 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.
|
|
|
|
**Old and new overlap, so nobody is ever without a credential that works.** A credential is never
|
|
changed in place. Each consumer has two logins, both derived by the mesh, and uses one at a time:
|
|
|
|
1. **The vault makes the new value.**
|
|
2. **Each applier adds it beside the old**, as the consumer's other login, through the adapter's
|
|
existing create. It verifies that the new login works and the old one still does, and confirms,
|
|
repeating that confirmation on every reconcile pass until the vault acknowledges it.
|
|
3. **Only then is the new login released to the readers**, such as gitea, login and value together.
|
|
4. **The host restarts every such reader**, 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 applier 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. **Each reader confirms** by passing its health check with the new login, where its definition
|
|
declares one.
|
|
6. **Only when every reader has confirmed is the old login retired**, through the adapter's existing
|
|
remove, and verified to no longer authenticate.
|
|
|
|
So a reader whose machine is offline keeps working on the old login until it returns, a bus account's
|
|
owner keeps its bus until it has moved, and a provisioner restarted mid-rotation is still delivered
|
|
both values. The rotation shows as waiting on whichever applier or reader has not moved, and is done
|
|
only when the old login is gone.
|
|
|
|
**No consumer module changes.** A provider's adapter gains one duty: both of a consumer's logins get the
|
|
same rights over its data, which in postgres means both belong to one role that owns it. The
|
|
alternation itself is the provider loop's.
|
|
|
|
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 with old and new
|
|
overlapping, 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 overlaps old and new | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): both logins authenticate while readers move; an offline reader keeps working on the old login; the old login is retired only after every reader confirms; an applied secret restarts nothing, and one read at start restarts its reader without a declared restart. |
|
|
| 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.
|