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
|
||||
|
||||
|
||||
@@ -51,12 +51,15 @@ provider on a different node. That coupling is exactly what may not be guessed,
|
||||
names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is
|
||||
to that provider and not to whichever one is nearest.
|
||||
|
||||
**A seat names the mesh's one provider of a kind.** Where a seat delivers the provision, its holder
|
||||
answers for it when several providers exist and the consumer named none. That is not picking: the
|
||||
choice was made once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
|
||||
**Some provisions have one provider for the whole mesh, and a seat names it.** Where a seat delivers
|
||||
the provision, its holder answers for it, **and co-location does not apply**: a second provider on the
|
||||
consumer's own machine does not take over for that consumer. That is not picking: the choice was made
|
||||
once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
|
||||
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
|
||||
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
|
||||
coupled to particular contents has said so.
|
||||
coupled to particular contents has said so. Only provisions the design makes one-per-mesh are
|
||||
delivered by a seat: the artifact store, a package registry, git and the vault. A database is not.
|
||||
Node-local stores, served by co-location, are the rule above.
|
||||
|
||||
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
|
||||
named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused
|
||||
|
||||
@@ -47,8 +47,9 @@ argued for is an entry nobody can explain.
|
||||
| seat | scope | delivers | typically held by |
|
||||
|---|---|---|---|
|
||||
| `mesh-controller` | mesh | — | the controller |
|
||||
| `mesh-store` | mesh | `postgres-database` | the store |
|
||||
| `mesh-broker` | mesh | `amqp` | the broker |
|
||||
| `mesh-store` | mesh | — | the foundation's store |
|
||||
| `mesh-broker` | mesh | — | the foundation's broker |
|
||||
| `mesh-vault` | mesh | `secret` | the vault |
|
||||
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
||||
| `the-catalogue` | mesh | — | the catalogue |
|
||||
| `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||
@@ -61,7 +62,7 @@ argued for is an entry nobody can explain.
|
||||
| `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen |
|
||||
| `the-showcase` | node | — | the showcase module |
|
||||
|
||||
The control plane holds this set in code, and a test asserts both its size and that every entry names
|
||||
The controller holds this set in code, and a test asserts both its size and that every entry names
|
||||
the record that made it a seat. This document follows the code, not the reverse. If the two disagree,
|
||||
the test has been changed without this table, and the table is what is wrong.
|
||||
|
||||
@@ -70,16 +71,23 @@ the test has been changed without this table, and the table is what is wrong.
|
||||
A seat that delivers a provision may only be held by a module that provides it, at the seat's scope.
|
||||
A mesh seat delivers a mesh-scoped provision.
|
||||
|
||||
**Its holder answers for that provision.** When a requirement for it has more than one provider in the
|
||||
mesh, the control plane takes, in order:
|
||||
**A seat delivers a provision only where the mesh has one answer for everyone.** The artifact store,
|
||||
the npm registry, git and the vault are each one per mesh by decision. The store and the broker are
|
||||
not: nodes run their own stores and a consumer uses the one on its machine
|
||||
([23 — Choosing a provider](23-choosing-a-provider.md)). So their seats guard that the foundation's
|
||||
own server is singular, and route nobody.
|
||||
|
||||
**Its holder answers for that provision.** A requirement for it resolves, in order, to:
|
||||
|
||||
1. the provider the consumer's node was pinned to, because a consumer coupled to one provider's
|
||||
contents has said so ([23 — Choosing a provider](23-choosing-a-provider.md));
|
||||
2. the holder of the seat;
|
||||
3. the only provider, when there is one;
|
||||
4. otherwise nothing, and the requirement is refused with the candidates named.
|
||||
2. the holder of the seat, **even when another provider runs on the consumer's own machine**;
|
||||
3. otherwise nothing, and the requirement is refused, naming the unheld seat.
|
||||
|
||||
So a second provider can run beside the holder and harm nothing. The forge holds
|
||||
Co-location, which answers first for every other provision, does not apply here: a seat says which
|
||||
one is the mesh's, and co-location answering first would let any second provider on a consumer's
|
||||
machine take over for that consumer, silently. So a second provider can run beside the holder and
|
||||
harm nothing. The forge holds
|
||||
`npm-package-registry`. An npm proxy may provide the same provision on another machine, and a module
|
||||
requiring an npm registry is still served by the forge, without anybody pinning it. Moving the role
|
||||
to the proxy is moving the seat: unassign the claim from one, assign it to the other, and every
|
||||
@@ -87,7 +95,7 @@ consumer follows.
|
||||
|
||||
**What a consumer receives is a grant**, the same as for any provision: where the provider answers,
|
||||
what it serves, and a credential where one is minted. A consumer never reads the seat directly. The
|
||||
one exception is the control plane itself, which reaches the store and the broker through a narrow
|
||||
one exception is the controller itself, which reaches the store and the broker through a narrow
|
||||
seat placeholder because it made them before any module existed and cannot be their consumer.
|
||||
|
||||
## A seat that delivers nothing
|
||||
@@ -98,7 +106,7 @@ their job, and it is a real one: it is the mesh saying what a machine is, in wor
|
||||
|
||||
## The overview
|
||||
|
||||
The control plane lists every seat in the set with its scope, what it delivers, and each holder as a
|
||||
The controller lists every seat in the set with its scope, what it delivers, and each holder as a
|
||||
node and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no
|
||||
forge", and not a fault.
|
||||
|
||||
@@ -115,7 +123,7 @@ mesh records which:
|
||||
| on the `git` seat | a repository on the forge that holds the seat | its path on the forge, and the seat |
|
||||
| external | a repository anywhere else, a public forge for instance | its URL, exactly as given |
|
||||
|
||||
For a repository on the seat, the control plane composes the clone URL at the moment of building,
|
||||
For a repository on the seat, the controller composes the clone URL at the moment of building,
|
||||
from where the holder runs and the scheme and port it serves for `git`. The recorded source never
|
||||
contains an address, so moving the forge changes nothing that was recorded. The build machine is not
|
||||
told the difference: it receives a URL either way.
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
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-a-provider-makes-what-it-provides-and-the-mesh-carries-it.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 118](../../04-ISSUES/118-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 mesh knows each contract, and a provider is checked against the one 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 | the controller | derived logins, generated names |
|
||||
| **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. **the provider on the consumer's own node**, for a provision no seat delivers;
|
||||
4. **the only provider** in the mesh;
|
||||
5. otherwise **refused**, naming the candidates, or the unheld seat.
|
||||
|
||||
The provider makes what it provides and answers with its contract's fields. The mesh carries the
|
||||
answer back to the consumer, sealing every secret field to the consumer's node
|
||||
([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)). The vault
|
||||
is a module provider like any other: it holds the `mesh-vault` seat and generates the secrets it
|
||||
provides.
|
||||
|
||||
### 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 instance 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, is held by the vault as an
|
||||
operator-delivered value ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)),
|
||||
never stored as a setting.
|
||||
|
||||
**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, machine facts and seats. The seat placeholder the controller uses to reach its own foundation
|
||||
stays, because the controller cannot be a consumer of a foundation it made before any module
|
||||
existed. It is the controller's own and no module uses it.
|
||||
|
||||
**A secret field reaches a process as a file**, as today ([ADR 0086](../../02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md)).
|
||||
Reading one into a plain value, such as an environment variable, is refused when the definition is
|
||||
parsed, unless the definition declares the exception 0086 allows, with its reason.
|
||||
|
||||
## An instance
|
||||
|
||||
An assignment has an identity: an **instance name**, which defaults to the module's name. Everything
|
||||
keyed by the module's name today is keyed by the instance: directories, containers, the login it
|
||||
presents, its broker account, the seats it holds, its settings and its identity as a provider.
|
||||
|
||||
So a module may run twice on one node, under two instance names. What must stay singular stays so:
|
||||
by a seat, or by an operator value colliding, as with a public name.
|
||||
|
||||
**A login still has to fit the tightest backend**, which is twenty characters today
|
||||
([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)). A node name and
|
||||
an instance name will not fit in full. The instance therefore gets a short form alongside the module's
|
||||
slug, under the same rules as a slug. This has to be settled before a second instance is allowed.
|
||||
|
||||
## Genesis
|
||||
|
||||
The one exception. Before any provider exists, genesis answers the foundation's own requirements
|
||||
itself: the store's and broker's credentials, the vault's own access, and the root secrets. It seals
|
||||
them to the operator key as it does now ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)).
|
||||
After genesis, nothing is answered except by a provider.
|
||||
|
||||
## 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 that has not answered yet, and which one.
|
||||
|
||||
The last one is a state, not a failure. A consumer waiting for its provider is shown as waiting, and
|
||||
nothing is delivered until the answer arrives ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)).
|
||||
|
||||
## 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, addressed to an instance |
|
||||
| a port the mesh assigns | a host requirement |
|
||||
| machine facts and machine placeholders | host requirements |
|
||||
| a secret the controller mints | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)) |
|
||||
| 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. **Resolution and the new form.** 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. **Providers answer.** The SDK harness answers with contract fields, the controller carries answers
|
||||
back and seals them, and the vault holds its seat and generates. *Ends when* the analytics and DNS
|
||||
providers answer their consumers and the broker's bootstrap step is removed.
|
||||
3. **Definitions move.** Every catalogue definition is rewritten, adopted and running assignments
|
||||
placed where their data already is. *Ends when* the list of definitions using an old form is
|
||||
empty, and the old forms are removed.
|
||||
4. **Instances.** The instance name and its short form, and everything keyed by it. *Ends when* one
|
||||
module runs twice on one lab machine with two public names.
|
||||
|
||||
## 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, and for a second provider on a consumer's own machine when a seat delivers the provision. |
|
||||
| A host requirement is answered on its own node | A resolution test placing one elsewhere: refused. |
|
||||
| An operator value needs no provider | 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 read into a plain value, unless the definition declares the 0086 exception with a reason. |
|
||||
| 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 instance.
|
||||
- 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.
|
||||
- A contract registry: where contracts live, and how a new provision gets one. Today contracts are
|
||||
implicit in each provider's served fields.
|
||||
@@ -35,6 +35,7 @@ document is written and this one's status becomes `implemented`.
|
||||
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
|
||||
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
|
||||
| [`26-the-seats.md`](26-the-seats.md) | What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
||||
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md), [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) |
|
||||
|
||||
## Not yet written
|
||||
|
||||
|
||||
Reference in New Issue
Block a user