From 64e83c7ee2dc89f2727846b0cf58127c059bb18d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 15:58:59 +0100 Subject: [PATCH] J8b: catalogue reads the holder's upstream naming, and a done key may name an upstream copy by it An upstream copy kept by the artifact store is named by the holder, upstream//@, in the seat form (container-image-registry/upstream/...) and the older one (artifact-store://upstream/...). InRegistrySeat reads it, InUpstream reads its host and path, and referenceProblem accepts it for an upstream artifact, so readers stop reading it on their own. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/catalogue/artifacts.go | 32 ++++++++++++++++++++++++++- go/catalogue/artifacts_test.go | 18 +++++++++++++++ go/delivery/builder.go | 10 +++++++++ go/delivery/builder_reference_test.go | 7 ++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/go/catalogue/artifacts.go b/go/catalogue/artifacts.go index 6bea900..42d0b99 100644 --- a/go/catalogue/artifacts.go +++ b/go/catalogue/artifacts.go @@ -89,9 +89,18 @@ func SeatReference(seat, module, artifact, digest string) string { // InRegistrySeat reads a reference in the form a registry seat answers, `//@sha256:`: // the seat it came through and the rest. A seat's name holds no dot or port, so no image of an outside registry reads // as one, and a kept `artifact-store://` reference is not one. +// +// It also reads the holder's own naming of an upstream copy through container-image-registry, +// `container-image-registry/upstream//@sha256:`, whose rest InUpstream reads. func InRegistrySeat(reference string) (seat, path string, ok bool) { first, rest, found := strings.Cut(reference, "/") - if !found || !strings.Contains(rest, "@sha256:") || strings.Count(rest, "/") != 1 { + if !found || !strings.Contains(rest, "@sha256:") { + return "", "", false + } + if _, _, upstream := InUpstream(rest); upstream && first == ContainerImageRegistrySeat { + return first, rest, true + } + if strings.Count(rest, "/") != 1 { return "", "", false } for _, s := range RegistrySeats { @@ -102,6 +111,27 @@ func InRegistrySeat(reference string) (seat, path string, ok bool) { return "", "", false } +// UpstreamPrefix opens the holder's own name for an upstream copy it keeps: `upstream//@`, after +// `container-image-registry/` in the seat's form and after `artifact-store://` in the older one. +const UpstreamPrefix = "upstream/" + +// InUpstream reads a path in the holder's naming of an upstream copy, `upstream//@sha256:` (the path +// InRegistrySeat or InArtifactStore gives back): the upstream registry host and the image's path there, with its digest. +// A host is read as the runtime reads one (a dot, a port, or localhost). +func InUpstream(path string) (host, image string, ok bool) { + rest, found := strings.CutPrefix(path, UpstreamPrefix) + if !found { + return "", "", false + } + host, image, found = strings.Cut(rest, "/") + name, digest, pinned := strings.Cut(image, "@") + if !found || !isRegistryHost(host) || name == "" || strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") || + strings.Contains(name, "//") || !pinned || !strings.HasPrefix(digest, "sha256:") || strings.Contains(digest, "/") { + return "", "", false + } + return host, image, true +} + // Routed is a kept reference as a machine fetches it, through the artifact store at `address` // (host:port). A reference that is not a kept one is what it was. func Routed(reference, address string) string { diff --git a/go/catalogue/artifacts_test.go b/go/catalogue/artifacts_test.go index 9ff124e..252ca22 100644 --- a/go/catalogue/artifacts_test.go +++ b/go/catalogue/artifacts_test.go @@ -37,6 +37,9 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { "oci-artifact-registry/gitea/config@" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest, "registry.example/gitea/server@" + digest: "registry.example/gitea/server@" + digest, "mesh-artifact-store/gitea/server@" + digest: "mesh-artifact-store/gitea/server@" + digest, + // An upstream copy under the holder's own naming, in both forms. + "container-image-registry/upstream/quay.io:443/org/app@" + digest: "10.0.0.1:5000/upstream/quay.io:443/org/app@" + digest, + ArtifactStoreScheme + "upstream/quay.io:443/org/app@" + digest: "10.0.0.1:5000/upstream/quay.io:443/org/app@" + digest, } { if got := Routed(reference, "10.0.0.1:5000"); got != want { t.Errorf("Routed(%q) = %q, want %q", reference, got, want) @@ -49,4 +52,19 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { t.Errorf("SeatReference(%s) = %q, read back as %q %q %v", seat, ref, got, path, ok) } } + // The holder's naming of an upstream copy reads through the image seat only, and InUpstream reads its host and path. + for reference, want := range map[string]bool{ + "container-image-registry/upstream/quay.io:443/org/app@" + digest: true, + "oci-artifact-registry/upstream/quay.io:443/org/app@" + digest: false, + "container-image-registry/upstream/org/app@" + digest: false, + "container-image-registry/upstream/quay.io/app": false, + } { + _, path, ok := InRegistrySeat(reference) + if ok != want { + t.Errorf("InRegistrySeat(%q) read %v, want %v", reference, ok, want) + } + if host, image, up := InUpstream(path); ok && (!up || host != "quay.io:443" || image != "org/app@"+digest) { + t.Errorf("InUpstream(%q) = %q %q %v", path, host, image, up) + } + } } diff --git a/go/delivery/builder.go b/go/delivery/builder.go index 32bf4c1..2c1439d 100644 --- a/go/delivery/builder.go +++ b/go/delivery/builder.go @@ -103,6 +103,16 @@ func (a BuiltArtifact) referenceProblem() string { if r == "" || a.Kind == "" { return "" } + // An upstream copy may be named by the holder's own naming, upstream//@, in either form: the + // holder names it, not the module that asked for it (catalogue.InUpstream). + if path, kept := catalogue.InArtifactStore(r); kept && a.Kind == "upstream" { + if _, image, ok := catalogue.InUpstream(path); ok { + if _, digest, _ := strings.Cut(image, "@"); digest != a.Digest { + return "does not name its digest " + a.Digest + } + return "" + } + } if seat, path, ok := catalogue.InRegistrySeat(r); ok { want := catalogue.ContainerImageRegistrySeat if a.Kind == "bundle" || a.Kind == "archive" { diff --git a/go/delivery/builder_reference_test.go b/go/delivery/builder_reference_test.go index d810b16..db1ef2a 100644 --- a/go/delivery/builder_reference_test.go +++ b/go/delivery/builder_reference_test.go @@ -29,6 +29,9 @@ func TestADoneKeyNamesEachArtifactsReferenceInTheFormOfItsKind(t *testing.T) { artifactOf("files", "archive", d, "oci-artifact-registry/mail/files@"+d), artifactOf("server", "image", d, "container-image-registry/mail/server@"+d), artifactOf("nats", "upstream", d, "container-image-registry/nats/nats@"+d), + // An upstream copy under the holder's own naming, upstream//, in either form (ADR 0624 ยง1). + artifactOf("app", "upstream", d, "container-image-registry/upstream/quay.io:443/org/app@"+d), + artifactOf("app", "upstream", d, "artifact-store://upstream/quay.io:443/org/app@"+d), } for _, a := range good { b := doneBuild() @@ -53,6 +56,10 @@ func TestADoneKeyNamesEachArtifactsReferenceInTheFormOfItsKind(t *testing.T) { artifactOf("server", "image", d, "container-image-registry/mail/server@"+other), artifactOf("server", "image", d, "container-image-registry/mail/other@"+d), artifactOf("lib", "package", d, "container-image-registry/mail/lib@"+d), + artifactOf("app", "upstream", d, "container-image-registry/upstream/quay.io:443/org/app@"+other), + artifactOf("app", "upstream", d, "artifact-store://upstream/quay.io:443/org/app@"+other), + artifactOf("app", "upstream", d, "oci-artifact-registry/upstream/quay.io:443/org/app@"+d), + artifactOf("server", "image", d, "container-image-registry/upstream/quay.io:443/org/server@"+d), } for _, a := range bad { b := doneBuild()