Files
mesh-catalog/modules/distribution/cmd/store-tools/classify.go
T
jochen 434b52614d distribution: say what the store holds and what the records name, so collection is no longer blind (hq ADR 0251)
The store had no working tools: its TypeScript client was never built, and nothing could count what
the store holds that no record names. A Go bundle lists the store's files through its own container,
reads each manifest through its door, and sets that beside the controller's records. Collection is
asked of the controller, which decides and records; the store's tools never delete. The image.pushed
event was declared and never emitted, and nothing consumes it, so it goes.
2026-10-08 11:59:39 +02:00

204 lines
5.6 KiB
Go

package main
import (
"sort"
)
// The states a manifest the store holds can be in, as the records see it (novox/hq to-be 51).
const (
StateKept = "kept"
StateHolderKept = "holder-of-kept-archive"
StateEligible = "eligible"
StateHolderEligible = "holder-of-eligible-archive"
StateCollectedPresent = "let-go-yet-present"
StateNamedDocument = "named-document"
StateUnrecorded = "unrecorded"
collectionRemovesThese = "eligible and holder-of-eligible-archive, through the controller's collect; nothing else"
)
// Entry is one manifest the store holds, and what the records say of it.
type Entry struct {
Repository string `json:"repository"`
Digest string `json:"digest"`
State string `json:"state"`
Why []string `json:"why,omitempty"`
// Archive is the archive a holder keeps.
Archive string `json:"archive,omitempty"`
Tags []string `json:"tags,omitempty"`
// Bytes are the blobs it marks, its own content included.
Bytes int64 `json:"bytes"`
// Unread says its content could not be read, so what it marks is not known beyond itself.
Unread string `json:"unread,omitempty"`
}
// recordIndex finds a record by repository and digest.
type recordIndex map[string]*Record
func indexRecords(r *Records) recordIndex {
idx := recordIndex{}
for i := range r.References {
rec := &r.References[i]
if rec.Repository == "" || rec.Digest == "" {
continue
}
idx[rec.Kind+" "+Key(rec.Repository, rec.Digest)] = rec
}
return idx
}
// Classify gives every manifest the store holds one state.
func Classify(v *View, recs *Records) []Entry {
idx := indexRecords(recs)
var out []Entry
for _, name := range v.L.RepoNames() {
r := v.L.Repos[name]
tagsAt := map[string][]string{}
for t, d := range r.Tags {
tagsAt[d] = append(tagsAt[d], t)
}
digests := make([]string, 0, len(r.Revisions))
for d := range r.Revisions {
digests = append(digests, d)
}
sort.Strings(digests)
for _, d := range digests {
e := Entry{Repository: name, Digest: d, State: StateUnrecorded, Tags: tagsAt[d]}
sort.Strings(e.Tags)
blobs := map[string]bool{}
for _, b := range v.Marks(name, d) {
blobs[b] = true
}
e.Bytes = v.Bytes(blobs)
e.Unread = v.Unread[Key(name, d)]
m := v.M[Key(name, d)]
if rec := idx["image "+Key(name, d)]; rec != nil {
e.State, e.Why = stateOf(rec, false), rec.Why
} else if m.Holder() {
archive := m.Layers[0]
if rec := idx["archive "+Key(name, archive)]; rec != nil {
e.State, e.Why, e.Archive = stateOf(rec, true), rec.Why, archive
} else if len(e.Tags) > 0 {
e.State = StateNamedDocument
}
}
out = append(out, e)
}
}
return out
}
func stateOf(rec *Record, holder bool) string {
switch rec.State {
case "kept":
if holder {
return StateHolderKept
}
return StateKept
case "eligible":
if holder {
return StateHolderEligible
}
return StateEligible
case "collected":
return StateCollectedPresent
}
// A state this bundle does not know is kept: never offered as removable.
return StateKept
}
// Missing are the references the records keep that the store does not hold: an image whose manifest
// is not in its repository, an archive whose blob is not in the store.
func Missing(v *View, recs *Records) []string {
var out []string
for _, rec := range recs.References {
if rec.State != "kept" || rec.Repository == "" || rec.Digest == "" {
continue
}
switch rec.Kind {
case "image":
if r := v.L.Repos[rec.Repository]; r == nil || !r.Revisions[rec.Digest] {
out = append(out, rec.Reference)
}
case "archive":
if _, ok := v.L.Blobs[rec.Digest]; !ok {
out = append(out, rec.Reference)
}
}
}
sort.Strings(out)
return out
}
// StateSum is the manifests of one state: how many, the bytes they mark together, and the bytes only
// they mark — what the store would give back if they alone went.
type StateSum struct {
Manifests int `json:"manifests"`
Bytes int64 `json:"bytes"`
OnlyTheirBytes int64 `json:"only_their_bytes"`
}
// Summarise sums entries by state, over the whole store or one repository's entries, the "only theirs"
// always judged against every manifest in the store.
func Summarise(v *View, entries []Entry, all []Entry) map[string]*StateSum {
statesOf := map[string]map[string]bool{}
for _, e := range all {
for _, b := range v.Marks(e.Repository, e.Digest) {
if statesOf[b] == nil {
statesOf[b] = map[string]bool{}
}
statesOf[b][e.State] = true
}
}
sums := map[string]*StateSum{}
blobs := map[string]map[string]bool{}
for _, e := range entries {
s := sums[e.State]
if s == nil {
s = &StateSum{}
sums[e.State] = s
blobs[e.State] = map[string]bool{}
}
s.Manifests++
for _, b := range v.Marks(e.Repository, e.Digest) {
blobs[e.State][b] = true
}
}
for state, set := range blobs {
sums[state].Bytes = v.Bytes(set)
only := map[string]bool{}
for b := range set {
if len(statesOf[b]) == 1 {
only[b] = true
}
}
sums[state].OnlyTheirBytes = v.Bytes(only)
}
return sums
}
// LetGoKeys are the manifests the store would no longer hold once the controller lets go of these
// references: an image's manifest, and an archive's holder.
func LetGoKeys(v *View, refs []string) map[string]bool {
out := map[string]bool{}
for _, ref := range refs {
repo, digest, kind := splitReference(ref)
r := v.L.Repos[repo]
if r == nil {
continue
}
switch kind {
case "image":
if r.Revisions[digest] {
out[Key(repo, digest)] = true
}
case "archive":
for d := range r.Revisions {
if m := v.M[Key(repo, d)]; m.Holder() && m.Layers[0] == digest {
out[Key(repo, d)] = true
}
}
}
}
return out
}