Genesis's builder step read modules/builder from the catalogue, which retired it on 2026-10-03 for the build agent (node-build-agent on every machine), so no genesis got past step 13. The step now installs build-agent from the catalogue, puts the builder image it carried and published where the manifest names the image it builds, and issues its bus account as any module's.
241 lines
9.8 KiB
Go
241 lines
9.8 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// 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.
|
|
|
|
// 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": [
|
|
{
|
|
"id": "package-binding",
|
|
"type": "file",
|
|
"path": "/var/lib/mesh/builder/package-registry.json",
|
|
"mode": "0600",
|
|
"merge": "json",
|
|
"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"
|
|
}
|
|
]
|
|
}`
|
|
|
|
func recording(t *testing.T) (*controlRecorder, controlPlane) {
|
|
t.Helper()
|
|
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 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}
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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])
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
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)
|
|
}
|
|
}
|
|
|
|
// theCataloguesModule is the catalogue's own manifest of a module, never a copy: the clone beside a merge
|
|
// check (MESH_CHECK_BESIDE), where it must be; else a checkout beside this one; else the test skips aloud.
|
|
func theCataloguesModule(t *testing.T, module string) []byte {
|
|
t.Helper()
|
|
inside := filepath.Join("mesh-catalog", "modules", module, "module.json")
|
|
if root := os.Getenv("MESH_CHECK_BESIDE"); root != "" {
|
|
raw, err := os.ReadFile(filepath.Join(root, inside))
|
|
if err != nil {
|
|
t.Fatalf("the catalogue beside this check (MESH_CHECK_BESIDE=%s) has no %s: %v", root, inside, err)
|
|
}
|
|
return raw
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "..", inside))
|
|
if err != nil {
|
|
t.Skipf("NOT JUDGED: no catalogue beside this checkout and no MESH_CHECK_BESIDE; the merge check judges it")
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// **Genesis installs the build agent the catalogue holds** (novox/hq issue 596, ADR 0190): the
|
|
// one-holder `builder` left the catalogue on 2026-10-03, and genesis stopped at step 13 asking for it.
|
|
// The catalogue's own manifest is read, and the image the installer carried is put where the manifest
|
|
// names the image it builds.
|
|
func TestGenesisInstallsTheCataloguesBuildAgent(t *testing.T) {
|
|
manifest := theCataloguesModule(t, BuilderModule)
|
|
const reference = "127.0.0.1:5000/mesh-builder@sha256:" + "4444444444444444444444444444444444444444444444444444444444444444"
|
|
pinned, places, err := pinTheCarriedBuilder(manifest, reference)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Module string `json:"module"`
|
|
Claims []struct {
|
|
Name string `json:"name"`
|
|
} `json:"claims"`
|
|
Resources []map[string]any `json:"resources"`
|
|
}
|
|
if err := json.Unmarshal(pinned, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m.Module != BuilderModule || len(m.Claims) == 0 || m.Claims[0].Name != "node-build-agent" {
|
|
t.Fatalf("genesis installs %q claiming %v; the build agent holds node-build-agent (ADR 0190)", m.Module, m.Claims)
|
|
}
|
|
running := 0
|
|
for _, r := range m.Resources {
|
|
if r["type"] != "container" {
|
|
continue
|
|
}
|
|
if _, still := r["artifact"]; still {
|
|
t.Errorf("the container %v still names an artifact nothing has built yet", r["name"])
|
|
}
|
|
if r["image"] == reference {
|
|
running++
|
|
}
|
|
}
|
|
if places != 1 || running != 1 {
|
|
t.Errorf("the carried builder runs in %d container(s) (%d place(s) said); the build agent runs one", running, places)
|
|
}
|
|
}
|
|
|
|
// A manifest that runs nothing from an image it builds has nowhere to put the carried builder, and is
|
|
// refused rather than registered running nothing.
|
|
func TestABuildAgentManifestWithNowhereForTheCarriedBuilderIsRefused(t *testing.T) {
|
|
_, _, err := pinTheCarriedBuilder([]byte(`{"module":"build-agent","resources":[{"id":"server","type":"container","name":"x","artifact":"server"}],"build":{"artifacts":[{"name":"server","kind":"bundle"}]}}`),
|
|
"127.0.0.1:5000/mesh-builder@sha256:"+strings.Repeat("4", 64))
|
|
if err == nil || !strings.Contains(err.Error(), "nowhere to put") {
|
|
t.Fatalf("a manifest with no image artifact was pinned: %v", err)
|
|
}
|
|
}
|