From df6cc0992bc696cce1cf812cc2b5db982f7400fc Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:27:03 +0100 Subject: [PATCH] =?UTF-8?q?mesh-delivery-sdk:=20the=20fingerprint's=20key?= =?UTF-8?q?=20part=20and=20the=20registry=20reference=20forms,=20seat-neut?= =?UTF-8?q?ral=20over=20the=20registry=20seats'=20names=20the=20caller=20g?= =?UTF-8?q?ives=20(D-10=20base,=20ADR=200634=20=C2=A71,=20ADR=200635=20?= =?UTF-8?q?=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/fingerprint/fingerprint.go | 42 ++++++++++ sdk/fingerprint/fingerprint_test.go | 34 ++++++++ sdk/reference/reference.go | 116 ++++++++++++++++++++++++++++ sdk/reference/reference_test.go | 69 +++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 sdk/fingerprint/fingerprint.go create mode 100644 sdk/fingerprint/fingerprint_test.go create mode 100644 sdk/reference/reference.go create mode 100644 sdk/reference/reference_test.go diff --git a/sdk/fingerprint/fingerprint.go b/sdk/fingerprint/fingerprint.go new file mode 100644 index 00000000..34b60bec --- /dev/null +++ b/sdk/fingerprint/fingerprint.go @@ -0,0 +1,42 @@ +// Package fingerprint holds the form of a module's source fingerprint, `src2:` and 64 lowercase hex digits (novox/hq ADR 0593 +// §2): the indexer computes it alone, in its own package; every part that keys a state by it reads this form. It is the +// Delivery domain's rule, over the `fp2` field of mesh-catalog's `modules` value (ADR 0634 §1). +package fingerprint + +import ( + "fmt" + "strings" +) + +// SourcePrefix2 opens every source fingerprint of the form. +const SourcePrefix2 = "src2:" + +// Toolchain2 is what an artifact's toolchain enters the fingerprint as (novox/hq ADR 0405 clause 7): the artifact's +// language, the installed toolchain's version and the build module's recipe version. +type Toolchain2 struct { + Language string + Version string + Recipe string +} + +// KeyPart is a fingerprint as a key part: its 64 hex digits without the prefix, since a bus key may not hold ':'. It +// refuses a value that is not `src2:` and 64 lowercase hex digits, a src1 fingerprint included, naming it. +func KeyPart(fp string) (string, error) { + part, ok := strings.CutPrefix(fp, SourcePrefix2) + if !ok || !isHex64(part) { + return "", fmt.Errorf("fingerprint: %q is not a fingerprint of the form, %s and 64 lowercase hex digits", fp, SourcePrefix2) + } + return part, nil +} + +// ParseKeyPart is the inverse of KeyPart. It refuses a part that is not 64 lowercase hex digits, naming it. +func ParseKeyPart(part string) (string, error) { + if !isHex64(part) { + return "", fmt.Errorf("fingerprint: %q is not the key part of a fingerprint, 64 lowercase hex digits", part) + } + return SourcePrefix2 + part, nil +} + +func isHex64(s string) bool { + return len(s) == 64 && strings.Trim(s, "0123456789abcdef") == "" +} diff --git a/sdk/fingerprint/fingerprint_test.go b/sdk/fingerprint/fingerprint_test.go new file mode 100644 index 00000000..11efe273 --- /dev/null +++ b/sdk/fingerprint/fingerprint_test.go @@ -0,0 +1,34 @@ +package fingerprint + +import ( + "strings" + "testing" +) + +// The key part holds only bus key characters and parsing gives the fingerprint back; a src1 or malformed value is refused, +// naming it. +func TestFingerprintKeyPart(t *testing.T) { + fp := SourcePrefix2 + strings.Repeat("ab", 32) + part, err := KeyPart(fp) + if err != nil { + t.Fatal(err) + } + if len(part) != 64 || strings.Trim(part, "0123456789abcdef") != "" { + t.Errorf("key part %q is not 64 lowercase hex characters", part) + } + back, err := ParseKeyPart(part) + if err != nil || back != fp { + t.Errorf("parse(%q) = %q, %v; want %q", part, back, err, fp) + } + src1 := "src1:" + strings.Repeat("c", 64) + for _, bad := range []string{src1, "src2:", "src2:xyz", "", "src2:" + strings.Repeat("A", 64), "src2:" + strings.Repeat("a", 63)} { + if _, err := KeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("KeyPart(%q) = %v, want a refusal naming it", bad, err) + } + } + for _, bad := range []string{src1, "src2:" + strings.Repeat("a", 64), strings.Repeat("a", 63), strings.Repeat("A", 64), "", "zz"} { + if _, err := ParseKeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("ParseKeyPart(%q) = %v, want a refusal naming it", bad, err) + } + } +} diff --git a/sdk/reference/reference.go b/sdk/reference/reference.go new file mode 100644 index 00000000..d451689b --- /dev/null +++ b/sdk/reference/reference.go @@ -0,0 +1,116 @@ +// Package reference holds the form of an artifact's reference once a registry seat published it: `//@` +// (novox/hq ADR 0605 §9), and the older `artifact-store://` form read beside it. It is the Delivery domain's rule over +// the names of the two registry seats that front the artifact store; it names no seat itself (ADR 0634 §4, ADR 0635 §4): the +// caller gives the seats' names from their contracts (Seats), so a package here cannot drift from a contract. +package reference + +import "strings" + +// ArtifactStoreScheme marks a reference to something in the mesh's artifact store, kept without the store's address. +const ArtifactStoreScheme = "artifact-store://" + +// UpstreamPrefix opens the holder's own name for an upstream copy it keeps: `upstream//@`, after +// `/` in the seat's form and after `artifact-store://` in the older one. +const UpstreamPrefix = "upstream/" + +// Seats are the names of the two registry seats a reference of the registry form names first: Image the seat that fronts +// images and upstream copies (container-image-registry), Object the seat that fronts bundles and archives +// (oci-artifact-registry). The caller reads them from the seats' contracts. +type Seats struct{ Image, Object string } + +// All is the two names. +func (s Seats) All() []string { return []string{s.Image, s.Object} } + +// Recorded is a reference as the mesh records it: the artifact store's address, if the builder wrote one, taken off. +func (s Seats) Recorded(reference string) string { + if _, _, seat := s.InRegistrySeat(reference); seat || strings.HasPrefix(reference, ArtifactStoreScheme) { + return reference + } + if rest, isURL := strings.CutPrefix(reference, "http://"); isURL { + if _, path, ok := strings.Cut(rest, "/v2/"); ok && strings.Contains(path, "/blobs/") { + return ArtifactStoreScheme + path + } + return reference + } + host, path, ok := strings.Cut(reference, "/") + if !ok || !isRegistryHost(host) || !strings.Contains(path, "@sha256:") { + return reference + } + return ArtifactStoreScheme + path +} + +// isRegistryHost is the runtime's own rule for reading the first component of a reference as a registry rather than a +// namespace: it has a dot or a port in it, or it is localhost. +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: `/@` for an +// image, `//blobs/` for a bundle or an archive. It reads both forms (ADR 0605 §9). +func (s Seats) InArtifactStore(reference string) (path string, kept bool) { + if seat, rest, ok := s.InRegistrySeat(reference); ok { + if seat == s.Object { + name, digest, _ := strings.Cut(rest, "@") + return name + "/blobs/" + digest, true + } + return rest, true + } + return strings.CutPrefix(reference, ArtifactStoreScheme) +} + +// SeatReference is the reference a build names an image, bundle, archive or upstream copy by once it published it through +// a registry seat: `//@sha256:`, as InRegistrySeat reads it back. +func SeatReference(seat, module, artifact, digest string) string { + return seat + "/" + module + "/" + artifact + "@" + digest +} + +// InRegistrySeat reads a reference in the form a registry seat answers: 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 the image seat, whose rest InUpstream reads. +func (s Seats) InRegistrySeat(reference string) (seat, path string, ok bool) { + first, rest, found := strings.Cut(reference, "/") + if !found || !strings.Contains(rest, "@sha256:") { + return "", "", false + } + if _, _, upstream := InUpstream(rest); upstream && first == s.Image { + return first, rest, true + } + if strings.Count(rest, "/") != 1 { + return "", "", false + } + for _, name := range s.All() { + if first == name { + return name, rest, true + } + } + return "", "", false +} + +// InUpstream reads a path in the holder's naming of an upstream copy, `upstream//@sha256:`: the +// upstream registry host and the image's path there, with its digest. +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 (s Seats) Routed(reference, address string) string { + path, kept := s.InArtifactStore(reference) + if !kept { + return reference + } + if strings.Contains(path, "/blobs/") { + return "http://" + address + "/v2/" + path + } + return address + "/" + path +} diff --git a/sdk/reference/reference_test.go b/sdk/reference/reference_test.go new file mode 100644 index 00000000..d707c9e4 --- /dev/null +++ b/sdk/reference/reference_test.go @@ -0,0 +1,69 @@ +package reference + +import ( + "strings" + "testing" +) + +var digest = "sha256:" + strings.Repeat("d", 64) + +var seats = Seats{Image: "container-image-registry", Object: "oci-artifact-registry"} + +func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { + cases := map[string]string{ + "anchor.internal:5100/gitea/server@" + digest: ArtifactStoreScheme + "gitea/server@" + digest, + "localhost:5000/gitea/server@" + digest: ArtifactStoreScheme + "gitea/server@" + digest, + "http://anchor.internal:5100/v2/gitea/config/blobs/" + digest: ArtifactStoreScheme + "gitea/config/blobs/" + digest, + ArtifactStoreScheme + "gitea/server@" + digest: ArtifactStoreScheme + "gitea/server@" + digest, + digest: digest, + "@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 := seats.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, + // 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 := seats.Routed(reference, "10.0.0.1:5000"); got != want { + t.Errorf("Routed(%q) = %q, want %q", reference, got, want) + } + } + // A build module names what it published in the seat's form, and reads it back as the seat it came through. + for _, seat := range seats.All() { + ref := SeatReference(seat, "gitea", "config", digest) + if got, path, ok := seats.InRegistrySeat(ref); !ok || got != seat || path != "gitea/config@"+digest { + 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 := seats.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) + } + } +}