diff --git a/internal/apply/replace_image_test.go b/internal/apply/replace_image_test.go index 48ff6a9..99f6b5b 100644 --- a/internal/apply/replace_image_test.go +++ b/internal/apply/replace_image_test.go @@ -111,6 +111,16 @@ func TestAReplacementImageIsPresentBeforeTheRunningContainerIsRemoved(t *testing {"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}} ]}`) want := containerSpec(d.Resources[0].(*declaration.Container), inputs{}) + // The seat and holder a routed image names (ENG-10) are no part of what a container is: another holder is the same spec, + // so a declaration composed again for it replaces no container. + for _, holder := range []string{"distribution", "elsewhere"} { + named := parseTrusted(t, `{"declaration":1,"resources":[ + {"id":"store","type":"container","name":"store","image":"`+pinned+`","image-seat":"mesh-artifact-store","image-holder":"`+holder+`","env":{"PGDATA":"/data"}} + ]}`) + if got := containerSpec(named.Resources[0].(*declaration.Container), inputs{}); got != want { + t.Errorf("a container naming the holder %s has another spec than one naming none", holder) + } + } var order []string created := false run := func(ctx context.Context, name string, args ...string) (string, error) { diff --git a/internal/compose/rendering.go b/internal/compose/rendering.go index 7aaae27..eb0e47d 100644 --- a/internal/compose/rendering.go +++ b/internal/compose/rendering.go @@ -168,6 +168,13 @@ func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) { // Where this machine reaches each mesh seat's holder, for ${seat::reach} (novox/hq ADR 0222). if store != "" { r.SeatReach = map[string]string{"mesh-artifact-store": store} + // And its holder, from the holdings on record, written beside each image routed through it (design 51, ENG-10). + for _, h := range c.in.Holdings { + if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == "mesh-artifact-store" && h.Scope == catalogue.ScopeMesh && h.Module != "" { + r.SeatHolder = map[string]string{"mesh-artifact-store": h.Module} + break + } + } } r.Settings = settings r.Given = given diff --git a/internal/compose/rendering_test.go b/internal/compose/rendering_test.go index 54b9c71..f8d1448 100644 --- a/internal/compose/rendering_test.go +++ b/internal/compose/rendering_test.go @@ -272,6 +272,22 @@ func TestTheArtifactStoreIsReachedOverTheNetworkOrOnLoopbackForItsHolder(t *test if got := placed(t, in, "anchor").Rendering.Built; !got["mesh-sdk"] { t.Errorf("built is %v", got) } + // Its holder, from the holdings on record, beside the address (design 51, ENG-10): none on record names none, and a change + // of the holder is the rendering's. + if got := placed(t, in, "anchor").Rendering.SeatHolder; got != nil { + t.Errorf("a store with no holder on record names %v", got) + } + in.Holdings = []catalogue.Held{{Claim: "mesh-artifact-store", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "artifacts"}} + if got := placed(t, in, "laptop").Rendering.SeatHolder["mesh-artifact-store"]; got != "" { + t.Errorf("a machine that reaches no store names its holder %q", got) + } + if got := placed(t, in, "anchor").Rendering.SeatHolder["mesh-artifact-store"]; got != "artifacts" { + t.Errorf("the store's holder is %q", got) + } + in.Holdings[0].Module = "distribution" + if got := placed(t, in, "anchor").Rendering.SeatHolder["mesh-artifact-store"]; got != "distribution" { + t.Errorf("after the holder changed it is %q", got) + } } // 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 diff --git a/internal/compose/resolution/artifacts.go b/internal/compose/resolution/artifacts.go index 559ac1b..ed10c5d 100644 --- a/internal/compose/resolution/artifacts.go +++ b/internal/compose/resolution/artifacts.go @@ -41,6 +41,7 @@ func artifactsInto(resource map[string]any, module string, with mcat.Rendering) module, resource["id"], written, mcat.ArtifactStoreProvision) } resource[key] = mcat.Routed(written, with.ArtifactStore) + imageSeatInto(resource, key, with) continue } if with.ArtifactStore == "" { @@ -59,7 +60,24 @@ func artifactsInto(resource map[string]any, module string, with mcat.Rendering) } if with.Built[repository] { resource[key] = mcat.Routed(recorded, with.ArtifactStore) + imageSeatInto(resource, key, with) } } return nil } + +// artifactSeat is the seat an image is routed through: the artifact store's (the registries' four seats follow as the +// references name them). +const artifactSeat = "mesh-artifact-store" + +// imageSeatInto writes beside a container's routed image the seat the reference came through and that seat's holder (design 51 +// *distribution's first delivery*, issue 1966 ENG-10), so the node-engine finds by the declaration whom a failed pull waits +// on, never by a module's name. Neither is part of what a container is: a declaration composed again for a new holder +// replaces no container. Nothing is written where the holder is not on record, or for a resource that is no container. +func imageSeatInto(resource map[string]any, key string, with mcat.Rendering) { + if key != "image" || resource["type"] != "container" || with.SeatHolder[artifactSeat] == "" { + return + } + resource["image-seat"] = artifactSeat + resource["image-holder"] = with.SeatHolder[artifactSeat] +} diff --git a/internal/compose/resolution/artifacts_test.go b/internal/compose/resolution/artifacts_test.go index e9ab076..9cdd471 100644 --- a/internal/compose/resolution/artifacts_test.go +++ b/internal/compose/resolution/artifacts_test.go @@ -46,6 +46,29 @@ func TestAnImageTheMeshBuiltIsRoutedThroughTheStoreWhenDeclared(t *testing.T) { t.Errorf("composing wrote the address into the catalogue's copy: %v", got) } + // The seat the image came through and its holder, beside it (ENG-10); a holder not on record writes neither, a public + // image and an archive carry none, and a holder changing changes the declaration but not the container's spec. + if got := fileNamed(out, "gitea.server"); got["image-seat"] != nil || got["image-holder"] != nil { + t.Errorf("a holder not on record was written: %v %v", got["image-seat"], got["image-holder"]) + } + held := mcat.Rendering{ArtifactStore: "anchor.internal:5101", SeatHolder: map[string]string{"mesh-artifact-store": "distribution"}} + held1, err := Declaration(r, held) + if err != nil { + t.Fatal(err) + } + if got := fileNamed(held1, "gitea.server"); got["image-seat"] != "mesh-artifact-store" || got["image-holder"] != "distribution" { + t.Errorf("the routed image names its seat and holder as %v %v", got["image-seat"], got["image-holder"]) + } + for _, id := range []string{"gitea.config", "gitea.cache"} { + if got := fileNamed(held1, id); got["image-seat"] != nil || got["image-holder"] != nil || got["seat"] != nil { + t.Errorf("%s names a seat: %v", id, got) + } + } + held.SeatHolder = map[string]string{"mesh-artifact-store": "other-holder"} + if held2, err := Declaration(r, held); err != nil || fileNamed(held2, "gitea.server")["image-holder"] != "other-holder" { + t.Errorf("a new holder is not written: %v", err) + } + // And the store moves: the same record, another address, without a rebuild. out, err = Declaration(r, mcat.Rendering{ArtifactStore: "laptop.internal:5000"}) if err != nil {