mesh-delivery-sdk: the fingerprint's key part and the registry reference forms, seat-neutral over the registry seats' names the caller gives (D-10 base, ADR 0634 §1, ADR 0635 §4; issue 1966)
This commit is contained in:
@@ -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") == ""
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Package reference holds the form of an artifact's reference once a registry seat published it: `<seat>/<module>/<artifact>@<digest>`
|
||||
// (novox/hq ADR 0605 §9), and the older `artifact-store://<path>` 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/<host>/<path>@<digest>`, after
|
||||
// `<image seat>/` 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: `<module>/<artifact>@<digest>` for an
|
||||
// image, `<module>/<artifact>/blobs/<digest>` 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: `<seat>/<module>/<artifact>@sha256:<digest>`, 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/<host>/<path>@sha256:<digest>`: 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user