97 lines
5.0 KiB
Markdown
97 lines
5.0 KiB
Markdown
---
|
|
status: resolved
|
|
opened: 2026-09-01
|
|
located-in: [mesh-control]
|
|
fixed-by: mesh-controller f5b03e1 (the fourteen declared); ADR 0091 and mesh-controller feat/multiple-fixes (the check, back, with the three declarations — the socket by capability, the operator's by accesses)
|
|
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
|
|
---
|
|
|
|
# 026 — The data directories are mounted and never declared
|
|
|
|
## Symptom
|
|
|
|
Four modules mount **fourteen host paths** that no resource in those modules declares:
|
|
|
|
```
|
|
gitea /services/gitea/gitea
|
|
postgres /services/postgres/db-data
|
|
minio /services/minio/data/data1-1
|
|
mailu eleven more, including the mail spool and the admin database
|
|
```
|
|
|
|
Each is a bind mount on a container. None is a `directory` resource. The mesh has never heard of
|
|
any of them.
|
|
|
|
## What that costs
|
|
|
|
**They are created by the container runtime, as root.** A bind mount whose source does not exist
|
|
is created for you, owned by root, with whatever mode the runtime picks. So `owner` and `mode` —
|
|
which exist precisely so a module can say who its data belongs to — are silently not applied to
|
|
the only directories that hold data.
|
|
|
|
**The protection that exists for exactly this does not reach them.** A directory the mesh declared
|
|
and no longer wants is *kept*, not removed, when it holds anything the mesh did not put there
|
|
([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)). That rule is the
|
|
answer to *what happens to my data when a module goes away*, and it is written in terms of
|
|
declared directories. **An undeclared one is not protected by it, because the mesh does not know
|
|
it is there.**
|
|
|
|
So the single rule guarding against data loss covers the configuration directories, which are
|
|
cheap to lose, and not the data directories, which are the reason the rule exists.
|
|
|
|
## Where it came from
|
|
|
|
These manifests were written by reading the arrangement being replaced and carrying its
|
|
`docker-compose` files across — service, image, ports, volumes, environment — into the new
|
|
manifest's container shape. That shape can express all of it, which is what made the
|
|
transliteration feel like progress.
|
|
|
|
**A container shape that can express a compose file will be filled in like a compose file.** The
|
|
mesh's model is larger than that: a directory is a thing the mesh owns, with an owner and a mode
|
|
and a rule about what happens when it is no longer wanted. A volume line borrowed from compose
|
|
declares none of it, and nothing complains, because a bind mount source is a string.
|
|
|
|
## What a fix has to keep
|
|
|
|
- **Every host path a container mounts is declared.** If a module wants a directory on the
|
|
machine, it says so, with who owns it and what mode — and gets the removal rule with it.
|
|
- **The check is mechanical.** A person comparing volumes against declared directories by hand is
|
|
the process that produced this. It is a few lines against the manifest and belongs beside the
|
|
other manifest checks.
|
|
- **Not by inventing directories at apply time.** The host creating what a mount needs would make
|
|
the mesh's ownership of a directory depend on which resource mentioned it first, and would put
|
|
the same undeclared path back a layer down.
|
|
|
|
## Not yet answered
|
|
|
|
**Where a module's data should live at all.** These paths were inherited whole from the
|
|
arrangement being replaced, which put everything under one directory per service. Whether that is
|
|
right here is a separate question, and a bigger one — it decides what a person backs up, and what
|
|
survives a module being removed.
|
|
|
|
## Half fixed
|
|
|
|
**All fourteen are declared**, across the forge, the mail system, the store and the object store —
|
|
each mount now resolves to a `directory` or to a file the module already names.
|
|
|
|
**Enforcing it was tried and withdrawn**, and the withdrawal is the interesting half. A refusal
|
|
for any mount no resource declares refuses the **builder**, which mounts the container runtime's
|
|
socket. That socket is not the builder's data. It does not belong to the module, it already exists,
|
|
and declaring it as one of the module's own directories would be a lie that the host would act on.
|
|
|
|
So the rule is right about data and wrong about everything else, because the manifest cannot
|
|
currently say which a path is. Two kinds of mount are spelled identically:
|
|
|
|
- **the directory my data lives in** — created if absent, owned by the module, protected by
|
|
[ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)
|
|
- **a machine facility I was granted** — a socket, a device; it exists, the machine owns it, and
|
|
the module is being given access to it
|
|
|
|
`capabilities` is the closest existing thing to the second and names no paths. Inventing a field
|
|
to separate them is a design decision, so it is recorded here rather than made to get a check
|
|
green.
|
|
|
|
**Until then the manifests are right by coincidence**, which is the state this issue was opened
|
|
about. What is kept is a check that every real manifest still parses — worth nothing against this
|
|
fault, and the reason the next attempt finds out in a second rather than in a fifteen-minute run.
|