The build agent left mesh-controller with R4g (ADR 0524 section 5); genesis still installs it until issue 1152 decides where a new mesh's builder comes from. The tests that read its manifest say NOT JUDGED and are judged again with 1152.
260 lines
11 KiB
Go
260 lines
11 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 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 a module's own manifest, 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. The catalogue holds it, but for the build agent,
|
|
// which moved whole into the controller's repository (ADR 0420, issue 1019, issue 1151) and is read from there.
|
|
//
|
|
// **The build agent is not there any more** (R4g, ADR 0524 section 5): the group deletes it from the controller with the seat
|
|
// node-build-agent. Genesis still installs it as a new mesh's builder, and where genesis gets a builder instead is issue
|
|
// 1152's decision, queued out of that milestone. Until then every test whose subject is the build agent's manifest skips
|
|
// aloud here, and is judged again with issue 1152.
|
|
func theCataloguesModule(t *testing.T, module string) []byte {
|
|
t.Helper()
|
|
if module == BuilderModule {
|
|
t.Skipf("NOT JUDGED: genesis still installs build-agent, which left mesh-controller with R4g (ADR 0524 section 5); " +
|
|
"a new mesh cannot raise a builder until issue 1152 gives genesis one, and this test is judged again with it")
|
|
}
|
|
repository := "mesh-catalog"
|
|
if module == BuilderModule {
|
|
repository = "mesh-controller"
|
|
}
|
|
inside := filepath.Join(repository, "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("%s beside this check (MESH_CHECK_BESIDE=%s) has no %s: %v", repository, root, inside, err)
|
|
}
|
|
return raw
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "..", inside))
|
|
if err != nil {
|
|
t.Skipf("NOT JUDGED: no %s beside this checkout and no MESH_CHECK_BESIDE; the merge check judges it", repository)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 1151: the build agent's manifest lives in the controller's repository now (ADR 0420, issue 1019), and a merge check of
|
|
// this repository has the controller cloned beside it, so a tree shaped like that check's, whose catalogue no longer holds
|
|
// the build agent, is read from the controller.
|
|
func TestTheBuildAgentsManifestIsReadFromTheControllerBesideTheCheck(t *testing.T) {
|
|
beside := t.TempDir()
|
|
for path, body := range map[string]string{
|
|
"mesh-catalog/modules/nats/module.json": `{"module":"nats"}`,
|
|
"mesh-controller/modules/build-agent/module.json": `{"module":"build-agent"}`,
|
|
} {
|
|
full := filepath.Join(beside, path)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
t.Setenv("MESH_CHECK_BESIDE", beside)
|
|
if got := string(theCataloguesModule(t, BuilderModule)); got != `{"module":"build-agent"}` {
|
|
t.Errorf("read %s", got)
|
|
}
|
|
if got := string(theCataloguesModule(t, "nats")); got != `{"module":"nats"}` {
|
|
t.Errorf("a module the catalogue holds was read as %s", got)
|
|
}
|
|
}
|