Files
mesh-catalog/modules/distribution/internal/holder/verbs.go
T
jschoubben 9ad22ad1e1 CAT-D1..D5 (holder core): distribution's holding of the two image seats, its caller list, incoming, the store's front and the two windows
The holder answers publish and publish-part into incoming (parts of at most 1 MiB, resumed from what is held,
hashed, only the missing layers sent, 20 GiB with its 80 % finding and a refusal at the cap, a transfer idle 24
hours dropped), asks builder_versions before it stores, and says published or refused (design 51 *Publishing*,
ADR 0605 §4, ADR 0611). versions, remove (a tagged version; untagged only for the sweep's retention, which a
hold keeps), hold and release (ADR 0617 §2: the sweep alone; the operator's session by hand; never
distribution's own account), tags and mirror. The caller list is a trusted config value checked against the
caller the tool runner proved. The store's port is the program's: reads open, writes refused but while the
direct-write window is open (never over a tag published through the seat) or with the bootstrap's one-off
credential. The old-name and direct-write windows read every node, away ones included, close once, and name a
node holding them seven days.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:52:37 +01:00

236 lines
8.6 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")
}
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: "upstream/" + r.Source, 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
}