Overlap as drafted in 0113 would have deleted consumer data: seven of eight providers name the resource after the login and five drop it on remove. Rotation is now undecided in 0113 and to-be 27, pending the survey. Also: a requirement naming a seat resolves to its holder, a person chooses among remaining candidates at assignment, the controller's secrets are requirements of its definition, genesis seals to the control-node key, and moving the vault or broker is break-glass.
346 lines
23 KiB
Markdown
346 lines
23 KiB
Markdown
---
|
|
layer: to-be
|
|
status: proposed
|
|
code: []
|
|
updated: 2026-09-26
|
|
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. **the holder of the seat the requirement names.** A requirement may name a seat instead of leaving
|
|
the provider open. It asks for *the mesh's* one, and the mesh answers with whichever assignment
|
|
holds that seat, with nothing asked of anyone. Unheld, the requirement is refused, naming the seat
|
|
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
|
|
[26 — The seats](26-the-seats.md)). A `secret` requirement always names `mesh-vault`, because
|
|
that provision is reserved;
|
|
2. **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));
|
|
3. **the provider on the consumer's own node**;
|
|
4. **the only provider** in the mesh;
|
|
5. otherwise **a person chooses, at assignment**. Assigning the module lists the candidates, with the
|
|
holder of a seat that delivers the provision suggested first, and the answer is recorded on the
|
|
assignment as its pin. Without an answer the module is not assigned, and the refusal names the
|
|
candidates. Nothing is ever guessed ([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)).
|
|
|
|
### 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.
|
|
|
|
**The controller takes the same path, because it is a module.** Its store logins and bus accounts are
|
|
own secrets of its definition today, and become requirements of that definition. **A node's host is the
|
|
one party with no definition**, because it is what runs definitions. Its bus account is a requirement
|
|
the mesh makes for each enrolled node, answered 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 (its account and the broker-management
|
|
login), the control-node's host, the builder, the broker's own provisioner and the vault;
|
|
- the controller's three store logins, 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
|
|
control-node's key, and when the vault is installed the controller **delivers the values to it,
|
|
recorded as the mesh's own**, with nobody present. 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.
|
|
|
|
**Raising the vault or the broker again is a genesis act.** Moving either seat to a new assignment, or
|
|
recovering either after it is lost, delivers the values it needs the way genesis did. It is a stated
|
|
break-glass procedure, and an ordinary assignment attempting it is refused.
|
|
|
|
## 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 requirement's contract, and the vault makes the new value
|
|
([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.
|
|
|
|
**Each requirement says how its recipient takes a new value.** It either *applies* it, through a
|
|
provisioner (a provider setting a login's password, the broker's provisioner updating an account, a
|
|
store's provisioner changing its own superuser), or *reads it at start*. Every module in the catalogue
|
|
reads its secrets at start, and none watches them. The host already recreates a container when a file
|
|
it read at creation changes, its env-files and files mounted into it directly
|
|
([issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)).
|
|
So a reader's restart is derived, and a definition declares `restart-on` only for a secret reaching a
|
|
process, or a file in a mounted directory. A secret a service takes only at first initialisation is
|
|
applied by its provisioner or marked not rotatable by the mesh, and a rotation of it is refused rather
|
|
than reported done.
|
|
|
|
**How old and new change over is not settled.** Until it is, rotation stays as the controller does it
|
|
today: in place, both ends sent in one push, with a stated window in which a consumer cannot
|
|
authenticate. [Research 016](../../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md)
|
|
measured three mechanisms against every provider. One constraint holds whichever is chosen: **retiring a
|
|
credential must never remove a consumer's resource.** Today's adapters remove both in one call, and in
|
|
five providers that deletes the consumer's data.
|
|
|
|
## 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;
|
|
- several candidates and no choice made, and which they are;
|
|
- 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 in the host already. *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, the
|
|
vault making the value, the consumer recreated by derivation, and its data intact.
|
|
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 named seat, pin, co-location, only one, a person's choice | Resolution tests for each step: a requirement naming a seat served by its holder even with another provider on the consumer's node, and refused when the seat is unheld; several candidates and none local, where assignment lists them with the seat's holder first and records the choice as a pin, and refuses without one. |
|
|
| 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. |
|
|
| Restarts are derived, and rotation keeps data | The tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): an applied secret restarts nothing, one read at start recreates its reader without a declared restart, and rotating a consumer's credential leaves its resource and data intact. |
|
|
| 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
|
|
|
|
- How old and new credentials change over on rotation: in place, as today, or two logins over one
|
|
resource, as [research 016](../../01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md)
|
|
recommends for credentials with two parties. It is decided in its own record.
|
|
|
|
- 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.
|