Files
mesh-catalog/modules/distribution/internal/holder/holder_test.go
T
jschoubben 75986b2960 CAT-D3: distribution's mirror copies an outside image by its pinned digest into upstream/<host>/<path>
Until now mirror only answered a copy the store already held, so mesh-build-image had nothing to call for an upstream
base (design 51: an upstream image is copied by mirror with its pinned digest). The copy takes an anonymous pull
token, checks every manifest's and blob's bytes against its digest, copies an index's every platform first, and
fetches only the blobs the store lacks. tags was already served.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:48:29 +01:00

492 lines
21 KiB
Go

package holder
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
)
type memObjects map[string][]byte
func (m memObjects) Put(n string, d []byte) error { m[n] = append([]byte(nil), d...); return nil }
func (m memObjects) Get(n string) ([]byte, error) {
if d, ok := m[n]; ok {
return d, nil
}
return nil, errors.New("no object " + n)
}
func (m memObjects) Delete(n string) error { delete(m, n); return nil }
func (m memObjects) Used() (uint64, error) {
var u uint64
for _, d := range m {
u += uint64(len(d))
}
return u, nil
}
type memTransfers map[string]Transfer
func (m memTransfers) Get(id string) (Transfer, bool, error) { t, ok := m[id]; return t, ok, nil }
func (m memTransfers) Put(t Transfer) error { m[t.Transfer] = t; return nil }
func (m memTransfers) Delete(id string) error { delete(m, id); return nil }
func (m memTransfers) List() ([]Transfer, error) {
var out []Transfer
for _, t := range m {
out = append(out, t)
}
return out, nil
}
type fakeBuilder struct {
ok bool
err error
}
func (b fakeBuilder) Recorded(string, string, string) (bool, string, error) {
return b.ok, "not numbered", b.err
}
type fakeStore struct {
has map[string]bool
put []map[string][]byte
}
func (s *fakeStore) Has(_, d string) (bool, error) { return s.has[d], nil }
func (s *fakeStore) Put(t Transfer, parts map[string][]byte) (string, error) {
s.put = append(s.put, parts)
return "registry.mesh/" + t.Name + "@" + t.Digest, nil
}
func (s *fakeStore) Held(string, string, string) (string, bool, error) { return "", false, nil }
type said struct {
seat, event string
body Event
}
type bed struct {
h *Holder
v *Verbs
store *fakeStore
said []said
findings map[string]bool
now time.Time
cat *fakeCatalog
}
type fakeCatalog struct {
tags map[string]string // name:version -> digest
removed []string
}
func (c *fakeCatalog) Versions(name string) ([]string, error) {
var out []string
for k := range c.tags {
if n, v, _ := strings.Cut(k, ":"); n == name {
out = append(out, v)
}
}
return out, nil
}
func (c *fakeCatalog) Digest(name, version string) (string, error) {
return c.tags[name+":"+version], nil
}
func (c *fakeCatalog) Remove(name, version string) error {
c.removed = append(c.removed, name+":"+version)
delete(c.tags, name+":"+version)
return nil
}
func (c *fakeCatalog) RemoveDigest(name, digest string) error {
c.removed = append(c.removed, name+"@"+digest)
return nil
}
func (c *fakeCatalog) Mirror(source, digest string) (string, error) {
return "registry.mesh/upstream/" + source + "@" + digest, nil
}
type memHolds map[string]Hold
func (m memHolds) Get(k string) (Hold, bool, error) { h, ok := m[k]; return h, ok, nil }
func (m memHolds) Put(h Hold) error { m[h.Key()] = h; return nil }
func (m memHolds) Delete(k string) error { delete(m, k); return nil }
func (m memHolds) List() ([]Hold, error) {
var out []Hold
for _, h := range m {
out = append(out, h)
}
return out, nil
}
func newBed(builderOK bool) *bed {
b := &bed{store: &fakeStore{has: map[string]bool{}}, findings: map[string]bool{}, now: time.Unix(1000, 0), cat: &fakeCatalog{tags: map[string]string{}}}
b.h = &Holder{Callers: DefaultCallers(), Objects: memObjects{}, Transfers: memTransfers{}, Builder: fakeBuilder{ok: builderOK},
Store: b.store, Now: func() time.Time { return b.now },
Say: func(seat, event string, body Event) error {
b.said = append(b.said, said{seat, event, body})
return nil
},
Finding: func(kind, subject, _ string, raised bool) { b.findings[kind+" "+subject] = raised }}
b.v = &Verbs{Holder: b.h, Catalog: b.cat, Holds: memHolds{}}
return b
}
func keys[V any](m map[string]V) []string {
var out []string
for k := range m {
out = append(out, k)
}
return out
}
func digestOf(b []byte) string { s := sha256.Sum256(b); return "sha256:" + hex.EncodeToString(s[:]) }
const builder = "ace.mesh-build-go"
// TestTheHolderOfTheTwoImageSeats holds design 51's holder cases at the unit level (CAT-D1, D2, D3, D3b, D4, D4b, D5):
// one table or case per rule, the caller list and the store front's.
func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
blob := []byte(strings.Repeat("a", 3000))
part := Part{Digest: digestOf(blob), Size: int64(len(blob))}
req := func(id string) PublishRequest {
return PublishRequest{Transfer: id, Name: "m/program", Version: "0.1.4", Kind: KindBundle, Digest: part.Digest, Parts: []Part{part}}
}
t.Run("a transfer in parts, cut and resumed, hashed, numbered by the builder, published", func(t *testing.T) {
b := newBed(true)
a, err := b.h.Publish(ArtifactSeat, builder, req("t1"))
if err != nil || !reflect.DeepEqual(a.Missing, []string{part.Digest}) {
t.Fatalf("publish = %+v, %v", a, err)
}
if _, err := b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t1", Digest: part.Digest, Sequence: 0, Data: blob[:1000]}); err != nil {
t.Fatal(err)
}
// Cut: the build module opens again and is told what is still missing; its next chunk goes on from there.
if a, _ := b.h.Publish(ArtifactSeat, builder, req("t1")); !reflect.DeepEqual(a.Missing, []string{part.Digest}) {
t.Errorf("resumed publish = %+v", a)
}
if _, err := b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t1", Digest: part.Digest, Sequence: 0, Data: blob[:1000]}); err == nil {
t.Error("a chunk sent twice was taken: the transfer goes on from the chunks held")
}
a, err = b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t1", Digest: part.Digest, Sequence: 1, Last: true, Data: blob[1000:]})
if err != nil || !a.Ended || len(b.said) != 1 || b.said[0].event != EventPublished || b.said[0].body.Reference == "" {
t.Fatalf("last chunk = %+v, %v; said %+v", a, err, b.said)
}
if len(b.h.Objects.(memObjects)) != 0 || len(b.h.Transfers.(memTransfers)) != 0 {
t.Error("the transfer's chunks or key stayed after its end")
}
})
t.Run("refusals", func(t *testing.T) {
for _, c := range []struct {
name string
do func(b *bed) error
want string
}{
{"a caller not on the list", func(b *bed) error { _, err := b.h.Publish(ArtifactSeat, "ace.mesh-provisioner", req("t")); return err }, "not on the holder's caller list"},
{"an unproven caller", func(b *bed) error { _, err := b.h.Publish(ArtifactSeat, Unproven, req("t")); return err }, "no caller the tool runner proved"},
{"a part from another caller than the opener", func(b *bed) error {
_, _ = b.h.Publish(ArtifactSeat, builder, req("t"))
_, err := b.h.PublishPart(ArtifactSeat, "ace.mesh-build-node", PartRequest{Transfer: "t", Digest: part.Digest, Last: true, Data: blob})
return err
}, "opened by another caller"},
{"a part whose bytes do not match its digest", func(b *bed) error {
_, _ = b.h.Publish(ArtifactSeat, builder, req("t"))
_, err := b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t", Digest: part.Digest, Last: true, Data: []byte("other")})
return err
}, "hash to"},
{"a chunk over 1 MiB", func(b *bed) error {
_, _ = b.h.Publish(ArtifactSeat, builder, req("t"))
_, err := b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t", Digest: part.Digest, Data: make([]byte, MaxPart+1)})
return err
}, "over the"},
{"a new transfer at the cap", func(b *bed) error { b.h.Cap = 100; _, err := b.h.Publish(ArtifactSeat, builder, req("t")); return err }, "at its cap"},
{"a version the builder did not number", func(b *bed) error {
b.h.Builder = fakeBuilder{ok: false}
b.store.has[part.Digest] = true // held already: the publish ends at once
_, err := b.h.Publish(ArtifactSeat, builder, req("t"))
return err
}, "the builder did not number it"},
{"an image on the artifact seat", func(b *bed) error {
r := req("t")
r.Kind = KindImage
_, err := b.h.Publish(ArtifactSeat, builder, r)
return err
}, "holds bundles and archives"},
{"remove of a version with no tag", func(b *bed) error {
return b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Version: "0.1.4", Reason: "branch ended"})
}, "has no tag"},
{"a retention removal from another caller than the sweep", func(b *bed) error {
return b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Digest: part.Digest, Reason: ReasonRetention})
}, "artifact sweep's alone"},
{"a retention removal of a held archive", func(b *bed) error {
_ = b.v.HoldOne(ArtifactSeat, "novox.artifact-store-tools", HoldRequest{Name: "m/a", Kind: KindArchive, Digest: part.Digest})
return b.v.Remove(ArtifactSeat, "novox.artifact-store-tools", RemoveRequest{Name: "m/a", Digest: part.Digest, Reason: ReasonRetention})
}, "is held"},
{"a hold of an image", func(b *bed) error {
return b.v.HoldOne(ImageSeat, "novox.artifact-store-tools", HoldRequest{Name: "m/i", Kind: KindImage, Digest: part.Digest})
}, "never held"},
{"a hold from another caller", func(b *bed) error {
return b.v.HoldOne(ArtifactSeat, "novox.mesh-build-go", HoldRequest{Name: "m/a", Kind: KindArchive, Digest: part.Digest})
}, "not on the holder's caller list"},
{"a release from a caller not on the list", func(b *bed) error {
return b.v.Release(ArtifactSeat, "novox.mesh-build-go", HoldRequest{Name: "m/a", Digest: part.Digest})
}, "not on the holder's caller list"},
{"a release in distribution's own name", func(b *bed) error {
return b.v.Release(ArtifactSeat, "novox.distribution", HoldRequest{Name: "m/a", Digest: part.Digest})
}, "in its own name"},
{"a mirror by no digest", func(b *bed) error {
_, err := b.v.Mirror(ImageSeat, "novox.mesh-build-image", MirrorRequest{Source: "docker.io/library/nats:2"})
return err
}, "pinned digest"},
} {
b := newBed(true)
if err := c.do(b); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: err = %v, want one saying %q", c.name, err, c.want)
}
}
if _, err := ParseCallers(`{"release":["distribution"]}`); err == nil {
t.Error("a caller list putting distribution on release's list is taken")
}
})
t.Run("only the missing layers are sent, and an idle transfer is dropped", func(t *testing.T) {
b := newBed(true)
other := []byte("layer two")
r := req("t2")
r.Kind, r.Parts = KindImage, []Part{part, {Digest: digestOf(other), Size: int64(len(other))}}
b.store.has[part.Digest] = true
a, err := b.h.Publish(ImageSeat, builder, r)
if err != nil || !reflect.DeepEqual(a.Missing, []string{digestOf(other)}) {
t.Fatalf("publish = %+v, %v; want only the layer the store lacks", a, err)
}
b.now = b.now.Add(TransferIdle)
if dropped, _ := b.h.DropIdle(); !reflect.DeepEqual(dropped, []string{"t2"}) {
t.Errorf("DropIdle = %v, want t2 after 24 hours with no part", dropped)
}
})
t.Run("incoming's finding at 80 %, cleared below", func(t *testing.T) {
b := newBed(true)
b.h.Cap = 3500
_, _ = b.h.Publish(ArtifactSeat, builder, req("t3"))
_, _ = b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t3", Digest: part.Digest, Data: blob[:2800]})
if !b.findings["incoming-full incoming"] {
t.Error("2800 of 3500 bytes wrote no finding")
}
_, _ = b.h.Drop("t3")
if b.findings["incoming-full incoming"] {
t.Error("the finding stayed after the transfer was dropped")
}
})
t.Run("the sweep's removal, hold and release", func(t *testing.T) {
b := newBed(true)
sweep := "novox.artifact-store-tools"
if err := b.v.Remove(ArtifactSeat, sweep, RemoveRequest{Name: "m/a", Digest: part.Digest, Kind: KindArchive, Reason: ReasonRetention}); err != nil {
t.Fatal(err)
}
if err := b.v.HoldOne(ArtifactSeat, sweep, HoldRequest{Name: "m/b", Kind: KindArchive, Digest: part.Digest}); err != nil {
t.Fatal(err)
}
if err := b.v.Release(ArtifactSeat, "person.jochen", HoldRequest{Name: "m/b", Digest: part.Digest, Why: "by hand"}); err != nil {
t.Errorf("the operator's session releasing through registry_holds: %v", err)
}
var events []string
for _, s := range b.said {
events = append(events, s.event+" "+s.body.By)
}
if want := []string{"removed " + sweep, "held " + sweep, "released person.jochen"}; !reflect.DeepEqual(events, want) {
t.Errorf("said %v, want %v", events, want)
}
b.cat.tags["m/p:0.1.5-feat-x.1"] = part.Digest
if err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.5-feat-x.1", Reason: "branch merged"}); err != nil || b.cat.tags["m/p:0.1.5-feat-x.1"] != "" {
t.Errorf("the builder's removal of a tagged version: %v", err)
}
})
t.Run("the store's port: reads open, writes from distribution alone, and the direct-write window", func(t *testing.T) {
store := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) }))
defer store.Close()
u, _ := url.Parse(store.URL)
var open atomic.Bool
token, kept := "s3cret", true
f := &Front{Store: u, Window: &open, Published: func(name, tag string) (bool, error) { return name == "m/i" && tag == "0.1.3", nil },
Bootstrap: func() (string, bool) { return token, kept }}
auth := ""
do := func(method, path string) int {
rec := httptest.NewRecorder()
req := httptest.NewRequest(method, path, nil)
if auth != "" {
req.Header.Set("Authorization", "Bearer "+auth)
}
f.ServeHTTP(rec, req)
return rec.Code
}
for _, c := range []struct {
open bool
method, path string
want int
}{
{false, http.MethodGet, "/v2/m/i/manifests/0.1.3", http.StatusCreated},
{false, http.MethodHead, "/v2/m/i/blobs/sha256:aa", http.StatusCreated},
{false, http.MethodPut, "/v2/m/i/manifests/0.1.9", http.StatusForbidden},
{false, http.MethodPost, "/v2/m/i/blobs/uploads/", http.StatusForbidden},
{true, http.MethodPost, "/v2/m/i/blobs/uploads/", http.StatusCreated},
{true, http.MethodPut, "/v2/m/i/manifests/0.1.9", http.StatusCreated},
{true, http.MethodPut, "/v2/m/i/manifests/0.1.3", http.StatusForbidden}, // published through the seat
} {
open.Store(c.open)
if got := do(c.method, c.path); got != c.want {
t.Errorf("window open %v: %s %s = %d, want %d", c.open, c.method, c.path, got, c.want)
}
}
// The bootstrap's one-off credential writes with the window closed, never over a published tag, and not once
// its file is gone; a wrong token writes nothing.
open.Store(false)
for _, c := range []struct {
auth string
kept bool
path string
want int
}{
{"s3cret", true, "/v2/gitea/program/manifests/0.1.0", http.StatusCreated},
{"s3cret", true, "/v2/m/i/manifests/0.1.3", http.StatusForbidden},
{"wrong", true, "/v2/gitea/program/manifests/0.1.0", http.StatusForbidden},
{"s3cret", false, "/v2/gitea/program/manifests/0.1.0", http.StatusForbidden},
} {
auth, kept = c.auth, c.kept
if got := do(http.MethodPut, c.path); got != c.want {
t.Errorf("bootstrap %q kept %v: PUT %s = %d, want %d", c.auth, c.kept, c.path, got, c.want)
}
}
})
t.Run("mirror copies an outside image by its pinned digest, every platform, only the parts the store lacks", func(t *testing.T) {
up, store := newFakeRegistry(), newFakeRegistry()
ups, sts := httptest.NewServer(up), httptest.NewServer(store)
defer ups.Close()
defer sts.Close()
up.token, up.realm = "anon", ups.URL+"/token"
host := strings.TrimPrefix(ups.URL, "http://")
repo := "library/nats"
layer, config := []byte(strings.Repeat("l", 5000)), []byte(`{"os":"linux"}`)
up.blobs[repo+"@"+digestOf(layer)], up.blobs[repo+"@"+digestOf(config)] = layer, config
img, _ := json.Marshal(map[string]any{"schemaVersion": 2, "mediaType": mediaManifest,
"config": map[string]any{"mediaType": "application/vnd.oci.image.config.v1+json", "digest": digestOf(config), "size": len(config)},
"layers": []map[string]any{{"mediaType": mediaLayer, "digest": digestOf(layer), "size": len(layer)}}})
up.manifests[repo+":"+digestOf(img)], up.types[repo+":"+digestOf(img)] = img, mediaManifest
idx, _ := json.Marshal(map[string]any{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": []map[string]any{{"mediaType": mediaManifest, "digest": digestOf(img), "size": len(img)}}})
up.manifests[repo+":"+digestOf(idx)] = idx
base, _ := url.Parse(sts.URL)
o := &OCI{Base: base, Registry: "registry.mesh:5000", Upstream: &Upstream{Scheme: "http"}}
name := "upstream/" + host + "/" + repo
store.blobs[name+"@"+digestOf(config)] = config // held already: not fetched again
ref, err := o.Mirror(host+"/"+repo+":2", digestOf(idx))
if err != nil {
t.Fatal(err)
}
if want := "registry.mesh:5000/" + name + "@" + digestOf(idx); ref != want {
t.Errorf("reference %s, want %s", ref, want)
}
if store.manifests[name+":"+digestOf(idx)] == nil || store.manifests[name+":"+digestOf(img)] == nil || store.blobs[name+"@"+digestOf(layer)] == nil {
t.Errorf("the store holds %v and %v, want the index, its image and the layer", keys(store.manifests), keys(store.blobs))
}
if !reflect.DeepEqual(up.gets, []string{digestOf(layer)}) {
t.Errorf("blobs fetched %v, want only the layer the store lacked", up.gets)
}
up.manifests[repo+":"+digestOf(idx)] = []byte(`{"tampered":true}`)
if _, err := o.Mirror(host+"/"+repo, digestOf(idx)); err != nil {
t.Errorf("a copy held whole is answered at once, without asking the upstream: %v", err)
}
delete(store.manifests, name+":"+digestOf(idx))
if _, err := o.Mirror(host+"/"+repo, digestOf(idx)); err == nil || !strings.Contains(err.Error(), "does not hash") {
t.Errorf("a manifest whose bytes are not its digest: %v", err)
}
for in, want := range map[string]string{"nats:2": "upstream/docker.io/library/nats", "ghcr.io/o/p@sha256:ab": "upstream/ghcr.io/o/p", "localhost:5000/x": "upstream/localhost:5000/x"} {
if s, _ := ParseSource(in); s.Name() != want {
t.Errorf("ParseSource(%q) = %s, want %s", in, s.Name(), want)
}
}
})
t.Run("the windows", func(t *testing.T) {
nodes := []NodeRead{
{Node: "ace", Serves: true, Stated: []string{NewNamesCapability}, NamesArtifact: true, BuildModules: map[string]bool{"mesh-build-go": true}},
{Node: "g14", NamesArtifact: true, BuildModules: map[string]bool{"mesh-build-go": false}}, // away, old node-engine
{Node: "novox", Serves: true, Stated: nil, NamesArtifact: false},
}
if got := HoldingOldName(nodes, nil); !reflect.DeepEqual(got, []string{"g14"}) {
t.Errorf("old name held by %v, want g14 (no manifest-reads: it states nothing)", got)
}
if got := HoldingDirectWrites(nodes, nil); !reflect.DeepEqual(got, []string{"g14"}) {
t.Errorf("direct writes held by %v, want g14", got)
}
t0 := time.Unix(0, 0)
w, closed, _ := Window{Name: OldNameWindow}.Step([]string{"g14"}, t0)
if closed != nil || !w.Open {
t.Fatalf("Step = %+v, %+v", w, closed)
}
if _, _, overdue := w.Step([]string{"g14"}, t0.Add(WindowFindingAfter)); !reflect.DeepEqual(overdue, []string{"g14"}) {
t.Errorf("seven days: overdue %v", overdue)
}
w, _ = w.Drop("g14", "person.jochen", "retired", t0)
w, closed, _ = w.Step(HoldingOldName(nodes, w.Dropped), t0.Add(time.Hour))
if closed == nil || closed.Window != OldNameWindow || !reflect.DeepEqual(closed.LastHolding, []string{"g14"}) || len(closed.Dropped) != 1 {
t.Errorf("closing = %+v", closed)
}
if _, again, _ := w.Step([]string{"g14"}, t0.Add(2*time.Hour)); again != nil {
t.Error("a closed window closed again")
}
})
t.Run("the manifest", func(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Provides []struct{ Name string } `json:"provides"`
Claims []struct {
Name string `json:"name"`
Serves []string `json:"serves"`
} `json:"claims"`
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
// E15: artifact-store is still offered until the old name closes, so a revert needs no edge.
if len(m.Provides) != 1 || m.Provides[0].Name != "artifact-store" {
t.Errorf("provides = %+v, want artifact-store kept", m.Provides)
}
claims := map[string][]string{}
for _, c := range m.Claims {
claims[c.Name] = c.Serves
}
if !reflect.DeepEqual(claims[ImageSeat], VerbsOf(ImageSeat)) || !reflect.DeepEqual(claims[ArtifactSeat], VerbsOf(ArtifactSeat)) {
t.Errorf("claims = %v, want each seat serving its verbs", claims)
}
if _, ok := claims[OldSeat]; !ok {
t.Error("the old seat mesh-artifact-store is no longer claimed while its window is open")
}
// E14: the registry container, on the loopback, is listed before the program, so the port is free when it starts.
store, program := -1, -1
for i, r := range m.Resources {
switch r["id"] {
case "store":
store = i
if ports, _ := r["ports"].([]any); len(ports) != 1 || ports[0] != "127.0.0.1:5001:5000" {
t.Errorf("the store's ports = %v, want the loopback only", r["ports"])
}
case "program":
program = i
}
}
if store < 0 || program < 0 || store > program {
t.Errorf("store at %d, program at %d: want the store listed first", store, program)
}
})
}