C3d REG2: distribution keeps no caller list; the build modules are the catalog half's claimants of node-build-* seats

The config `callers`, the tool registry_callers and its invokes, the caller-list code and the holder's per-verb
Allowed checks go. The windows' driver counts as build modules the modules claiming a `node-build-*` seat in the
catalog half, followed by the seat mesh-catalog's branch follower on the program's views reader (one shared events
hub); it wakes again when a build module's entry moves. The retention check of remove finds the artifact sweep's
holder through the seat mesh-module-assigner's holders (seatlist.HolderOf), not a name.

Builds: design 47 G §610, §7, §428, A-156, A-82
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:30:54 +01:00
parent 9fe3f41ac0
commit 8d73b5ba1b
17 changed files with 273 additions and 332 deletions
@@ -3,7 +3,6 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
@@ -31,7 +30,7 @@ func TestTheVerbsSpeakTheSeatsContract(t *testing.T) {
reg.VerbVersions: reg.VersionsInput{}, reg.VerbRemove: reg.RemoveInput{}, reg.VerbHold: reg.HoldInput{},
reg.VerbRelease: reg.ReleaseInput{}, reg.VerbMirror: reg.MirrorInput{}, reg.VerbTags: reg.TagsInput{}}
seen := 0
for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}, func() (holder.Callers, error) { return holder.DefaultCallers(), nil }) {
for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}) {
seat, verb, ok := strings.Cut(tool.Name, ".")
in, isVerb := inputs[verb]
if !ok || !isVerb || (seat != holder.ImageSeat && seat != holder.ArtifactSeat) {
@@ -57,28 +56,6 @@ func TestTheVerbsSpeakTheSeatsContract(t *testing.T) {
t.Errorf("%s's body %s is not the contract's: %v", event, b, err)
}
}
// The caller list is read held: one never read answers no verb, and registry_callers says it as read.
unread := func() (holder.Callers, error) { return nil, errors.New("config refused") }
for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}, unread) {
if tool.Name == holder.ImageSeat+"."+holder.VerbVersions {
if _, err := tool.Run(map[string]any{"name": "m/a"}); err == nil || !strings.Contains(err.Error(), "caller list") {
t.Errorf("versions with an unread caller list = %v, want refused naming the caller list", err)
}
}
}
answered := false
for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}, func() (holder.Callers, error) { return holder.Callers{holder.VerbPublish: {"mesh-build-go"}}, nil }) {
if tool.Name == holder.CallersTool {
answered = true
got, err := tool.Run(map[string]any{})
if b, _ := json.Marshal(got); err != nil || string(b) != `{"callers":{"publish":["mesh-build-go"]}}` {
t.Errorf("%s = %s, %v", holder.CallersTool, b, err)
}
}
}
if !answered {
t.Errorf("no tool %s: the program could not read the caller list", holder.CallersTool)
}
if a := publishAnswer(holder.PublishAnswer{Transfer: "t", Missing: []string{"sha256:bb"}}); !reflect.DeepEqual(a.Lacks, []string{"sha256:bb"}) || a.Held {
t.Errorf("publish answers %+v", a)
}
@@ -23,6 +23,7 @@ import (
reg "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-delivery/sdk/parts"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
"distribution/internal/holder"
@@ -232,16 +233,8 @@ func arg[T any](args map[string]any) (T, error) {
func str(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
// tools are distribution's tools over v; each call that checks a caller reads the caller list held (callers:
// the config value as mesh-config resolves it now), so an operator's change of it holds from the next call on.
func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.Callers, error)) []stdio.Tool {
now := func() (*holder.Verbs, error) {
c, err := callers()
if err != nil {
return nil, fmt.Errorf("the caller list cannot be read, so no verb is answered: %w", err)
}
return v.WithCallers(c), nil
}
// tools are distribution's tools over v.
func tools(v *holder.Verbs, windows kept[holder.Window]) []stdio.Tool {
obj := func(props map[string]any, req ...string) map[string]any {
return map[string]any{"type": "object", "required": req, "properties": props}
}
@@ -252,10 +245,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: seat + "." + holder.VerbPublish, Description: "Open a transfer of a publish (ADR 0611): its name, version, kind, digest and parts; answers the parts the holder lacks.",
Input: obj(map[string]any{"transfer": s("the transfer's id"), "name": s("<module>/<artifact>"), "version": s("the builder's version"), "kind": s("image, bundle, archive or upstream"), "digest": s("sha256:…"), "parts": map[string]any{"type": "array"}}, "transfer", "name", "kind", "digest", "parts"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
in, err := arg[reg.PublishInput](args)
if err != nil {
return nil, err
@@ -269,10 +258,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: seat + "." + holder.VerbPublishPart, Description: "Send one chunk (at most 1 MiB) of one part of an open transfer, from the caller that opened it.",
Input: obj(map[string]any{"transfer": s("its id"), "part": s("the part's digest"), "sequence": map[string]any{"type": "integer"}, "last": map[string]any{"type": "boolean"}, "data": s("base64")}, "transfer", "part", "sequence", "last", "data"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
in, err := arg[reg.PublishPartInput](args)
if err != nil {
return nil, err
@@ -286,10 +271,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
}},
stdio.Tool{Name: seat + "." + holder.VerbVersions, Description: "The versions the seat holds of a name.", Input: obj(map[string]any{"name": s("<module>/<artifact>")}, "name"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
a, err := v.Versions(seat, stdio.CallerOf(args), str(args, "name"))
if err != nil {
return nil, err
@@ -299,10 +280,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: seat + "." + holder.VerbRemove, Description: "Remove a tagged version (the builder), or for retention an untagged one by its digest (the artifact sweep; an index with the platform manifests no tagged or held index names, and never a manifest such an index names); says removed.",
Input: obj(map[string]any{"name": s("name"), "version": s("a tagged version"), "tag": s("its tag"), "digest": s("for retention"), "reason": s("why")}, "name", "reason"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
in, err := arg[reg.RemoveInput](args)
if err != nil {
return nil, err
@@ -317,10 +294,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: seat + "." + holder.VerbList, Description: "What the store holds of one repository, or of every repository the seat holds: each manifest's digest, whether it is an index and the manifests it names, the tags naming it, its size and whether it is held (ADR 0632 §3). Read from the store's own files; changes nothing.",
Input: obj(map[string]any{"name": s("<module>/<artifact>; every repository when not given")}),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
repos, err := v.List(context.Background(), seat, stdio.CallerOf(args), str(args, "name"))
if err != nil {
return nil, err
@@ -330,10 +303,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: seat + "." + holder.VerbHold, Description: "Keep one untagged bundle, archive or copied base from retention (the artifact sweep alone); says held.",
Input: obj(map[string]any{"name": s("name"), "digest": s("sha256:…"), "reason": s("which of the sweep's rules keeps it")}, "name", "digest", "reason"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
in, err := arg[reg.HoldInput](args)
if err != nil {
return nil, err
@@ -346,10 +315,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: seat + "." + holder.VerbRelease, Description: "Let a hold go (the artifact sweep, or the operator's session through registry_holds); says released.",
Input: obj(map[string]any{"name": s("name"), "digest": s("sha256:…"), "reason": s("why it is let go")}, "name", "digest", "reason"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
in, err := arg[reg.ReleaseInput](args)
if err != nil {
return nil, err
@@ -365,10 +330,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
out = append(out,
stdio.Tool{Name: holder.ImageSeat + "." + holder.VerbTags, Description: "The tags of an image.", Input: obj(map[string]any{"name": s("name")}, "name"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
a, err := v.Tags(holder.ImageSeat, stdio.CallerOf(args), str(args, "name"))
if err != nil {
return nil, err
@@ -378,10 +339,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: holder.ImageSeat + "." + holder.VerbMirror, Description: "Copy an outside image, pinned by its digest, into the store as upstream/<host>/<path> (every platform, only the parts it lacks): answers at once with the reference the copy will have, and says published (or refused, with why) on the seat's events when the copy ends; wait on that event (mesh-build-image alone).",
Input: obj(map[string]any{"source": s("<host>/<path>"), "digest": s("sha256:…")}, "source", "digest"),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
in, err := arg[reg.MirrorInput](args)
if err != nil {
return nil, err
@@ -392,15 +349,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
}
return reg.MirrorAnswer{Digest: in.Digest, Reference: ref}, nil
}},
stdio.Tool{Name: holder.CallersTool, Description: "Who may call which verb of distribution's seats: the trusted config value callers as mesh-config resolves it on this machine now (design 51's list where unset). The program asks it at each wake of the windows, as it has no config of its own.",
Input: obj(map[string]any{}),
Run: func(map[string]any) (any, error) {
c, err := callers()
if err != nil {
return nil, err
}
return map[string]any{"callers": c}, nil
}},
stdio.Tool{Name: "registry_health", Description: "distribution's health check, run by the runtime each minute: versions of each seat asked through the store's read side (the program's port 5100); fails naming the program when it does not answer.",
Input: obj(map[string]any{}),
Run: func(map[string]any) (any, error) {
@@ -410,10 +358,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: "registry_incoming", Description: "Open transfers of incoming and its fill; with drop, drops one by hand (a hand act, why required).",
Input: obj(map[string]any{"drop": s("a transfer id to drop"), "why": s("why it is dropped")}),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
idle, _ := v.DropIdle()
if id := str(args, "drop"); id != "" {
t, err := v.DropByHand(id, stdio.CallerOf(args), str(args, "why"))
@@ -456,10 +400,6 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
stdio.Tool{Name: "registry_holds", Description: "What is held, since when and for whom; with release, lets one go through the seat's release as the operator's session (a hand act).",
Input: obj(map[string]any{"seat": s("the seat"), "release": s("the name to release"), "digest": s("its digest"), "why": s("why")}),
Run: func(args map[string]any) (any, error) {
v, err := now()
if err != nil {
return nil, err
}
if name := str(args, "release"); name != "" {
if str(args, "why") == "" {
return nil, errors.New("a release by hand is a hand act: say why")
@@ -532,7 +472,7 @@ func main() {
Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState), Cap: parts.BusCap(holder.IncomingObjects), Record: recordAct, Idle: parts.BusArm(holder.IdleState)}
files := &holder.Files{Container: or(os.Getenv("MESH_REGISTRY_CONTAINER"), "mesh-registry"), Run: holder.ExecRunner, UID: os.Getuid()}
v := &holder.Verbs{Holder: h, Catalog: store, Holds: kept[holder.Hold]{state: "holds", key: holder.Hold.Key}, Mirrors: mirrors{},
Files: files}
Files: files, SweepHolder: func() (string, error) { return seatlist.HolderOf(stdio.Ask, holder.SweepSeat) }}
go followMirrors(v)
go parts.FollowIdle(holder.IdleState, parts.IDs(h.Transfers), h.Expired, func(id string, dropped bool, err error) {
if err != nil {
@@ -542,7 +482,7 @@ func main() {
logf("transfer %s had no part for %s: dropped with its parts", id, holder.TransferIdle)
})
windows := kept[holder.Window]{state: "windows", key: func(w holder.Window) string { return w.Name }}
if err := stdio.Serve("", tools(v, windows, holder.ConfiguredCallers)); err != nil {
if err := stdio.Serve("", tools(v, windows)); err != nil {
logf("%v", err)
os.Exit(1)
}
@@ -0,0 +1,66 @@
package main
// The build modules the windows' driver counts (design 47 G §610, A-156, A-82): the modules that claim a build seat
// (`node-build-*`) in the catalog half, followed per watched branch by the seat mesh-catalog's client on the program's
// one views reader. No list of names is kept here, and none is configured.
import (
"encoding/json"
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// buildSeatPrefix starts the name of every seat a build module claims.
const buildSeatPrefix = "node-build-"
// buildModulesOf are the modules among entries whose manifest claims a build seat, sorted.
func buildModulesOf(entries map[string]catalog.CatalogEntry) []string {
var out []string
for module, e := range entries {
claims, _ := e.Manifest["claims"].([]any)
for _, c := range claims {
if name, _ := c.(map[string]any)["name"].(string); strings.HasPrefix(name, buildSeatPrefix) {
out = append(out, module)
break
}
}
}
slices.Sort(out)
return out
}
// claimsBuildSeat says whether a module's entry, as the branch follower tells it, claims a build seat.
func claimsBuildSeat(module string, raw json.RawMessage) bool {
var e catalog.CatalogEntry
if json.Unmarshal(raw, &e) != nil {
return true // an entry not read may be one: the driver judges again
}
return len(buildModulesOf(map[string]catalog.CatalogEntry{module: e})) == 1
}
// branchesBuilds answers the driver's build modules from the catalog half of every followed branch; with a branch that has
// no first view yet it is an error naming it, never no build module.
type branchesBuilds struct{ b *meshcatalogclient.Branches }
func (h branchesBuilds) Modules() ([]string, error) {
if err := h.b.Ready(); err != nil {
return nil, err
}
all := map[string]catalog.CatalogEntry{}
for _, branch := range h.b.Followed() {
c, ok := h.b.Catalog(branch)
if !ok {
continue
}
half, _ := c.View() // a branch at none-yet is ready and empty (§614)
for m, e := range half.Entries {
if _, ok := all[m]; !ok {
all[m] = e
}
}
}
return buildModulesOf(all), nil
}
@@ -0,0 +1,37 @@
package main
import (
"encoding/json"
"reflect"
"testing"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
func entry(claims ...string) catalog.CatalogEntry {
var cs []any
for _, c := range claims {
cs = append(cs, map[string]any{"name": c, "scope": "node"})
}
return catalog.CatalogEntry{Manifest: map[string]any{"claims": cs}}
}
// Design 47 G §610, A-156: the build modules the driver counts are the modules claiming a `node-build-*` seat in the
// catalog half, whatever their names; no list of names is kept.
func TestBuildModulesAreTheClaimantsOfBuildSeats(t *testing.T) {
entries := map[string]catalog.CatalogEntry{
"a-compiler": entry("node-build-go"), "an-imager": entry("node-build-image", "something"),
"gitea": entry("git"), "mesh-build-go": entry("not-a-build-seat"), "no-claims": {Manifest: map[string]any{}},
}
if got, want := buildModulesOf(entries), []string{"a-compiler", "an-imager"}; !reflect.DeepEqual(got, want) {
t.Errorf("build modules = %v, want %v", got, want)
}
raw, _ := json.Marshal(entry("node-build-node"))
if !claimsBuildSeat("m", raw) {
t.Error("an entry claiming a build seat is not told as one")
}
raw, _ = json.Marshal(entry("git"))
if claimsBuildSeat("m", raw) {
t.Error("an entry claiming no build seat wakes the driver")
}
}
+24 -2
View File
@@ -21,6 +21,7 @@ import (
"time"
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"github.com/nats-io/nats.go/jetstream"
@@ -82,8 +83,29 @@ func run(ctx context.Context, getenv func(string) string) error {
return err
}
defer w.Close()
go drive(ctx, js, cred, &holder.Driver{Delivery: busDelivery{calls}, State: kvWindows{windows}, Callers: busCallers{calls}.Read,
Closed: sayClosed(js, cred), Finding: findings.Say}, logger)
// Which modules are the build modules is the catalog half's (design 47 G §610, A-156): the branch follower of the seat
// mesh-catalog's client on a views reader that shares the program's one events hub. The driver wakes again when a
// build module's entry comes, changes or goes, so the start's wake, made before the first view, is not the last.
hub := process.NewEventHub(logger.Printf)
reader := &views.ProcessReader{Module: holder.Self, Call: calls.Call, Logf: logger.Printf, Hub: hub}
branches, err := meshcatalogclient.FollowBranchesProcess(reader, calls.Call)
if err != nil {
return err
}
branches.Logf = logger.Printf
catalogMoved := make(chan struct{}, 1)
branches.Changed = func(_, module string, entry json.RawMessage, deleted bool, _ time.Time) {
if !deleted && !claimsBuildSeat(module, entry) {
return
}
select {
case catalogMoved <- struct{}{}:
default:
}
}
reader.Start(ctx, js, cred, nil)
go drive(ctx, js, cred, hub, catalogMoved, &holder.Driver{Delivery: busDelivery{calls}, State: kvWindows{windows},
BuildModules: branchesBuilds{branches}.Modules, Closed: sayClosed(js, cred), Finding: findings.Say}, logger)
}
front := &holder.Front{Store: store, Window: &open, Logf: logger.Printf,
Published: func(name, tag string) (bool, error) {
@@ -7,8 +7,8 @@ package main
// OCI store on the loopback, the status view's node_haves and the node-engine seat's manifest-reads answered on their
// call subjects. An away node holds the old name open; its return (manifest-reads-changed) closes it, said once on
// distribution.events, after which a direct write is refused naming publish; reads stay open; a waiter on a seat's
// `published` hears none of the windows' events; and the build modules counted are publish's callers as the tools
// bundle answers the config value, not design 51's defaults.
// `published` hears none of the windows' events; and the build modules counted are the modules claiming a `node-build-*` seat
// in the catalog half the seat mesh-catalog answers (design 47 G §610, A-156), not a list of names.
import (
"context"
@@ -29,6 +29,8 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/testbus"
"distribution/internal/holder"
@@ -56,7 +58,8 @@ func TestDistributionWhole(t *testing.T) {
t.Fatal(err)
}
if _, err := js.CreateOrUpdateConsumer(ctx, "EVENTS", jetstream.ConsumerConfig{Durable: "novox_distribution", AckPolicy: jetstream.AckExplicitPolicy,
FilterSubjects: []string{"mesh.mod.mesh-delivery-view.event.delivered", "mesh.seat.node-engine.event.manifest-reads-changed.*"}}); err != nil {
FilterSubjects: []string{"mesh.mod.mesh-delivery-view.event.delivered", "mesh.seat.node-engine.event.manifest-reads-changed.*",
"mesh.seat.mesh-catalog.event.catalog-revision.>", "mesh.seat.mesh-catalog.event.registration-changed", "mesh.seat.mesh-delivery.event.source-moved"}}); err != nil {
t.Fatal(err)
}
@@ -77,21 +80,36 @@ func TestDistributionWhole(t *testing.T) {
answer(m, map[string]any{"keys": map[string]any{
"ace.mesh-build-go": map[string]any{"fp": "fp-new"}, "g14.mesh-build-go": map[string]any{"fp": fp},
"ace.gitea": map[string]any{"fp": "x"}, "g14.gitea": map[string]any{"fp": "x"},
// A build module the operator took off publish's list: it holds no window (the caller list as configured).
// A module claiming no build seat in the catalog half: it holds no window.
"g14.mesh-build-node": map[string]any{"fp": "fp-node-old"},
}})
}); err != nil {
t.Fatal(err)
}
// distribution's own tools bundle answers the config value callers as mesh-config resolved it: publish by
// mesh-build-go alone. The program reads it there, never from an environment variable nothing renders.
if _, err := nc.Subscribe("mesh.mod.distribution.>", func(m *nats.Msg) {
if !strings.Contains(m.Subject, holder.CallersTool) {
return
// The catalog half, as the seats mesh-catalog and mesh-delivery answer it: one watched branch, main, whose trunk revision
// has mesh-build-go claiming the build seat node-build-go; mesh-build-node claims no build seat, so it holds no window.
entryOf := func(module string, claims ...string) catalog.CatalogEntry {
var cs []any
for _, c := range claims {
cs = append(cs, map[string]any{"name": c, "scope": "node"})
}
return catalog.CatalogEntry{Module: module, Manifest: map[string]any{"claims": cs}}
}
catalogHalf := func(m *nats.Msg) {
switch {
case strings.Contains(m.Subject, ".call.registrations."):
answer(m, catalog.RegistrationsAnswer{Registrations: []catalog.Registration{{Module: "mesh-build-go", Branches: []string{"main"}}}})
case strings.Contains(m.Subject, ".call.revision."):
answer(m, catalog.RevisionAnswer{Revision: &catalog.CatalogRevision{Revision: 1, Kind: meshcatalogclient.KindTrunk, Branch: "main"},
Entries: []catalog.CatalogEntry{entryOf("mesh-build-go", "node-build-go"), entryOf("mesh-build-node"), entryOf("gitea", "git")}})
case strings.Contains(m.Subject, ".call.source."):
answer(m, map[string]any{"branches": []string{"main"}})
}
}
for _, subject := range []string{"mesh.seat.mesh-catalog.>", "mesh.seat.mesh-delivery.call.>"} {
if _, err := nc.Subscribe(subject, catalogHalf); err != nil {
t.Fatal(err)
}
answer(m, map[string]any{"callers": map[string]any{"publish": []string{"mesh-build-go"}}})
}); err != nil {
t.Fatal(err)
}
engine := func(node string) {
for _, subj := range []string{"mesh.seat.node-engine.call.manifest-reads." + node + ".>", "mesh.seat.node-engine.tool.manifest-reads." + node} {
@@ -27,8 +27,6 @@ import (
const (
havesAsk = "mesh-delivery-view.node_haves"
readsAsk = "seat:node-engine.manifest-reads"
// callersAsk is distribution's own tool answering its config value callers as resolved.
callersAsk = holder.Self + "." + holder.CallersTool
)
// caller is a process's call of a tool or a seat verb.
@@ -37,25 +35,6 @@ type caller interface {
CallOn(node, key string, body any) (json.RawMessage, error)
}
// busCallers reads the caller list from distribution's own tools bundle (holder.CallersTool, manifest invokes): a
// program has no runtime that resolves its module's config values, the tools bundle has (stdio.Config), so the program
// asks it on each wake rather than read an environment variable nothing renders.
type busCallers struct{ c caller }
func (b busCallers) Read() (holder.Callers, error) {
raw, err := b.c.Call(callersAsk, map[string]any{})
if err != nil {
return nil, fmt.Errorf("%s: %w", callersAsk, err)
}
var a struct {
Callers json.RawMessage `json:"callers"`
}
if err := json.Unmarshal(unwrap(raw), &a); err != nil || len(a.Callers) == 0 {
return nil, fmt.Errorf("%s answered what is not a caller list: %s", callersAsk, unwrap(raw))
}
return holder.ParseCallers(string(a.Callers))
}
// busDelivery answers holder.Delivery through the bus.
type busDelivery struct{ c caller }
@@ -203,7 +182,7 @@ func wakes(subject string) (string, bool) {
// drive wakes the driver at start and on each waking event of the program's durable consumer of the events stream.
// A wake that could not judge is said and its event asked again a minute later.
func drive(ctx context.Context, js jetstream.JetStream, cred process.Account, d *holder.Driver, logger *log.Logger) {
func drive(ctx context.Context, js jetstream.JetStream, cred process.Account, hub *process.EventHub, catalogMoved <-chan struct{}, d *holder.Driver, logger *log.Logger) {
wake := func(why string) error {
said, err := d.Wake(why)
for _, s := range said {
@@ -215,9 +194,9 @@ func drive(ctx context.Context, js jetstream.JetStream, cred process.Account, d
return err
}
_ = wake("distribution started")
// The module's one durable on the events stream is pulled by the events hub (mesh-sdk process), each waking event
// registered by its subject pattern: the seat of the view says delivered, the node-engine says a manifest read changed.
hub := process.NewEventHub(logger.Printf)
// The module's one durable on the events stream is pulled by the events hub (mesh-sdk process), which the views reader
// of the catalog half shares: each waking event registered by its subject pattern, the seat of the view saying
// delivered, the node-engine saying a manifest read changed.
handle := func(m jetstream.Msg) {
why, ok := wakes(m.Subject())
if !ok {
@@ -240,6 +219,13 @@ func drive(ctx context.Context, js jetstream.JetStream, cred process.Account, d
logger.Printf("the events stream cannot be heard (%v): the windows move only at the next start", err)
return
}
<-ctx.Done()
hub.Stop()
for {
select {
case <-catalogMoved:
_ = wake("a build module moved in the catalog half")
case <-ctx.Done():
hub.Stop()
return
}
}
}
+2 -2
View File
@@ -5,8 +5,10 @@ go 1.26.0
require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
@@ -29,7 +31,5 @@ require (
require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect
)
@@ -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) {
t.Run("a publish is checked against the builder's reservation", reservedChecksAPublishAgainstTheReservation)
blob := []byte(strings.Repeat("a", 3000))
@@ -270,8 +271,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})
@@ -320,13 +319,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
@@ -341,9 +343,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)
@@ -777,7 +776,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 {
@@ -870,8 +869,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", "mesh-conditions.findings-asked"}) {
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")
}
+10 -13
View File
@@ -50,7 +50,6 @@
}
],
"tools": [
"registry_callers",
"registry_direct_writes",
"registry_health",
"registry_holds",
@@ -59,26 +58,24 @@
"registry_old_name"
],
"invokes": [
"distribution.registry_callers",
"seat:mesh-builds.reserved",
"mesh-delivery-view.node_haves",
"seat:node-engine.manifest-reads",
"seat:mesh-hand-acts.hand-act"
"seat:mesh-hand-acts.hand-act",
"seat:mesh-catalog.entry",
"seat:mesh-catalog.registrations",
"seat:mesh-catalog.revision",
"seat:mesh-delivery.source",
"seat:mesh-module-assigner.assignments"
],
"consumes": [
"mesh-delivery-view.delivered",
"node-engine.manifest-reads-changed",
"mesh-conditions.findings-asked"
"mesh-conditions.findings-asked",
"mesh-catalog.catalog-revision",
"mesh-catalog.registration-changed",
"mesh-delivery.source-moved"
],
"config": {
"callers": {
"type": "string",
"trusted": true,
"runtime": true,
"default": "",
"description": "per verb, the modules that may call it (JSON object of verb to modules; operator for the operator's session); empty is design 51's list"
}
},
"capabilities": [
"container-runtime"
],