Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
5 changed files with 57 additions and 13 deletions
Showing only changes of commit bdc9c436b4 - Show all commits
+14 -1
View File
@@ -108,7 +108,7 @@ func parseArgs(args []string) (string, options, error) {
set.StringVar(&opts.state, "state", opts.state, "where this node keeps what it knows")
set.BoolVar(&opts.dryRun, "dry-run", false, "read and check the declaration, change nothing")
set.StringVar(&opts.token, "token", "", "enrol: the one-time token, carried here by a person")
set.StringVar(&opts.nodeName, "name", "", "enrol: what this machine is called in the mesh")
set.StringVar(&opts.nodeName, "name", "", "enrol: override the name the token carries")
// Parsed in a loop, because the standard library stops at the FIRST non-flag argument.
// `mesh-host inventory --json` hit that once, and taking the subcommand off the front
@@ -405,6 +405,19 @@ func enrol(ctx context.Context, opts options) error {
return err
}
// The name comes from the token, because the node cannot work it out: the broker account it
// authenticates as is named after it, and that account exists before this machine has been
// told anything. --name remains for a token issued before the name travelled in one, and
// saying so beats a connection refused with an empty username — which is what this was.
if strings.TrimSpace(*name) == "" {
*name = token.Node
}
if strings.TrimSpace(*name) == "" {
return errors.New(
"this token does not say what the mesh calls this machine, and no --name was given. " +
"A token issued by a current control plane carries the name")
}
// Before anything else: an already-enrolled machine must not quietly acquire a second
// identity. The mesh believes the first one, and re-enrolling is a deliberate act that
// starts with a person issuing a new token for that node record.
+12 -8
View File
@@ -2,20 +2,24 @@
## `substrate-first-node.lock`
What a machine must be before a mesh exists — steps 0 to 4 of the bootstrap in
[novox/hq `07-the-substrate.md`](https://git.novox.be/novox/hq):
What a machine must be before a mesh exists — the bootstrap in
[novox/hq `07-the-substrate.md`](https://git.novox.be/novox/hq), whole:
```
0 a container runtime
1 the store runs
2 a database per context one today, `inventory`
3 that context's schema mesh-control migrate
4 the broker runs
2 a database per context `inventory` and `identity`
3 those contexts' schemas mesh-control migrate
4 the broker runs with a certificate it generated itself
5 the control plane runs mesh-control serve
```
**It stops there, and the file says why.** Step 5 is a virtual host, a credential and a
certificate; step 6 is the control plane running. Nothing consumes any of them yet, and a bundle
whose last step cannot be checked is worse than a shorter one.
**A machine that applies this is a mesh** — one node, with nothing joined to it yet, which is
exactly what the first node is (novox/hq ADR 0004). From here it hands out tokens and everything
else joins the ordinary way.
This file said it stopped at step 4 for longer than that was true, which is its own small lesson:
a comment about what something does not do is a comment nobody updates.
Build a host carrying it:
+4 -3
View File
@@ -1,9 +1,10 @@
// substrate-first-node.lock — what a machine must be before a mesh exists.
//
// Steps 0 to 5 of the bootstrap (novox/hq 03-DESIGN/01-to-be/07-the-substrate.md): a container
// runtime, a store, a database per context, that context's schema, and the broker.
// The whole bootstrap (novox/hq 03-DESIGN/01-to-be/07-the-substrate.md): a container runtime, a
// store, a database per context, those contexts' schemas, the broker, and the control plane
// running on top of them.
//
// It stops before step 6, where the control plane runs.
// It stopped before the control plane once, and this comment said so for longer than it was true.
//
// The broker generates its OWN certificate, in its own image, into a volume it then mounts read
// only. Self-signed, because at this moment there is no mesh to issue one and no public name to
+19
View File
@@ -377,3 +377,22 @@ func TestASealingKeyOnDiskSurvivesATrailingNewline(t *testing.T) {
t.Fatalf("a round trip through the disk changed the key")
}
}
func TestATokenSaysWhatTheMeshCallsThisMachine(t *testing.T) {
// The node cannot work its own name out. The broker account it authenticates as is named
// after it and exists before this machine has been told anything — so without the name in the
// token, enrolment is a connection refused with an empty username, which names nothing about
// the cause. That is exactly how the first end-to-end raise went.
raw := base64.RawURLEncoding.EncodeToString([]byte(
`{"v":1,"node":"anchor","broker":"192.0.2.10:5671",` +
`"fingerprint":"sha256:` + strings.Repeat("ab", 32) + `",` +
`"signer":"` + base64.StdEncoding.EncodeToString(make([]byte, 32)) + `",` +
`"secret":"a-one-time-secret"}`))
token, err := ParseToken(raw)
if err != nil {
t.Fatal(err)
}
if token.Node != "anchor" {
t.Fatalf("the name did not survive the token: %q", token.Node)
}
}
+8 -1
View File
@@ -18,7 +18,14 @@ import (
// so they are held together by a test on each side asserting the exact field names rather than by
// a shared type. If a field is renamed here and not there, that test fails on both sides.
type Token struct {
Version int `json:"v"`
Version int `json:"v"`
// Node is what the mesh calls this machine, and it arrives here because the node cannot work
// it out. The broker account it must authenticate as is named after it, so it has to be known
// before the mesh can say anything — and without it enrolment is a connection refused with an
// empty username, which names nothing.
Node string `json:"node,omitempty"`
Broker string `json:"broker,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Signer []byte `json:"signer,omitempty"`