Adoption mode: a node in use is adopted before it is converged (hq ADR 0100–0103) #20

Merged
jschoubben merged 52 commits from feat/adoption-mode into main 2026-09-22 19:01:47 +00:00
2 changed files with 96 additions and 0 deletions
Showing only changes of commit 48a8f4cf9d - Show all commits
+27
View File
@@ -1,8 +1,10 @@
package bootstrap package bootstrap
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"strconv"
"strings" "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 "+ "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) "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) pinned, places, err := pinPlaceholder(manifest, published.Reference, BuilderModule)
if err != nil { if err != nil {
return out, err 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) out.Installed.Pushed, err = pushNode(ctx, o, control, say)
return out, err 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
}
+69
View File
@@ -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)
}
}