Files
mesh-controller/cmd/mesh-controller/same_source_test.go
T
jschoubben 792352dfad
mesh/merge-gate error: the check could not run: a throwaway postgres:17-alpine could not be raised: docker run --label mesh.build=build-1791317509716888018…
mesh/delivery delivered
Read a rebuild of an unchanged source as no move, whatever image digest it made (hq issue 280)
An image is not byte-reproducible, so ADR 0236's 'same artifacts is no move'
never held for one: a catalogue merge that did not touch the bus rebuilt it,
and every send to the control node waited for a planned bus upgrade.

The builder now records a source fingerprint per build (module tree, context
trees, bases and toolchains by digest). A rebuild with the fingerprint of the
build it repeats is registered with that build's artifacts, handed to modules
standing on it, holds no push, demands no bus step, and a plan sends and
gates nothing for it. Identical artifacts remain a second way to be no move.
2026-10-06 22:09:11 +02:00

210 lines
8.8 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"reflect"
"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 build whose source is unchanged is never a move (novox/hq issue 280): an image is not
// byte-reproducible, so two builds of one source make two digests, and the rule that a rebuild with
// identical artifacts is no move (ADR 0236) never held for one.
// anImageBuild is a build outcome of an image module: its manifest names the image by the digest made.
func anImageBuild(t *testing.T, module, id, commit, digest, source string, asked time.Time) link.BuildResult {
t.Helper()
image := "registry.invalid:5000/" + module + "/server@sha256:" + digest
manifest, _ := json.Marshal(map[string]any{"module": module, "version": "1",
"resources": []any{map[string]any{"id": "svc", "type": "container", "name": module, "image": image}}})
return link.BuildResult{ID: link.NewBuildID(asked), Repository: "novox/mesh-catalog", Path: "modules/" + module,
On: "anchor", Module: module, Commit: commit, Manifest: manifest, SourceFingerprint: source,
Made: []link.MadeArtifact{{Name: "server", Kind: "image", Reference: image}},
Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}}
}
// shelfNames is whether the module the mesh holds names this digest.
func shelfNames(t *testing.T, inv *inventory.Inventory, module, digest string) bool {
t.Helper()
shelf, err := inv.Catalogue(t.Context())
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(shelf[module])
return strings.Contains(string(raw), digest)
}
// A rebuild of an unchanged source is registered with the artifacts of the build it was first made
// as: no machine is sent a new digest, a module standing on it is handed the same base, and the two
// builds are one to every rule that asks whether a send moves something. A real source change moves.
func TestARebuildOfAnUnchangedSourceKeepsItsArtifacts(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
start := time.Now().Add(-time.Hour)
for i, b := range []link.BuildResult{
anImageBuild(t, "app", "", "c1aaaaaa", strings.Repeat("a", 64), "src1:same", start),
// Another module's merge rebuilt it: a new commit, a new image digest, the same source.
anImageBuild(t, "app", "", "c2bbbbbb", strings.Repeat("b", 64), "src1:same", start.Add(time.Minute)),
} {
if _, _, err := takeIn(ctx, inv, b); err != nil {
t.Fatalf("build %d: %v", i, err)
}
}
if !shelfNames(t, inv, "app", strings.Repeat("a", 64)) || shelfNames(t, inv, "app", strings.Repeat("b", 64)) {
t.Fatal("a rebuild of an unchanged source registered the digest it made, not the one the mesh holds")
}
if src, err := inv.SourceOf(ctx, "app"); err != nil || src.BuiltFrom != "c2bbbbbb" {
t.Fatalf("the module is not at the commit it was rebuilt from: %+v %v", src, err)
}
held, err := inv.Held(ctx)
if err != nil || !strings.HasSuffix(held["app/server"], strings.Repeat("a", 64)) {
t.Fatalf("a module standing on it is handed %q, not the build the mesh holds (%v)", held["app/server"], err)
}
f, err := readMoveFacts(ctx, inv)
if err != nil {
t.Fatal(err)
}
if !f.identical("app", "c1aaaaaa", "c2bbbbbb") {
t.Fatal("two builds of one source are not one build")
}
// A real change to the source moves: its own digest registered, and not identical.
if _, _, err := takeIn(ctx, inv, anImageBuild(t, "app", "", "c3cccccc", strings.Repeat("c", 64), "src1:changed",
start.Add(2*time.Minute))); err != nil {
t.Fatal(err)
}
if !shelfNames(t, inv, "app", strings.Repeat("c", 64)) {
t.Fatal("a changed source did not register what it made")
}
if f, _ = readMoveFacts(ctx, inv); f.identical("app", "c2bbbbbb", "c3cccccc") {
t.Fatal("a changed source is read as the same build")
}
// And a builder that says no fingerprint is told apart by its artifacts alone, as before.
if _, _, err := takeIn(ctx, inv, anImageBuild(t, "app", "", "c4dddddd", strings.Repeat("d", 64), "",
start.Add(3*time.Minute))); err != nil {
t.Fatal(err)
}
if !shelfNames(t, inv, "app", strings.Repeat("d", 64)) {
t.Fatal("a build with no fingerprint did not register what it made")
}
}
// The bus rebuilt for another module's merge, its source untouched and its image digest new: no move —
// no push to its machine is held, no bus step is demanded, and `bus upgrade` has nothing to do. A real
// change to the bus's source is the planned step again.
func TestABusRebuiltFromAnUnchangedSourceDemandsNoBusStep(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
start := time.Now().Add(-time.Hour)
bus := func(id, commit, digest, source string, at time.Time) link.BuildResult {
b := anImageBuild(t, "nats", id, commit, digest, source, at)
manifest, _ := json.Marshal(map[string]any{"module": "nats", "version": "2",
"provides": []any{map[string]any{"name": "mesh-bus"}},
"resources": []any{map[string]any{"id": "svc", "type": "container", "name": "nats",
"image": b.Made[0].Reference}}})
b.Manifest = manifest
return b
}
if _, _, err := takeIn(ctx, inv, bus("", "n1111111", strings.Repeat("a", 64), "src1:bus", start)); err != nil {
t.Fatal(err)
}
if _, err := inv.Assign(ctx, "anchor", "nats"); err != nil {
t.Fatal(err)
}
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"nats": "n1111111"}); err != nil {
t.Fatal(err)
}
// The wide rebuild: a new commit, a new image digest, the same source.
if _, _, err := takeIn(ctx, inv, bus("", "n2222222", strings.Repeat("b", 64), "src1:bus", start.Add(time.Minute))); err != nil {
t.Fatal(err)
}
if held, err := busHeld(ctx, inv, []string{"anchor"}); err != nil || len(held) != 0 {
t.Fatalf("a bus rebuilt from an unchanged source held its machine: %v %v", held, err)
}
wasSnapshot := takeBusSnapshot
t.Cleanup(func() { takeBusSnapshot = wasSnapshot })
takeBusSnapshot = func(context.Context, string, string) (string, error) {
return "", errors.New("no snapshot is taken for a bus step nobody needs")
}
var sent [][]string
wasSend := sendRollout
sendRollout = func(_ context.Context, _ *stores, names []string) ([]string, error) {
sent = append(sent, names)
return names, nil
}
t.Cleanup(func() { sendRollout = wasSend })
if err := busCommand(ctx, []string{"upgrade", "--why", "nothing changed", "--reversible"}); err != nil || len(sent) != 0 {
t.Fatalf("a bus step ran for a bus whose source is unchanged: %v, sent %v", err, sent)
}
if !shelfNames(t, inv, "nats", strings.Repeat("a", 64)) {
t.Fatal("the bus the mesh holds is not the image its machine runs")
}
// A real change to the bus's source: the planned step.
if _, _, err := takeIn(ctx, inv, bus("", "n3333333", strings.Repeat("c", 64), "src1:bus-2.12", start.Add(2*time.Minute))); err != nil {
t.Fatal(err)
}
held, err := busHeld(ctx, inv, []string{"anchor"})
if err != nil || !strings.Contains(held["anchor"], "planned step") {
t.Fatalf("a changed bus did not demand its planned step: %v %v", held, err)
}
}
// A plan's build made from the source every machine running the module runs is not sent and not
// judged: nothing moves. A build of a changed source is sent to its first machine, gated, as before.
func TestAPlanSendsNothingForAnUnchangedSource(t *testing.T) {
for _, tc := range []struct {
name string
source string
sent [][]string
}{
{"unchanged", "src1:app", nil},
{"changed", "src1:app-changed", [][]string{{"anchor"}}},
} {
t.Run(tc.name, func(t *testing.T) {
g := aGateMesh(t)
ctx := t.Context()
inv := g.open.inventory
// What anchor and laptop run (c1) was made from src1:app; the plan's build of c2 from the source
// the case says.
for _, b := range []inventory.Build{
{ID: "build-1s", Module: "app", Commit: "c1", SourceFingerprint: "src1:app",
Asked: time.Now().Add(-30 * time.Second), At: time.Now().Add(-30 * time.Second)},
{ID: "build-2s", Module: "app", Commit: "c2", SourceFingerprint: tc.source,
Asked: time.Now(), At: time.Now()},
} {
b.Manifest, _ = json.Marshal(catalogue.Manifest{Module: "app", Version: "1"})
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
p := g.plan(t)
p.Modules["app"].Build = "build-2s"
if err := inv.SavePlan(ctx, &p); err != nil {
t.Fatal(err)
}
advancePlans(ctx, g.open)
if !reflect.DeepEqual(g.sent, tc.sent) {
t.Fatalf("sent %v, want %v", g.sent, tc.sent)
}
p = g.plan(t)
s := p.Modules["app"]
if tc.sent == nil && (s.SentAt == nil || s.Gate != nil || !strings.Contains(s.Why, "no move")) {
t.Fatalf("an unchanged source was not read as no move: %+v", s)
}
if tc.sent != nil && (s.Gate == nil || len(s.First) == 0) {
t.Fatalf("a changed source was not sent to its first machine, gated: %+v", s)
}
})
}
}