//go:build integration package inventory import ( "context" "errors" "reflect" "testing" "github.com/novox/mesh-controller/internal/chain" ) // A build row as the store held it before the move: written by SQL, the only hand-written state these tests have, for // it stands for what the mesh held before this release. func aBuildRow(t *testing.T, inv *Inventory, id, module, commit, failed string) { t.Helper() inv.ExecForTest(t, `insert into build (id, repository, ref, module, commit_hash, built_on, failed, made, source_path, manifest, built_against, built_contexts, asked, source_fingerprint, build_sources) values ($1::text, 'novox/' || $2::text, 'main', $2::text, $3::text, 'ace', $4::text, case when $4::text = '' then ('[{"name":"app","kind":"image","reference":"artifact-store://' || $2::text || '@sha256:' || $1::text || '"}]')::jsonb else '[]'::jsonb end, 'modules/' || $2::text, ('{"module":"' || $2::text || '"}')::jsonb, '["artifact-store://base@sha256:aa"]'::jsonb, '[{"repository":"novox/other","ref":"main"}]'::jsonb, now() - interval '1 hour', 'fp-' || $1::text, ('[{"paths":["modules/' || $2::text || '/module.json"]}]')::jsonb)`, id, module, commit, failed) } func rowsInTheTable(t *testing.T, inv *Inventory, table string) string { t.Helper() rows := inv.QueryForTest(t, `select count(*) from `+table) return rows[0][0] } // **The move copies every build row into the builds bucket, and a second run writes nothing** (ADR 0408 §8, issue // 1090): each row under its id, whole; the kind recorded in chain_moved; and from then on a build is read from the // bucket and written to it, the table taking no more. func TestTheMoveCopiesTheBuildRowsAndAgainWritesNothing(t *testing.T) { inv, finish := ForTestBefore(t, 105) finish() ctx := context.Background() aBuildRow(t, inv, "build-1", "notes", "c1", "") aBuildRow(t, inv, "build-2", "notes", "c2", "") aBuildRow(t, inv, "build-3", "wiki", "", "compile error") // Before the move the tables are read, and the bucket is empty. if before, err := inv.Builds(ctx, "", 10); err != nil || len(before) != 3 { t.Fatalf("before the move the tables hold %d builds: %v", len(before), err) } if moved, _ := inv.ChainMoved(ctx); len(moved) != 0 { t.Fatalf("moved before the move: %v", moved) } reports, err := inv.MoveChain(ctx, nil) if err != nil { t.Fatal(err) } if len(reports) != 1 || reports[0].Kind != KindBuilds || reports[0].Rows != 3 || reports[0].Written != 3 || reports[0].Largest == 0 { t.Fatalf("the move reported %+v", reports) } moved, err := inv.ChainMoved(ctx) if err != nil || moved[KindBuilds] != 3 { t.Fatalf("chain_moved says %v: %v", moved, err) } kept, found, err := inv.chain.Builds.Get(ctx, "build-2") if err != nil || !found { t.Fatalf("build-2 is not in the bucket: %v %v", found, err) } if v := kept.Value; v.Module != "notes" || v.Commit != "c2" || v.Repository != "novox/notes" || v.Path != "modules/notes" || v.SourceFingerprint != "fp-build-2" || len(v.Made) != 1 || len(v.Against) != 1 || len(v.Read) != 1 || len(v.Sources) != 1 || v.Published == nil || v.Published.Artifact != "sha256:build-2" || v.Asked.IsZero() || v.At.IsZero() || len(v.Manifest) == 0 { t.Fatalf("build-2 was moved as %+v", v) } // From now on the bucket is what is read and written. if err := inv.RecordBuild(ctx, aBuild("build-4", "notes", "")); err != nil { t.Fatal(err) } if n := rowsInTheTable(t, inv, "build"); n != "3" { t.Fatalf("the table took a build after the move: %s rows", n) } after, err := inv.Builds(ctx, "notes", 10) if err != nil || len(after) != 3 { t.Fatalf("notes has %d builds on the bus: %v", len(after), err) } // A second run finds the kind moved and writes nothing: every key still has the one value it was made with. again, err := inv.MoveChain(ctx, nil) if err != nil || len(again) != 1 || !again[0].Already || again[0].Written != 0 { t.Fatalf("the second move %+v: %v", again, err) } for _, id := range []string{"build-1", "build-2", "build-3", "build-4"} { if history, err := inv.chain.Builds.History(ctx, chainBuildKey(id)); err != nil || len(history) != 1 { t.Fatalf("%s has %d values: %v", id, len(history), err) } } } // **A move that stopped half way is finished by the next start, and never lays a row over a newer bucket value** (ADR // 0408 §8): the keys that exist are present, the others are written. func TestAControllerRestartedMidMoveFinishesItAndOverwritesNothing(t *testing.T) { inv, finish := ForTestBefore(t, 105) finish() ctx := context.Background() aBuildRow(t, inv, "build-1", "notes", "c1", "") aBuildRow(t, inv, "build-2", "notes", "c2", "") aBuildRow(t, inv, "build-3", "wiki", "c3", "") // An earlier start wrote build-1, and the bucket has had a newer value since. newer := chain.Build{ID: "build-1", Repository: "novox/notes", Module: "notes", Commit: "newer"} if _, err := inv.chain.Builds.Create(ctx, "build-1", newer); err != nil { t.Fatal(err) } reports, err := inv.MoveChain(ctx, nil) if err != nil { t.Fatal(err) } if reports[0].Written != 2 || reports[0].Present != 1 || reports[0].Rows != 3 { t.Fatalf("the move reported %+v", reports[0]) } got, _, _ := inv.chain.Builds.Get(ctx, "build-1") if !reflect.DeepEqual(got.Value, newer) { t.Fatalf("the newer value was laid over: %+v", got.Value) } if moved, _ := inv.ChainMoved(ctx); moved[KindBuilds] != 3 { t.Fatalf("chain_moved says %v", moved) } } // **A value over the bus's largest message stops the move with nothing written and nothing switched** (ADR 0408 §6): // a Refusal, no key in the bucket, no kind recorded, and the tables still what is read. func TestTheMoveRefusesAValueOverTheLargestMessage(t *testing.T) { inv, finish := ForTestBefore(t, 105) finish() ctx := context.Background() aBuildRow(t, inv, "build-1", "notes", "c1", "") inv.ExecForTest(t, `insert into build (id, repository, module, commit_hash, manifest) values ('build-big', 'novox/wiki', 'wiki', 'c9', ('{"x":"' || repeat('a', 1100000) || '"}')::jsonb)`) _, err := inv.MoveChain(ctx, nil) var refusal *Refusal if !errors.As(err, &refusal) || refusal.Kind != KindBuilds { t.Fatalf("the move answered %v", err) } if keys, _ := inv.chain.Builds.Keys(ctx); len(keys) != 0 { t.Fatalf("the bucket holds %v after a refused move", keys) } if moved, _ := inv.ChainMoved(ctx); len(moved) != 0 { t.Fatalf("a refused move recorded %v", moved) } if builds, err := inv.Builds(ctx, "", 10); err != nil || len(builds) != 2 { t.Fatalf("the tables are no longer read: %d builds, %v", len(builds), err) } } // losing is a bucket that says it wrote a key and kept nothing: the move's count of keys against rows must see it. type losing struct { chain.Backend lose string } func (l losing) Create(ctx context.Context, key string, value []byte) (uint64, error) { if key == l.lose { return 1, nil } return l.Backend.Create(ctx, key, value) } // **The move refuses on a count that differs** (ADR 0408 §8): a row whose key is not in the bucket afterwards stops it, // nothing is recorded, and the tables are still read. func TestTheMoveRefusesOnACountThatDiffers(t *testing.T) { inv, finish := ForTestBefore(t, 105) finish() ctx := context.Background() aBuildRow(t, inv, "build-1", "notes", "c1", "") aBuildRow(t, inv, "build-2", "notes", "c2", "") inv.UseChain(chain.New(losing{chain.NewMemory(), "build-2"}, chain.NewMemory(), chain.NewMemory(), func(context.Context) error { return nil })) _, err := inv.MoveChain(ctx, nil) var refusal *Refusal if !errors.As(err, &refusal) { t.Fatalf("the move answered %v", err) } if moved, _ := inv.ChainMoved(ctx); len(moved) != 0 { t.Fatalf("a refused move recorded %v", moved) } } // **Without the lease the move writes nothing**, and fails the way the bus being away does — tried again, not refused: // the tables are still read. func TestTheMoveWithoutTheLeaseWritesNothing(t *testing.T) { inv, finish := ForTestBefore(t, 105) finish() ctx := context.Background() aBuildRow(t, inv, "build-1", "notes", "c1", "") mem := chain.NewMemory() inv.UseChain(chain.New(mem, chain.NewMemory(), chain.NewMemory(), func(context.Context) error { return errors.New("another controller holds the lease") })) _, err := inv.MoveChain(ctx, nil) var refusal *Refusal if err == nil || errors.As(err, &refusal) || !errors.Is(err, chain.ErrNotHeld) { t.Fatalf("the move answered %v", err) } if mem.Writes() != 0 { t.Fatalf("the bucket took %d writes without the lease", mem.Writes()) } if moved, _ := inv.ChainMoved(ctx); len(moved) != 0 { t.Fatalf("recorded %v", moved) } } // **Once builds are on the bus every reader of them reads the bucket** (issue 1090): a build recorded is the key of its // id, recorded again it stays one, and the lists the planner reads are made from the bucket — the table takes none. func TestBuildsOnTheBusAreReadBackByEveryReader(t *testing.T) { inv := fresh(t) ctx := context.Background() first := aBuild("build-1", "notes", "") first.Commit = "c1" first.Against = []string{"artifact-store://base@sha256:aa"} first.ReuseKey = "rk-1" second := aBuild("build-2", "notes", "") second.Commit = "c2" failed := aBuild("build-3", "notes", "compile error") other := aBuild("build-4", "wiki", "") for _, b := range []Build{first, second, failed, other, first} { if err := inv.RecordBuild(ctx, b); err != nil { t.Fatal(err) } } if n := rowsInTheTable(t, inv, "build"); n != "0" { t.Fatalf("the table took %s builds", n) } kept, found, err := inv.chain.Builds.Get(ctx, "build-1") if err != nil || !found || kept.Value.ReuseKey != "rk-1" { t.Fatalf("build-1 on the bus: %+v %v %v", kept.Value, found, err) } if history, _ := inv.chain.Builds.History(ctx, "build-1"); len(history) != 1 { t.Fatalf("a build recorded twice has %d values", len(history)) } list, err := inv.Builds(ctx, "notes", 10) if err != nil || len(list) != 3 { t.Fatalf("notes has %d builds: %v", len(list), err) } one, found, err := inv.BuildByID(ctx, "build-2") if err != nil || !found || one.Commit != "c2" || one.Module != "notes" { t.Fatalf("build-2 reads as %+v %v %v", one, found, err) } if _, found, _ := inv.BuildByID(ctx, "build-nope"); found { t.Fatal("a build nobody recorded was found") } if id, err := inv.BuildOf(ctx, "notes", "c1"); err != nil || id != "build-1" { t.Fatalf("the build of notes at c1 is %q: %v", id, err) } held, err := inv.Held(ctx) if err != nil || held["notes/config"] == "" || held["wiki/config"] == "" { t.Fatalf("held %v: %v", held, err) } against, err := inv.BuiltAgainst(ctx) if err != nil || len(against["notes"]) != 0 { // The newest successful build of notes, build-2, stood on nothing. t.Fatalf("built against %v: %v", against, err) } announced, err := inv.Announceable(ctx) if err != nil || len(announced) != 3 { t.Fatalf("announceable %d: %v", len(announced), err) } prints, err := inv.Fingerprints(ctx) if err != nil || prints["notes"]["c1"] == "" || prints["notes"]["c2"] == "" || prints["wiki"]["c0ffeebuild-4"] == "" { t.Fatalf("fingerprints %v: %v", prints, err) } }