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
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.
210 lines
8.8 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|