To-be 27 (proposed): a module requires, the mesh resolves — with ADRs 0109–0114, research 016 and issue 119 #113
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: proposed
|
||||||
|
date: 2026-09-25
|
||||||
|
deciders: jochen
|
||||||
|
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
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[Issue 118](../04-ISSUES/118-a-module-definition-decides-where-its-files-live/00-report.md) found
|
||||||
|
**789 host-path strings in 70 of the catalogue's 71 module definitions.** Each definition chooses
|
||||||
|
where on the machine its directories, mounts, bindings, secrets, env-files and received files live,
|
||||||
|
and often repeats that path in an environment variable or in code. Mounts are checked against what
|
||||||
|
the definition declares ([ADR 0091](0091-a-mount-is-declared-three-ways.md)); nothing checks the
|
||||||
|
copies. The issue records what that has already allowed:
|
||||||
|
|
||||||
|
- a provider that would provision nobody without a word;
|
||||||
|
- a contributions file that carries host paths into containers, so every provider must mount its
|
||||||
|
grants directory at the identical path;
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. Keep host paths in definitions, and check that every copy agrees.** Rejected. It checks the
|
||||||
|
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.
|
||||||
|
|
||||||
|
**3. A definition names variables, and the assignment resolves them.** 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.
|
||||||
|
|
||||||
|
**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:
|
||||||
|
|
||||||
|
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.** A database, a bucket, a vhost, the
|
||||||
|
occupant of a seat, another assignment: each with the contract the mesh and the module's
|
||||||
|
specification define. Which node answers one is part of the assignment
|
||||||
|
([ADR 0084](0084-which-provider-serves-a-consumer.md)), so a module may take its database from
|
||||||
|
another node.
|
||||||
|
3. **What the mesh generates or knows.** Minted secrets, the ports it assigns
|
||||||
|
([ADR 0038](0038-the-mesh-assigns-the-port.md)), facts about the machine.
|
||||||
|
|
||||||
|
**A directory is a provision.** A module requires one by name, such as its configuration or its
|
||||||
|
data, and the host on the node where the assignment runs answers it. Its contract is what the
|
||||||
|
module needs from it:
|
||||||
|
|
||||||
|
- the owner and mode, including the owner the image expects
|
||||||
|
([ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md));
|
||||||
|
- whether it holds data that outlives the module ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md))
|
||||||
|
or is disposable.
|
||||||
|
|
||||||
|
*Where* it 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)). An operator's shared data
|
||||||
|
([ADR 0051](0051-shared-data-is-the-operators.md)) is the same provision with the operator as owner:
|
||||||
|
the module says it needs read or read-write access, and the assignment says where the data is. A
|
||||||
|
directory is always answered on the module's own node, because a host path means nothing on any
|
||||||
|
other.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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 it instead: directories, container names,
|
||||||
|
the login a consumer presents, broker accounts, a seat's holder. So **one module may be assigned to
|
||||||
|
one node more than once.** What must stay singular stays so by a seat, or by the assignment's own
|
||||||
|
configuration colliding: a public name already taken is refused like any other singular thing.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Every definition changes.** 70 of 71 name host paths today. The change is mechanical for most
|
||||||
|
and needs the design to say how existing modules migrate without their data moving: an adopted
|
||||||
|
or already-running assignment is placed where its data already is.
|
||||||
|
- The control plane resolves variables at assignment and refuses unresolved ones. The host answers
|
||||||
|
directory provisions. The contributions file's format changes, and so does the SDK's reconcile
|
||||||
|
loop that reads it.
|
||||||
|
- Identity moves from the module to the assignment, which touches logins, broker accounts 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.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A definition names no host path | A catalogue test fails on any absolute path outside the image side of a mount, with a declared list of exceptions that shrinks to empty as definitions move. |
|
||||||
|
| Installation resolves every variable | A resolution test with one variable unanswered: refused, naming the variable and its possible sources. |
|
||||||
|
| 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. |
|
||||||
|
| 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, no collision. |
|
||||||
|
|
||||||
|
## 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 node answers is the assignment's
|
||||||
|
- [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
|
||||||
@@ -157,6 +157,7 @@ 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)
|
- **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)
|
- **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)
|
- **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)*
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-25
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 118 — A module definition decides where its files live on the machine
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
A review of where module code reads its files turned up a cross-cutting pattern. Every module
|
||||||
|
definition in the catalogue chooses, in its own manifest, where on the machine its files live.
|
||||||
|
Counted on the catalogue's `main`, 2026-09-25:
|
||||||
|
|
||||||
|
| where in the definition | host-path strings |
|
||||||
|
|---|---|
|
||||||
|
| directory and file resources | 257 |
|
||||||
|
| container mounts, host side | 230 |
|
||||||
|
| own secrets | 78 |
|
||||||
|
| bindings | 53 |
|
||||||
|
| env-files | 50 |
|
||||||
|
| secrets | 35 |
|
||||||
|
| container environment | 28 |
|
||||||
|
| accesses | 21 |
|
||||||
|
| receives, grants | 24 |
|
||||||
|
| everything else | 13 |
|
||||||
|
|
||||||
|
**789 host-path strings in 70 of the 71 definitions.** Mounts are checked: a container may not
|
||||||
|
mount a path its module never declared ([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)).
|
||||||
|
Nothing checks the same path where it is retyped as a value: an environment variable, an env-file
|
||||||
|
line, a literal in module code.
|
||||||
|
|
||||||
|
### Where that has already gone wrong
|
||||||
|
|
||||||
|
- **A provider that would provision nobody, silently.** One DNS provider mounts its grants
|
||||||
|
directory at a short path inside its container, then tells its provisioner to read the
|
||||||
|
contributions file at the host path, which does not exist in there. Nothing requires the
|
||||||
|
provision today, so it has not failed yet. When a consumer arrives, it will get no record, and
|
||||||
|
nobody will be told.
|
||||||
|
- **The mesh's own wire carries host paths into containers.** Each contribution names its
|
||||||
|
consumer's credential as "the file on this machine holding that consumer's credential", a host
|
||||||
|
path computed from the provider's grants directory. So every provider has to mount that directory
|
||||||
|
at the *identical* path, or it cannot read what it was given. Eleven of the twelve providers do.
|
||||||
|
It is a convention nothing states or checks, and the twelfth is the provider above.
|
||||||
|
- **The warning that would have caught it is lost in the SDK.** The control plane always writes the
|
||||||
|
contributions file, even when empty, so a provider can tell "nothing asked" from "never written".
|
||||||
|
The SDK's reconcile loop treats an unreadable file as empty, and logs nothing.
|
||||||
|
- **Code carries copies with nothing checking them.** Several modules default a path in code when an
|
||||||
|
environment variable is unset. Five of those defaults disagree with the value their own manifest
|
||||||
|
sets. One of them is a host path used inside a container that does not mount it.
|
||||||
|
|
||||||
|
### And a module cannot be assigned to one node twice
|
||||||
|
|
||||||
|
Everything that identifies a running module is keyed by the module's name: its directories, its
|
||||||
|
container names, the login it presents to a provider, its broker account. Two assignments of one
|
||||||
|
module to one node would share every one of them. Assigning the same application twice is an
|
||||||
|
ordinary need: production beside staging, one site per customer, two instances of one service
|
||||||
|
configured differently, two stores of one engine.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
A definition that names machine paths is not portable between nodes. It cannot follow data onto a
|
||||||
|
second disk, or onto a machine being adopted with its data already in place, without editing the
|
||||||
|
module. It cannot run twice on one node. It keeps every path in two or three places with nothing
|
||||||
|
checking that they agree. The defects above are what that allows, and each was found by reading,
|
||||||
|
not by any check.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should a definition name any host path at all, or should every location come from the
|
||||||
|
assignment and the mesh?
|
||||||
|
- If a directory is something a module *requires* rather than *declares*, what is its contract:
|
||||||
|
ownership, mode, whether it is kept when the module goes?
|
||||||
|
- What identifies an assignment, if a module may be assigned to one node more than once?
|
||||||
|
- What would the contributions file carry instead of host paths, so a provider needs no
|
||||||
|
identical-path mount?
|
||||||
Reference in New Issue
Block a user