From c4ce57997e6f379ba44d450851a7cc677b413ae4 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 21:40:02 +0200 Subject: [PATCH 1/2] The packages port given at genesis is a module's setting, like every other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every foundation port given at genesis became a per-node setting of the module that binds it, except the package registry's: that one was fixed by rewriting the builder's manifest when the installer registered it. Registering the builder again from the catalogue undid it, and the forge's own module, when it took the bootstrap forge over, came up on the catalogue's port — which on a machine where a predecessor holds 3000 points the builder at the predecessor's forge. So the rewrite is gone, and the port is recorded twice as a setting, both from the one input: - the forge's module is registered at genesis — not assigned, nothing of it runs — so the controller has something to hold `{"ports": {"3000": }}` against. Assigning the forge later raises it on the port this machine was given, and its container, its filter rule, its opening, what it serves and what consumers are told all read it from there. - the builder is given `{"serves": {"port": }}`, which merges into the binding it carries in place of one nothing can resolve yet. A genesis on the catalogue's port records nothing and registers nothing, so it does exactly what it did before. novox/hq 04-ISSUES/085, ADR 0100 --- cmd/mesh-bootstrap/main.go | 6 +- internal/bootstrap/builder.go | 27 ------ internal/bootstrap/builder_test.go | 138 ++++++++++++++++++++------- internal/bootstrap/phase_packages.go | 52 ++++++++++ internal/bootstrap/ports.go | 20 ++++ internal/bootstrap/ports_test.go | 6 +- 6 files changed, 179 insertions(+), 70 deletions(-) diff --git a/cmd/mesh-bootstrap/main.go b/cmd/mesh-bootstrap/main.go index f8ae993..c001e7a 100644 --- a/cmd/mesh-bootstrap/main.go +++ b/cmd/mesh-bootstrap/main.go @@ -98,8 +98,8 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh everything this mesh will ever run --source-path the module's directory inside that repository, if not its root --catalog a checkout of the mesh's catalogue, holding the registry's, the - control plane's and the builder's manifests. Without it this stops - after step 6 + control plane's, the builder's and — when a packages port is given + — the forge's manifests. Without it this stops after step 6 --node the name this machine is known by (default: its hostname) --registry where this mesh keeps its own images (default ` + defaultRegistry + `) every node pulls the control plane from this, so on a mesh of more @@ -279,7 +279,7 @@ func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet { set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written") set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied") set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue, - "a checkout of the mesh's catalogue, for the registry's and the builder's manifests and what phase two installs; without it this stops after the foundation") + "a checkout of the mesh's catalogue, for the registry's, the builder's and the forge's manifests and what phase two installs; without it this stops after the foundation") set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository, "the repository the control plane is built from, on a mesh that already exists") set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref, diff --git a/internal/bootstrap/builder.go b/internal/bootstrap/builder.go index e8ca324..b86c1b1 100644 --- a/internal/bootstrap/builder.go +++ b/internal/bootstrap/builder.go @@ -1,10 +1,8 @@ package bootstrap import ( - "bytes" "context" "fmt" - "strconv" "strings" ) @@ -55,9 +53,6 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane "This is the manifest that makes the builder an ordinary module. Without it the mesh "+ "has the image and no way to run it, so nothing can be built here", err) } - if manifest, err = followPackagesPort(manifest, o.Ports.orDefaults().Packages); err != nil { - return out, err - } pinned, places, err := pinPlaceholder(manifest, published.Reference, BuilderModule) if err != nil { return out, err @@ -89,25 +84,3 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane out.Installed.Pushed, err = pushNode(ctx, o, control, say) return out, err } - -// packagesPortInBinding is the package registry's port as the builder's manifest names it, in the -// binding file it carries — JSON inside a JSON string, so its quotes are escaped. -const packagesPortInBinding = `\"port\": 3000` - -// followPackagesPort points the builder's package binding at the port the node gave the package -// registry (novox/hq ADR 0100). Genesis raises the registry by hand before gitea is a module, so no -// binding the controller resolves can say where it is; the builder carries the address in its own -// manifest, and a port given at genesis must reach it there or the base build dials a port nothing -// answers on. At the default it is left byte for byte as the catalogue has it. -func followPackagesPort(manifest []byte, port int) ([]byte, error) { - if port == defaultGiteaPort { - return manifest, nil - } - if n := bytes.Count(manifest, []byte(packagesPortInBinding)); n != 1 { - return nil, fmt.Errorf("the builder's manifest names the package registry's port %d time(s) where "+ - "this installer looks for it once (%s), so the port given with --packages-port cannot reach "+ - "it; nothing was changed", n, packagesPortInBinding) - } - return bytes.Replace(manifest, []byte(packagesPortInBinding), - []byte(`\"port\": `+strconv.Itoa(port)), 1), nil -} diff --git a/internal/bootstrap/builder_test.go b/internal/bootstrap/builder_test.go index c2f9848..fa61eba 100644 --- a/internal/bootstrap/builder_test.go +++ b/internal/bootstrap/builder_test.go @@ -1,16 +1,21 @@ package bootstrap import ( - "encoding/json" + "context" + "os" + "path/filepath" "strings" "testing" + "time" ) -// Defends novox/hq ADR 0100: a port given for the package registry at genesis reaches the one -// thing that dials it by a fixed number, the builder's package binding. +// Defends novox/hq 04-ISSUES/085: the port given for the package registry at genesis is a setting +// of a module and not text in a manifest, so registering that manifest again does not put the +// catalogue's number back. -// The builder's package binding exactly as the catalogue's manifest carries it. -const builderManifest = `{ +// theBuildersBinding is the resource the builder carries in place of a binding nothing can resolve +// yet, exactly as the catalogue's manifest has it — settable, with the port as its only default. +const theBuildersBinding = `{ "module": "builder", "resources": [ { @@ -18,52 +23,113 @@ const builderManifest = `{ "type": "file", "path": "/var/lib/mesh/builder/package-registry.json", "mode": "0600", + "merge": "json", + "protected": ["provision", "from", "as"], "content": "{\"provision\": \"package-registry\", \"from\": \"gitea\", \"at\": \"127.0.0.1\", \"as\": \"mesh-builder\", \"serves\": {\"scheme\": \"http\", \"port\": 3000, \"npm-path\": \"/api/packages/novox/npm/\"}}\n" } ] }` -func bindingPort(t *testing.T, manifest []byte) float64 { +func recording(t *testing.T) (*controlRecorder, controlPlane) { t.Helper() - var m struct { - Resources []struct { - Content string `json:"content"` - } `json:"resources"` - } - if err := json.Unmarshal(manifest, &m); err != nil { - t.Fatal(err) - } - var binding struct { - Serves struct { - Port float64 `json:"port"` - } `json:"serves"` - } - if err := json.Unmarshal([]byte(m.Resources[0].Content), &binding); err != nil { - t.Fatal(err) - } - return binding.Serves.Port + t.Setenv("TMPDIR", t.TempDir()) + c := &controlRecorder{settings: map[string]string{}} + return c, controlPlane{container: "temp-mesh-controller", run: c.run, timeout: time.Second} } -func TestTheBuilderFollowsThePackageRegistrysGivenPort(t *testing.T) { - got, err := followPackagesPort([]byte(builderManifest), 3100) - if err != nil { +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 p := bindingPort(t, got); p != 3100 { - t.Errorf("the builder's binding dials %v, not the port given", p) + 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 TestTheBuilderOnTheDefaultPortIsUnchanged(t *testing.T) { - got, err := followPackagesPort([]byte(builderManifest), 3000) - if err != nil || string(got) != builderManifest { - t.Errorf("the default port changed the manifest: %v", err) +func TestTheBuilderOnTheDefaultPackagesPortIsToldNothing(t *testing.T) { + c, control := recording(t) + o := Options{Node: "anchor", Wait: time.Second} + if _, err := installModule(context.Background(), o, control, BuilderModule, + []byte(theBuildersBinding), quietly); err != nil { + t.Fatal(err) + } + if c.index("settings") >= 0 { + t.Errorf("a genesis on the catalogue's packages port set a setting: %v", c.told) } } -func TestABuilderManifestThatNoLongerNamesThePortIsRefused(t *testing.T) { - moved := strings.Replace(builderManifest, `\"port\": 3000`, `\"port\": 3001`, 1) - if _, err := followPackagesPort([]byte(moved), 3100); err == nil || !strings.Contains(err.Error(), "--packages-port") { - t.Errorf("a manifest the port cannot reach was accepted: %v", err) +// aCatalogueWith writes a checkout holding one module's manifest, which is all the installer reads +// a catalogue for. +func aCatalogueWith(t *testing.T, module, manifest string) string { + t.Helper() + dir := t.TempDir() + at := filepath.Join(dir, catalogueDir, module) + if err := os.MkdirAll(at, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(at, "module.json"), []byte(manifest), 0o600); err != nil { + t.Fatal(err) + } + return dir +} + +const theForgesManifest = `{"module": "gitea", "version": "1"}` + +func TestThePackagesPortIsTheForgeModulesSettingOnThisNode(t *testing.T) { + c, control := recording(t) + o := Options{Node: "anchor", Catalogue: aCatalogueWith(t, ForgeModule, theForgesManifest), + Ports: FoundationPorts{Packages: 3100}, Wait: time.Second} + if err := recordTheForgesPort(context.Background(), o, control, quietly); err != nil { + t.Fatal(err) + } + if got := c.settings["gitea-settings.json"]; got != `{"ports":{"3000":3100}}` { + t.Errorf("the forge module was told %q about its port", got) + } + // Registered so there is something to hold the setting against, and never assigned: the forge + // module cannot run until the base it stands on has been built. + add, set := c.index("module add"), c.index("settings set gitea") + if add < 0 || set < 0 || add > set { + t.Fatalf("the forge's setting was recorded against a module the mesh does not know: %v", c.told) + } + if c.index("assign") >= 0 { + t.Errorf("genesis assigned the forge module: %v", c.told) + } + if !strings.Contains(c.told[set], "--node anchor") { + t.Errorf("the forge's port was set for the whole mesh, not this machine: %s", c.told[set]) + } +} + +func TestAGenesisOnTheCataloguesPackagesPortRegistersNoForge(t *testing.T) { + c, control := recording(t) + o := Options{Node: "anchor", Catalogue: aCatalogueWith(t, ForgeModule, theForgesManifest), + Wait: time.Second} + if err := recordTheForgesPort(context.Background(), o, control, quietly); err != nil { + t.Fatal(err) + } + if len(c.told) != 0 { + t.Errorf("a genesis on the defaults told the mesh something new: %v", c.told) + } +} + +func TestAForgeManifestTheCatalogueDoesNotHaveIsNamed(t *testing.T) { + _, control := recording(t) + o := Options{Node: "anchor", Catalogue: t.TempDir(), + Ports: FoundationPorts{Packages: 3100}, Wait: time.Second} + err := recordTheForgesPort(context.Background(), o, control, quietly) + if err == nil || !strings.Contains(err.Error(), ForgeModule) || + !strings.Contains(err.Error(), "package registry") { + t.Errorf("a missing forge manifest was not explained: %v", err) } } diff --git a/internal/bootstrap/phase_packages.go b/internal/bootstrap/phase_packages.go index bb8414b..551f81f 100644 --- a/internal/bootstrap/phase_packages.go +++ b/internal/bootstrap/phase_packages.go @@ -47,6 +47,16 @@ const ( defaultGiteaPort = 3000 ) +// ForgeModule is the module that owns the package registry — the one the bootstrap forge above is +// a stand-in for, and which takes it over once the base exists. +// +// Named here because the port given for the package registry is that module's setting on this node +// and not this installer's private number (novox/hq 04-ISSUES/085): the forge's own container, its +// filter rule, its opening, what it says it serves and what consumers are told all read it from +// there, so taking the bootstrap forge over does not move the registry back to the catalogue's +// port. +const ForgeModule = "gitea" + // RaisePackageRegistry puts a working npm registry in front of the base build. It is idempotent: // every step tolerates having been done, because genesis is safe to run again. func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control controlPlane, @@ -100,6 +110,11 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro return err } + say(" recording the port it was given as the forge module's own") + if err := recordTheForgesPort(ctx, o, control, say); err != nil { + return err + } + say(" delivering the builder its registry credential") if err := deliverBuilderNpm(ctx, o, control, builderPassword, say); err != nil { return err @@ -108,6 +123,43 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro return nil } +// recordTheForgesPort makes the port given for the package registry a setting of the module that +// serves it, on this node (novox/hq 04-ISSUES/085, ADR 0100). +// +// **The forge is the one foundation port that was not a setting.** The store's, the bus's, the +// broker's management port and the registry's each become a `ports` setting of the module that +// binds them, set where that module is registered and assigned; the forge is raised by hand here, +// long before its module can be built, so there was no registration to hang it on and the number +// lived in rewritten manifest text instead. So the manifest is registered here — not assigned, and +// nothing of it runs — for the one thing registering buys: the controller will hold a setting +// against it. Whenever somebody later assigns the forge on this node, it comes up on the port this +// machine was given rather than on the catalogue's, and so does the address its consumers are told. +// +// A node given the catalogue's own port records nothing and registers nothing, so a genesis on the +// defaults does exactly what it did before. +func recordTheForgesPort(ctx context.Context, o Options, control controlPlane, + say func(string)) error { + if o.Ports.orDefaults().Packages == DefaultPorts().Packages { + return nil + } + manifest, err := readManifest(o.Catalogue, ForgeModule) + if err != nil { + return fmt.Errorf("%w\n"+ + "This is the module that owns the forge genesis just raised. Without it the port this "+ + "machine was given for the package registry is nobody's setting, and taking the forge "+ + "over would put it back on the catalogue's port", err) + } + remote := "/" + ForgeModule + "-module.json" + if err := control.carrying(ctx, ForgeModule+"-module.json", manifest, remote); err != nil { + return err + } + if _, err := control.tell(ctx, "module", "add", remote); err != nil { + return err + } + say(" registered " + ForgeModule + " — registered, not assigned: nothing of it runs yet") + return setFoundationSettings(ctx, o, control, ForgeModule, say) +} + // seedGiteaDatabase creates gitea's role and database inside the foundation postgres, the same way // the foundation creates its own — psql run through the store container (the map's Route B). The role // is created before the database because the database is owned by it. Both are tolerant of already diff --git a/internal/bootstrap/ports.go b/internal/bootstrap/ports.go index f7a77c3..a580a01 100644 --- a/internal/bootstrap/ports.go +++ b/internal/bootstrap/ports.go @@ -105,12 +105,25 @@ func (p FoundationPorts) moduleSettings() map[string]map[string]int { add("lavinmq", d.AMQP, p.AMQP) add("lavinmq", d.Management, p.Management) add(RegistryModule, d.Registry, p.Registry) + add(ForgeModule, d.Packages, p.Packages) return out } // 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. +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 @@ -124,6 +137,13 @@ 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} + } if len(values) == 0 { return nil } diff --git a/internal/bootstrap/ports_test.go b/internal/bootstrap/ports_test.go index a94371a..366a3dd 100644 --- a/internal/bootstrap/ports_test.go +++ b/internal/bootstrap/ports_test.go @@ -220,7 +220,7 @@ func TestAPredecessorsContainerUnderTheMeshsNameIsRefused(t *testing.T) { } // controlRecorder is a control plane that answers everything and writes down what it was told, -// with the content of every settings file carried to it. +// with the content of every file carried to it, by the name it was carried under. type controlRecorder struct { told []string settings map[string]string @@ -229,9 +229,7 @@ type controlRecorder struct { func (c *controlRecorder) run(_ context.Context, name string, args ...string) (string, error) { if name == "docker" && args[0] == "cp" { raw, _ := os.ReadFile(args[1]) - if strings.HasSuffix(args[2], "-settings.json") { - c.settings[filepath.Base(args[2])] = string(raw) - } + c.settings[filepath.Base(args[2])] = string(raw) return "", nil } if name == "docker" && args[0] == "exec" { -- 2.54.0 From 744beb6c510321c9660596f0a20d2884d98d458c Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 21:56:42 +0200 Subject: [PATCH 2/2] A re-run does not replace the registered forge with whatever checkout it was given MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registering a module is an overwrite. Recording the forge's port ran `module add` every time, so a genesis re-run pointed at an older catalogue would replace the manifest of a forge that is built and assigned — with a push a few lines later. Registering is only here so a settings row has a module row to hang on, and that row is already there on a mesh that knows the forge. So: ask first, and skip. Two comments narrowed to what is true. What follows the node's setting is what the mesh derives from a module's ports — its container mapping, its filter rule, its opening and what it serves. The forge's own address in its runtime's environment (hq 088) and its route contribution's port do not, and are already wrong for any port the mesh assigned. And a settings layer is the module's, not one resource's: a second mergeable file on the builder would be given `serves` too. novox/hq 04-ISSUES/085 --- internal/bootstrap/builder_test.go | 36 +++++++++++++++++- internal/bootstrap/phase_packages.go | 57 +++++++++++++++++++--------- internal/bootstrap/ports.go | 6 +++ 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/internal/bootstrap/builder_test.go b/internal/bootstrap/builder_test.go index fa61eba..26368e9 100644 --- a/internal/bootstrap/builder_test.go +++ b/internal/bootstrap/builder_test.go @@ -24,7 +24,7 @@ const theBuildersBinding = `{ "path": "/var/lib/mesh/builder/package-registry.json", "mode": "0600", "merge": "json", - "protected": ["provision", "from", "as"], + "protected": ["provision", "from", "at", "as"], "content": "{\"provision\": \"package-registry\", \"from\": \"gitea\", \"at\": \"127.0.0.1\", \"as\": \"mesh-builder\", \"serves\": {\"scheme\": \"http\", \"port\": 3000, \"npm-path\": \"/api/packages/novox/npm/\"}}\n" } ] @@ -111,6 +111,40 @@ func TestThePackagesPortIsTheForgeModulesSettingOnThisNode(t *testing.T) { } } +// knowingControl is a control plane that already has the forge in its catalogue, as a mesh that +// has been running has. +type knowingControl struct{ controlRecorder } + +func (k *knowingControl) run(ctx context.Context, name string, args ...string) (string, error) { + out, err := k.controlRecorder.run(ctx, name, args...) + if name == "docker" && args[0] == "exec" && len(args) > 4 && + args[3] == "module" && args[4] == "list" { + return ForgeModule + " 1 assigned to anchor\n", nil + } + return out, err +} + +func TestARerunDoesNotReplaceAForgeTheMeshAlreadyHas(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + k := &knowingControl{controlRecorder{settings: map[string]string{}}} + control := controlPlane{container: "mesh-controller", run: k.run, timeout: time.Second} + // An older checkout than the mesh is running, which is what makes an overwrite here damage. + o := Options{Node: "anchor", Catalogue: aCatalogueWith(t, ForgeModule, `{"module": "gitea"}`), + Ports: FoundationPorts{Packages: 3100}, Wait: time.Second} + if err := recordTheForgesPort(context.Background(), o, control, quietly); err != nil { + t.Fatal(err) + } + // Registering is an overwrite, and the manifest of a forge that is built and assigned is not + // this installer's to replace with whatever checkout it was pointed at. + if k.index("module add") >= 0 { + t.Errorf("a re-run replaced the registered forge's manifest: %v", k.told) + } + // The port is still recorded: a settings row needs the module row, and it is already there. + if got := k.settings["gitea-settings.json"]; got != `{"ports":{"3000":3100}}` { + t.Errorf("the forge's port was not recorded on a mesh that knows it: %q", got) + } +} + func TestAGenesisOnTheCataloguesPackagesPortRegistersNoForge(t *testing.T) { c, control := recording(t) o := Options{Node: "anchor", Catalogue: aCatalogueWith(t, ForgeModule, theForgesManifest), diff --git a/internal/bootstrap/phase_packages.go b/internal/bootstrap/phase_packages.go index 551f81f..c2b00d7 100644 --- a/internal/bootstrap/phase_packages.go +++ b/internal/bootstrap/phase_packages.go @@ -51,10 +51,15 @@ const ( // a stand-in for, and which takes it over once the base exists. // // Named here because the port given for the package registry is that module's setting on this node -// and not this installer's private number (novox/hq 04-ISSUES/085): the forge's own container, its -// filter rule, its opening, what it says it serves and what consumers are told all read it from -// there, so taking the bootstrap forge over does not move the registry back to the catalogue's -// port. +// and not this installer's private number (novox/hq 04-ISSUES/085). What follows that setting is +// what the mesh derives from a module's ports: the forge's container mapping, its rule in the +// filter, its opening on an adopted node, and what it says it serves — so a consumer the mesh binds +// is told where the machine actually put the registry. +// +// What does NOT follow it, and is not this change's to fix: the address of itself the forge's +// runtime is given, which the catalogue writes as a literal (novox/hq 04-ISSUES/088), and the port +// in its route contribution. Both are already wrong for any machine port the mesh assigned, with a +// given port or without one. const ForgeModule = "gitea" // RaisePackageRegistry puts a working npm registry in front of the base build. It is idempotent: @@ -131,9 +136,17 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro // binds them, set where that module is registered and assigned; the forge is raised by hand here, // long before its module can be built, so there was no registration to hang it on and the number // lived in rewritten manifest text instead. So the manifest is registered here — not assigned, and -// nothing of it runs — for the one thing registering buys: the controller will hold a setting -// against it. Whenever somebody later assigns the forge on this node, it comes up on the port this -// machine was given rather than on the catalogue's, and so does the address its consumers are told. +// nothing of it runs — for the one thing registering buys: a settings row needs the module row to +// exist. Whenever somebody later assigns the forge on this node, it comes up on the port this +// machine was given rather than on the catalogue's. +// +// **Registered only when the mesh does not already know it, which is the opposite of every other +// `module add` here.** The rest of the installer registers every run on purpose: the manifest is +// what changes between runs, and skipping it would pin the mesh to a previous image. This one +// changes nothing about the forge and wants nothing of the checkout's manifest — and registering +// is an overwrite, so a re-run of genesis pointed at an older catalogue would replace the manifest +// of a forge that is built and assigned, with a push a few lines later. The port is the only thing +// this is here to record, and the setting does not need the manifest to be this checkout's. // // A node given the catalogue's own port records nothing and registers nothing, so a genesis on the // defaults does exactly what it did before. @@ -142,21 +155,29 @@ func recordTheForgesPort(ctx context.Context, o Options, control controlPlane, if o.Ports.orDefaults().Packages == DefaultPorts().Packages { return nil } - manifest, err := readManifest(o.Catalogue, ForgeModule) + known, err := control.tell(ctx, "module", "list") if err != nil { - return fmt.Errorf("%w\n"+ - "This is the module that owns the forge genesis just raised. Without it the port this "+ - "machine was given for the package registry is nobody's setting, and taking the forge "+ - "over would put it back on the catalogue's port", err) - } - remote := "/" + ForgeModule + "-module.json" - if err := control.carrying(ctx, ForgeModule+"-module.json", manifest, remote); err != nil { return err } - if _, err := control.tell(ctx, "module", "add", remote); err != nil { - return err + if mentions(known, ForgeModule) { + say(" known " + ForgeModule + " — left as the mesh has it; only its port is set here") + } else { + manifest, err := readManifest(o.Catalogue, ForgeModule) + if err != nil { + return fmt.Errorf("%w\n"+ + "This is the module that owns the forge genesis just raised. Without it the port this "+ + "machine was given for the package registry is nobody's setting, and taking the forge "+ + "over would put it back on the catalogue's port", err) + } + remote := "/" + ForgeModule + "-module.json" + if err := control.carrying(ctx, ForgeModule+"-module.json", manifest, remote); err != nil { + return err + } + if _, err := control.tell(ctx, "module", "add", remote); err != nil { + return err + } + say(" registered " + ForgeModule + " — registered, not assigned: nothing of it runs yet") } - say(" registered " + ForgeModule + " — registered, not assigned: nothing of it runs yet") return setFoundationSettings(ctx, o, control, ForgeModule, say) } diff --git a/internal/bootstrap/ports.go b/internal/bootstrap/ports.go index a580a01..c3c1ed8 100644 --- a/internal/bootstrap/ports.go +++ b/internal/bootstrap/ports.go @@ -122,6 +122,12 @@ const PortsSetting = "ports" // 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, -- 2.54.0