package artifacts import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // Asking the store to let go of what the mesh no longer keeps (novox/hq ADR 0189, issue 108). // // A fake store records what it was asked to delete, so what is asserted is the mesh's decision // and the shape of the request — not the registry's behaviour, which is the registry's to test. func fakeStore(t *testing.T, answer int) (Store, *[]string) { t.Helper() var asked []string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { t.Errorf("the store was asked %s %s; collecting is a delete", r.Method, r.URL.Path) } asked = append(asked, r.URL.Path) w.WriteHeader(answer) })) t.Cleanup(server.Close) return Store{Address: strings.TrimPrefix(server.URL, "http://")}, &asked } func TestAnImageAndAnArchiveAreAskedForAtTheirOwnEndpoints(t *testing.T) { // The two shapes the mesh records: a manifest by digest, and a blob by digest. They are // different endpoints, and asking at the wrong one answers 404 — which this would then // record as collected, leaving the bytes on disk for ever while the record says otherwise. store, asked := fakeStore(t, http.StatusAccepted) ctx := context.Background() image := catalogue.ArtifactStoreScheme + "web/app@sha256:abc123" archive := catalogue.ArtifactStoreScheme + "web/config/blobs/sha256:def456" if err := store.LetGo(ctx, image); err != nil { t.Fatal(err) } if err := store.LetGo(ctx, archive); err != nil { t.Fatal(err) } want := []string{"/v2/web/app/manifests/sha256:abc123", "/v2/web/config/blobs/sha256:def456"} if len(*asked) != 2 || (*asked)[0] != want[0] || (*asked)[1] != want[1] { t.Fatalf("the store was asked %v; want %v", *asked, want) } } func TestAStoreThatDoesNotHaveItAnswersGone(t *testing.T) { // The outcome wanted, already true. Told apart from success only so the sweep can say which // happened; both are recorded, because retrying for ever is the thing to avoid. store, _ := fakeStore(t, http.StatusNotFound) err := store.LetGo(context.Background(), catalogue.ArtifactStoreScheme+"web/app@sha256:abc123") if !errors.Is(err, Gone) { t.Fatalf("a store that does not hold it answered %v, want Gone", err) } } func TestAStoreWithDeletionOffSaysSoAndNamesTheRemedy(t *testing.T) { // The registry answers 405 when it was started without deletion enabled. The remedy is a // setting on the store's module, and saying "405" would send somebody to the wrong place. store, _ := fakeStore(t, http.StatusMethodNotAllowed) err := store.LetGo(context.Background(), catalogue.ArtifactStoreScheme+"web/app@sha256:abc123") if err == nil { t.Fatal("a store that refuses deletion was read as success") } if !strings.Contains(err.Error(), "REGISTRY_STORAGE_DELETE_ENABLED") { t.Fatalf("the refusal does not name the remedy: %v", err) } } func TestAReferenceThatIsNotTheMeshsOwnIsNeverAsked(t *testing.T) { // The whole safety of the sweep is that it names only what the mesh recorded putting there. // A reference of another shape — a vendor's image, a package version — is refused rather // than composed into a delete somewhere that is not the mesh's store. store, asked := fakeStore(t, http.StatusAccepted) for _, reference := range []string{ "docker.io/library/registry@sha256:abc123", "registry@sha256:abc123", "1.4.2", } { if err := store.LetGo(context.Background(), reference); err == nil { t.Errorf("%s was asked about; it is not a reference into the mesh's store", reference) } } if len(*asked) != 0 { t.Fatalf("the store was asked about %v", *asked) } } // A reference this sweep will not address says so as ErrNotOurs, which is a fact about the // record and not about the store (novox/hq issue 226). // // The sweep skips one and abandons itself for the other, so they cannot be the same error. The // first live run met a reference recorded with the store's old address, read the refusal as "the // store refuses everything", and collected none of the 1681 it had found. func TestAReferenceThisSweepWillNotAddressIsToldApartFromAStoreRefusing(t *testing.T) { store, asked := fakeStore(t, http.StatusAccepted) for _, reference := range []string{ "docker.io/library/registry@sha256:abc123", "127.0.0.1:5100/mesh-tools/build@sha256:abc123", "1.4.2", } { err := store.LetGo(context.Background(), reference) if !errors.Is(err, ErrNotOurs) { t.Errorf("%s answered %v; a sweep must be able to skip it and go on", reference, err) } } if len(*asked) != 0 { t.Fatalf("the store was asked about %v", *asked) } }