//go:build integration package inventory import ( "context" "fmt" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // What the store keeps, and what it may let go (novox/hq ADR 0189, issue 108). // // The store has collected nothing since it was raised, and the registry's own answer — collect // what no tag names — would delete images machines are running, because the mesh pushes under one // moving tag and pins by digest. So the rule is the mesh's, read from its own records, and these // are the three reasons an artifact stays and the one reason it goes. // ref is an artifact reference as the mesh records one. func ref(module, artifact string, n int) string { return fmt.Sprintf("%s%s/%s@sha256:%064x", catalogue.ArtifactStoreScheme, module, artifact, n) } // holding registers a definition for each module that names no artifact, so the mesh holds the // module and its recent builds are somewhere it can go back to — and nothing more. func holding(t *testing.T, inv *Inventory, modules ...string) { t.Helper() for _, module := range modules { m := catalogue.Manifest{Module: module, Version: "1"} if err := inv.RegisterModule(context.Background(), m, Source{Repository: "https://forge.invalid/" + module + ".git"}); err != nil { t.Fatal(err) } } } // built records one successful build of a module publishing one image. func built(t *testing.T, inv *Inventory, id, module string, n int) string { t.Helper() reference := ref(module, "app", n) b := aBuild(id, module, "") b.Made = []Artifact{{Name: "app", Kind: "image", Reference: reference}} if err := inv.RecordBuild(context.Background(), b); err != nil { t.Fatal(err) } return reference } func TestTheStoreKeepsTheRecentBuildsAndLetsGoOfTheRest(t *testing.T) { inv := fresh(t) ctx := context.Background() holding(t, inv, "web") // Eight builds of one module, oldest first. Five are kept — the newest, and the four a // release that turns out wrong can be taken back to. var made []string for i := 1; i <= 8; i++ { made = append(made, built(t, inv, fmt.Sprintf("b%02d", i), "web", i)) } go_, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } want := made[:3] // the three oldest if len(go_) != len(want) { t.Fatalf("offered %v to collect; want the %d oldest of %d", go_, len(want), len(made)) } for i := range want { if go_[i] != want[i] { t.Fatalf("offered %v; want %v — and in that order, so a failed sweep is safe to run again", go_, want) } } } func TestADefinitionNamingAnArtifactKeepsItHoweverOldItIs(t *testing.T) { // The floor: no age limit. A module recorded at an older commit still names what the mesh // would hand a machine now, and that is what must not be collected out from under it. inv := fresh(t) ctx := context.Background() var made []string for i := 1; i <= 8; i++ { made = append(made, built(t, inv, fmt.Sprintf("b%02d", i), "web", i)) } oldest := made[0] // A definition the mesh holds, whose container runs that oldest image. m := catalogue.Manifest{Module: "web", Version: "1", Resources: []map[string]any{{ "id": "app", "type": "container", "name": "web", "image": oldest, }}} if err := inv.RegisterModule(ctx, m, Source{Repository: "https://forge.invalid/web.git"}); err != nil { t.Fatal(err) } go_, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } for _, reference := range go_ { if reference == oldest { t.Fatalf("the mesh offered to collect %s, which a definition it holds names", oldest) } } if len(go_) != 2 { t.Fatalf("offered %v; want the two oldest that nothing names", go_) } } func TestWhatHasBeenCollectedIsNotOfferedAgain(t *testing.T) { // Without this the sweep reissues a delete for every artifact it has ever collected, every // time it runs, for ever — a number of requests that grows with the mesh's whole history. inv := fresh(t) ctx := context.Background() holding(t, inv, "web") for i := 1; i <= 7; i++ { built(t, inv, fmt.Sprintf("b%02d", i), "web", i) } first, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } if len(first) != 2 { t.Fatalf("offered %v, want two", first) } if err := inv.MarkCollected(ctx, first); err != nil { t.Fatal(err) } again, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } if len(again) != 0 { t.Fatalf("offered %v again after collecting it", again) } } func TestAFailedBuildNamesNothingToCollectAndEachModuleIsCountedOnItsOwn(t *testing.T) { inv := fresh(t) ctx := context.Background() holding(t, inv, "web", "db") // A failed build published nothing, so it is neither kept nor collected — and it must not // count against the module's five. for i := 1; i <= 6; i++ { built(t, inv, fmt.Sprintf("w%02d", i), "web", i) } if err := inv.RecordBuild(ctx, aBuild("w99", "web", "the recipe would not build")); err != nil { t.Fatal(err) } // And a second module with three builds keeps all three: five each, not five between them. for i := 1; i <= 3; i++ { built(t, inv, fmt.Sprintf("d%02d", i), "db", 100+i) } go_, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } if len(go_) != 1 || go_[0] != ref("web", "app", 1) { t.Fatalf("offered %v; want only web's oldest — db's three are all within its five", go_) } } // An artifact recorded with the store's old address is offered for collection, in the vocabulary // the rest of the mesh speaks (novox/hq issue 226). // // Before references were kept without an address the mesh recorded // `:/@sha256:…` (04-ISSUES/102). Those are the oldest artifacts, which makes // them exactly the ones an oldest-first sweep reaches first — and the first live run met one, // read "I will not address this" as "the store refuses everything", and collected none of 1681. func TestAnArtifactRecordedWithAnAddressIsOfferedAsTheMeshRecordsOne(t *testing.T) { inv := fresh(t) ctx := context.Background() holding(t, inv, "tools") // The oldest build published the old way; five newer ones fill the module's five. old := aBuild("a00", "tools", "") old.Made = []Artifact{{Name: "build", Kind: "image", Reference: "127.0.0.1:5100/tools/build@sha256:" + fmt.Sprintf("%064x", 1)}} if err := inv.RecordBuild(ctx, old); err != nil { t.Fatal(err) } for i := 2; i <= 6; i++ { built(t, inv, fmt.Sprintf("a%02d", i), "tools", i) } go_, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } want := ref("tools", "build", 1) if len(go_) != 1 || go_[0] != want { t.Fatalf("offered %v; want %q — the address is a route to the artifact, not part of its "+ "name, and the sweep speaks the name", go_, want) } // And marking it collected uses that same name, so the next sweep does not offer it again // under a spelling it has not seen. if err := inv.MarkCollected(ctx, go_); err != nil { t.Fatal(err) } again, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } if len(again) != 0 { t.Fatalf("offered %v again after collecting it", again) } } // A forgotten module keeps nothing beyond what a held definition names (novox/hq issue 253). // // "Somewhere to go back to" is a reason about a module's releases, and a module the mesh no // longer holds has none. Its build rows stay as history; its artifacts go — except one a module // the mesh still holds names, which is the floor whatever built it. func TestAForgottenModuleKeepsNothingAHeldDefinitionDoesNotName(t *testing.T) { inv := fresh(t) ctx := context.Background() // Three builds of a module that was never held, or was held and then forgotten: within its // five, and kept for that reason until now. var gone []string for i := 1; i <= 3; i++ { gone = append(gone, built(t, inv, fmt.Sprintf("o%02d", i), "old", 200+i)) } // A module the mesh holds, whose definition runs the forgotten module's newest image. named := gone[2] m := catalogue.Manifest{Module: "web", Version: "1", Resources: []map[string]any{{ "id": "app", "type": "container", "name": "web", "image": named, }}} if err := inv.RegisterModule(ctx, m, Source{Repository: "https://forge.invalid/web.git"}); err != nil { t.Fatal(err) } go_, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } if len(go_) != 2 || go_[0] != gone[0] || go_[1] != gone[1] { t.Fatalf("offered %v; want %v — a forgotten module's builds are no release to go back to, "+ "and only what a held definition names stays", go_, gone[:2]) } // And once the module is held again, its five are kept again. holding(t, inv, "old") again, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } if len(again) != 0 { t.Fatalf("offered %v for a module the mesh holds, within its five", again) } } // The archives the mesh keeps are what the sweep holds before it lets anything go, and what an // operator reads as all held before the store's collector is let loose (novox/hq issue 253). func TestKeptArchivesAreTheKeptBlobsOnly(t *testing.T) { inv := fresh(t) ctx := context.Background() holding(t, inv, "shell") archive := func(n int) string { return fmt.Sprintf("%sshell/config/blobs/sha256:%064x", catalogue.ArtifactStoreScheme, n) } for i := 1; i <= 6; i++ { b := aBuild(fmt.Sprintf("s%02d", i), "shell", "") b.Made = []Artifact{ {Name: "app", Kind: "image", Reference: ref("shell", "app", i)}, {Name: "config", Kind: "archive", Reference: archive(i)}, } if err := inv.RecordBuild(ctx, b); err != nil { t.Fatal(err) } } kept, err := inv.KeptArchives(ctx) if err != nil { t.Fatal(err) } want := []string{archive(2), archive(3), archive(4), archive(5), archive(6)} if len(kept) != len(want) { t.Fatalf("kept archives %v; want the five recent ones and no images", kept) } for i := range want { if kept[i] != want[i] { t.Fatalf("kept archives %v; want %v", kept, want) } } } // 1933/AC1: a new module's first build, recorded and not yet registered (its take-in still runs), is not offered to // let go of while it was recorded within the hour; turned off, the window shows what it guards. func TestANewModulesBuildIsKeptWhileItsTakeInRuns(t *testing.T) { inv := fresh(t) ctx := context.Background() made := built(t, inv, "key-new-1", "brand-new", 1) inv.keptLately = 0 // KeptLately, as the controller runs eligible, err := inv.ToCollect(ctx) if err != nil { t.Fatal(err) } for _, r := range eligible { if r == made { t.Fatalf("a build recorded a moment ago, its module not yet registered, is offered: %v", eligible) } } keep, err := inv.KeepSet(ctx) if err != nil { t.Fatal(err) } if !has(keep[made], KeptByRecentRecord) { t.Errorf("the keep set says %v of a build recorded a moment ago", keep[made]) } inv.keptLately = -1 if eligible, err = inv.ToCollect(ctx); err != nil || len(eligible) != 1 || eligible[0] != made { t.Errorf("without the window the unregistered module's build is offered (the fault of issue 1933): %v %v", eligible, err) } } // 1933/AC3: a reference the records say was collected, and a build then records again, is no longer collected: it is // held by the keep set and offered again once nothing keeps it. func TestABuildRecordingACollectedReferenceHoldsItAgain(t *testing.T) { inv := fresh(t) ctx := context.Background() holding(t, inv, "web") reference := built(t, inv, "b01", "web", 1) if err := inv.MarkCollected(ctx, []string{reference}); err != nil { t.Fatal(err) } again := aBuild("b02", "web", "") again.Made = []Artifact{{Name: "app", Kind: "image", Reference: reference}} if err := inv.RecordBuild(ctx, again); err != nil { t.Fatal(err) } collected, err := inv.alreadyCollected(ctx) if err != nil { t.Fatal(err) } if collected[reference] { t.Errorf("a reference a build recorded again is still collected") } }