The language and the bundle ordering went into ADR 0006, where the substrate and the control plane already live, and the store-per-context mechanics into 0008, which already decided the rule. Nothing changed but where the reasoning is kept.
128 lines
5.9 KiB
Markdown
128 lines
5.9 KiB
Markdown
# mesh-control
|
|
|
|
**Tier 2 of Novox Mesh — the control plane.** Everything that needs to know about more than one
|
|
node.
|
|
|
|
That is the whole test, and it draws the line the host cannot: the host applies and does not
|
|
decide, *because deciding needs knowledge one machine does not have*. Which nodes should run the
|
|
store, which peers belong in an overlay, whether a node has been unreachable for a week — nobody
|
|
on a single machine can answer any of them.
|
|
|
|
The reasoning lives in [novox/hq](https://git.novox.be/novox/hq); this repository carries no
|
|
argument that is not settled there.
|
|
|
|
## What it is not
|
|
|
|
- **Not the thing that changes machines.** It decides; the host applies. It never reaches into a
|
|
node except through the host, over the link, in a bounded vocabulary.
|
|
- **Not a database.** There is no mesh database. Each context owns its own store and nothing
|
|
outside a context touches it — including nodes, which hold no credential to any of them.
|
|
- **Not privileged.** It has no more access to a machine than a declaration can express.
|
|
|
|
## What exists today
|
|
|
|
**One context of seven, and one of the things it will do.**
|
|
|
|
| | |
|
|
|---|---|
|
|
| `inventory` | the node records — **the schema exists** |
|
|
| `config`, `connectivity`, `provisioning`, `delivery`, `observability`, `identity` | not built |
|
|
| the interface every surface speaks to | not built; its shape is not decided |
|
|
|
|
```
|
|
mesh-control migrate bring each context's schema up to date
|
|
mesh-control version what this binary is
|
|
```
|
|
|
|
`migrate` is **step 3 of the substrate bootstrap** — the step the first node cannot get past, run
|
|
against a database raised moments earlier from the bundle the host carries.
|
|
|
|
### Where this stops, and why there
|
|
|
|
At **identity**. A node's own identity is the next thing needed and its cryptographic form is not
|
|
decided anywhere: whether a node holds a keypair whose public half the mesh keeps, or something
|
|
else. Modelling it would have meant guessing, in a migration — which is the most expensive place
|
|
in this system to guess, because a schema that ran is finished and the only way back is another
|
|
migration.
|
|
|
|
So the node table holds what a node record *is* — a name, when it was made, what the machine last
|
|
reported about itself, when it was last heard from — and stops before what a node *presents*.
|
|
|
|
## Reaching a store
|
|
|
|
**A context is granted only what it exclusively owns.** No shared writes, no read-only role on
|
|
another context's store, and no connection string that reaches more than one.
|
|
|
|
That is a rule about credentials, so it is built as one. There is no mesh-wide connection setting
|
|
and no way to ask for one:
|
|
|
|
```
|
|
MESH_STORE_INVENTORY=postgres://…/inventory
|
|
```
|
|
|
|
A process granted `inventory` holds that variable and no other. Reaching another context's store
|
|
is not a matter of restraint — it has no address for it and no credential to present. And it is
|
|
how the rule is *checked*: what a context can reach is visible in the declaration that runs it,
|
|
as the list of variables it was given.
|
|
|
|
Each context's database is named after the context. There is deliberately no database named for
|
|
the mesh as a whole.
|
|
|
|
## Migrations
|
|
|
|
Numbered, embedded in the binary, applied in order, each in a transaction with the row recording
|
|
it. The applying is four lines; the rest is refusals, and the refusals are the point:
|
|
|
|
| it stops when | because |
|
|
|---|---|
|
|
| a migration that ran has since been edited | the database holds the old version, the repository holds the new one, and nothing holds the difference |
|
|
| a migration is numbered below one that already ran | usually two branches taking the same next number — applying it now runs the schema in an order nobody tested |
|
|
| two migrations share a number | order is the entire guarantee, and two files with one number have none |
|
|
| a file in the directory is not a valid migration name | a misnamed migration would otherwise never run and nothing would say so |
|
|
| a migration is empty | it records that something happened and changes nothing, which cannot be told from a mistake |
|
|
|
|
All of them stop rather than warn. At the moment any of them is true, nobody knows what the
|
|
database contains, and there is no correct guess about a schema.
|
|
|
|
Running it again does nothing. Two copies running at once take an advisory lock, so a restart
|
|
during a slow migration does not become two runners racing.
|
|
|
|
## Building
|
|
|
|
```
|
|
make build the binary
|
|
make image the container image
|
|
make check gofmt, vet, and every test against a real PostgreSQL
|
|
```
|
|
|
|
`make check` raises a throwaway PostgreSQL in a container and takes it down afterwards, including
|
|
when the tests fail. Without one the tests that need a database **skip and say so** rather than
|
|
passing quietly — `make test` is the honest subset, not the gate.
|
|
|
|
Tests run against a real database rather than a fake because what is being tested *is* the
|
|
database's behaviour: that DDL is transactional, that an advisory lock serialises, that a
|
|
checksum mismatch is caught against a record PostgreSQL actually kept. A fake would assert that
|
|
the fake behaves as expected.
|
|
|
|
Every test here has been confirmed to fail when the behaviour it defends is removed. Two did not,
|
|
when first written, and both are now commented with what they were missing.
|
|
|
|
## The image
|
|
|
|
`FROM scratch`, holding one statically linked binary and nothing else — no shell, no package
|
|
manager, no libc, no CA certificates.
|
|
|
|
Not a size optimisation. The bundle a host carries pins this image by digest, and it is fetched
|
|
and run on a machine where no mesh exists to check anything and a person is expected to have read
|
|
the bundle and believed it. Everything in the image is something that person would have to audit.
|
|
|
|
## Where the reasoning lives
|
|
|
|
| | |
|
|
|---|---|
|
|
| what the control plane is | novox/hq ADR 0006 |
|
|
| what it takes to run one, and why Go | novox/hq ADR 0006 |
|
|
| a context owns its store, exclusively | novox/hq ADR 0008 |
|
|
| schema changes are numbered migrations | novox/hq ADR 0013 |
|
|
| a test defends a decision | novox/hq ADR 0017 |
|