Written after porting the first real workload end to end. Every step exists because skipping it cost something, and the ratio is recorded because it is the lesson: six attempts, one real bug, and the mesh was right every time. The rule worth carrying out of it: read the host's log before theorising. A declaration that was sent and not applied says so there and nowhere else — it took an hour to look, and the answer was one line.
83 lines
4.6 KiB
Markdown
83 lines
4.6 KiB
Markdown
# Playbook 06 — Writing a module
|
|
|
|
**Trigger.** Something that runs today must run on the mesh, or a new capability must be
|
|
declarable.
|
|
|
|
**Who runs it.** Whoever is porting or writing it.
|
|
|
|
*Written 2026-09-01 from doing this for the first time end to end. Every step below exists
|
|
because skipping it cost something.*
|
|
|
|
## Before anything: read what runs
|
|
|
|
**A module is written from the thing, not from memory of the thing.** For a port, that means its
|
|
current compose file, its environment, and where its data actually sits. Assumptions about any of
|
|
the three have been wrong every time they were not checked.
|
|
|
|
Three questions, answered from the machine:
|
|
|
|
| | why it decides something |
|
|
|---|---|
|
|
| **what containers, and how do they find each other?** | more than one means a `network`; names between them must match what the software is configured to dial |
|
|
| **where is its data?** | a bind mount moves with a path; a named volume does not; an anonymous volume is already losing data on every redeploy |
|
|
| **which values are secret, and which are merely settings?** | a secret goes in `own-secrets` or a grant; a setting goes in the manifest and may be overridden per node |
|
|
|
|
## The steps
|
|
|
|
1. **Name what it provides and requires**, if anything. A name is what a consumer is coupled to,
|
|
not the role it plays ([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
|
|
`postgres-database`, not `database`. Most modules provide nothing and require nothing — an
|
|
application is usually a leaf.
|
|
|
|
2. **Declare capabilities, not dependencies, for facts about the machine.** `container-runtime`,
|
|
`package-manager`, `seat`. A capability is detected and refused against; it is not something a
|
|
module can install.
|
|
|
|
3. **Write the resources in the order they must happen.** They are applied in the order written
|
|
and orphans are removed in reverse, so a `network` is written before the containers that join
|
|
it and removed after them.
|
|
|
|
4. **Put every secret in a file, never in `env`.** A declaration travels over the broker in plain
|
|
text: a password in `env` is a password the broker sees. Use `env-file` for containers, or
|
|
`${secret:name}` inside a config file the module supplies. **The mesh delivers parts; a module
|
|
that needs them combined combines them.**
|
|
|
|
5. **Pin every image by digest.** A tag moves. The manifest in a repository names artifacts; the
|
|
manifest the mesh holds names digests, and they are not the same document.
|
|
|
|
6. **Decide generate or accept.** A new module's credential is generated. **An adopted one keeps
|
|
the credential it already has** — `secret accept` — because minting a new password for a
|
|
database that already exists locks the application out of its own data.
|
|
|
|
7. **Add a provisioner only if the software cannot read a file.** A proxy that watches a
|
|
directory needs nothing. PostgreSQL needs `CREATE ROLE`, an object store needs a bucket and a
|
|
policy, an identity provider needs a realm and a client — those need a small program beside
|
|
them. It reads what the mesh granted and reconciles; it does not decide anything.
|
|
|
|
8. **Prove it in the lab, against the real software.** Not that a container started — that the
|
|
thing works: the credential authenticates, a wrong one is refused, the containers reach each
|
|
other, the data survives a restart.
|
|
|
|
## What the first port actually cost
|
|
|
|
Six attempts, one real bug. Recorded because the ratio is the lesson: **the mesh was right every
|
|
time and the scaffolding was not.**
|
|
|
|
- A shape existed in the language and no host implemented it, so every declaration carrying one
|
|
was refused whole — correctly, and the host said exactly that. **Nobody was reading the host's
|
|
log.** Read it first; it is the only place that says why a machine did nothing.
|
|
- A blind find-and-replace renamed a provision in quotes and missed the same word bare.
|
|
- A command was tested only for the invocations that should fail, so it rejected every real one
|
|
and the suite stayed green.
|
|
- A test asserted on a helper rather than on the code that calls it, three separate times. **A
|
|
test that cannot fail when the behaviour is deleted is not defending the behaviour.**
|
|
|
|
## Rules
|
|
|
|
- **Read the host's log before theorising.** A declaration that was sent and not applied says so
|
|
there and nowhere else.
|
|
- **A failing test is kept, not skipped.** It is the reproduction.
|
|
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
|
|
- **A data directory is never removed by the mesh** ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)),
|
|
and that protects against the mesh only — not against a disk or a mistaken command.
|