From 63f983897c01fa5cdf57c884fbf4d898b5409b40 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:00:10 +0100 Subject: [PATCH] =?UTF-8?q?SDK-2:=20every=20reader=20reads=20both=20refere?= =?UTF-8?q?nce=20forms:=20artifact-store://=20and=20the=20one=20a=20regist?= =?UTF-8?q?ry=20seat=20answers=20(ADR=200605=20=C2=A79,=20design=2051)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A done key's reference may be //@ through oci-artifact-registry (a bundle, an archive) or container-image-registry (an image, an upstream copy); catalogue.InRegistrySeat reads it, InArtifactStore and Routed read it as they read a kept reference, Recorded keeps it as it is. Every exported name stays. A build's list of every form (ADR 0624) is lead-47's, on feat/adr0618-reads. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/catalogue/artifacts.go | 36 +++++++++++++++++++++++++-- go/catalogue/artifacts_test.go | 16 ++++++++++++ go/delivery/builder.go | 20 +++++++++++++++ go/delivery/builder_reference_test.go | 10 ++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/go/catalogue/artifacts.go b/go/catalogue/artifacts.go index cb4bc06..2a4c7d5 100644 --- a/go/catalogue/artifacts.go +++ b/go/catalogue/artifacts.go @@ -2,6 +2,8 @@ package catalogue import ( "strings" + + contractseats "git.novox.be/novox/mesh-contracts/go/seats" ) // ArtifactStoreScheme marks a reference to something in the mesh's artifact store, kept without @@ -17,7 +19,7 @@ const ArtifactStoreScheme = ArtifactStoreProvision + "://" // in it — an image id from a genesis build that had nowhere to publish, a package version — is // what it was. func Recorded(reference string) string { - if strings.HasPrefix(reference, ArtifactStoreScheme) { + if _, _, seat := InRegistrySeat(reference); seat || strings.HasPrefix(reference, ArtifactStoreScheme) { return reference } if rest, isURL := strings.CutPrefix(reference, "http://"); isURL { @@ -39,11 +41,41 @@ func isRegistryHost(component string) bool { return component == "localhost" || strings.ContainsAny(component, ".:") } -// InArtifactStore reports whether a reference is a kept one, and what it names there. +// InArtifactStore reports whether a reference is a kept one, and what it names there: `/@` +// for an image, `//blobs/` for a bundle or an archive. **It reads both forms** (novox/hq ADR +// 0605 §9): the older `artifact-store://`, and the form a registry seat answers, `//@` +// through container-image-registry or oci-artifact-registry, whose blob it names the way the older form did. func InArtifactStore(reference string) (path string, kept bool) { + if seat, rest, ok := InRegistrySeat(reference); ok { + if seat == contractseats.OciArtifactRegistry { + name, digest, _ := strings.Cut(rest, "@") + return name + "/blobs/" + digest, true + } + return rest, true + } return strings.CutPrefix(reference, ArtifactStoreScheme) } +// RegistrySeats are the seats a reference of the registry form names first, the two that front the artifact store +// (novox/hq ADR 0605 §1, §9). +var RegistrySeats = []string{contractseats.ContainerImageRegistry, contractseats.OciArtifactRegistry} + +// 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. +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 { + return "", "", false + } + for _, s := range RegistrySeats { + if first == s { + return s, rest, true + } + } + return "", "", false +} + // 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 17ca60c..a59c2e2 100644 --- a/go/catalogue/artifacts_test.go +++ b/go/catalogue/artifacts_test.go @@ -20,10 +20,26 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { "@novox/sdk@1.2.3": "@novox/sdk@1.2.3", "gitea/gitea@" + digest: "gitea/gitea@" + digest, "https://registry.example/v2/gitea/config/blobs/" + digest: "https://registry.example/v2/gitea/config/blobs/" + digest, + // The form a registry seat answers is recorded as it is (ADR 0605 §9). + "container-image-registry/gitea/server@" + digest: "container-image-registry/gitea/server@" + digest, + "oci-artifact-registry/gitea/config@" + digest: "oci-artifact-registry/gitea/config@" + digest, } for announced, want := range cases { if got := Recorded(announced); got != want { t.Errorf("Recorded(%q) = %q, want %q", announced, got, want) } } + // And every reader reads both forms: a kept reference and the seat's are fetched from the same place. + for reference, want := range map[string]string{ + ArtifactStoreScheme + "gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest, + "container-image-registry/gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest, + ArtifactStoreScheme + "gitea/config/blobs/" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest, + "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, + } { + if got := Routed(reference, "10.0.0.1:5000"); got != want { + t.Errorf("Routed(%q) = %q, want %q", reference, got, want) + } + } } diff --git a/go/delivery/builder.go b/go/delivery/builder.go index 95468e3..42bee50 100644 --- a/go/delivery/builder.go +++ b/go/delivery/builder.go @@ -5,6 +5,9 @@ import ( "fmt" "reflect" "strings" + + contractseats "git.novox.be/novox/mesh-contracts/go/seats" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // The builder's keys (novox/hq ADR 0429 clauses 5 and 16, ADR 0437 clause 5, ADR 0405 clauses 2 and 7, ADR 0453 @@ -72,6 +75,12 @@ func ParseBuilderBuildKey(key string) (module, fingerprint string, err error) { // artifact-store:///@ catalogue.Recorded keeps without its address // package @ the version published from the trunk // +// From the registry milestone a done key names the reference its seat answered (novox/hq ADR 0605 §9, design 51 *The +// done key names the reference the seat answered*): `//@`, the registry seat it +// came through (oci-artifact-registry for a bundle or an archive, container-image-registry for an image or an upstream +// copy), and `@` for a package as before. A key written in the older form keeps it; this reads both, and +// no build is built again for its form (catalogue.InArtifactStore reads both the same way). +// // is Name and is Digest. A reference is empty for a candidate's image or package, which is never // pushed or published (ADR 0498 clause 6), and in a key done before R4e; the controller delivers only from a key whose // every artifact names one (Referenced, clause 8). A kind left empty (a key written before kinds) is not checked. @@ -90,6 +99,17 @@ func (a BuiltArtifact) referenceProblem() string { if r == "" || a.Kind == "" { return "" } + if seat, path, ok := catalogue.InRegistrySeat(r); ok { + want := contractseats.ContainerImageRegistry + if a.Kind == "bundle" || a.Kind == "archive" { + want = contractseats.OciArtifactRegistry + } + module, rest, _ := strings.Cut(path, "/") + if a.Kind == "package" || seat != want || module == "" || rest != a.Name+"@"+a.Digest { + return "is not " + want + "//" + a.Name + "@" + a.Digest + } + return "" + } switch a.Kind { case "bundle", "archive": path, ok := strings.CutPrefix(r, artifactStore) diff --git a/go/delivery/builder_reference_test.go b/go/delivery/builder_reference_test.go index 4a0dd3c..5e38964 100644 --- a/go/delivery/builder_reference_test.go +++ b/go/delivery/builder_reference_test.go @@ -24,6 +24,11 @@ func TestADoneKeyNamesEachArtifactsReferenceInTheFormOfItsKind(t *testing.T) { artifactOf("lib", "package", d, "@novox/mesh-sdk@0.1.15"), artifactOf("lib", "package", d, "left-pad@1.3.0"), artifactOf("old", "", d, "anything at all"), + // The form a registry seat answers (ADR 0605 §9), beside the older one. + artifactOf("code", "bundle", d, "oci-artifact-registry/mail/code@"+d), + 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), } for _, a := range good { b := doneBuild() @@ -43,6 +48,11 @@ func TestADoneKeyNamesEachArtifactsReferenceInTheFormOfItsKind(t *testing.T) { artifactOf("server", "image", d, "artifact-store://mail/server/blobs/"+d), artifactOf("lib", "package", d, "@novox/mesh-sdk"), artifactOf("lib", "package", d, "0.1.15"), + artifactOf("code", "bundle", d, "container-image-registry/mail/code@"+d), + artifactOf("server", "image", d, "oci-artifact-registry/mail/server@"+d), + 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), } for _, a := range bad { b := doneBuild()