The mesh names what may go from its own build records — a digest it did not record making is never named, which is what keeps the sweep away from the images genesis pushed. An artifact stays because a definition the mesh holds names it, or because it belongs to one of the five most recent successful builds of its module. internal/artifacts asks the store to let go of one; internal/inventory decides and remembers (migration 0055); the sweep runs after a build the mesh recorded, which is when both the bytes and the keep set moved. Never fatal to a build. And the manifest side of while-stopped, refused from the definition alone: no schedule, run-once, a container the module does not declare, itself.
148 lines
4.6 KiB
Go
148 lines
4.6 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/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)
|
|
}
|
|
|
|
// 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()
|
|
|
|
// 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()
|
|
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()
|
|
|
|
// 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_)
|
|
}
|
|
}
|