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 6f13813a59dc merged into trunk 0d11b4fc930d — fail: head 6f13813a59dc does not merge cleanly into trunk 0d11b4fc930d: in conflict inte…
mesh/repo-check head 6f13813a59dc merged into trunk 0d11b4fc930d — fail: head 6f13813a59dc does not merge cleanly into trunk 0d11b4fc930d: in conflict inte…
mesh/delivery-group group fix/121-genesis-package-registry rejected: its order contradicts itself: novox/mesh-catalog@e0bc919d4945 and novox/mesh-controller@a6…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 6f13813a59dc merged into trunk 0d11b4fc930d — fail: head 6f13813a59dc does not merge cleanly into trunk 0d11b4fc930d: in conflict inte…
mesh/repo-check head 6f13813a59dc merged into trunk 0d11b4fc930d — fail: head 6f13813a59dc does not merge cleanly into trunk 0d11b4fc930d: in conflict inte…
mesh/delivery-group group fix/121-genesis-package-registry rejected: its order contradicts itself: novox/mesh-catalog@e0bc919d4945 and novox/mesh-controller@a6…
mesh/delivery superseded: a newer head of the same pull request
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:
@@ -37,27 +37,6 @@ func recording(t *testing.T) (*controlRecorder, controlPlane) {
|
|||||||
return c, controlPlane{container: "temp-mesh-controller", run: c.run, timeout: time.Second}
|
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["builder-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["builder-module.json"]; got != theBuildersBinding {
|
|
||||||
t.Errorf("the installer changed the builder's manifest:\n%s", got)
|
|
||||||
}
|
|
||||||
set, push := c.index("settings set builder"), 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) {
|
func TestTheBuilderOnTheDefaultPackagesPortIsToldNothing(t *testing.T) {
|
||||||
c, control := recording(t)
|
c, control := recording(t)
|
||||||
o := Options{Node: "anchor", Wait: time.Second}
|
o := Options{Node: "anchor", Wait: time.Second}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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) {
|
||||||
|
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{BuildAgentModule, 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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -49,8 +50,8 @@ const (
|
|||||||
packagesTeam = "packages"
|
packagesTeam = "packages"
|
||||||
// giteaAdminUser is the admin the bootstrap creates and the provisioner later authenticates as.
|
// giteaAdminUser is the admin the bootstrap creates and the provisioner later authenticates as.
|
||||||
giteaAdminUser = "mesh-admin"
|
giteaAdminUser = "mesh-admin"
|
||||||
// builderGiteaUser is the gitea account the builder publishes and pulls with at genesis. It is
|
// builderGiteaUser is the gitea account the build agent publishes and pulls with at genesis: the
|
||||||
// the `as` the builder's static package binding names.
|
// `as` of the genesis document (novox/hq ADR 0336 §3). Retiring it is issue 602.
|
||||||
builderGiteaUser = "mesh-builder"
|
builderGiteaUser = "mesh-builder"
|
||||||
// giteaDBRole/giteaDBName is gitea's own database in the foundation store.
|
// giteaDBRole/giteaDBName is gitea's own database in the foundation store.
|
||||||
giteaDBRole = "mesh_gitea"
|
giteaDBRole = "mesh_gitea"
|
||||||
@@ -133,8 +134,8 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
say(" delivering the builder its registry credential")
|
say(" sealing the build agent the genesis document for the forge it raised")
|
||||||
if err := deliverBuilderNpm(ctx, o, control, builderPassword, say); err != nil {
|
if err := deliverGenesisRegistry(ctx, o, control, genesisRegistry(ports, builderPassword), say); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,27 +363,69 @@ func packagePasswords() (db, admin, builder string, err error) {
|
|||||||
return db, admin, builder, nil
|
return db, admin, builder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deliverBuilderNpm seals the builder's registry password to this node as its `npm-password`
|
// BuildAgentModule is the module genesis seals the genesis document to: the build agent, which holds
|
||||||
// own-secret, the same way the control plane's store connections are delivered — carry the value in,
|
// the node seat `node-build-agent` (novox/hq ADR 0190).
|
||||||
// `secret accept`, and the next push writes it sealed where the builder reads it.
|
const BuildAgentModule = "build-agent"
|
||||||
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 {
|
say func(string)) error {
|
||||||
at := "/accepting-npm-password"
|
raw, err := json.Marshal(doc)
|
||||||
if err := control.carryingSecret(ctx, "mesh-accepting-npm-password", []byte(password), at); err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
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, BuildAgentModule, 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 {
|
if _, err := control.tell(ctx, "push", o.Node); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishTheSDK dispatches a build of the SDK to the builder. The builder has its registry
|
// PublishTheSDK dispatches a build of the SDK to the build agent. The agent has the genesis document by
|
||||||
// credential by now, so the build's `npm publish` authenticates; the artifact is a `package`, built
|
// 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.
|
// 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 {
|
func PublishTheSDK(ctx context.Context, o Options, control controlPlane, say func(string)) error {
|
||||||
if o.SDKSource.Repository == "" {
|
if o.SDKSource.Repository == "" {
|
||||||
|
|||||||
@@ -117,24 +117,6 @@ const busModulePort = 4222
|
|||||||
// PortsSetting is the controller's settings key for a module's given ports.
|
// PortsSetting is the controller's settings key for a module's given ports.
|
||||||
const PortsSetting = "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,
|
// 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
|
// 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
|
// 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 {
|
if o.Adopted && module == RegistryModule {
|
||||||
values["expose"] = map[string]string{strconv.Itoa(DefaultPorts().Registry): "anywhere"}
|
values["expose"] = map[string]string{strconv.Itoa(DefaultPorts().Registry): "anywhere"}
|
||||||
}
|
}
|
||||||
if packages := o.Ports.orDefaults().Packages; module == BuilderModule &&
|
// No `serves` setting for the build agent: the port given for the package registry reaches it in
|
||||||
packages != DefaultPorts().Packages {
|
// the genesis document (novox/hq ADR 0336 §3), and from the binding once the registry binds.
|
||||||
// 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}
|
|
||||||
}
|
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user