To-be 27 and ADR 0113 (proposed): a module requires, the mesh resolves
The design pass. Everything a module needs is a requirement: a name, a contract, and one of four kinds of provider — a module, the node's host, the mesh, the operator. Installing a module resolves every requirement or refuses, naming everything missing at once. It retires six mechanisms that grew separately: provisions through bindings, settings, assigned ports, machine facts, minted secrets and literals in the definition. ADR 0113, proposed: a provider makes what it provides, and the mesh carries it back sealed to the consumer's node. It is the return path ADR 0048 left "to a separate decision", now needed three ways: data provisions with nothing to answer with, contracts needing a value the controller cannot make, and a vault that generates nothing. Who a consumer is stays the mesh's (ADR 0049). Genesis is the one exception. On acceptance it supersedes 0048 and amends 0085. ADR 0112 is revised from three sources to that single concept. ADR 0110 is amended for two points raised in review. The vault gets the mesh-vault seat (issue 106). A seat's holder outranks co-location for a provision it delivers. Writing that down exposed an inconsistency: mesh-store delivering postgres-database would have sent every database consumer to the control-node, against to-be 23's node-local stores. So a seat delivers a provision only where the mesh has one answer for everyone — artifact store, npm registry, git, vault — and mesh-store and mesh-broker deliver nothing. 23 and 26 follow. 'Control plane' becomes 'controller' in the records written today.
This commit is contained in:
@@ -16,7 +16,7 @@ singular, at a scope, and a second holder is refused. [ADR 0079](0079-the-founda
|
||||
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
|
||||
**Any well-formed name becomes a seat by being claimed.** The controller'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`.
|
||||
@@ -24,9 +24,9 @@ The names in use were each invented by the module that claims them: `the-showcas
|
||||
**Nothing can say what a mesh has, or who fills it.** There is no seat table and no command that
|
||||
lists seats. Holdings are assembled while planning, one node at a time, and discarded afterwards.
|
||||
The only way to answer "which seats does this mesh have, and which module holds each" is to read
|
||||
every manifest in two repositories, because the core modules' manifests moved into the control
|
||||
plane's own repository ([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), and then the
|
||||
control plane's code, because one module it ships has its manifest composed there. While this
|
||||
every manifest in two repositories, because the core modules' manifests moved into the controller's
|
||||
own repository ([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), and then the controller's
|
||||
code, because one module it ships has its manifest composed there. While this
|
||||
record was being prepared, that enumeration was done by hand, and it missed both of the last two
|
||||
sources: eleven claims were reported where there are thirteen.
|
||||
|
||||
@@ -68,34 +68,49 @@ 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:
|
||||
not is refused. A requirement for that provision resolves, in order, to:
|
||||
|
||||
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.
|
||||
2. **the holder of the seat** that delivers it, **even when another provider runs on the consumer's
|
||||
own node**;
|
||||
3. otherwise refused, naming the unheld seat.
|
||||
|
||||
**Co-location does not apply to a provision a seat delivers.** For every other provision, a provider
|
||||
on the consumer's own node answers first, then the only provider, then refusal
|
||||
([to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md)). A seat exists to say *which one is the
|
||||
mesh's*, and co-location answering first would let any second provider on a consumer's machine take
|
||||
over silently for that consumer. That is the failure [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md)
|
||||
names for the vault.
|
||||
|
||||
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,
|
||||
**Seats are also informational.** The controller lists every seat in the set, what it delivers,
|
||||
and which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh
|
||||
has no X", not an error.
|
||||
|
||||
**The first set is the twelve seats already claimed, plus one.** Thirteen claims are in use, and
|
||||
**A seat delivers a provision only where the mesh has one answer for everyone.** That is a design
|
||||
decision about the provision, not about the seat. The artifact store, the npm registry, git and the
|
||||
vault are each one per mesh by their own records, so their seats deliver them. The store and the
|
||||
broker are not: [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md) has each node running its
|
||||
own stores, with a consumer served by the one on its own machine. So `mesh-store` and `mesh-broker`
|
||||
keep guarding that the foundation's own server is singular, and deliver nothing. Were they to
|
||||
deliver, every database consumer on every node would be sent to the control-node's store.
|
||||
|
||||
**The first set is the twelve seats already claimed, plus two.** Thirteen claims are in use, and
|
||||
they name twelve seats because two alternative modules claim `the-resolver-configuration`. This
|
||||
record admits every seat the catalogue and the control plane claim today, so no module is refused
|
||||
by it:
|
||||
record admits every seat the catalogue and the controller 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) |
|
||||
| `mesh-store` | mesh | — | `postgres` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
||||
| `mesh-broker` | mesh | — | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
||||
| `mesh-vault` | mesh | `secret` | nothing yet: `mesh-vault` claims it | this record, for [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.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) |
|
||||
@@ -103,15 +118,21 @@ by it:
|
||||
| `the-dns-port` | node | — | `dnsmasq` | this record |
|
||||
| `the-intrusion-prevention` | node | — | `fail2ban` | this record |
|
||||
| `the-packet-filter` | node | — | `nftables` | this record |
|
||||
| `the-private-network` | node | — | the control plane's private-network module | this record |
|
||||
| `the-private-network` | node | — | the controller's private-network module | this record |
|
||||
| `the-resolver-configuration` | node | — | `resolv-conf` or `resolved-split-dns` | this record |
|
||||
| `the-showcase` | node | — | `showcase` | this record |
|
||||
|
||||
`npm-package-registry` is the one addition. It is [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)'s
|
||||
There are two additions. `npm-package-registry` 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.
|
||||
|
||||
`mesh-vault` answers [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md). The vault
|
||||
is one per mesh ([ADR 0085](0085-a-secret-is-a-provision.md), as amended), and until now that was
|
||||
enforced by nothing. A second vault would have answered requirements silently, and any consumer on
|
||||
its machine would have been served by it through co-location. The seat is named after its server, by
|
||||
the 0079 convention, and the vault module claims it.
|
||||
|
||||
`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
|
||||
@@ -119,7 +140,7 @@ question real.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The control plane carries the set in code. A test asserts its size, and that every entry names the
|
||||
- The controller 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
|
||||
@@ -133,7 +154,7 @@ question real.
|
||||
`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
|
||||
- **Not changed:** `${seat:<seat>:<port>}` stays as it is. It exists so the controller 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.
|
||||
|
||||
@@ -12,7 +12,7 @@ extends: 0069-a-module-is-a-repository-and-a-path.md
|
||||
## 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
|
||||
ref, and the controller 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.
|
||||
@@ -52,7 +52,7 @@ serving how a repository on it is cloned: the scheme and the port. gitea claims
|
||||
|
||||
- `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
|
||||
address.** At the moment of building, the controller 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.
|
||||
@@ -63,11 +63,11 @@ mesh without a forge of its own builds from external repositories only, and says
|
||||
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.
|
||||
the controller's job, because only the controller 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
|
||||
- The controller'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
|
||||
|
||||
@@ -7,7 +7,7 @@ reconstructed: false
|
||||
extends: 0046-a-module-configuration-is-its-assignments-not-its-manifest.md
|
||||
---
|
||||
|
||||
# 112. A module definition names no node, no mesh and no path: everything it needs is resolved at assignment
|
||||
# 112. A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves
|
||||
|
||||
## Context
|
||||
|
||||
@@ -24,12 +24,20 @@ copies. The issue records what that has already allowed:
|
||||
- defaults in code that disagree with their own manifests;
|
||||
- no way to assign one module to one node twice, because every identity is keyed by the module's name.
|
||||
|
||||
**The mesh has already decided this once, for ports.** [ADR 0038](0038-the-mesh-assigns-the-port.md):
|
||||
the mesh assigns the machine-side port and the module says only what it needs, and three copies
|
||||
became one fact. [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)
|
||||
made the manifest identity and defaults, and the assignment's settings the configuration.
|
||||
[ADR 0084](0084-which-provider-serves-a-consumer.md) made which provider serves a consumer part of
|
||||
the assignment. Paths are the largest thing still left in the definition.
|
||||
**Paths are one case of a wider pattern.** A module gets what it needs through at least six separate
|
||||
mechanisms today, each with its own syntax and its own failure modes:
|
||||
|
||||
- provisions, read through bindings;
|
||||
- settings on the assignment ([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md));
|
||||
- ports the mesh assigns ([ADR 0038](0038-the-mesh-assigns-the-port.md));
|
||||
- machine facts a manifest asks for;
|
||||
- secrets, either minted or accepted from an operator;
|
||||
- literals carried in the definition itself.
|
||||
|
||||
The mesh has already unified parts of this. Ports became the mesh's rather than the module's (0038),
|
||||
configuration became the assignment's (0046), and which provider serves a consumer became the
|
||||
assignment's choice ([ADR 0084](0084-which-provider-serves-a-consumer.md)). What remains is the
|
||||
concept that joins them.
|
||||
|
||||
## Considered Options
|
||||
|
||||
@@ -37,114 +45,123 @@ the assignment. Paths are the largest thing still left in the definition.
|
||||
agreement of something that should not be there. A definition still could not follow its data to
|
||||
another disk, be adopted onto a machine whose data is already somewhere, or run twice on one node.
|
||||
|
||||
**2. Keep host paths in definitions, and have the mesh rewrite them per assignment.** Rejected. It is
|
||||
string surgery on paths: deciding which strings are machine paths by their shape, which is the
|
||||
inference this repository has refused elsewhere. And the definition would still read as though it
|
||||
decided where things live.
|
||||
**2. Keep the separate mechanisms, and add directories as a seventh.** Rejected. It fixes paths and
|
||||
keeps the pattern that produced them: each mechanism is resolved, validated and refused differently,
|
||||
so a module author learns six systems and a reviewer checks six kinds of gap.
|
||||
|
||||
**3. A definition names variables, and the assignment resolves them.** Chosen.
|
||||
**3. One concept: a module requires, and the mesh resolves every requirement against a contract.**
|
||||
Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**A module definition is node-agnostic and mesh-agnostic.** It names no node, no mesh and no host
|
||||
path. Everything that makes a running instance *this* instance is a variable.
|
||||
path. **Everything a module needs is a requirement**: a name, a contract saying what the module may
|
||||
read from it, and which kind of provider answers it.
|
||||
|
||||
**Installing a module on a node resolves every variable, or refuses.** A refusal names each
|
||||
unresolved variable and what could answer it. Variables are answered from three sources:
|
||||
**Installing a module on a node resolves every requirement, or refuses.** A refusal names each
|
||||
unresolved requirement and what could answer it, all at once.
|
||||
|
||||
1. **The assignment's own configuration.** Values chosen for this module on this node, carried as
|
||||
settings ([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)). An
|
||||
endpoint binding a public name to a port is one.
|
||||
2. **Provisions, resolved by the mesh against a contract.** What other modules provide: a database,
|
||||
a bucket, a vhost, a secret. Each comes with the contract the mesh and the module's
|
||||
specification define. Where a provision has several providers, which one answers is part of the
|
||||
assignment ([ADR 0084](0084-which-provider-serves-a-consumer.md)), so a module may take its
|
||||
database from another node. Some have one provider per mesh by decision: a module's own secret
|
||||
is a `secret` provision the controller mints and the vault records
|
||||
([ADR 0085](0085-a-secret-is-a-provision.md), as amended). Whether the vault should instead
|
||||
generate a secret against its contract is an open question, raised while reviewing this, and not
|
||||
decided here.
|
||||
3. **What the mesh generates or knows.** The credential the controller mints for each provision a
|
||||
module takes ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)), the ports it
|
||||
assigns ([ADR 0038](0038-the-mesh-assigns-the-port.md)), facts about the machine.
|
||||
**There are four kinds of provider, and the set is closed:**
|
||||
|
||||
**A directory is a provision, provided by the node's host.** A module requires one by name, such as
|
||||
its configuration or its data. Its contract is the owner and mode it needs, including the owner its
|
||||
image expects ([ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)). It
|
||||
carries no persistence flag. A directory is kept while it holds anything
|
||||
| provider | answers | today's mechanism it replaces |
|
||||
|---|---|---|
|
||||
| **another module** | a database, a bucket, a vhost, a secret, a route | provisions and bindings |
|
||||
| **the node's host** | a directory, a port, facts about the machine | resource paths, `${port:}`, `${machine:}`, facts |
|
||||
| **the mesh** | the module's identity and names, and the delivery of every answer | derived logins, minted delivery |
|
||||
| **the operator, through the assignment** | a value a person chooses: a public name, a greeting, an external key | settings, carried literals |
|
||||
|
||||
A module provider is chosen as [ADR 0084](0084-which-provider-serves-a-consumer.md) and
|
||||
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) say: a pin, then the holder of a
|
||||
seat that delivers the provision, then co-location, then the only one. A host provider is always the
|
||||
module's own node, because a host path or a port means nothing on any other. An operator value is the
|
||||
assignment's, or the requirement's default, or unresolved.
|
||||
|
||||
**A person's value stays cheap.** An operator requirement's contract is a type and, optionally, a
|
||||
default. It needs no provider module, no grant and no credential. An operator value that is secret,
|
||||
like an external API key, is kept by the vault as an operator-delivered value
|
||||
([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)).
|
||||
|
||||
**What a provider answers with is its contract's fields**, made by the provider and carried back to
|
||||
the consumer by the mesh ([ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)).
|
||||
|
||||
**A directory is a host provision.** Its contract is the owner and mode the module needs, including
|
||||
the owner its image expects ([ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)).
|
||||
It carries no persistence flag. A directory is kept while it holds anything
|
||||
([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md), which refused a `keep` flag for good
|
||||
reason), and data that is disposable is not a directory at all but a named volume (0107).
|
||||
|
||||
*Where* a directory is on the machine is the assignment's. A node has a default layout, and an
|
||||
assignment may place one directory elsewhere: on a second disk, or where an adopted machine's data
|
||||
already is ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)). A directory is
|
||||
always provided on the module's own node, because a host path means nothing on any other.
|
||||
reason), and data that is disposable is not a directory but a named volume (0107). *Where* it is on
|
||||
the machine is the assignment's. A node has a default layout, and an assignment may place a directory
|
||||
elsewhere: on a second disk, or where an adopted machine's data already is
|
||||
([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||
|
||||
**An operator's shared data stays an `access`** ([ADR 0051](0051-shared-data-is-the-operators.md)),
|
||||
not a directory provision. 0051 rejected giving a directory an operator owner, because the mesh
|
||||
must never create, chown or remove such data, and that stands. What changes is only where its
|
||||
location is written: the module says it needs read or read-write access, and the assignment says
|
||||
where the data is.
|
||||
not a directory. 0051 rejected giving a directory an operator owner, because the mesh must never
|
||||
create, chown or remove such data, and that stands. Only where its location is written changes: the
|
||||
module requires read or read-write access, and the assignment says where the data is.
|
||||
|
||||
**Inside a container, a module sees its own paths.** The definition says where the image expects
|
||||
each directory. The mesh mounts the assignment's location there. No host path is ever a value a
|
||||
process reads.
|
||||
|
||||
**The mesh's own files carry no host path.** Bindings, secrets and contributions are named relative
|
||||
to where the module receives them, so a provider reads what it was given without mounting anything
|
||||
at a machine-identical path.
|
||||
**Inside a container, a module sees its own paths.** The definition says where the image expects each
|
||||
directory. The mesh mounts the assignment's location there. No host path is ever a value a process
|
||||
reads, and the mesh's own files (answers, contributions) name nothing by host path, so a provider needs
|
||||
no mount at a machine-identical path.
|
||||
|
||||
**An assignment has an identity of its own: an instance name**, defaulting to the module's name.
|
||||
Everything keyed by the module's name today is keyed by the instance instead: directories,
|
||||
container names, the login a consumer presents, broker accounts, a claim's holder, the settings
|
||||
an assignment carries, and a provider's identity. So **one module may be assigned to one node more
|
||||
than once.** What must stay singular stays so by a claim, or by the assignment's own configuration
|
||||
colliding: a public name already taken is refused like any other singular thing.
|
||||
Everything keyed by the module's name today is keyed by the instance instead: directories, container
|
||||
names, the login a consumer presents, broker accounts, a claim's holder, the settings an assignment
|
||||
carries, and a provider's identity. So **one module may be assigned to one node more than once.** What
|
||||
must stay singular stays so by a claim, or by an operator value colliding: a public name already taken
|
||||
is refused like any other singular thing.
|
||||
|
||||
**Genesis is the one exception**, as [ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)
|
||||
states: the foundation's requirements are answered by genesis itself, before any provider exists.
|
||||
|
||||
## What this changes in earlier records
|
||||
|
||||
On acceptance, each of these is amended by a record of its own, not edited:
|
||||
|
||||
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): settings become
|
||||
operator requirements, addressed to an instance rather than to a module on a node.
|
||||
- [ADR 0038](0038-the-mesh-assigns-the-port.md): a port becomes a host requirement. What 0038 decided
|
||||
is unchanged; it is the first case of this rule.
|
||||
- [ADR 0051](0051-shared-data-is-the-operators.md): an access keeps its shape and its semantics; its
|
||||
path moves from the definition to the assignment.
|
||||
- [ADR 0091](0091-a-mount-is-declared-three-ways.md): a mount's host side is a resolved variable,
|
||||
- [ADR 0091](0091-a-mount-is-declared-three-ways.md): a mount's host side is a resolved requirement,
|
||||
checked as resolved rather than as a path the definition declares.
|
||||
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): settings are
|
||||
addressed to an instance, not to a module on a node.
|
||||
- [ADR 0084](0084-which-provider-serves-a-consumer.md): a provider is a (node, instance) pair, not a
|
||||
(node, module) pair, so a consumer can name one of two instances on one node.
|
||||
- The [glossary](../00-META/glossary.md): *provision* widens from "a service one module provides" to
|
||||
include a directory the node's host provides, and *instance* is added. Neither lands while this
|
||||
- The [glossary](../00-META/glossary.md): *provision* widens from "a service one module provides" to a
|
||||
requirement answered by any of the four providers, and *instance* is added. Neither lands while this
|
||||
record is only proposed, because the glossary is the authority on the words in use, not on words
|
||||
under review.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Every definition changes.** 70 of 71 name host paths today. The change is mechanical for most.
|
||||
The design has to say how existing modules migrate without their data moving: an adopted or
|
||||
already-running assignment is placed where its data already is.
|
||||
- The controller resolves variables at assignment and refuses unresolved ones. The host provides
|
||||
directories. The contributions file's format changes, and so does the SDK's reconcile loop that
|
||||
reads it.
|
||||
- **Every definition changes.** 70 of 71 name host paths today, and most use at least three of the
|
||||
mechanisms this replaces. The change is mechanical for most. The design has to say how existing
|
||||
modules move without their data moving: an adopted or already-running assignment is placed where
|
||||
its data already is.
|
||||
- The controller resolves every requirement at assignment and refuses unresolved ones. The host
|
||||
answers directories and ports. The settings, placeholders, facts and bindings that exist today
|
||||
are retired as separate mechanisms, once nothing uses them.
|
||||
- Identity moves from the module to the instance, which touches logins, broker accounts, settings,
|
||||
provider selection and every resource name.
|
||||
- **What got harder:** a definition no longer says where a module's data is on a machine. The
|
||||
assignment does, and `plan` shows it. That is the point, and it is also a real loss of
|
||||
at-a-glance legibility, which the overview has to give back.
|
||||
- **Not decided here:** the variable syntax; a node's default layout; whether a second instance of a
|
||||
module is supported from the first step or after the definitions have moved; whether the vault
|
||||
generates secrets.
|
||||
provider selection and every resource name. A login already has a 20-character limit
|
||||
([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)), which a node and an instance
|
||||
name will strain. The design must answer that before a second instance is possible.
|
||||
- **What got harder:** a definition no longer says where a module's data is on a machine, or what a
|
||||
setting's value is. The assignment does, and `plan` shows it. That is the point, and it is also a
|
||||
real loss of at-a-glance legibility, which the overview has to give back.
|
||||
- **Not decided here:** the syntax a definition reads a requirement's fields with; a node's default
|
||||
layout; the order in which the mechanisms are retired. [To-be 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
|
||||
proposes all three.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A definition names no host path | A catalogue test: the host side of every mount, and every resource location, binding, secret, receives and grants entry, is a variable rather than an absolute path. A declared list of exceptions shrinks to empty as definitions move. |
|
||||
| A definition names no host path | A catalogue test: the host side of every mount, and every resource location, is a requirement rather than an absolute path. A declared list of exceptions shrinks to empty as definitions move. |
|
||||
| No host path is a value a process reads | A catalogue test: every absolute path in a container's environment or env-files lies on the container side of one of its mounts, or is declared the image's own. A second test finds literal paths in module code used as fallbacks for an environment variable. |
|
||||
| A definition names no node and no mesh | The parser has no field that names a node; a node is named only in an assignment. A catalogue test finds no domain name in any definition value. |
|
||||
| Installation resolves every variable | A resolution test with one variable unanswered: refused, naming the variable and its possible sources. |
|
||||
| A directory is provided on its module's own node | A resolution test: an assignment placing a directory on another node is refused. |
|
||||
| A provider reads what it was given without an identical mount | A provisioner test reading a contributions file whose credentials are named relative to where it is mounted. |
|
||||
| Every requirement has one of the four providers | The parser refuses a requirement whose provider kind is not one of the four. |
|
||||
| Installation resolves every requirement | A resolution test with one requirement unanswered: refused, naming it and what could answer it. |
|
||||
| A host requirement is answered on its module's own node | A resolution test: an assignment placing a directory or a port on another node is refused. |
|
||||
| A module can run twice on one node | A resolution test assigning one module twice under two instance names: two directories, two logins, two containers, separate settings, no collision. |
|
||||
| A public name already taken is refused | A resolution test: a second instance asking for a public name the first holds is refused, naming the first. |
|
||||
| An adopted assignment is placed where its data is | An adoption test: the directory resolves to the data's existing location, and nothing is moved. |
|
||||
@@ -152,10 +169,10 @@ On acceptance, each of these is amended by a record of its own, not edited:
|
||||
## References
|
||||
|
||||
- [Issue 118](../04-ISSUES/118-a-module-definition-decides-where-its-files-live/00-report.md): the evidence
|
||||
- [ADR 0038](0038-the-mesh-assigns-the-port.md): the same decision, for ports
|
||||
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): configuration is the assignment's
|
||||
- [ADR 0084](0084-which-provider-serves-a-consumer.md): which provider answers is the assignment's
|
||||
- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md):
|
||||
secrets as a provision, and who mints what
|
||||
- [ADR 0038](0038-the-mesh-assigns-the-port.md), [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md),
|
||||
[ADR 0084](0084-which-provider-serves-a-consumer.md): the parts already unified
|
||||
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): which module provider answers
|
||||
- [ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md): who makes an answer, and how it travels
|
||||
- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md): the operator as a provider
|
||||
- [ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md),
|
||||
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): what a directory's contract carries, and what it must not
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: proposed
|
||||
date: 2026-09-25
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
---
|
||||
|
||||
# 113. A provider makes what it provides, and the mesh carries it back to the consumer
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) decided that a provider is
|
||||
handed the credential and makes none: the controller mints one per consumer and provider pair, seals
|
||||
it to both nodes, and the provider creates the login under it. It fixed a real fault. The provider
|
||||
harness of the time generated its own password and sealed it with a symmetric key nobody held, so a
|
||||
consumer could never receive what the provider made. Controller-minting worked because it needed no
|
||||
way back from provider to consumer.
|
||||
|
||||
**It left that way back undecided, deliberately.** 0048 says so: *"delivering provider-generated data
|
||||
back to a consumer is a return path the mesh does not have and this decision does not build — a
|
||||
separate shape, left to a separate decision."* Since then, the missing return path has come up
|
||||
repeatedly:
|
||||
|
||||
- **Data provisions have nothing to answer with.** The analytics provider assigns a site id the
|
||||
consumer needs. The DNS provider registers a name the consumer should be told. Both have no path
|
||||
back, and say so in their code.
|
||||
- **Some contracts need a value the controller cannot make.** The broker needs its admin password in
|
||||
a hashed form the mesh's plain secret delivery cannot produce, so a module-specific bootstrap step
|
||||
was written to derive it. A provider making the value to its own contract would not need one.
|
||||
- **The vault is a ledger.** A module's own secret is "a `secret` provision the controller mints and
|
||||
the vault records" ([ADR 0085](0085-a-secret-is-a-provision.md), as amended). The one module whose
|
||||
job is secrets generates none, and cannot apply a policy (length, form, lifetime) because it never
|
||||
makes one. Every other provider creates what it provides. The vault is the exception.
|
||||
|
||||
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) proposes that everything a module
|
||||
needs is a requirement answered by a provider against a contract. Under that, "the controller mints
|
||||
this one kind of answer on the provider's behalf" is a special case the model would carry forever.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Keep 0048: the controller mints credentials, and data provisions stay without a way back.**
|
||||
Rejected. The vault stays a ledger, contracts needing a derived form keep needing bespoke steps, and
|
||||
a data provision stays unable to answer at all.
|
||||
|
||||
**2. The provider makes the value and hands it to the consumer itself.** Rejected. This is the fault
|
||||
0048 fixed. The provider cannot seal to the consumer's node, the two may be on different machines,
|
||||
and a key both ends hold is the distribution problem one level down.
|
||||
|
||||
**3. The provider makes the value and gives it to the mesh, and the mesh carries it to the consumer.**
|
||||
Chosen. The provider answers over its own scoped account. The controller, which already seals to
|
||||
every node, seals each secret field to the consumer's node and delivers it the way it delivers
|
||||
everything else.
|
||||
|
||||
## Decision
|
||||
|
||||
**A provider makes what it provides.** Given a consumer, it creates the resource and answers with
|
||||
the fields its contract names: a password, an access key, a site id, a registered name, a hashed
|
||||
admin secret. The vault generates the secrets it provides, to their contract, and rotates them.
|
||||
|
||||
**The mesh carries the answer back.** The provider hands its answer to the controller over its own
|
||||
scoped broker account. The controller seals every field the contract marks secret to the consumer's
|
||||
node, and delivers the answer as the consumer's resolved values. A provider never reaches a consumer
|
||||
directly. Plaintext exists on the provider's machine, as it does today, and on the consumer's, and
|
||||
nowhere between.
|
||||
|
||||
**Who a consumer is stays the mesh's.** The login a consumer presents is the mesh's derivation, which
|
||||
both ends agree on by construction ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md),
|
||||
issue 023). A provider makes what a consumer is *given*, never what it is *called*.
|
||||
|
||||
**Rotation is the provider's act.** Asked to rotate, a provider makes a new value and answers again,
|
||||
and the mesh redelivers it. An operator-delivered value ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md))
|
||||
is still never replaced by the mesh: its provider is the operator.
|
||||
|
||||
**Genesis is the one exception.** The foundation's own credentials and the vault's own access exist
|
||||
before any provider can answer. Genesis mints those itself, seals them to the operator key as today,
|
||||
and hands them to their holders. Nothing else is minted by the controller.
|
||||
|
||||
## What this changes in earlier records
|
||||
|
||||
On acceptance, each of these is superseded or amended by this record, not edited:
|
||||
|
||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) is superseded: a provider no
|
||||
longer receives a minted credential. Its refusal of provider-held symmetric keys stands, and is why
|
||||
option 2 is rejected here.
|
||||
- [ADR 0085](0085-a-secret-is-a-provision.md) is amended: the vault generates a module's own secret
|
||||
rather than recording one the controller minted. "The vault stores no plaintext, ever" stands. It
|
||||
makes a value, hands it to the mesh and keeps only what it keeps today.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A consumer waits for its provider.** Its requirement is not resolved until the provider has
|
||||
answered, so resolution gains a state, *waiting on a provider*, that is shown rather than silent.
|
||||
Today a consumer can receive a credential before the resource behind it exists. After this it
|
||||
cannot.
|
||||
- The provider harness in the SDK changes: an adapter's create answers with its contract's fields
|
||||
instead of returning nothing, and every provider in the catalogue moves to it. This is one
|
||||
migration per provider, the cost 0048 named for changing the contract, and it is paid once.
|
||||
- The controller gains the return path: receiving an answer on a provider's account, sealing its
|
||||
secret fields, and delivering them. Data provisions gain the same path, so the analytics and DNS
|
||||
providers can finally answer.
|
||||
- Bespoke derivation steps, like the broker's admin-hash bootstrap, become the provider's own
|
||||
answer and can be removed.
|
||||
- **What got harder:** a provider that is down cannot hand out credentials, where today the controller
|
||||
could mint one in its absence. That is honest, because a credential for a resource that does not
|
||||
exist yet was never usable, but it moves a failure from later and silent to earlier and visible.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A provider's answer reaches the consumer sealed to its node | A controller test delivering a provider's answer: each secret field is sealed to the consumer's node key and to nothing else. |
|
||||
| A consumer's identity is still the mesh's | A resolution test: the login a consumer presents is the mesh's derivation, whatever the provider answers. |
|
||||
| A consumer waits for its provider | A resolution test with no answer yet: the requirement shows as waiting on a provider, and nothing is delivered. |
|
||||
| The controller mints nothing outside genesis | A controller test: outside genesis, no code path mints a credential. The minting function is reachable only from genesis. |
|
||||
| The vault generates and rotates | A vault test: a requested secret is generated to its contract, and a rotation answers with a new value. |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md): the decision this supersedes,
|
||||
and the return path it left open
|
||||
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): identity stays the mesh's
|
||||
- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0092](0092-an-operator-delivers-a-pair-credential.md):
|
||||
the vault, and the operator as a provider
|
||||
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md): everything a module needs is a requirement
|
||||
@@ -157,7 +157,8 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **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)
|
||||
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is resolved at assignment](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
|
||||
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
|
||||
- **0113** — [A provider makes what it provides, and the mesh carries it back to the consumer](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md) *(proposed)*
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
Reference in New Issue
Block a user