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.
204 lines
5.6 KiB
Go
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
|
|
}
|