diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/door.go b/modules/artifact-store-tools/cmd/artifact-store-tools/door.go index bd730cfe..1f010869 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/door.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/door.go @@ -66,6 +66,19 @@ func (runtimeBus) Get(state, key string) ([]byte, bool, error) { func (runtimeBus) History(state, key string) ([]stdio.StateRevision, error) { return stdio.State(state).History(key) } +func (runtimeBus) LogSince(read string, at time.Time) ([]stdio.LogEntry, error) { + var out []stdio.LogEntry + page, err := stdio.Log(read).Since(at, "", 0) + for err == nil { + out = append(out, page.Entries...) + if page.Next == 0 { + return out, nil + } + page, err = stdio.Log(read).Read(page.Next, "", 0) + } + return nil, err +} + func (runtimeBus) Put(state, key string, value any) error { _, err := stdio.State(state).Put(key, value) return err diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go b/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go index bfdb1ecf..cc839dea 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go @@ -38,7 +38,9 @@ const ( WantsRead = delivery.AssignerModule + ".wants" BuildsRead = delivery.BuilderModule + ".builds" HavesRead = "node-engine.have" - LeaseRead = buscompose.ControllerLease + // ManifestsRead is the indexer's manifests: the images a manifest pins by itself. + ManifestsRead = delivery.IndexerModule + ".manifests" + LeaseRead = buscompose.ControllerLease // FindingsState is the holder's own findings (health.FindingsState): image-missing. FindingsState = health.FindingsState // Module is this module, the observer its findings name. @@ -62,6 +64,8 @@ type Bus interface { Keys(state string) ([]string, error) Get(state, key string) ([]byte, bool, error) History(state, key string) ([]stdio.StateRevision, error) + // LogSince is every entry of a read log from a time on (stdio.Log(read).Since). + LogSince(read string, at time.Time) ([]stdio.LogEntry, error) Put(state, key string, value any) error Delete(state, key string) error Publish(subject string, body any) error @@ -98,6 +102,8 @@ type Build struct { Module, FP, Key string At time.Time Artifacts []delivery.BuiltArtifact + // Manifest is the digest of the manifest it built, in the indexer's `manifests`. + Manifest string } // LeaseAway says the controller's lease names a holder that is no controller: then the seat sweeps. @@ -134,7 +140,7 @@ func (h *Holder) Builds() ([]Build, error) { if err != nil { return nil, err } - out = append(out, Build{Module: module, FP: fp, Key: k, At: at, Artifacts: b.Artifacts}) + out = append(out, Build{Module: module, FP: fp, Key: k, At: at, Artifacts: b.Artifacts, Manifest: b.Manifest}) } return out, nil } @@ -216,6 +222,44 @@ type Ref struct { // KeepSet judges every artifact the done builds name in the store: kept, and why, or eligible. Packages and // references into no store of the mesh's are no business of the store's. func KeepSet(builds []Build, wants []Want) map[string]*Ref { + keeps := keptBuilds(builds, wants) + out := map[string]*Ref{} + for _, b := range builds { + for _, a := range b.Artifacts { + reference := catalogue.Recorded(a.Reference) + if _, ours := catalogue.InArtifactStore(reference); !ours || a.Kind == "package" { + continue + } + r := out[reference] + if r == nil { + _, _, kind := splitReference(reference) + r = &Ref{Reference: reference, Kind: kind} + out[reference] = r + } + r.Builds = append(r.Builds, b) + r.keptBy(b, keeps[b.Key]) + } + } + return out +} + +// keptBy adds a build naming r, kept for why (none: eligible). +func (r *Ref) keptBy(b Build, why []string) { + if b.At.After(r.At) { + r.At = b.At + } + if len(why) > 0 { + r.Kept = true + for _, w := range why { + if !contains(r.Why, w) { + r.Why = append(r.Why, w) + } + } + } +} + +// keptBuilds is, by build key, why each kept build is kept: one of its module's five newest, or a want names it. +func keptBuilds(builds []Build, wants []Want) map[string][]string { wanted := map[string][]string{} for _, w := range wants { wanted[w.Module+" "+w.FP] = append(wanted[w.Module+" "+w.FP], w.Node) @@ -242,34 +286,7 @@ func KeepSet(builds []Build, wants []Want) map[string]*Ref { } } } - out := map[string]*Ref{} - for _, b := range builds { - for _, a := range b.Artifacts { - reference := catalogue.Recorded(a.Reference) - if _, ours := catalogue.InArtifactStore(reference); !ours || a.Kind == "package" { - continue - } - r := out[reference] - if r == nil { - _, _, kind := splitReference(reference) - r = &Ref{Reference: reference, Kind: kind} - out[reference] = r - } - r.Builds = append(r.Builds, b) - if b.At.After(r.At) { - r.At = b.At - } - if why := keeps[b.Key]; len(why) > 0 { - r.Kept = true - for _, w := range why { - if !contains(r.Why, w) { - r.Why = append(r.Why, w) - } - } - } - } - } - return out + return keeps } func contains(s []string, v string) bool { @@ -304,6 +321,10 @@ type judged struct { view *View wants []Want built []Build + // mirrors are the copies in the store's mirror repositories, by reference: recorded ones also in refs. + mirrors map[string]*Ref + // mirrorsHeld says why no copy is eligible now, when one is not: a kept build whose bases the log does not name. + mirrorsHeld string } func (h *Holder) judge(ctx context.Context) (*judged, error) { @@ -322,9 +343,89 @@ func (h *Holder) judge(ctx context.Context) (*judged, error) { j.view = nil } } + if j.view != nil { + against, err := h.against() + if err != nil { + h.logf("the builder's log could not be read, so every copy is kept as unrecorded: %v", err) + against = nil + } + j.mirrors, j.mirrorsHeld = Mirrors(j.view, builds, keptBuilds(builds, wants), against) + for r, m := range j.mirrors { + if len(m.Builds) > 0 { + j.refs[r] = m + } + } + } return j, nil } +// against is what each build the builder's log says ended stood on, by build key: the bases' references. +func (h *Holder) against() (map[string][]string, error) { + entries, err := h.Bus.LogSince(delivery.BuildLogRead, h.now().Add(-delivery.BuilderLogDays*24*time.Hour)) + if err != nil { + return nil, err + } + out := map[string][]string{} + for _, raw := range entries { + e, err := delivery.DecodeBuildLogEntry(raw.Value) + if err != nil || e.Event != delivery.BuildEndedEvent { + continue + } + key, err := delivery.BuilderBuildKey(e.Module, e.FP) + if err != nil { + continue + } + out[key] = e.Against + } + return out, nil +} + +// Mirrors judges each copy in the store's mirror repositories (upstream//, /on-; ADR +// 0257) by what the builder's log says each build stood on (ADR 0571 clause 1): named by a kept build, kept; named +// only by builds no longer kept, eligible; named by no build the log holds, unrecorded and never removed (ADR 0189 +// §10). While a kept build's bases are not in the log (it ended before the log's 30 days), no copy is eligible: what +// it stood on cannot be told, and held says so. +func Mirrors(v *View, builds []Build, kept map[string][]string, against map[string][]string) (map[string]*Ref, string) { + out := map[string]*Ref{} + for _, name := range v.L.RepoNames() { + if !mirrorRepository.MatchString(name) { + continue + } + for d := range v.L.Repos[name].Revisions { + r := catalogue.ArtifactStoreScheme + name + "@" + d + out[r] = &Ref{Reference: r, Kind: "image"} + } + } + held := "" + for _, b := range builds { + bases, logged := against[b.Key] + if !logged { + if len(kept[b.Key]) > 0 && held == "" { + held = "the builder's log does not say what the kept build " + b.Key + " stood on, so every copy is kept" + } + continue + } + for _, base := range bases { + if m := out[catalogue.Recorded(base)]; m != nil { + m.Builds = append(m.Builds, b) + why := kept[b.Key] + if len(why) > 0 { + why = []string{"a kept build stood on it: " + b.Key} + } + m.keptBy(b, why) + } + } + } + if held != "" { + for _, m := range out { + if len(m.Builds) > 0 && !m.Kept { + m.Kept, m.Why = true, []string{held} + } + } + } + return out, held +} + // Artifacts is the seat's `artifacts`, in the controller's answer's shape (Records): every reference the mesh made that // lives in the store, kept with why or eligible, and with collected the ones the store no longer holds. func (h *Holder) Artifacts(repository string) (*Records, error) { @@ -651,11 +752,17 @@ func (h *Holder) ImagesOf(node string, address string) (Images, error) { if err != nil { return nil } + images := []string{} for _, a := range b.Artifacts { - if a.Kind != "image" && a.Kind != "upstream" || a.Reference == "" { - continue + if (a.Kind == "image" || a.Kind == "upstream") && a.Reference != "" { + images = append(images, catalogue.Routed(catalogue.Recorded(a.Reference), address)) } - image := catalogue.Routed(catalogue.Recorded(a.Reference), address) + } + pinned, err := h.Pinned(b.Manifest) + if err != nil { + return err + } + for _, image := range append(images, pinned...) { i := byImage[image] if i == nil { i = &Image{Image: image} @@ -708,6 +815,54 @@ func (h *Holder) ImagesOf(node string, address string) (Images, error) { return out, nil } +// Pinned is every container image a manifest names by itself, by digest (`/@sha256:…`), read from the +// indexer's manifests: what a definition pins outright rather than through a build's artifact. A placeholder is not one. +func (h *Holder) Pinned(manifest string) ([]string, error) { + if manifest == "" { + return nil, nil + } + key, err := delivery.ManifestsKey(manifest) + if err != nil { + return nil, nil + } + raw, found, err := h.Bus.Get(ManifestsRead, key) + if err != nil || !found { + return nil, err + } + m, err := delivery.DecodeManifest(raw) + if err != nil { + return nil, nil + } + var parsed struct { + Resources []map[string]any `json:"resources"` + } + if json.Unmarshal([]byte(m.JSON), &parsed) != nil { + return nil, nil + } + var out []string + for _, r := range parsed.Resources { + image, _ := r["image"].(string) + if fmt.Sprint(r["type"]) == "container" && strings.Contains(image, "@sha256:") && !strings.Contains(image, "${") && + !contains(out, image) { + out = append(out, image) + } + } + return out, nil +} + +// inStoreAt is the store's reference of an image its address names, "" for an image of another registry (the +// controller's imageInStore). +func inStoreAt(image, address string) string { + if address == "" || !strings.HasPrefix(image, address+"/") || !strings.Contains(image, "@sha256:") { + return "" + } + r := catalogue.Recorded(image) + if _, ours := catalogue.InArtifactStore(r); !ours { + return "" + } + return r +} + // Observe writes S25's findings (image-missing): each want whose build names an image the store does not hold, on the // want's machine, and deletes each finding that no longer holds. Nothing is said without a listing. func (h *Holder) Observe(ctx context.Context) error { @@ -718,6 +873,10 @@ func (h *Holder) Observe(ctx context.Context) error { if j.view == nil { return nil } + address := "" + if d, err := h.Door(); err == nil { + address = d.Address() + } builds := map[string]Build{} for _, b := range j.built { builds[b.Module+" "+b.FP] = b @@ -728,11 +887,21 @@ func (h *Holder) Observe(ctx context.Context) error { if !ok { continue } + var refs []string for _, a := range b.Artifacts { - reference := catalogue.Recorded(a.Reference) - if a.Kind != "image" { - continue + if a.Kind == "image" { + refs = append(refs, catalogue.Recorded(a.Reference)) } + } + // And what its manifest pins by itself under the store's address, as the controller judged S25. + if pinned, err := h.Pinned(b.Manifest); err == nil && address != "" { + for _, image := range pinned { + if r := inStoreAt(image, address); r != "" { + refs = append(refs, r) + } + } + } + for _, reference := range refs { if held, known := inStore(j.view, reference); !known || held { continue } @@ -808,3 +977,44 @@ func (h *Holder) logf(format string, a ...any) { h.Logf(format, a...) } } + +// Mirror is one copy as the seat's `mirrors` answers it. +type Mirror struct { + Reference string `json:"reference"` + State string `json:"state"` + Why []string `json:"why,omitempty"` + StoodOn []string `json:"stood_on_by,omitempty"` +} + +// MirrorsAnswer is the seat's `mirrors`: every copy, and why none is eligible when a kept build's bases are unknown. +type MirrorsAnswer struct { + Copies []Mirror `json:"copies"` + Held string `json:"held,omitempty"` +} + +// MirrorsNow is every copy in the store's mirror repositories as the keep set judges it now. +func (h *Holder) MirrorsNow() (MirrorsAnswer, error) { + j, err := h.judge(context.Background()) + if err != nil { + return MirrorsAnswer{}, err + } + if j.view == nil { + return MirrorsAnswer{}, errors.New("the store's listing could not be read, so no copy can be named") + } + out := MirrorsAnswer{Copies: []Mirror{}, Held: j.mirrorsHeld} + for _, r := range j.mirrors { + m := Mirror{Reference: r.Reference, State: "unrecorded", Why: r.Why} + switch { + case r.Kept: + m.State = "kept" + case len(r.Builds) > 0: + m.State = "eligible" + } + for _, b := range r.Builds { + m.StoodOn = append(m.StoodOn, b.Key) + } + out.Copies = append(out.Copies, m) + } + sort.Slice(out.Copies, func(a, b int) bool { return out.Copies[a].Reference < out.Copies[b].Reference }) + return out, nil +} diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/holder_integration_test.go b/modules/artifact-store-tools/cmd/artifact-store-tools/holder_integration_test.go index c71a6a58..50a3482f 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/holder_integration_test.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/holder_integration_test.go @@ -30,6 +30,19 @@ type realBus struct { ctx context.Context nc *nats.Conn js jetstream.JetStream + // log is the builder's log as the runtime's log verbs would read it (mesh-tools adds them; the sdk's own read of a + // log is tested in mesh-sdk's process package). + log []stdio.LogEntry +} + +func (b realBus) LogSince(_ string, at time.Time) ([]stdio.LogEntry, error) { + var out []stdio.LogEntry + for _, e := range b.log { + if !e.At.Before(at) { + out = append(out, e) + } + } + return out, nil } func bucketOf(state string) string { return strings.Replace(state, ".", "_", 1) } @@ -116,7 +129,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) { if err != nil { t.Fatal(err) } - for _, s := range []string{BuildsRead, WantsRead, HavesRead, LeaseRead, "artifact-store-tools." + FindingsState} { + for _, s := range []string{BuildsRead, WantsRead, HavesRead, LeaseRead, ManifestsRead, "artifact-store-tools." + FindingsState} { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(s), History: 5}); err != nil { t.Fatal(err) } @@ -125,7 +138,8 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) { // The mesh as aMesh lays it, copied onto the real buckets: letta's seven builds, a want, the store's listing. mem, memb, door, _ := aMesh(t, &buscompose.LeaseHolder{Instance: "c1", Host: "novox", Epoch: 3}) - for _, s := range []string{BuildsRead, WantsRead, LeaseRead} { + bus.log = memb.log + for _, s := range []string{BuildsRead, WantsRead, LeaseRead, ManifestsRead} { keys, _ := memb.Keys(s) for _, k := range keys[:min(len(keys), 6)] { // the seventh build ends later, on the bus raw, _, _ := memb.Get(s, k) @@ -207,7 +221,8 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) { } mu.Lock() defer mu.Unlock() - if len(acts) != 2 || !strings.Contains(acts[0], archiveRef("letta", 2)) || !strings.Contains(acts[1], imageRef("letta", 2)) { + if len(acts) != 3 || !strings.Contains(acts[0], archiveRef("letta", 2)) || !strings.Contains(acts[1], imageRef("letta", 2)) || + !strings.Contains(acts[2], mirror(1)) { t.Fatalf("the hand acts recorded are %v; the store was asked %v", acts, door.asked) } if len(gone) != 1 || !strings.Contains(gone[0], `"fingerprint":"`+fp(2)+`"`) { diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/holder_test.go b/modules/artifact-store-tools/cmd/artifact-store-tools/holder_test.go index 94d51300..f1d6b7a1 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/holder_test.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/holder_test.go @@ -24,9 +24,20 @@ type memBus struct { mu sync.Mutex states map[string]map[string][]byte history map[string][]stdio.StateRevision + log []stdio.LogEntry published []string } +func (b *memBus) LogSince(read string, at time.Time) ([]stdio.LogEntry, error) { + var out []stdio.LogEntry + for _, e := range b.log { + if read == delivery.BuildLogRead && !e.At.Before(at) { + out = append(out, e) + } + } + return out, nil +} + func (b *memBus) Keys(state string) ([]string, error) { b.mu.Lock() defer b.mu.Unlock() @@ -107,6 +118,14 @@ func (d *memDoor) SpareKept(context.Context, []string, []string) error { return nil } +// mirror is a copy of an upstream base in the store, the n-th. +func mirror(n int) string { + return "artifact-store://upstream/docker.io/library/alpine@sha256:" + strings.Repeat(fmt.Sprintf("%x", n%16), 64) +} + +// pinnedImage is the image letta's manifest names by itself, at the store's address. +const pinnedImage = "10.0.0.5:5000/letta/pinned@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + func digestOf(ref string) string { _, d, _ := splitReference(ref); return d } func fp(n int) string { return "src2:" + strings.Repeat(fmt.Sprintf("%x", n%16), 64) } @@ -149,6 +168,30 @@ func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memD } } listing.repo("letta/image").Revisions["sha256:"+strings.Repeat("e", 64)] = true // no build names it + listing.repo("letta/pinned").Revisions["sha256:"+strings.Repeat("c", 64)] = true + // Three copies of a base: letta's 2nd stood on the first, its 7th on the second, and no build on the third. + for n := 1; n <= 3; n++ { + repo, digest, _ := splitReference(mirror(n)) + listing.repo(repo).Revisions[digest] = true + door.holds[mirror(n)] = true + } + for n := 1; n <= 7; n++ { + e := delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: "letta", FP: fp(n), Repository: "novox/x", Commit: "c", + Node: "ace", At: day.Add(time.Duration(n) * time.Hour), Outcome: "done"} + switch n { + case 2: + e.Against = []string{mirror(1)} + case 7: + e.Against = []string{mirror(2)} + } + raw, _ := delivery.EncodeBuildLogEntry(e) + b.log = append(b.log, stdio.LogEntry{Sequence: uint64(n), Key: "letta", Value: raw, At: e.At}) + } + manifest, _ := delivery.EncodeManifest(delivery.Manifest{Module: "letta", + JSON: `{"module":"letta","resources":[{"id":"app","type":"container","image":"` + pinnedImage + `"},` + + `{"id":"built","type":"container","image":"${artifact:image}"}]}`}) + mk, _ := delivery.ManifestsKey(strings.Repeat("d", 64)) + _ = b.Put(ManifestsRead, mk, json.RawMessage(manifest)) want, _ := json.Marshal(delivery.Want{V: delivery.Version, FP: fp(1), Self: true}) _ = b.Put(WantsRead, "want.ace.letta", json.RawMessage(want)) if lease != nil { @@ -190,7 +233,14 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t t.Errorf("letta's build %d is %s and %s, not %s", n, states[imageRef("letta", n)], states[archiveRef("letta", n)], want) } } - if len(r.References) != 14 || r.Counts["kept"] != 12 || r.Counts["eligible"] != 2 || r.Store != "10.0.0.5:5000" { + // And the copies: the one letta's 2nd stood on is eligible with it, the 7th's kept; the third no build names. + if states[mirror(1)] != "eligible" || states[mirror(2)] != "kept" || states[mirror(3)] != "" { + t.Errorf("the copies are %s, %s and %q", states[mirror(1)], states[mirror(2)], states[mirror(3)]) + } + if m, err := h.MirrorsNow(); err != nil || len(m.Copies) != 3 || m.Copies[2].State != "unrecorded" || m.Held != "" { + t.Errorf("mirrors answered %+v: %v", m, err) + } + if len(r.References) != 16 || r.Counts["kept"] != 13 || r.Counts["eligible"] != 3 || r.Store != "10.0.0.5:5000" { t.Errorf("the references are %d, counted %v, at %q", len(r.References), r.Counts, r.Store) } @@ -202,16 +252,22 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t letGo []string left int skipped int + unlog int }{ {name: "the lease names a controller", lease: controller}, {name: "no lease key", lease: nil}, - {name: "handed away", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}}, - {name: "an index waits", lease: handed, index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2)}, skipped: 1}, - {name: "bounded", lease: handed, most: 1, letGo: []string{archiveRef("letta", 2)}, left: 1}, + {name: "handed away", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2), mirror(1)}}, + {name: "an index waits", lease: handed, index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), mirror(1)}, skipped: 1}, + {name: "bounded", lease: handed, most: 1, letGo: []string{archiveRef("letta", 2)}, left: 2}, + // A kept build the log no longer holds: what it stood on is unknown, so no copy goes. + {name: "a kept build's bases unknown", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}, unlog: 3}, } { t.Run(c.name, func(t *testing.T) { h, b, door, acts := aMesh(t, c.lease) door.indexes[imageRef("letta", 2)] = c.index + if c.unlog > 0 { + b.log = append(b.log[:c.unlog-1:c.unlog-1], b.log[c.unlog:]...) + } if c.most == 0 { h.AfterBuild("letta.x") if _, err := h.Collect("tidy", true, 0); err == nil { @@ -275,12 +331,21 @@ func TestAWantedImageTheStoreLacksIsAFindingAndImagesNamesWantsAndHaves(t *testi if _, found, _ := b.Get(FindingsState, key); found { t.Fatal("the finding stayed once the store held the image again") } + // An image the manifest pins by itself, under the store's address, is judged as the controller judged it. + delete(view.L.Repos["letta/pinned"].Revisions, "sha256:"+strings.Repeat("c", 64)) + if err := h.Observe(context.Background()); err != nil { + t.Fatal(err) + } + if raw, found, _ := b.Get(FindingsState, key); !found || !strings.Contains(string(raw), "letta/pinned@sha256:") { + t.Fatalf("the pinnedImage image missing is %s", raw) + } have, _ := json.Marshal(delivery.Have{V: delivery.Version, FP: fp(7), Phase: "running"}) _ = b.Put(HavesRead, "ace.letta", json.RawMessage(have)) images, err := h.ImagesOf("ace", "10.0.0.5:5000") - if err != nil || len(images.Images) != 2 || !images.SentKnown || images.Images[0].Image != "10.0.0.5:5000/letta/image@sha256:"+strings.Repeat("1", 64) || - fmt.Sprint(images.Images[0].In) != "[declared]" || fmt.Sprint(images.Images[1].In) != "[sent]" { + if err != nil || len(images.Images) != 3 || !images.SentKnown || images.Images[0].Image != "10.0.0.5:5000/letta/image@sha256:"+strings.Repeat("1", 64) || + fmt.Sprint(images.Images[0].In) != "[declared]" || fmt.Sprint(images.Images[1].In) != "[sent]" || + images.Images[2].Image != pinnedImage || fmt.Sprint(images.Images[2].In) != "[declared sent]" { t.Fatalf("the images of ace are %+v: %v", images, err) } } diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/seat.go b/modules/artifact-store-tools/cmd/artifact-store-tools/seat.go index 6e8fe4cc..4af9f960 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/seat.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/seat.go @@ -5,9 +5,7 @@ package main // ok, answer}. import ( - "context" "fmt" - "sort" "strings" stdio "git.novox.be/novox/mesh-sdk/go" @@ -67,29 +65,19 @@ func SeatTools(h *Holder, s *Store) []stdio.Tool { return verbAnswer{Output: fmt.Sprintf("let go of %d, would let go of %d, %d left\n", len(a.LetGo), len(a.WouldLetGo), a.Left), OK: true, Answer: a}, nil }}, - {Name: seat + "mirrors", Description: "The bases the mesh copied into its store, as the store lists them.", + {Name: seat + "mirrors", Description: "The bases the mesh copied into its store, each kept (a kept build stood on " + + "it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood " + + "on it: never removed).", Run: func(args map[string]any) (any, error) { if text(args, "record") != "" { - return nil, fmt.Errorf("mirrors records nothing on this seat: a copy is kept or let go of by the builds " + - "the builder's log says stood on it (novox/hq ADR 0571 clause 1), and until that log names them every " + - "copy is unrecorded and never removed") + return nil, fmt.Errorf("mirrors records nothing by hand on this seat: a copy is recorded by the builds the " + + "builder's log says stood on it (novox/hq ADR 0571 clause 1)") } - v, err := s.View(context.Background()) + m, err := h.MirrorsNow() if err != nil { return nil, err } - var copies []string - for _, name := range v.L.RepoNames() { - if !mirrorRepository.MatchString(name) { - continue - } - for d := range v.L.Repos[name].Revisions { - copies = append(copies, catalogue.ArtifactStoreScheme+name+"@"+d) - } - } - sort.Strings(copies) - return verbAnswer{Output: fmt.Sprintf("%d copies, each unrecorded and never removed\n", len(copies)), OK: true, - Answer: map[string]any{"unrecorded": nonNil(copies)}}, nil + return verbAnswer{Output: fmt.Sprintf("%d copies\n", len(m.Copies)), OK: true, Answer: m}, nil }}, {Name: seat + "images", Description: "Every container image one machine's wants and haves name.", Run: func(args map[string]any) (any, error) { diff --git a/modules/artifact-store-tools/module.json b/modules/artifact-store-tools/module.json index bf70046b..ac4938d7 100644 --- a/modules/artifact-store-tools/module.json +++ b/modules/artifact-store-tools/module.json @@ -25,6 +25,8 @@ "reads": [ "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", + "mesh-delivery-builder.build-log", + "mesh-delivery-indexer.manifests", "node-engine.have", "mesh-controller.lease" ],