Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24
@@ -49,6 +49,7 @@ the expensive half.
|
||||
| [03](03-issues.md) | Issues | Something is wrong — often with the owner unknown |
|
||||
| [04](04-build-handoff.md) | Build handoff | A design is ready to be built |
|
||||
| [05](05-constitution-sync.md) | Constitution sync | `how-we-build.md` changed a rule the mesh enforces |
|
||||
| [06](06-writing-a-module.md) | Writing a module | Something that runs today must run on the mesh |
|
||||
|
||||
## Status lives in frontmatter
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user