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/<host>/<path>@<digest>, 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
This commit is contained in:
2026-11-04 15:58:59 +01:00
parent 7eba326d3d
commit 64e83c7ee2
4 changed files with 66 additions and 1 deletions
+31 -1
View File
@@ -89,9 +89,18 @@ func SeatReference(seat, module, artifact, digest string) string {
// InRegistrySeat reads a reference in the form a registry seat answers, `<seat>/<module>/<artifact>@sha256:<digest>`:
// 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/<host>/<path>@sha256:<digest>`, 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/<host>/<path>@<digest>`, 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/<host>/<path>@sha256:<digest>` (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 {
+18
View File
@@ -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)
}
}
}
+10
View File
@@ -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/<host>/<path>@<digest>, 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" {
+7
View File
@@ -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/<host>/<path>, 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()