diff --git a/internal/bootstrap/enrol.go b/internal/bootstrap/enrol.go index 0137c3b..a8f39ff 100644 --- a/internal/bootstrap/enrol.go +++ b/internal/bootstrap/enrol.go @@ -119,6 +119,11 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla // its ports and range on and not another that came up since. args = append(args, "--tunnel", o.Tunnel) } + // The enrolment account the token's secret is the password of exists in the mesh's records + // and nowhere on the bus yet (novox/hq 04-ISSUES/146): placed before the machine presents it. + if err := placeTheBusUsers(ctx, control, say); err != nil { + return out, err + } joined, err := control.run(joining, o.Host, args...) cancel() if err != nil { @@ -137,6 +142,10 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla } out.Joined = true say(" enrolled as " + o.Node) + // And the node's own account, minted as it enrolled, before its agent connects as it. + if err := placeTheBusUsers(ctx, control, say); err != nil { + return out, err + } } // 4. The agent. @@ -148,6 +157,40 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla return out, nil } +// busAccounts and busContainer are where the installer's bundle raises the bus: the file its +// configuration includes, and the container that reads it. The installer raised them, so it is the +// one that knows them (examples/foundation-first-node-nats.lock). +const ( + busAccounts = "/var/lib/mesh-bus-conf/accounts.conf" + busContainer = "mesh-broker" +) + +// placeTheBusUsers writes the mesh's composed user list beside the bus the installer raised, and makes +// the bus re-read it. +// +// **Genesis's own step** (novox/hq 04-ISSUES/146). Every account on the bus reaches it in the +// declaration of the machine that runs it — which needs that machine to be an enrolled node, and at +// genesis it is not. The control plane composes the list and says it; whoever raised the bus places +// it. That is this installer: it carried the bus in its bundle, so it knows where the bus reads it, +// and the control plane never has to. +func placeTheBusUsers(ctx context.Context, control controlPlane, say func(string)) error { + users, err := control.tell(ctx, "broker", "accounts") + if err != nil { + return fmt.Errorf("the control plane would not say the bus's users, so no machine could "+ + "join it: %w", err) + } + if !strings.Contains(users, "accounts") { + return fmt.Errorf("the control plane's account of the bus's users is not one:\n%s", indent(users)) + } + script := "umask 077 && cat > " + busAccounts + ".next <<'MESHBUSUSERS'\n" + users + "\nMESHBUSUSERS\n" + + "mv " + busAccounts + ".next " + busAccounts + " && docker kill -s HUP " + busContainer + " >/dev/null" + if out, err := control.run(ctx, "sh", "-c", script); err != nil { + return fmt.Errorf("the bus's users could not be placed at %s: %w\n%s", busAccounts, err, indent(out)) + } + say(" bus users placed") + return nil +} + // runTheHost makes sure something on this machine is listening to the mesh, and proves it. // // **The installer does not install the service, and says so.** A unit file is a packaging decision diff --git a/internal/bootstrap/enrol_test.go b/internal/bootstrap/enrol_test.go index 48e71cb..1d66a25 100644 --- a/internal/bootstrap/enrol_test.go +++ b/internal/bootstrap/enrol_test.go @@ -261,3 +261,56 @@ func TestAListingIsMatchedByNameAndNotBySubstring(t *testing.T) { t.Error("registry-mirror was not found") } } + +// **Genesis places the bus's users, before the machine enrols and again after** (novox/hq +// 04-ISSUES/146). The enrolment account exists only in the mesh's records until somebody writes it +// beside the bus; so does the node's own, minted as it enrols. Without the first, the machine is +// refused by the bus it just raised; without the second, its agent is. +func TestAFirstNodeIsLetOntoTheBusItRaised(t *testing.T) { + enrolled := false + runtime := &asked{answer: func(name string, args []string) (string, error) { + joined := strings.Join(args, " ") + switch { + case strings.Contains(joined, "node list"): + if enrolled { + return "anchor here 01J0\n", nil + } + return "", nil + case strings.Contains(joined, "node add"): + return "added anchor\n", nil + case strings.Contains(joined, "token issue"): + return "a token for anchor, good once:\n\n " + strings.Repeat("t", 240) + "\n\n", nil + case strings.Contains(joined, "broker accounts"): + return "accounts {\n MESH { users = [] }\n}\n", nil + case name == "/usr/local/bin/mesh-host": + enrolled = true + return "enrolled as anchor\n", nil + case name == "pgrep", name == "sh": + return "", nil + } + return "", fmt.Errorf("unexpected: %s %v", name, args) + }} + + if _, err := Enrol(context.Background(), Options{ + Node: "anchor", State: filepath.Join(t.TempDir(), "state.json"), Timeout: time.Second, + Host: "/usr/local/bin/mesh-host", HostInBackground: true, + }, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second}, + func(string) {}); err != nil { + t.Fatal(err) + } + + placed, enrol := []int{}, -1 + for i, c := range runtime.commands { + if strings.HasPrefix(c, "sh -c") && strings.Contains(c, "kill -s HUP mesh-broker") && + strings.Contains(c, "/var/lib/mesh-bus-conf/accounts.conf") { + placed = append(placed, i) + } + if strings.HasPrefix(c, "/usr/local/bin/mesh-host enrol") { + enrol = i + } + } + if enrol < 0 || len(placed) != 2 || placed[0] > enrol || placed[1] < enrol { + t.Fatalf("the bus's users were not placed before the machine enrolled and again after "+ + "(placed at %v, enrolled at %d):\n%s", placed, enrol, strings.Join(runtime.commands, "\n")) + } +}