diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 6352d4d..3140aa9 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -146,6 +146,15 @@ func pushNode(ctx context.Context, o Options, control controlPlane, say func(str "already registered and try the push again", err) } say(" pushed " + o.Node) + // **Every push may carry a new bus user, and until the adoption the bus hears of it only from here** + // (novox/hq issue 608, ADR 0331 §5, ADR 0333 §4). A module's account is minted into the mesh's + // records and sealed to this node with the push; the bus reads its user list from a file, and until + // the broker step adopts the bus as the nats module nothing but this installer writes that file. The + // list was placed only when the node enrolled, so the build agent's account (step 13) never reached + // the bus and the agent was refused ("Authorization Violation") until the sdk step gave up. + if err := placeUntilAdopted(ctx, control, say); err != nil { + return "", err + } return strings.TrimSpace(said), nil } diff --git a/internal/bootstrap/phase_packages.go b/internal/bootstrap/phase_packages.go index d9d4156..02bc574 100644 --- a/internal/bootstrap/phase_packages.go +++ b/internal/bootstrap/phase_packages.go @@ -413,11 +413,10 @@ func deliverGenesisRegistry(ctx context.Context, o Options, control controlPlane if _, err := control.tell(ctx, "secret", "accept", o.Node, BuilderModule, GenesisRegistrySecret, "--from", at); err != nil { return err } - // Push so the sealed document reaches the build agent, which reads it at its next build. - if _, err := control.tell(ctx, "push", o.Node); err != nil { - return err - } - return nil + // Push so the sealed document reaches the build agent, which reads it at its next build — and, + // like every push before the adoption, the bus hears the users it carries (issue 608). + _, err = pushNode(ctx, o, control, say) + return err } // PublishTheSDK dispatches a build of the SDK to the build agent. The agent has the genesis document by diff --git a/internal/bootstrap/ports_test.go b/internal/bootstrap/ports_test.go index 41b33d1..0e2c008 100644 --- a/internal/bootstrap/ports_test.go +++ b/internal/bootstrap/ports_test.go @@ -239,6 +239,12 @@ func (c *controlRecorder) run(_ context.Context, name string, args ...string) (s } if name == "docker" && args[0] == "exec" { c.told = append(c.told, strings.Join(args[3:], " ")) + if strings.Join(args[3:], " ") == "broker accounts" { + return "accounts {}\n", nil // the composed user list, placed after every push (issue 608) + } + } + if name == "sh" { + c.told = append(c.told, "placed the bus's users") } return "", nil } diff --git a/internal/bootstrap/push_places_users_test.go b/internal/bootstrap/push_places_users_test.go new file mode 100644 index 0000000..77d5f44 --- /dev/null +++ b/internal/bootstrap/push_places_users_test.go @@ -0,0 +1,97 @@ +package bootstrap + +import ( + "context" + "strings" + "testing" + "time" +) + +// Defends novox/hq issue 608 (ADR 0331 §5, ADR 0333 §4): until the bus is adopted as the nats module, +// every push genesis makes is followed by the installer placing the composed user list where the bus +// reads it, and making the bus re-read it. The build agent's account was minted at step 13 and sealed +// with that push, and the list was placed only at enrolment, so the bus refused the agent +// ("Authorization Violation") and genesis stopped at step 15. + +// placing answers the controller as a mesh whose bus is or is not the nats module yet, and records +// what was run, in order. +func placing(adopted bool) (*asked, controlPlane) { + listed := "build-agent 1\n" + if adopted { + listed += "nats 1\n" + } + runtime := &asked{} + runtime.answer = func(name string, args []string) (string, error) { + joined := strings.Join(args, " ") + switch { + case name == "sh": + return "", nil + case strings.HasSuffix(joined, "module list"): + return listed, nil + case strings.HasSuffix(joined, "broker accounts"): + return "accounts {\n MESH { users = [ { user: \"anchor.build-agent\" } ] }\n}\n", nil + } + return "", nil + } + return runtime, controlPlane{container: "mesh-controller", run: runtime.run, timeout: time.Second} +} + +// placedAfterThePush is whether the bus's users were placed — written at the module's bus-users path, +// the bus told to re-read them — after the push, and how often. +func placedAfterThePush(t *testing.T, commands []string) int { + t.Helper() + pushed, placed := -1, 0 + for i, c := range commands { + if strings.HasSuffix(c, "push anchor") && pushed < 0 { + pushed = i + } + if strings.HasPrefix(c, "sh -c") && strings.Contains(c, busAccounts) && + strings.Contains(c, "kill -s HUP "+busContainer) { + if pushed < 0 || i < pushed { + t.Fatalf("the bus's users were placed before the push that carries the account: %v", commands) + } + placed++ + } + } + if pushed < 0 { + t.Fatalf("nothing was pushed: %v", commands) + } + return placed +} + +func TestTheBusHearsTheBuildAgentsAccountAfterItsPush(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + runtime, control := placing(false) + o := Options{Node: "anchor", Wait: time.Second} + if _, err := installModule(context.Background(), o, control, BuilderModule, []byte(`{}`), quietly); err != nil { + t.Fatal(err) + } + if n := placedAfterThePush(t, runtime.commands); n != 1 { + t.Fatalf("the bus's users were placed %d time(s) after the build agent's push; once: %v", n, runtime.commands) + } +} + +func TestTheGenesisDocumentsPushIsHeardByTheBusToo(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + runtime, control := placing(false) + o := Options{Node: "anchor", Wait: time.Second} + if err := deliverGenesisRegistry(context.Background(), o, control, genesisRegistry(DefaultPorts(), "pw"), quietly); err != nil { + t.Fatal(err) + } + if n := placedAfterThePush(t, runtime.commands); n != 1 { + t.Fatalf("the bus's users were placed %d time(s) after the genesis document's push; once: %v", n, runtime.commands) + } +} + +// From the adoption on the nats module writes the list, and the installer does not (ADR 0333 §4). +func TestOnceTheBusIsTheNatsModulesThePushPlacesNothing(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + runtime, control := placing(true) + o := Options{Node: "anchor", Wait: time.Second} + if _, err := installModule(context.Background(), o, control, BuilderModule, []byte(`{}`), quietly); err != nil { + t.Fatal(err) + } + if n := placedAfterThePush(t, runtime.commands); n != 0 { + t.Fatalf("the installer placed the bus's users over the nats module's: %v", runtime.commands) + } +} diff --git a/internal/bootstrap/registry_test.go b/internal/bootstrap/registry_test.go index d05bb83..f9a0280 100644 --- a/internal/bootstrap/registry_test.go +++ b/internal/bootstrap/registry_test.go @@ -58,8 +58,12 @@ func aMeshThatAgrees(answers map[string]string) func(string, []string) (string, } } switch { + case name == "sh" && len(args) == 2 && strings.Contains(args[1], "accounts.conf"): + return "", nil // the bus's users placed after a push (issue 608) case name != "docker": return "", fmt.Errorf("unexpected program %q", name) + case strings.HasSuffix(joined, "broker accounts"): + return "accounts {}\n", nil case args[0] == "cp": return "", nil case args[0] == "container":