diff --git a/internal/bootstrap/builder_test.go b/internal/bootstrap/builder_test.go index 18feaf2..f4158f1 100644 --- a/internal/bootstrap/builder_test.go +++ b/internal/bootstrap/builder_test.go @@ -38,27 +38,6 @@ func recording(t *testing.T) (*controlRecorder, controlPlane) { return c, controlPlane{container: "temp-mesh-controller", run: c.run, timeout: time.Second} } -func TestTheBuildersPackageRegistryPortIsASettingAndNotTheManifest(t *testing.T) { - c, control := recording(t) - o := Options{Node: "anchor", Ports: FoundationPorts{Packages: 3100}, Wait: time.Second} - if _, err := installModule(context.Background(), o, control, BuilderModule, - []byte(theBuildersBinding), quietly); err != nil { - t.Fatal(err) - } - if got := c.settings[BuilderModule+"-settings.json"]; got != `{"serves":{"port":3100}}` { - t.Errorf("the builder was told %q about the package registry's port", got) - } - // The manifest reaches the mesh as the catalogue wrote it. A port rewritten into it here is a - // port the next registration from the catalogue silently takes back. - if got := c.settings[BuilderModule+"-module.json"]; got != theBuildersBinding { - t.Errorf("the installer changed the builder's manifest:\n%s", got) - } - set, push := c.index("settings set "+BuilderModule), c.index("push anchor") - if set < 0 || push < 0 || set > push { - t.Fatalf("the port was not set before the push that raises the builder: %v", c.told) - } -} - func TestTheBuilderOnTheDefaultPackagesPortIsToldNothing(t *testing.T) { c, control := recording(t) o := Options{Node: "anchor", Wait: time.Second} diff --git a/internal/bootstrap/genesis_registry_test.go b/internal/bootstrap/genesis_registry_test.go new file mode 100644 index 0000000..7cd3448 --- /dev/null +++ b/internal/bootstrap/genesis_registry_test.go @@ -0,0 +1,65 @@ +package bootstrap + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" +) + +// Genesis seals the build agent the forge it raised (novox/hq ADR 0336 §3, issue 121): the agent's +// npm-package-registry requirement is optional and binds only once the gitea module is assigned, after +// the base, so until then the packages step's genesis document is how the sdk and base steps reach the +// forge. + +func TestThePackagesStepSealsTheGenesisDocumentToTheBuildAgent(t *testing.T) { + if BuilderModule != "build-agent" { + t.Fatalf("genesis installs %q; the document is the build agent's", BuilderModule) + } + c, control := recording(t) + o := Options{Node: "anchor", Ports: FoundationPorts{Packages: 3100}, Wait: time.Second} + doc := genesisRegistry(o.Ports.orDefaults(), "builder-password") + if err := deliverGenesisRegistry(context.Background(), o, control, doc, quietly); err != nil { + t.Fatal(err) + } + carried := c.settings["accepting-"+GenesisRegistrySecret] + var got GenesisRegistry + if err := json.Unmarshal([]byte(carried), &got); err != nil { + t.Fatalf("the genesis document carried is not JSON: %q", carried) + } + want := GenesisRegistry{Scheme: "http", At: "127.0.0.1", Port: 3100, NpmPath: "/api/packages/novox/npm/", + As: "mesh-builder", Password: "builder-password"} + if got != want { + t.Fatalf("the genesis document says %+v, want %+v", got, want) + } + accept := c.index("secret accept anchor build-agent genesis-package-registry --from /accepting-genesis-package-registry") + push := c.index("push anchor") + if accept < 0 || push < 0 || accept > push { + t.Fatalf("the genesis document was not sealed to the build agent before the push: %v", c.told) + } +} + +// What the retired builder was given and the build agent declares neither of: an `npm-password` +// own-secret and a `serves` setting for its carried binding (ADR 0336 §3). +func TestNoStepGivesTheBuildAgentAServesSettingOrAnNpmPassword(t *testing.T) { + for _, module := range []string{BuilderModule} { + c, control := recording(t) + o := Options{Node: "anchor", Ports: FoundationPorts{Packages: 3100}, Wait: time.Second} + if _, err := installModule(context.Background(), o, control, module, []byte(`{"module":"`+module+`"}`), quietly); err != nil { + t.Fatal(err) + } + if got, set := c.settings[module+"-settings.json"]; set { + t.Errorf("%s was given a setting on a node with a non-default packages port: %s", module, got) + } + if err := deliverGenesisRegistry(context.Background(), o, control, + genesisRegistry(o.Ports.orDefaults(), "p"), quietly); err != nil { + t.Fatal(err) + } + for _, told := range c.told { + if strings.Contains(told, "npm-password") || strings.Contains(told, "serves") { + t.Errorf("genesis told the controller %q", told) + } + } + } +} diff --git a/internal/bootstrap/phase_packages.go b/internal/bootstrap/phase_packages.go index cb3541d..d9d4156 100644 --- a/internal/bootstrap/phase_packages.go +++ b/internal/bootstrap/phase_packages.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "encoding/json" "fmt" "net/http" "os" @@ -49,8 +50,8 @@ const ( packagesTeam = "packages" // giteaAdminUser is the admin the bootstrap creates and the provisioner later authenticates as. giteaAdminUser = "mesh-admin" - // builderGiteaUser is the gitea account the builder publishes and pulls with at genesis. It is - // the `as` the builder's static package binding names. + // builderGiteaUser is the gitea account the build agent publishes and pulls with at genesis: the + // `as` of the genesis document (novox/hq ADR 0336 §3). Retiring it is issue 602. builderGiteaUser = "mesh-builder" // giteaDBRole/giteaDBName is gitea's own database in the foundation store. giteaDBRole = "mesh_gitea" @@ -133,8 +134,8 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro return err } - say(" delivering the builder its registry credential") - if err := deliverBuilderNpm(ctx, o, control, builderPassword, say); err != nil { + say(" sealing the build agent the genesis document for the forge it raised") + if err := deliverGenesisRegistry(ctx, o, control, genesisRegistry(ports, builderPassword), say); err != nil { return err } @@ -362,27 +363,65 @@ func packagePasswords() (db, admin, builder string, err error) { return db, admin, builder, nil } -// deliverBuilderNpm seals the builder's registry password to this node as its `npm-password` -// own-secret, the same way the control plane's store connections are delivered — carry the value in, -// `secret accept`, and the next push writes it sealed where the builder reads it. -func deliverBuilderNpm(ctx context.Context, o Options, control controlPlane, password string, +// GenesisRegistrySecret is the build agent's own-secret the genesis document is sealed as (novox/hq +// ADR 0336 §3). The build-agent manifest declares it, issued outside the mesh: only this installer +// gives one, on genesis's node. +const GenesisRegistrySecret = "genesis-package-registry" + +// GenesisRegistry is the genesis document (novox/hq ADR 0336 §3): the forge this step raised, the port +// this node gave the package registry, its npm path, and the account this step made with its password. +// mesh-controller's cmd/mesh-builder reads it under the same field names while the build agent's +// npm-package-registry requirement is unbound, and prefers the binding once the gitea module is +// assigned and it binds. +type GenesisRegistry struct { + Scheme string `json:"scheme"` + At string `json:"at"` + Port int `json:"port"` + NpmPath string `json:"npm-path"` + As string `json:"as"` + Password string `json:"password"` +} + +// genesisRegistry is the document for the forge raised here: on the machine's own loopback, where it +// answers on the host network and where its ROOT_URL says it is (raiseGiteaServer), on the port this +// node gave the package registry. +func genesisRegistry(ports FoundationPorts, password string) GenesisRegistry { + return GenesisRegistry{Scheme: "http", At: "127.0.0.1", Port: ports.Packages, + NpmPath: "/api/packages/" + packagesOrg + "/npm/", As: builderGiteaUser, Password: password} +} + +// deliverGenesisRegistry seals the genesis document to the build agent on this node as its +// `genesis-package-registry` own-secret (novox/hq ADR 0336 §3), the same way the control plane's store +// connections are delivered — carry the value in, `secret accept`, and the next push writes it sealed +// where the build agent reads it. +// +// **The build agent's one way to the forge until the registry binds.** Its npm-package-registry +// requirement is optional (ADR 0336 §1), and nothing provides it until the gitea module is assigned +// after the base, so the sdk step (15) publishes and the base step (16) resolves with the account in +// this document. It replaces the retired builder's `npm-password` own-secret and the `serves.port` +// setting its carried binding took, neither of which the build agent declares. +func deliverGenesisRegistry(ctx context.Context, o Options, control controlPlane, doc GenesisRegistry, say func(string)) error { - at := "/accepting-npm-password" - if err := control.carryingSecret(ctx, "mesh-accepting-npm-password", []byte(password), at); err != nil { + raw, err := json.Marshal(doc) + if err != nil { return err } - if _, err := control.tell(ctx, "secret", "accept", o.Node, BuilderModule, "npm-password", "--from", at); err != nil { + at := "/accepting-" + GenesisRegistrySecret + if err := control.carryingSecret(ctx, "mesh-accepting-"+GenesisRegistrySecret, raw, at); err != nil { return err } - // Push so the sealed secret reaches the builder, which restarts on it and comes back credentialed. + 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 } -// PublishTheSDK dispatches a build of the SDK to the builder. The builder has its registry -// credential by now, so the build's `npm publish` authenticates; the artifact is a `package`, built +// PublishTheSDK dispatches a build of the SDK to the build agent. The agent has the genesis document by +// now (novox/hq ADR 0336 §7), so the build's `npm publish` authenticates; the artifact is a `package`, built // on a public base, so this needs no toolchain — which is the whole point of doing it before the base. func PublishTheSDK(ctx context.Context, o Options, control controlPlane, say func(string)) error { if o.SDKSource.Repository == "" { diff --git a/internal/bootstrap/ports.go b/internal/bootstrap/ports.go index efbe5e9..dbedf50 100644 --- a/internal/bootstrap/ports.go +++ b/internal/bootstrap/ports.go @@ -117,24 +117,6 @@ const busModulePort = 4222 // PortsSetting is the controller's settings key for a module's given ports. const PortsSetting = "ports" -// ServesSetting is the settings key that carries what a provider serves, inside the one binding a -// module carries rather than resolves — the builder's package binding (novox/hq 04-ISSUES/085). -// -// **A port given at genesis has to be a setting wherever it is read, or something puts it back.** -// The builder reaches the package registry through a binding written in its own manifest, because -// at genesis no module yet provides `package-registry` and so there is nothing for the controller -// to resolve it from. That binding used to be rewritten, as text, when the installer registered the -// builder — which a later registration from the catalogue undid silently. Set as a node's setting -// instead, it is held by the controller rather than by the manifest, so re-registering the builder -// leaves it where it was. -// -// **A settings layer is the module's, not one resource's.** The controller lays it over every file -// of that module which merges as JSON, so `serves` lands in the package binding only because that -// binding is the builder's one mergeable file. A second mergeable file added to the builder would -// be given a `serves` key too, meaning nothing to whatever reads it. Worth knowing before adding -// one. -const ServesSetting = "serves" - // setFoundationSettings tells the controller the ports this node gave a foundation module — and, // on an adopted node, that the registry is reached from anywhere, as a node pulls from it before it // has a private-network address (novox/hq ADR 0100). Done after the module is registered and before @@ -148,13 +130,8 @@ func setFoundationSettings(ctx context.Context, o Options, control controlPlane, if o.Adopted && module == RegistryModule { values["expose"] = map[string]string{strconv.Itoa(DefaultPorts().Registry): "anywhere"} } - if packages := o.Ports.orDefaults().Packages; module == BuilderModule && - packages != DefaultPorts().Packages { - // The one binding nothing resolves: the builder dials the forge by a number it carries. - // The rest of the binding — the provision, the forge it is from, the account it presents — - // is the manifest's and stays there. - values[ServesSetting] = map[string]int{"port": packages} - } + // No `serves` setting for the build agent: the port given for the package registry reaches it in + // the genesis document (novox/hq ADR 0336 §3), and from the binding once the registry binds. if len(values) == 0 { return nil }