Issue 225. The mesh seals one credential per consumer beside the provider's contributions file, and wrote it root-owned. That was right while a module's own code ran in a container as root; ADR 0198 moved that code under the node's runtime, as the node's account, and the secret stayed root's. On the control machine two consumers went unprovisioned for three hours and the only sign was a line reading 'secret not readable yet', 4330 times. The same sentence is already written for a module's own secrets a few hundred lines above — 'a root-owned 0600 file is one that process cannot read'. This is that rule reaching the other kind of secret the mesh writes for a module. Issue 226. The sweep met a reference recorded with the store's old address, read 'I will not address this' as 'the store refuses everything', and collected none of the 1681 it had found. Two changes: references from build records are read through Recorded, where the provenance is known — not in LetGo, which cannot tell one registry host from another and must stay strict — and a reference the sweep will not address is now ErrNotOurs, skipped, never a reason to stop. Only the store refusing ends a sweep. make check: the two failures both fail on main as well — the resolver test (hq 202/203) and the service-manager test, which reads this machine's own shell environment.
118 lines
4.6 KiB
Go
118 lines
4.6 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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|