Issue 118 and ADR 0112 (proposed): a module definition names no node, no mesh and no path
Issue 118 records what a review of where module code reads its files found: 789 host-path strings in 70 of the catalogue's 71 definitions, every one a decision the definition makes about a machine. Mounts are checked (ADR 0091); the same paths retyped as values are not. It records what that has already allowed — a DNS provider that would provision nobody silently, a contributions file that names credentials by host path and so forces every provider to mount at the identical path, an SDK loop that treats an unwritten contributions file as empty without a word, defaults in code that disagree with their own manifests — and that no module can be assigned to one node twice, because every identity is keyed by the module's name. ADR 0112, proposed for review, answers it the way ADR 0038 answered ports: a definition names variables, and installing it resolves every one or refuses, from three sources — the assignment's own configuration, provisions the mesh resolves against a contract, and what the mesh generates or knows. A directory becomes a provision: the module requires one by name with its owner, mode and persistence, and where it lands is the assignment's. The mesh's own files stop carrying host paths. An assignment gets an identity of its own, so a module may run twice on one node. Checking copies for agreement was rejected as checking something that should not exist; rewriting paths per assignment was rejected as inferring which strings are paths by their shape. Syntax, a node's default layout, and when a second instance becomes possible are left to the design.
This commit is contained in:
@@ -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