mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
224 lines
9.0 KiB
Go
224 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A recorded build reaches a machine only by a person's push (novox/hq issue 295, ADR 0242).
|
|
|
|
// aContainerBuild is a build outcome of a module of containers, each named by id with the image and the
|
|
// health it is given; a policy when one is said.
|
|
func aContainerBuild(t *testing.T, module, commit, policy string, asked time.Time, containers map[string][2]string) link.BuildResult {
|
|
t.Helper()
|
|
var resources []any
|
|
for id, c := range containers {
|
|
r := map[string]any{"id": id, "type": "container", "name": module + "-" + id, "image": c[0]}
|
|
if c[1] != "" {
|
|
r["health"] = map[string]any{"kind": c[1]}
|
|
}
|
|
resources = append(resources, r)
|
|
}
|
|
m := map[string]any{"module": module, "version": "1", "resources": resources}
|
|
if policy != "" {
|
|
m["upgrade"] = map[string]any{"policy": policy, "why": "a provider whose restart drops every consumer"}
|
|
}
|
|
manifest, _ := json.Marshal(m)
|
|
return link.BuildResult{ID: link.NewBuildID(asked), Repository: "novox/mesh-catalog", Path: "modules/" + module,
|
|
On: "anchor", Module: module, Commit: commit, Manifest: manifest,
|
|
Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}}
|
|
}
|
|
|
|
// imageOf is the image the composed plan of a machine gives one of a module's containers.
|
|
func imageOf(t *testing.T, plan catalogue.Resolution, module, id string) string {
|
|
t.Helper()
|
|
for _, m := range plan.Modules {
|
|
if m.Module != module {
|
|
continue
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r["id"] == id {
|
|
image, _ := r["image"].(string)
|
|
return image
|
|
}
|
|
}
|
|
}
|
|
t.Fatalf("%s has no container %s in the plan", module, id)
|
|
return ""
|
|
}
|
|
|
|
// Tonight's case, 2026-10-07: a catalogue merge adopting the images' own health checks rebuilt the
|
|
// database (policy record) with mail (policy roll). The plan's gated send for mail carried the
|
|
// database's new build to the control node and recreated it, unjudged, with nobody's word. A send that
|
|
// is not a person's push now composes the database at the build the machine runs and records that it
|
|
// still carries it; a person's push composes the new one; the bus step moves the bus alone.
|
|
func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
start := time.Now().Add(-time.Hour)
|
|
pgImage := "registry.invalid:5000/postgres/server@sha256:" + strings.Repeat("a", 64)
|
|
mailImage := "registry.invalid:5000/mailu/smtp@sha256:" + strings.Repeat("c", 64)
|
|
for _, b := range []link.BuildResult{
|
|
aContainerBuild(t, "postgres", "c1111111", catalogue.PolicyRecord, start,
|
|
map[string][2]string{"server": {pgImage, ""}}),
|
|
aContainerBuild(t, "mailu", "c1111111", "", start.Add(time.Second),
|
|
map[string][2]string{"smtp": {mailImage, ""}, "imap": {mailImage, ""}}),
|
|
} {
|
|
if _, _, err := takeIn(ctx, inv, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, m := range []string{"postgres", "mailu"} {
|
|
if _, err := inv.Assign(ctx, "anchor", m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor",
|
|
map[string]string{"postgres": "c1111111", "mailu": "c1111111"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The merge: both adopt a health check; the images are the same.
|
|
for _, b := range []link.BuildResult{
|
|
aContainerBuild(t, "postgres", "c2222222", catalogue.PolicyRecord, start.Add(time.Minute),
|
|
map[string][2]string{"server": {pgImage, "runtime"}}),
|
|
aContainerBuild(t, "mailu", "c2222222", "", start.Add(time.Minute+time.Second),
|
|
map[string][2]string{"smtp": {mailImage, "runtime"}, "imap": {mailImage, "runtime"}}),
|
|
} {
|
|
if _, _, err := takeIn(ctx, inv, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
healthOf := func(plan catalogue.Resolution, module, id string) any {
|
|
for _, m := range plan.Modules {
|
|
for _, r := range m.Resources {
|
|
if m.Module == module && r["id"] == id {
|
|
return r["health"]
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A plan's, a release plan's, a healer's send: the database is kept as it runs, mail moves.
|
|
kept := keepingRecorded(ctx)
|
|
plan, settings, err := planFor(kept, open, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if healthOf(plan, "postgres", "server") != nil {
|
|
t.Fatal("a send that is not a person's push composed the recorded module's new build")
|
|
}
|
|
if healthOf(plan, "mailu", "smtp") == nil {
|
|
t.Fatal("the module that rolls out was not composed at its new build")
|
|
}
|
|
if imageOf(t, plan, "postgres", "server") != pgImage {
|
|
t.Fatal("the recorded module's container lost its image")
|
|
}
|
|
gens, err := generators(kept, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
declared, err := declarationWith(kept, open, "anchor", plan, settings, gens, Allocating)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if declared.Builds["postgres"] != "c1111111" || declared.Builds["mailu"] != "c2222222" {
|
|
t.Fatalf("the send records it carries %v; want postgres still at c1111111 and mailu at c2222222", declared.Builds)
|
|
}
|
|
|
|
// A person's push: the recorded module's new build.
|
|
plan, _, err = planFor(ctx, open, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if healthOf(plan, "postgres", "server") == nil {
|
|
t.Fatal("a person's push did not compose the recorded module's new build")
|
|
}
|
|
|
|
// The bus step moves the bus alone: a recorded module it is told it may move moves, the others stay.
|
|
plan, _, err = planFor(keepingRecorded(ctx, "postgres"), open, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if healthOf(plan, "postgres", "server") == nil {
|
|
t.Fatal("the module a send is let move was kept")
|
|
}
|
|
|
|
// What a send composes under (sendToEach): every recorded module kept; on the bus step, the bus moves.
|
|
if under, err := sendKeeps(ctx, inv); err != nil {
|
|
t.Fatal(err)
|
|
} else if skip, on := keptExcept(under); !on || len(skip) != 0 {
|
|
t.Fatalf("an ordinary send keeps %v %v", on, skip)
|
|
}
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "nats", Version: "2",
|
|
Provides: []catalogue.Offer{{Name: "mesh-bus"}}}, inventory.Source{Repository: "novox/mesh-catalog",
|
|
Seat: "git", Path: "modules/nats", BuiltFrom: "n2", Head: "n2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if under, err := sendKeeps(withBusStep(ctx), inv); err != nil {
|
|
t.Fatal(err)
|
|
} else if skip, on := keptExcept(under); !on || !skip["nats"] || len(skip) != 1 {
|
|
t.Fatalf("the bus step keeps %v %v; want everything recorded but the bus", on, skip)
|
|
}
|
|
|
|
// And a recorded module whose kept build the records no longer hold refuses the send, said.
|
|
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor",
|
|
map[string]string{"postgres": "c0000000", "mailu": "c2222222"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := planFor(kept, open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") {
|
|
t.Fatalf("a recorded build that cannot be kept did not refuse the send with its remedy: %v", err)
|
|
}
|
|
|
|
// And the gated send for mail says what it recreates: both containers, no new image, at once.
|
|
moves := []inventory.CarriedMove{{Module: "mailu", Node: "anchor", From: "c1111111", To: "c2222222"}}
|
|
sayRecreations(ctx, open, moves)
|
|
if !strings.Contains(moves[0].Recreates, "recreates 2 of mailu's 2") || !strings.Contains(moves[0].Recreates, "no new image") {
|
|
t.Fatalf("the send says %q of mail's containers", moves[0].Recreates)
|
|
}
|
|
if said := recreationsSaid(moves); !strings.HasPrefix(said, "on anchor recreates") {
|
|
t.Fatalf("the plan's note says %q", said)
|
|
}
|
|
}
|
|
|
|
// The send says what it recreates (ADR 0242): mail's health checks adopted recreate both its
|
|
// containers, with no new image, every one at once.
|
|
func TestASendSaysWhatItRecreates(t *testing.T) {
|
|
image := "registry.invalid:5000/mailu/smtp@sha256:" + strings.Repeat("c", 64)
|
|
from := catalogue.Manifest{Module: "mailu", Resources: []map[string]any{
|
|
{"id": "smtp", "type": "container", "image": image},
|
|
{"id": "imap", "type": "container", "image": image},
|
|
{"id": "redis", "type": "container", "image": image},
|
|
{"id": "config", "type": "file", "path": "/etc/x"}}}
|
|
to := catalogue.Manifest{Module: "mailu", Resources: []map[string]any{
|
|
{"id": "smtp", "type": "container", "image": image, "health": map[string]any{"kind": "runtime"}},
|
|
{"id": "imap", "type": "container", "image": image, "health": map[string]any{"kind": "runtime"}},
|
|
{"id": "redis", "type": "container", "image": image},
|
|
{"id": "config", "type": "file", "path": "/etc/y"}}}
|
|
r := catalogue.Recreates(from, to)
|
|
if !r.SpecOnly() || len(r.Recreated) != 2 || r.Containers != 3 {
|
|
t.Fatalf("recreation %+v", r)
|
|
}
|
|
said := r.Say("mailu")
|
|
for _, want := range []string{"recreates 2 of mailu's 3", "no new image", "imap, smtp", "interrupted"} {
|
|
if !strings.Contains(said, want) {
|
|
t.Fatalf("%q does not say %q", said, want)
|
|
}
|
|
}
|
|
if catalogue.Recreates(from, from).Say("mailu") != "" {
|
|
t.Fatal("a move that recreates nothing said something")
|
|
}
|
|
to.Resources[2]["image"] = strings.Replace(image, "c", "d", 1)
|
|
if r := catalogue.Recreates(from, to); r.SpecOnly() || len(r.Images) != 1 {
|
|
t.Fatalf("a new image read as a declaration only: %+v", r)
|
|
}
|
|
}
|