Files
mesh-catalog/modules/distribution/internal/holder/verbs.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

240 lines
8.7 KiB
Go

package holder
import (
"errors"
"fmt"
"slices"
"strings"
"time"
)
// Catalog is what the store answers of what it holds.
type Catalog interface {
// Versions are the versions (tags) of name, sorted.
Versions(name string) ([]string, error)
// Digest is the digest name holds at version, empty when none.
Digest(name, version string) (string, error)
// Remove deletes name at version (its manifest); the store's collector frees its blobs.
Remove(name, version string) error
// RemoveDigest deletes an untagged manifest of name by its digest (retention).
RemoveDigest(name, digest string) error
// Mirror copies an outside image, pinned by digest, into upstream/<host>/<path>, answering the reference.
Mirror(source, digest string) (string, error)
}
// HoldState is distribution's own state holds (bus state, template state; ADR 0617 §2).
type HoldState interface {
Get(key string) (Hold, bool, error)
Put(h Hold) error
Delete(key string) error
List() ([]Hold, error)
}
// Hold keeps one untagged bundle, archive or copied base from retention's removal.
type Hold struct {
Seat string `json:"seat"`
Name string `json:"name"`
Kind string `json:"kind"`
Digest string `json:"digest"`
By string `json:"by"`
Why string `json:"why,omitempty"`
Since time.Time `json:"since"`
}
// Key is the hold's key in the state.
func (h Hold) Key() string {
return h.Seat + "." + strings.ReplaceAll(strings.ReplaceAll(h.Name, ".", "="), "@", "=") + "." + strings.TrimPrefix(h.Digest, "sha256:")
}
// Verbs answers the verbs other than the publish: versions, remove, hold, release, tags and mirror.
type Verbs struct {
*Holder
Catalog Catalog
Holds HoldState
}
// VersionsAnswer is `versions`: the versions the seat holds of name.
type VersionsAnswer struct {
Seat string `json:"seat"`
Name string `json:"name"`
Versions []string `json:"versions"`
}
// Versions is `versions`.
func (v *Verbs) Versions(seat, caller, name string) (VersionsAnswer, error) {
if err := v.Callers.Allowed(VerbVersions, caller); err != nil {
return VersionsAnswer{}, err
}
if strings.TrimSpace(name) == "" {
return VersionsAnswer{}, errors.New("versions: no name")
}
got, err := v.Catalog.Versions(name)
if err != nil {
return VersionsAnswer{}, fmt.Errorf("versions: the store cannot list %s: %w", name, err)
}
if got == nil {
got = []string{}
}
return VersionsAnswer{Seat: seat, Name: name, Versions: got}, nil
}
// RemoveRequest is `remove`: a tagged version, or with the reason retention an untagged one by its digest.
type RemoveRequest struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Digest string `json:"digest,omitempty"`
Kind string `json:"kind,omitempty"`
Reason string `json:"reason"`
}
// ReasonRetention is the reason of the sweep's removal of an untagged version (ADR 0610 §3).
const ReasonRetention = "retention"
// Remove is `remove`: the builder removes a tagged version; the artifact sweep's holder an untagged bundle, archive or
// copied base with the reason retention, which a hold keeps. Every removal says `removed` with its reason.
func (v *Verbs) Remove(seat, caller string, r RemoveRequest) error {
if err := v.Callers.Allowed(VerbRemove, caller); err != nil {
return err
}
if strings.TrimSpace(r.Name) == "" || strings.TrimSpace(r.Reason) == "" {
return errors.New("remove: a name and a reason are required")
}
ev := Event{Name: r.Name, Version: r.Version, Kind: r.Kind, Digest: r.Digest, Holder: Self, Reason: r.Reason, By: caller}
switch {
case r.Reason == ReasonRetention:
if CallerModule(caller) != SweepHolder {
return fmt.Errorf("remove: refused for %s: a removal for retention is the artifact sweep's alone", caller)
}
if r.Digest == "" {
return errors.New("remove: a removal for retention names the digest")
}
if _, held, err := v.Holds.Get(Hold{Seat: seat, Name: r.Name, Digest: r.Digest}.Key()); err != nil {
return fmt.Errorf("remove: the holds cannot be read, so nothing is removed: %w", err)
} else if held {
return fmt.Errorf("remove: refused: %s@%s is held; release it first", r.Name, r.Digest)
}
if err := v.Catalog.RemoveDigest(r.Name, r.Digest); err != nil {
return fmt.Errorf("remove: the store did not remove %s@%s: %w", r.Name, r.Digest, err)
}
default:
if r.Version == "" || !strings.Contains(r.Version, "-") {
return fmt.Errorf("remove: refused: %s@%s has no tag; only a tagged version is removed, but for retention", r.Name, r.Version)
}
d, err := v.Catalog.Digest(r.Name, r.Version)
if err != nil {
return fmt.Errorf("remove: the store cannot read %s@%s: %w", r.Name, r.Version, err)
}
if d != "" {
if err := v.Catalog.Remove(r.Name, r.Version); err != nil {
return fmt.Errorf("remove: the store did not remove %s@%s: %w", r.Name, r.Version, err)
}
}
ev.Digest = d // a version already gone is said removed all the same, so the builder's retry ends
}
return v.Say(seat, EventRemoved, ev)
}
// HoldRequest is `hold` and `release`.
type HoldRequest struct {
Name string `json:"name"`
Kind string `json:"kind"`
Digest string `json:"digest"`
Why string `json:"why,omitempty"`
}
// HoldOne is `hold`: the artifact sweep's holder alone; an image is never held (its own rules keep images).
func (v *Verbs) HoldOne(seat, caller string, r HoldRequest) error {
if err := v.Callers.Allowed(VerbHold, caller); err != nil {
return err
}
switch r.Kind {
case KindBundle, KindArchive, KindUpstream:
default:
return fmt.Errorf("hold: refused: a %s is never held; only an untagged bundle, archive or copied base", r.Kind)
}
if r.Name == "" || !strings.HasPrefix(r.Digest, "sha256:") {
return errors.New("hold: a name and a sha256 digest are required")
}
h := Hold{Seat: seat, Name: r.Name, Kind: r.Kind, Digest: r.Digest, By: caller, Why: r.Why, Since: v.now()}
if err := v.Holds.Put(h); err != nil {
return fmt.Errorf("hold: the holds did not take it: %w", err)
}
return v.Say(seat, EventHeld, Event{Name: r.Name, Kind: r.Kind, Digest: r.Digest, Holder: Self, By: caller})
}
// Release is `release`: the artifact sweep's holder, or the operator's session through registry_holds (a hand act,
// recorded by the tool); distribution's own account is never on its list.
func (v *Verbs) Release(seat, caller string, r HoldRequest) error {
if CallerModule(caller) == Self && !strings.HasPrefix(caller, "person.") {
return fmt.Errorf("release: refused for %s: distribution cannot release in its own name", caller)
}
if err := v.Callers.Allowed(VerbRelease, caller); err != nil {
return err
}
key := Hold{Seat: seat, Name: r.Name, Digest: r.Digest}.Key()
h, held, err := v.Holds.Get(key)
if err != nil {
return err
}
if !held {
return fmt.Errorf("release: %s@%s is not held on %s", r.Name, r.Digest, seat)
}
if err := v.Holds.Delete(key); err != nil {
return err
}
return v.Say(seat, EventReleased, Event{Name: h.Name, Kind: h.Kind, Digest: h.Digest, Holder: Self, By: caller, Reason: r.Why})
}
// Tags is `tags` on the image seat: the tags of an image.
func (v *Verbs) Tags(seat, caller, name string) (VersionsAnswer, error) {
if seat != ImageSeat {
return VersionsAnswer{}, fmt.Errorf("tags: %s serves no tags", seat)
}
if err := v.Callers.Allowed(VerbTags, caller); err != nil {
return VersionsAnswer{}, err
}
got, err := v.Catalog.Versions(name)
if err != nil {
return VersionsAnswer{}, err
}
return VersionsAnswer{Seat: seat, Name: name, Versions: got}, nil
}
// MirrorRequest is `mirror`: an outside image by its pinned digest.
type MirrorRequest struct {
Source string `json:"source"`
Digest string `json:"digest"`
}
// Mirror is `mirror` on the image seat: mesh-build-image alone; an image pinned by no digest is refused.
func (v *Verbs) Mirror(seat, caller string, r MirrorRequest) (string, error) {
if seat != ImageSeat {
return "", fmt.Errorf("mirror: %s serves no mirror", seat)
}
if err := v.Callers.Allowed(VerbMirror, caller); err != nil {
return "", err
}
if r.Source == "" || !strings.HasPrefix(r.Digest, "sha256:") {
return "", errors.New("mirror: refused: an upstream image is copied only by its pinned digest")
}
src, err := ParseSource(r.Source)
if err != nil {
return "", err
}
ref, err := v.Catalog.Mirror(r.Source, r.Digest)
if err != nil {
return "", fmt.Errorf("mirror: %s@%s was not copied: %w", r.Source, r.Digest, err)
}
return ref, v.Say(seat, EventPublished, Event{Name: src.Name(), Kind: KindUpstream, Digest: r.Digest, Holder: Self, Reference: ref, By: caller})
}
// HeldNow lists the holds, for registry_holds.
func (v *Verbs) HeldNow() ([]Hold, error) {
all, err := v.Holds.List()
if err != nil {
return nil, err
}
slices.SortFunc(all, func(a, b Hold) int { return strings.Compare(a.Key(), b.Key()) })
return all, nil
}