Merge branch 'feat/47-G-fix11-c3d' into feat/47-G-join11

# Conflicts:
#	modules/distribution/go.mod
#	modules/distribution/internal/holder/holder_test.go
This commit is contained in:
2026-11-12 10:26:55 +01:00
17 changed files with 273 additions and 332 deletions
@@ -1,135 +0,0 @@
// Package holder is distribution's holding of the two image seats, `container-image-registry` and
// `oci-artifact-registry`, and of the old seat `mesh-artifact-store` until its window closes (novox/hq design 51 *The
// four registry seats*, ADR 0605, ADR 0611, ADR 0617 §2). A build module publishes into a seat through `publish` and
// `publish-part`: the parts land in distribution's own object store `incoming`, are hashed, checked against the
// builder's reservation (`reserved`) and stored in the artifact store, which listens on its node's loopback; the store's port
// is the program's, reads open, writes from its own process alone (front.go).
package holder
import (
"fmt"
"slices"
"sort"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// The seats distribution holds, and the old one it holds until its window closes.
const (
ImageSeat = "container-image-registry"
ArtifactSeat = "oci-artifact-registry"
OldSeat = "mesh-artifact-store"
)
// The verbs of the seats (design 26 *A seat's definition*; design 51 *One contract*).
const (
VerbPublish = "publish"
VerbPublishPart = "publish-part"
VerbVersions = "versions"
VerbRemove = "remove"
VerbMirror = "mirror"
VerbTags = "tags"
VerbHold = "hold"
VerbRelease = "release"
)
// Seats are the seats distribution answers verbs on.
var Seats = []string{ImageSeat, ArtifactSeat}
// VerbsOf are a seat's verbs: mirror and tags on the image seat alone; hold, release and list on both of distribution's.
func VerbsOf(seat string) []string {
v := []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease, VerbList}
if seat == ImageSeat {
v = append(v, VerbMirror, VerbTags)
}
sort.Strings(v)
return v
}
// Callers is the holder's trusted config value `callers`: per verb, the callers that may call it, read as mesh-sdk's
// seat-neutral list per verb reads it (ADR 0627 §2). The holder checks the caller the tool runner proved and stamped on
// the request (design 25 § 12.4), never a name in the request's body.
type Callers = stdio.VerbCallers
// DefaultCallers is the list design 51 *Who may call which verb* names: publish and publish-part the build modules;
// mirror mesh-build-image; remove the builder (a tagged version) and the artifact sweep's holder (retention); hold the
// artifact sweep's holder alone; release that holder, and the operator's session only through registry_holds (Operator
// below). versions, tags and list are read by anyone on the mesh, as the store's reads are.
func DefaultCallers() Callers {
builds := []string{"mesh-build-go", "mesh-build-image", "mesh-build-node"}
return Callers{
VerbPublish: builds,
VerbPublishPart: builds,
VerbMirror: {"mesh-build-image"},
VerbRemove: {"mesh-delivery-builder", SweepHolder},
VerbHold: {SweepHolder},
VerbRelease: {SweepHolder, Operator},
VerbVersions: {Anyone},
VerbTags: {Anyone},
VerbList: {Anyone},
}
}
// SweepHolder is the module holding the seat `mesh-artifact-sweep`, the artifact sweep.
const SweepHolder = "artifact-store-tools"
// The entries of a list that stand for a class, and the caller the runtime names for a call it could not prove.
const (
Anyone = stdio.AnyCaller
Operator = stdio.AnyPerson
Unproven = stdio.Unproven
)
// Self is distribution's own account: never on release's list, so registry_holds cannot release in its own name.
const Self = "distribution"
// ParseCallers reads the config value; an empty value is DefaultCallers. A verb no seat has is refused, and Self on
// release's list is refused (design 51).
func ParseCallers(raw string) (Callers, error) {
c, err := stdio.ParseVerbCallers(raw, VerbsOf(ImageSeat), DefaultCallers())
if err != nil {
return nil, err
}
return selfNotReleasing(c)
}
// configured holds the config value `callers`: asked of the runtime once, by a watch, and replaced on each change of
// the value, so a call never asks (issue 1966: an ask that timed out refused every verb).
var configured = stdio.HoldVerbCallers(CallersConfig, VerbsOf(ImageSeat), DefaultCallers())
// ConfiguredCallers is the config value `callers` as mesh-config resolved it for this machine, held from the tools
// runtime's watch of it (never an environment variable: nothing renders a config value into a program's environment);
// unset is DefaultCallers, and Self on release's list is refused as ParseCallers refuses it.
func ConfiguredCallers() (Callers, error) {
c, err := configured()
if err != nil {
return nil, err
}
return selfNotReleasing(c)
}
// CallersConfig is the config value's key, and CallersTool the tools bundle's tool answering it as resolved: the
// program, which has no tools runtime, asks that tool over the bus (manifest invokes).
const (
CallersConfig = "callers"
CallersTool = "registry_callers"
)
// WithCallers is v with the caller list c: a copy for one call, so a list read on that call never changes another's.
func (v *Verbs) WithCallers(c Callers) *Verbs {
h := *v.Holder
h.Callers = c
out := *v
out.Holder = &h
return &out
}
func selfNotReleasing(c Callers) (Callers, error) {
if slices.Contains(c[VerbRelease], Self) {
return nil, fmt.Errorf("callers puts %s on release's list: registry_holds would release in its own name", Self)
}
return c, nil
}
// CallerModule is the module of a proved module caller, which the runtime stamps `<node>.<module>`.
func CallerModule(proved string) string { return stdio.ModuleOf(proved) }
@@ -65,8 +65,9 @@ type WindowState interface {
type Driver struct {
Delivery Delivery
State WindowState
// Callers is the caller list as it holds on this wake: publish's callers are the build modules counted.
Callers func() (Callers, error)
// BuildModules are the modules claiming a build seat (`node-build-*`) in the catalog half as it holds on this wake:
// the build modules counted (design 47 G §610, A-156).
BuildModules func() ([]string, error)
// Closed says a window's closing on distribution.events.
Closed func(c Closed) error
Finding func(kind, subject, words string, raised bool)
@@ -87,11 +88,10 @@ func (d *Driver) Read() ([]NodeRead, error) {
if err != nil {
return nil, fmt.Errorf("the publishing builds cannot be read, so no window moves: %w", err)
}
callers, err := d.Callers()
builds, err := d.BuildModules()
if err != nil {
return nil, fmt.Errorf("the caller list cannot be read, so no window moves: %w", err)
return nil, fmt.Errorf("the build modules cannot be read from the catalog half, so no window moves: %w", err)
}
builds := callers[VerbPublish]
byNode := map[string]*NodeRead{}
var order []string
for _, h := range haves {
@@ -194,14 +194,15 @@ func (m memHolds) List() ([]Hold, error) {
func newBed(builderOK bool) *bed {
b := &bed{store: &fakeStore{has: map[string]bool{}}, findings: map[string]bool{}, now: time.Unix(1000, 0), cat: &fakeCatalog{tags: map[string]string{}}}
b.h = &Holder{Callers: DefaultCallers(), Objects: memObjects{}, Transfers: memTransfers{}, Builder: fakeBuilder{ok: builderOK},
b.h = &Holder{Objects: memObjects{}, Transfers: memTransfers{}, Builder: fakeBuilder{ok: builderOK},
Store: b.store, Now: func() time.Time { return b.now },
Say: func(seat, event string, body Event) error {
b.said = append(b.said, said{seat, event, body})
return nil
},
Finding: func(kind, subject, _ string, raised bool) { b.findings[kind+" "+subject] = raised }}
b.v = &Verbs{Holder: b.h, Catalog: b.cat, Holds: memHolds{}, Mirrors: memMirrors{}}
b.v = &Verbs{Holder: b.h, Catalog: b.cat, Holds: memHolds{}, Mirrors: memMirrors{},
SweepHolder: func() (string, error) { return "artifact-store-tools", nil }}
return b
}
@@ -221,7 +222,7 @@ func digestOf(b []byte) string { s := sha256.Sum256(b); return "sha256:" + hex.E
const builder = "ace.mesh-build-go"
// TestTheHolderOfTheTwoImageSeats holds design 51's holder cases at the unit level (CAT-D1, D2, D3, D3b, D4, D4b, D5):
// one table or case per rule, the caller list and the store front's.
// one table or case per rule, the store front's.
func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
blob := []byte(strings.Repeat("a", 3000))
part := Part{Digest: digestOf(blob), Size: int64(len(blob))}
@@ -269,8 +270,6 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
do func(b *bed) error
want string
}{
{"a caller not on the list", func(b *bed) error { _, err := b.h.Publish(ArtifactSeat, "ace.mesh-provisioner", req("t")); return err }, "not on the holder's caller list"},
{"an unproven caller", func(b *bed) error { _, err := b.h.Publish(ArtifactSeat, Unproven, req("t")); return err }, "no caller the tool runner proved"},
{"a part from another caller than the opener", func(b *bed) error {
_, _ = b.h.Publish(ArtifactSeat, builder, req("t"))
_, err := b.h.PublishPart(ArtifactSeat, "ace.mesh-build-node", PartRequest{Transfer: "t", Digest: part.Digest, Last: true, Data: blob})
@@ -319,13 +318,16 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
{"a hold of an image", func(b *bed) error {
return b.v.HoldOne(ImageSeat, "novox.artifact-store-tools", HoldRequest{Name: "m/i", Kind: KindImage, Digest: part.Digest})
}, "never held"},
{"a hold from another caller", func(b *bed) error {
return b.v.HoldOne(ArtifactSeat, "novox.mesh-build-go", HoldRequest{Name: "m/a", Kind: KindArchive, Digest: part.Digest})
}, "not on the holder's caller list"},
{"a release from a caller not on the list", func(b *bed) error {
_, err := b.v.Release(ArtifactSeat, "novox.mesh-build-go", HoldRequest{Name: "m/a", Digest: part.Digest})
{"a retention removal by the module that held the sweep seat before", func(b *bed) error {
b.v.SweepHolder = func() (string, error) { return "artifact-sweeper", nil } // the seat's holder as the seat says it now
_, err := b.v.Remove(ArtifactSeat, "novox.artifact-store-tools", RemoveRequest{Name: "m/program", Digest: part.Digest, Reason: ReasonRetention})
return err
}, "not on the holder's caller list"},
}, "artifact sweep's alone"},
{"a retention removal while the sweep's holder cannot be read", func(b *bed) error {
b.v.SweepHolder = func() (string, error) { return "", errors.New("the seat's holders are not answered") }
_, err := b.v.Remove(ArtifactSeat, "novox.artifact-store-tools", RemoveRequest{Name: "m/program", Digest: part.Digest, Reason: ReasonRetention})
return err
}, "cannot be read"},
{"a release in distribution's own name", func(b *bed) error {
_, err := b.v.Release(ArtifactSeat, "novox.distribution", HoldRequest{Name: "m/a", Digest: part.Digest})
return err
@@ -340,9 +342,6 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Errorf("%s: err = %v, want one saying %q", c.name, err, c.want)
}
}
if _, err := ParseCallers(`{"release":["distribution"]}`); err == nil {
t.Error("a caller list putting distribution on release's list is taken")
}
})
t.Run("only the missing layers are sent, and an idle transfer is dropped", func(t *testing.T) {
b := newBed(true)
@@ -776,7 +775,7 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
var closed []string
found := map[string]bool{}
now := time.Unix(0, 0)
d := &Driver{Delivery: del, State: st, Callers: func() (Callers, error) { return DefaultCallers(), nil }, Now: func() time.Time { return now },
d := &Driver{Delivery: del, State: st, BuildModules: func() ([]string, error) { return []string{"mesh-build-go", "mesh-build-image", "mesh-build-node"}, nil }, Now: func() time.Time { return now },
Closed: func(c Closed) error { closed = append(closed, c.Window+" "+c.Seat); return nil },
Finding: func(kind, subject, _ string, raised bool) { found[kind+" "+subject] = raised }}
if _, err := d.Wake("start"); err != nil {
@@ -869,8 +868,9 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
if _, ok := claims[OldSeat]; !ok {
t.Error("the old seat mesh-artifact-store is no longer claimed while its window is open")
}
if !reflect.DeepEqual(m.Consumes, []string{"mesh-delivery-view.delivered", "node-engine.manifest-reads-changed"}) {
t.Errorf("consumes = %v, want delivered and manifest-reads-changed (the windows' wakes)", m.Consumes)
if !reflect.DeepEqual(m.Consumes, []string{"mesh-delivery-view.delivered", "node-engine.manifest-reads-changed", "mesh-conditions.findings-asked",
"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved"}) {
t.Errorf("consumes = %v, want delivered and manifest-reads-changed (the windows' wakes) and the catalog half's events", m.Consumes)
}
// E14: the registry container, on the loopback, is listed before the program, so the port is free when it starts.
store, program := -1, -1
@@ -29,9 +29,6 @@ type StoreFiles interface {
// and name not held is an empty answer. A manifest whose content cannot be read fails the answer, by name: a listing
// missing one would read as the store not holding it.
func (v *Verbs) List(ctx context.Context, seat, caller, name string) ([]reg.Repository, error) {
if err := v.Callers.Allowed(VerbList, caller); err != nil {
return nil, err
}
if seat != ImageSeat && seat != ArtifactSeat {
return nil, fmt.Errorf("list: %s is no seat of distribution's", seat)
}
@@ -128,7 +128,7 @@ func TestListAnswersTheStoreThroughTheSeat(t *testing.T) {
f := filesWorld()
b := newBed(true)
b.v.Files = &Files{Container: "mesh-registry", Run: f.run(t)}
if err := b.v.HoldOne(ArtifactSeat, SweepHolder, HoldRequest{Name: "app/tools", Digest: dg("a-kept"), Why: "kept"}); err != nil {
if err := b.v.HoldOne(ArtifactSeat, "novox.artifact-store-tools", HoldRequest{Name: "app/tools", Digest: dg("a-kept"), Why: "kept"}); err != nil {
t.Fatal(err)
}
ctx := context.Background()
@@ -0,0 +1,54 @@
// Package holder is distribution's holding of the two image seats, `container-image-registry` and
// `oci-artifact-registry`, and of the old seat `mesh-artifact-store` until its window closes (novox/hq design 51 *The
// four registry seats*, ADR 0605, ADR 0611, ADR 0617 §2). A build module publishes into a seat through `publish` and
// `publish-part`: the parts land in distribution's own object store `incoming`, are hashed, checked against the
// builder's reservation (`reserved`) and stored in the artifact store, which listens on its node's loopback; the store's port
// is the program's, reads open, writes from its own process alone (front.go).
package holder
import (
"sort"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// The seats distribution holds, and the old one it holds until its window closes.
const (
ImageSeat = "container-image-registry"
ArtifactSeat = "oci-artifact-registry"
OldSeat = "mesh-artifact-store"
)
// SweepSeat is the seat of the artifact sweep, whose holder alone removes an untagged version for retention.
const SweepSeat = "mesh-artifact-sweep"
// The verbs of the seats (design 26 *A seat's definition*; design 51 *One contract*).
const (
VerbPublish = "publish"
VerbPublishPart = "publish-part"
VerbVersions = "versions"
VerbRemove = "remove"
VerbMirror = "mirror"
VerbTags = "tags"
VerbHold = "hold"
VerbRelease = "release"
)
// Seats are the seats distribution answers verbs on.
var Seats = []string{ImageSeat, ArtifactSeat}
// VerbsOf are a seat's verbs: mirror and tags on the image seat alone; hold, release and list on both of distribution's.
func VerbsOf(seat string) []string {
v := []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease, VerbList}
if seat == ImageSeat {
v = append(v, VerbMirror, VerbTags)
}
sort.Strings(v)
return v
}
// Self is distribution's own account: never the caller of a release, so registry_holds cannot release in its own name.
const Self = "distribution"
// CallerModule is the module of a proved module caller, which the runtime stamps `<node>.<module>`.
func CallerModule(proved string) string { return stdio.ModuleOf(proved) }
@@ -158,7 +158,6 @@ type Finding func(kind, subject, words string, raised bool)
// Holder answers the verbs.
type Holder struct {
Callers Callers
Objects Objects
Transfers TransferState
Builder Builder
@@ -245,9 +244,6 @@ func answerOf(r parts.Received) PublishAnswer {
// Publish is `publish` on seat for the proved caller.
func (h *Holder) Publish(seat, caller string, r PublishRequest) (PublishAnswer, error) {
if err := h.Callers.Allowed(VerbPublish, caller); err != nil {
return PublishAnswer{}, err
}
if err := checkPublish(seat, r); err != nil {
return PublishAnswer{}, err
}
@@ -272,9 +268,6 @@ func (h *Holder) Publish(seat, caller string, r PublishRequest) (PublishAnswer,
// PublishPart is `publish-part`: one chunk, from the caller that opened the transfer alone.
func (h *Holder) PublishPart(seat, caller string, r PartRequest) (PublishAnswer, error) {
if err := h.Callers.Allowed(VerbPublishPart, caller); err != nil {
return PublishAnswer{}, err
}
got, err := h.receiver().Take(r.Transfer, caller, r.Digest, r.Sequence, r.Last, r.Data, func(t Transfer) error {
if t.Shape.Seat != seat {
return fmt.Errorf("transfer %s is %s's, not %s's", r.Transfer, t.Shape.Seat, seat)
+8 -19
View File
@@ -85,6 +85,9 @@ type Verbs struct {
Mirrors Mirrors
// Files are the store's own files, which list reads (ADR 0632 §3).
Files StoreFiles
// SweepHolder is the module holding the seat `mesh-artifact-sweep`, as the seat's holders say it now (design 47 G §7, §428);
// a removal for retention is that module's alone.
SweepHolder func() (string, error)
}
// VersionsAnswer is `versions`: the versions the seat holds of name.
@@ -96,9 +99,6 @@ type VersionsAnswer struct {
// 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")
}
@@ -139,16 +139,17 @@ const ReasonSharedDigest = reg.RemoveAnswerReasonSharedDigest
// 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) (Removal, error) {
if err := v.Callers.Allowed(VerbRemove, caller); err != nil {
return Removal{}, err
}
if strings.TrimSpace(r.Name) == "" || strings.TrimSpace(r.Reason) == "" {
return Removal{}, 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 {
sweep, err := v.SweepHolder()
if err != nil {
return Removal{}, fmt.Errorf("remove: the holder of %s cannot be read, so nothing is removed for retention: %w", SweepSeat, err)
}
if CallerModule(caller) != sweep {
return Removal{}, fmt.Errorf("remove: refused for %s: a removal for retention is the artifact sweep's alone", caller)
}
if r.Digest == "" {
@@ -231,9 +232,6 @@ type HoldRequest struct {
// 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
}
// The seat's contract names no kind: on the image seat a hold is of a copied base (upstream/…), never an image of
// its own; on the artifact seat of a bundle or an archive.
if r.Kind == "" && seat == ImageSeat && strings.HasPrefix(r.Name, "upstream/") {
@@ -261,9 +259,6 @@ func (v *Verbs) Release(seat, caller string, r HoldRequest) (bool, error) {
if CallerModule(caller) == Self && !strings.HasPrefix(caller, "person.") {
return false, fmt.Errorf("release: refused for %s: distribution cannot release in its own name", caller)
}
if err := v.Callers.Allowed(VerbRelease, caller); err != nil {
return false, err
}
key := Hold{Seat: seat, Name: r.Name, Digest: r.Digest}.Key()
h, held, err := v.Holds.Get(key)
if err != nil {
@@ -283,9 +278,6 @@ 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
@@ -317,9 +309,6 @@ 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")
}