Unify trunk on main: initialization → main #3
+14
-1
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user