Seal the build agent the forge genesis raised, in place of the builder's npm-password (hq issue 121, ADR 0336)
mesh/merge-gate head 5f147fe6b9c7 merged into trunk 0d11b4fc930d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 5f147fe6b9c7 merged into trunk 0d11b4fc930d — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/121-genesis-package-registry delivering: 2 of 4 delivered
mesh/merge-gate head 5f147fe6b9c7 merged into trunk 0d11b4fc930d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 5f147fe6b9c7 merged into trunk 0d11b4fc930d — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/121-genesis-package-registry delivering: 2 of 4 delivered
The build agent declares neither the retired builder's npm-password own-secret nor a serves setting, so the packages step handed its credential to nothing and genesis's sdk step had no way to publish. The packages step now seals the genesis-package-registry own-secret to build-agent: the forge on this node's loopback, the port given for the package registry, its npm path, and the mesh-builder account with its password. setFoundationSettings no longer gives the builder a serves.port.
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user