To-be 27 (proposed): a module requires, the mesh resolves — with ADRs 0109–0114, research 016 and issue 119 #113
@@ -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