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.
95 lines
3.7 KiB
Go
95 lines
3.7 KiB
Go
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)
|
|
}
|
|
}
|