diff --git a/internal/bootstrap/builder.go b/internal/bootstrap/builder.go index b86c1b1..e8ca324 100644 --- a/internal/bootstrap/builder.go +++ b/internal/bootstrap/builder.go @@ -1,8 +1,10 @@ package bootstrap import ( + "bytes" "context" "fmt" + "strconv" "strings" ) @@ -53,6 +55,9 @@ 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 @@ -84,3 +89,25 @@ 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 new file mode 100644 index 0000000..c2f9848 --- /dev/null +++ b/internal/bootstrap/builder_test.go @@ -0,0 +1,69 @@ +package bootstrap + +import ( + "encoding/json" + "strings" + "testing" +) + +// 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. + +// The builder's package binding exactly as the catalogue's manifest carries it. +const builderManifest = `{ + "module": "builder", + "resources": [ + { + "id": "package-binding", + "type": "file", + "path": "/var/lib/mesh/builder/package-registry.json", + "mode": "0600", + "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 { + 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 +} + +func TestTheBuilderFollowsThePackageRegistrysGivenPort(t *testing.T) { + got, err := followPackagesPort([]byte(builderManifest), 3100) + if 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) + } +} + +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 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) + } +}