Composer: the store's address is the image seat holder's listen open to the mesh (ADR 0632 §1; issue 1966)

The provision artifact-store retires, so the store is found by the holder of
container-image-registry and the listen it opens to the mesh, on the port its
node put it on; every name of the store, oci-artifact-registry's too, answers
that address. The engine states form:seat-reach with its contract.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 20:56:43 +01:00
parent 66e89b71a2
commit 6adaf44f65
13 changed files with 167 additions and 49 deletions
+3 -3
View File
@@ -9,10 +9,10 @@ import (
"time"
)
// RegistryModule is the module that provides the mesh's artifact store.
// RegistryModule is the module that holds the mesh's artifact store.
//
// **Named for the software, not the job.** The job is `artifact-store`, which is the provision this
// module offers; the software is Distribution, the OCI reference implementation. Calling the module
// **Named for the software, not the job.** The job is the artifact store, which this module holds as
// the seat container-image-registry (novox/hq ADR 0632 §1); the software is Distribution, the OCI reference implementation. Calling the module
// `registry` named neither — and promised that any registry could sit there, which is the false
// genericity the naming rule forbids (novox/hq ADR 0040).
const RegistryModule = "distribution"
+2 -1
View File
@@ -109,7 +109,8 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
Bundles: []catalogue.Bundle{{Name: "runtime", Source: catalogue.ArtifactStoreScheme + "node-tools/runtime/blobs/" + dg('a'),
Digest: dg('a'), Language: "go", Binary: "node-tools"}}}
store := catalogue.Manifest{Module: "artifacts", Version: "1",
Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5100}}}
Claims: []catalogue.Claim{{Name: "container-image-registry", Scope: catalogue.ScopeMesh}},
Listens: []catalogue.Listening{{Name: "registry", Port: 5100, From: catalogue.FromMesh}}}
lamp := catalogue.Manifest{Module: "lamp", Version: "1", Tools: []string{"on"},
Resources: []map[string]any{{"id": "state", "type": "directory", "path": "/var/lib/lamp", "mode": "0700"}},
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "tools", Kind: catalogue.ArtifactBundle,
+6 -4
View File
@@ -606,13 +606,15 @@ func otherNodeNotResolvedIsNamed(t *testing.T) {
// "reaches no artifact store", and nothing said why).
func noStoreReachedSaysWhy(t *testing.T) {
in := compose.Input{Catalogue: map[string]catalogue.Manifest{
"distribution": {Module: "distribution", Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5000}}}},
Nodes: []compose.Node{{Name: "g14", Assigned: []string{"adwaita"}}, {Name: "novox", Assigned: []string{"distribution"}}}}
if got := storeSought(in); got != "the composition holds g14, novox; artifact-store is served by distribution on novox, none of it on the private network with a port" {
"distribution": {Module: "distribution", Claims: []catalogue.Claim{{Name: "container-image-registry", Scope: catalogue.ScopeMesh}}},
// A module serving the retired provision artifact-store is no store (ADR 0632 §1).
"old-store": {Module: "old-store", Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5000}}}},
Nodes: []compose.Node{{Name: "g14", Assigned: []string{"adwaita", "old-store"}}, {Name: "novox", Assigned: []string{"distribution"}}}}
if got := storeSought(in); got != "the composition holds g14, novox; container-image-registry is held by distribution on novox, none of it on the private network with a listen open to the mesh" {
t.Errorf("said %q", got)
}
in.Nodes = in.Nodes[:1]
if got := storeSought(in); got != "the composition holds g14; no module placed there serves artifact-store" {
if got := storeSought(in); got != "the composition holds g14; no module placed there holds container-image-registry" {
t.Errorf("said %q", got)
}
}
+16 -8
View File
@@ -1136,16 +1136,14 @@ func sortedKeys[V any](m map[string]V) []string {
}
// storeSought says, for a node that reaches no artifact store, what the composition holds of one: the nodes it holds and
// where each module serving the artifact store is placed, so the refusal names why (issue 1126).
// where each module claiming the store's seat is placed, so the refusal names why (issue 1126, ADR 0632 §1).
func storeSought(in compose.Input) string {
var nodes, where []string
for _, n := range in.Nodes {
nodes = append(nodes, n.Name)
for _, a := range n.Assigned {
if m, ok := in.Catalogue[a]; ok {
if _, serves := m.Serves[catalogue.ArtifactStoreProvision]; serves {
where = append(where, a+" on "+n.Name)
}
if m, ok := in.Catalogue[a]; ok && claimsSeat(m, compose.StoreSeat) {
where = append(where, a+" on "+n.Name)
}
}
}
@@ -1153,10 +1151,20 @@ func storeSought(in compose.Input) string {
sort.Strings(where)
said := "the composition holds " + strings.Join(nodes, ", ")
if len(where) == 0 {
return said + "; no module placed there serves " + catalogue.ArtifactStoreProvision
return said + "; no module placed there holds " + compose.StoreSeat
}
return said + "; " + catalogue.ArtifactStoreProvision + " is served by " + strings.Join(where, ", ") +
", none of it on the private network with a port"
return said + "; " + compose.StoreSeat + " is held by " + strings.Join(where, ", ") +
", none of it on the private network with a listen open to the mesh"
}
// claimsSeat says whether m claims seat at the mesh's scope.
func claimsSeat(m catalogue.Manifest, seat string) bool {
for _, cl := range m.Claims {
if cs, ok := catalogue.SeatNamed(cl.Name); ok && cs.Name == seat && cl.At() == catalogue.ScopeMesh {
return true
}
}
return false
}
// keptOf is the operator export's file (catalogue.KeptID) of a module's composition as it runs, or nil.
+2 -2
View File
@@ -577,8 +577,8 @@ func TestInput(t *testing.T) {
if err != nil {
t.Fatal(err)
}
store := []byte(`{"module":"store","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],` +
`"serves":{"artifact-store":{"port":5000}}}`)
store := []byte(`{"module":"store","version":"1",` +
`"claims":[{"name":"container-image-registry","scope":"mesh"}],"listens":[{"name":"registry","port":5000,"from":"mesh"}]}`)
texts := map[string]string{}
var packed bytes.Buffer
zw := gzip.NewWriter(&packed)
@@ -119,8 +119,8 @@ func Provisioned() (*Bus, error) {
if err != nil {
return nil, err
}
store := []byte(`{"module":"store","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],` +
`"serves":{"artifact-store":{"port":5000}}}`)
store := []byte(`{"module":"store","version":"1",` +
`"claims":[{"name":"container-image-registry","scope":"mesh"}],"listens":[{"name":"registry","port":5000,"from":"mesh"}]}`)
postgres := []byte(`{"module":"postgres","version":"1","provides":[{"name":"postgres-database","scope":"mesh"}],` +
`"serves":{"postgres-database":{"port":5432}}}`)
+2 -2
View File
@@ -119,7 +119,7 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) {
tools := `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` +
`"build":{"artifacts":[{"name":"runtime","kind":"bundle","language":"go","system":"arch","from":"cmd/node-tools"}]}}`
lamp := `{"module":"lamp","version":"1","resources":[{"id":"service","type":"service","unit":"lamp.service","state":"running","boot":"enabled"}]}`
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"serves":{"artifact-store":{"port":5100}},` +
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"claims":[{"name":"container-image-registry","scope":"mesh"}],"listens":[{"name":"registry","port":5100,"from":"mesh"}],` +
`"receives":{"artifact-store":"/var/lib/artifacts/received.json"}}`
old, next := "src2:"+strings.Repeat("a", 64), "src2:"+strings.Repeat("b", 64)
running := []delivery.Composed{{Node: "n1", Module: "node-tools", FP: old, Resources: []json.RawMessage{
@@ -215,7 +215,7 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) {
`"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["tools/index.js"]}]}}`
}
lamp := `{"module":"lamp","version":"1","resources":[{"id":"service","type":"service","unit":"lamp.service","state":"running","boot":"enabled"}]}`
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"serves":{"artifact-store":{"port":5100}},` +
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"claims":[{"name":"container-image-registry","scope":"mesh"}],"listens":[{"name":"registry","port":5100,"from":"mesh"}],` +
`"receives":{"artifact-store":"/var/lib/artifacts/received.json"}}`
for _, row := range []struct {
name string
+71 -16
View File
@@ -167,10 +167,10 @@ func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) {
// Where this machine reaches each mesh seat's holder, for ${seat:<seat>:reach} (novox/hq ADR 0222).
if store != "" {
// Both names the store is reached through answer the one address (ENG-3), each with its holder from the holdings on
// record, written beside every image routed through it (design 51, ENG-10).
// Every name the store is reached through answers the one address (ENG-3, ADR 0632 §1), each with its holder from the
// holdings on record, written beside every image routed through it (design 51, ENG-10).
r.SeatReach, r.SeatHolder = map[string]string{}, map[string]string{}
for _, seat := range []string{"mesh-artifact-store", "container-image-registry"} {
for _, seat := range resolution.StoreSeats {
r.SeatReach[seat] = store
for _, h := range c.in.Holdings {
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat && h.Scope == catalogue.ScopeMesh && h.Module != "" {
@@ -326,18 +326,67 @@ func (c *composer) namesInTheMesh() map[string]string {
return out
}
// artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the port that machine put it on.
func (c *composer) artifactStoreOnNetwork(on map[string]bool) (node, port string, found bool, err error) {
providers := map[string]catalogue.Manifest{}
for name, m := range c.in.Catalogue {
if _, offers := m.Serves[catalogue.ArtifactStoreProvision]; offers {
providers[name] = m
// StoreSeat is the seat whose holder is the artifact store (novox/hq ADR 0632 §1): the store's address is that holder's
// listen open to the mesh, on the machine placement puts it on. The provision artifact-store it was found by before is
// retired; no module serves it.
const StoreSeat = "container-image-registry"
// storeHolders are the modules holding the store's seat: the holdings on record, and only where none is on record every
// module of the catalogue claiming it at the mesh's scope, as a seat holder's reach is read (replicatedHolders).
func (c *composer) storeHolders() map[string]catalogue.Manifest {
out := map[string]catalogue.Manifest{}
for _, h := range c.in.Holdings {
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == StoreSeat && h.Scope == catalogue.ScopeMesh && h.Module != "" {
if m, known := c.in.Catalogue[h.Module]; known {
out[h.Module] = m
}
}
}
if len(providers) == 0 {
if len(out) > 0 {
return out
}
for name, m := range c.in.Catalogue {
for _, cl := range m.Claims {
if cs, ok := catalogue.SeatNamed(cl.Name); ok && cs.Name == StoreSeat && cl.At() == catalogue.ScopeMesh {
out[name] = m
}
}
}
return out
}
// storeListen is the store holder's listen every machine pulls from: its one listen open to the mesh. A holder opening
// none, or more than one, says no address, by name: which of two ports is the store is no guess the composer makes.
func storeListen(m catalogue.Manifest) (catalogue.Listening, error) {
var open []catalogue.Listening
for _, l := range m.Listens {
if l.From == catalogue.FromMesh && l.At() == "tcp" {
open = append(open, l)
}
}
switch len(open) {
case 1:
return open[0], nil
case 0:
return catalogue.Listening{}, fmt.Errorf("%s holds %s and opens no tcp listen to the mesh: the artifact store has no "+
"address (novox/hq ADR 0632 §1)", m.Module, StoreSeat)
}
var said []string
for _, l := range open {
said = append(said, fmt.Sprintf("%s (%d)", l.Name, l.Port))
}
return catalogue.Listening{}, fmt.Errorf("%s holds %s and opens %s to the mesh: the artifact store's address is its one "+
"listen open to the mesh (novox/hq ADR 0632 §1)", m.Module, StoreSeat, strings.Join(said, ", "))
}
// artifactStoreOnNetwork is the machine on this network that holds the store's seat, and the port that machine put the
// holder's listen on (novox/hq ADR 0632 §1).
func (c *composer) artifactStoreOnNetwork(on map[string]bool) (node, port string, found bool, err error) {
holders := c.storeHolders()
if len(holders) == 0 {
return "", "", false, nil
}
// In a stated order, so two machines offering it would always answer the same one.
// In a stated order, so two machines holding it would always answer the same one.
machines := make([]string, 0, len(on))
for machine := range on {
machines = append(machines, machine)
@@ -349,17 +398,23 @@ func (c *composer) artifactStoreOnNetwork(on map[string]bool) (node, port string
return "", "", false, fmt.Errorf("reading what %s is assigned: %w", machine, err)
}
for _, a := range n.Assigned {
m, offers := providers[a]
if !offers {
m, holds := holders[a]
if !holds {
continue
}
serves, _, err := c.servedOnNode(machine, m, catalogue.ArtifactStoreProvision)
l, err := storeListen(m)
if err != nil {
return "", "", false, err
}
ports, _, err := c.portsGivenOn(machine, m)
if err != nil {
return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err)
}
if p, ok := serves["port"]; ok {
return machine, fmt.Sprintf("%v", p), true, nil
at := l.Port
if given, ok := ports[l.Port]; ok {
at = given
}
return machine, fmt.Sprintf("%d", at), true, nil
}
}
return "", "", false, nil
+37 -1
View File
@@ -254,7 +254,8 @@ func TestTheNodeEnginesContractGatesWhatItIsSent(t *testing.T) {
func TestTheArtifactStoreIsReachedOverTheNetworkOrOnLoopbackForItsHolder(t *testing.T) {
in := aMesh(t)
in.Catalogue["artifacts"] = catalogue.Manifest{Module: "artifacts", Version: "1",
Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5100}}}
Claims: []catalogue.Claim{{Name: "container-image-registry", Scope: catalogue.ScopeMesh}},
Listens: []catalogue.Listening{{Name: "registry", Port: 5100, From: catalogue.FromMesh}}}
anchor := nodeOf(t, &in, "anchor")
anchor.Assigned = append(anchor.Assigned, "artifacts")
if got := placed(t, in, "laptop").Rendering.ArtifactStore; got != "anchor.internal:5100" {
@@ -290,6 +291,41 @@ func TestTheArtifactStoreIsReachedOverTheNetworkOrOnLoopbackForItsHolder(t *test
}
}
// The store's address is the image seat's holder's listen open to the mesh, on the port its node put it on, and every
// name of the store answers it (novox/hq ADR 0632 §1): a module serving the retired provision artifact-store is no store,
// and a holder opening two listens to the mesh says which is the store by no guess.
func TestTheStoreIsFoundByTheImageSeatsHolderAndItsListen(t *testing.T) {
in := aMesh(t)
in.Catalogue["old"] = catalogue.Manifest{Module: "old", Version: "1",
Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5100}}}
anchor := nodeOf(t, &in, "anchor")
anchor.Assigned = append(anchor.Assigned, "old")
if got := placed(t, in, "laptop").Rendering.ArtifactStore; got != "" {
t.Errorf("a module serving artifact-store is reached as the store at %q", got)
}
in.Catalogue["registry"] = catalogue.Manifest{Module: "registry", Version: "1",
Claims: []catalogue.Claim{{Name: "container-image-registry", Scope: catalogue.ScopeMesh}},
Listens: []catalogue.Listening{{Name: "local", Port: 5001, From: catalogue.FromMachine},
{Name: "registry", Port: 5000, From: catalogue.FromMesh}}}
anchor.Assigned = append(anchor.Assigned, "registry")
anchor.Ports = map[string]map[int]int{"registry": {5000: 5200}}
r := placed(t, in, "laptop").Rendering
if r.ArtifactStore != "anchor.internal:5200" {
t.Errorf("laptop reaches the store at %q, want the holder's listen open to the mesh on anchor's port", r.ArtifactStore)
}
for _, seat := range []string{"mesh-artifact-store", "container-image-registry", "oci-artifact-registry"} {
if r.SeatReach[seat] != "anchor.internal:5200" {
t.Errorf("${seat:%s:reach} is %q", seat, r.SeatReach[seat])
}
}
m := in.Catalogue["registry"]
m.Listens = append(m.Listens, catalogue.Listening{Name: "other", Port: 5300, From: catalogue.FromMesh})
in.Catalogue["registry"] = m
if _, err := Places(in, "laptop"); err == nil || !strings.Contains(err.Error(), "opens registry (5000), other (5300) to the mesh") {
t.Errorf("a holder opening two listens to the mesh: %v", err)
}
}
// A zone a module declares is placed where the mesh put it: settled from that node's settings, at the node's private address, on
// the port the answering listen is published on there (novox/hq ADR 0199); the same zone declared twice is refused, by name.
func TestAZoneIsPlacedWhereTheMeshPutItAndTheSameZoneTwiceIsRefused(t *testing.T) {
+3 -3
View File
@@ -36,9 +36,9 @@ func artifactsInto(resource map[string]any, module string, with mcat.Rendering)
if with.ArtifactStore == "" {
return fmt.Errorf(
"%s's %v names %s, which is in the mesh's artifact store, and this mesh has no "+
"artifact store on its network to fetch it from — nothing assigned offers "+
"%s, or the machine offering it is not on the private network",
module, resource["id"], written, mcat.ArtifactStoreProvision)
"artifact store on its network to fetch it from — nothing assigned holds the seat "+
"container-image-registry, or the machine holding it is not on the private network",
module, resource["id"], written)
}
resource[key] = mcat.Routed(written, with.ArtifactStore)
imageSeatInto(resource, key, written, with)
+12 -3
View File
@@ -34,9 +34,18 @@ var ofSeatReach = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):reach\}`)
// reachedSeats is every seat ${seat:…:reach} answers for.
//
// Both names of the store are answered with one address (ADR 0605, issue 1966 ENG-3): the seat it was held through and the
// seat the registries' holder serves the same port as.
var reachedSeats = map[string]bool{"mesh-artifact-store": true, "container-image-registry": true}
// Every name of the store is answered with one address (ADR 0605, issue 1966 ENG-3, ADR 0632 §1): the seat it was held
// through and the two registry seats its holder serves on the same port, the image seat's holder's listen open to the mesh.
var reachedSeats = map[string]bool{}
// StoreSeats are the seats the artifact store is reached through, each answered with the one address.
var StoreSeats = []string{"mesh-artifact-store", "container-image-registry", "oci-artifact-registry"}
func init() {
for _, s := range StoreSeats {
reachedSeats[s] = true
}
}
// seatInto replaces a resource's ${seat:…} placeholders with where this machine put each seat's
// holder — in a file's content, and in a value of a container's or a process's environment. The
@@ -111,15 +111,18 @@ func TestASeatsReachIsWhereThisMachineReachesItsHolder(t *testing.T) {
t.Fatalf("an environment value was filled with %q", got)
}
// And the seat the registries' holder serves the same port through answers the same address (ENG-3).
// And the seats the registries' holder serves the same port through answer the same address (ENG-3, ADR 0632 §1).
both := map[string]any{"type": "process", "name": "p", "env": map[string]any{
"OLD": "${seat:mesh-artifact-store:reach}", "NEW": "${seat:container-image-registry:reach}"}}
"OLD": "${seat:mesh-artifact-store:reach}", "NEW": "${seat:container-image-registry:reach}",
"OCI": "${seat:oci-artifact-registry:reach}"}}
with.SeatReach["container-image-registry"] = "anchor.internal:5100"
with.SeatReach["oci-artifact-registry"] = "anchor.internal:5100"
if err := seatInto(both, "x", with); err != nil {
t.Fatal(err)
}
if env := both["env"].(map[string]any); env["OLD"] != "anchor.internal:5100" || env["NEW"] != "anchor.internal:5100" {
t.Fatalf("the two names of the store answer %v", env)
if env := both["env"].(map[string]any); env["OLD"] != "anchor.internal:5100" || env["NEW"] != "anchor.internal:5100" ||
env["OCI"] != "anchor.internal:5100" {
t.Fatalf("the three names of the store answer %v", env)
}
}
+4
View File
@@ -286,6 +286,10 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) {
if !delivery.SameNodeRecord(*got.Record, wantRecord) {
t.Errorf("record = %+v, want %+v", *got.Record, wantRecord)
}
// Among them, that it fills a seat's reach from the seat holder's listen (ADR 0632 §1).
if !slices.Contains(got.Record.ManifestReads, catalogue.ReadForm+catalogue.ReferenceFormSeatReach) {
t.Errorf("the engine does not state it reads a seat's reach from its holder: %v", got.Record.ManifestReads)
}
at := l.kv["n1.node"].rev
q.SayFacts(context.Background(), rep)
if l.kv["n1.node"].rev != at {