catalogue: the registry reference forms and ArtifactSweepSeat leave for the registry clients and the sweep seat's contract; seat-names keeps one rule's seat (S-8; issue 1966)
Resolve keeps its own address strip (kept, ADR 0155), which needs no seat name. The one seat name left is ManifestNeeds' store holder (ADR 0632 §2), beside its rule in manifest_reads.go. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
// ArtifactSweepSeat is the seat artifact-store-tools holds. Not the seat mesh-artifact-store, the store itself held by
|
||||
// distribution, and not the provision artifact-store it delivers.
|
||||
const ArtifactSweepSeat = "mesh-artifact-sweep"
|
||||
+2
-130
@@ -1,134 +1,6 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ArtifactStoreScheme marks a reference to something in the mesh's artifact store, kept without
|
||||
// the store's address.
|
||||
// the store's address. How a reference is recorded, read and routed (the registry seat's form beside this one) is the
|
||||
// registry clients' (mesh-delivery's contracts, `containerimageregistryclient/reference`; ADR 0634 §4).
|
||||
const ArtifactStoreScheme = ArtifactStoreProvision + "://"
|
||||
|
||||
// Recorded is a reference as the mesh records it: the artifact store's address, if the builder wrote
|
||||
// one, taken off.
|
||||
//
|
||||
// For a reference the builder announced — one it pushed to the store — which is the only kind
|
||||
// this is called on. An image the builder named `<host>/<path>@sha256:…` is kept as its path; an
|
||||
// archive it named `http://<host>/v2/<path>/blobs/<digest>` likewise. A reference with no address
|
||||
// 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 _, _, seat := 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 as 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** (novox/hq ADR
|
||||
// 0605 §9): the older `artifact-store://<path>`, and the form a registry seat answers, `<seat>/<module>/<artifact>@<digest>`
|
||||
// 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 == ociArtifactRegistrySeat {
|
||||
name, digest, _ := strings.Cut(rest, "@")
|
||||
return name + "/blobs/" + digest, true
|
||||
}
|
||||
return rest, true
|
||||
}
|
||||
return strings.CutPrefix(reference, ArtifactStoreScheme)
|
||||
}
|
||||
|
||||
// The registry seats' names, which this file's reference forms read and write. **They leave with these forms:** a whole
|
||||
// seat's name is its contract's (ADR 0634 §4, ADR 0635 §4), and the reference of a registry seat is the registry
|
||||
// clients' in mesh-delivery-contracts (prepare D-1); until those clients hold it, this package keeps the one copy and
|
||||
// the package-list test names it as the one exception (S-8).
|
||||
const (
|
||||
containerImageRegistrySeat = "container-image-registry"
|
||||
ociArtifactRegistrySeat = "oci-artifact-registry"
|
||||
)
|
||||
|
||||
// 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{containerImageRegistrySeat, ociArtifactRegistrySeat}
|
||||
|
||||
// SeatReference is the reference a build names an image, bundle, archive or upstream copy by once it published it
|
||||
// through a registry seat (novox/hq ADR 0605 §9): `<seat>/<module>/<artifact>@sha256:<digest>`, as InRegistrySeat reads
|
||||
// it back. The store's address is left out, as in the older form: a reader routes it (Routed).
|
||||
func SeatReference(seat, module, artifact, digest string) string {
|
||||
return seat + "/" + module + "/" + artifact + "@" + digest
|
||||
}
|
||||
|
||||
// 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:") {
|
||||
return "", "", false
|
||||
}
|
||||
if _, _, upstream := InUpstream(rest); upstream && first == containerImageRegistrySeat {
|
||||
return first, rest, true
|
||||
}
|
||||
if strings.Count(rest, "/") != 1 {
|
||||
return "", "", false
|
||||
}
|
||||
for _, s := range RegistrySeats {
|
||||
if first == s {
|
||||
return s, rest, true
|
||||
}
|
||||
}
|
||||
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 {
|
||||
path, kept := InArtifactStore(reference)
|
||||
if !kept {
|
||||
return reference
|
||||
}
|
||||
if strings.Contains(path, "/blobs/") {
|
||||
return "http://" + address + "/v2/" + path
|
||||
}
|
||||
return address + "/" + path
|
||||
}
|
||||
|
||||
@@ -1,70 +1,6 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A build is recorded by what it is; where it is pushed is composed where it is used (novox/hq
|
||||
// 04-ISSUES/102).
|
||||
import "strings"
|
||||
|
||||
// digest is an artifact digest the tests build references with.
|
||||
var digest = "sha256:" + strings.Repeat("d", 64)
|
||||
|
||||
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 := 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 := 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 RegistrySeats {
|
||||
ref := SeatReference(seat, "gitea", "config", digest)
|
||||
if got, path, ok := 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 := 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -117,9 +117,9 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
|
||||
// it reached it by, and a manifest carrying that address names an installation —
|
||||
// registration refused node-tools for exactly this on 2026-10-02. The build record
|
||||
// already keeps the store-relative form; the resolved manifest keeps the same, and
|
||||
// composition routes it through the store a machine reaches (Routed).
|
||||
// composition routes it through the store a machine reaches (the registry clients' Routed).
|
||||
out.Bundles = append(out.Bundles, Bundle{
|
||||
Name: a.Name, Source: Recorded(made.Reference), Digest: made.Digest,
|
||||
Name: a.Name, Source: kept(made.Reference), Digest: made.Digest,
|
||||
Language: a.Language, Entrypoints: append([]string(nil), a.Entrypoints...),
|
||||
Loads: loads, Env: copyWords(a.Env), Launchers: copyWords(made.Launchers),
|
||||
Binary: BinaryOf(a), Bus: a.Bus,
|
||||
@@ -171,7 +171,7 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
|
||||
// how they were made — one packed as it stood, the other compiled first — and a
|
||||
// machine has no reason to care which.
|
||||
// Kept, not routed, for the reason the bundles above are (ADR 0155).
|
||||
filled["source"] = Recorded(artifact.Reference)
|
||||
filled["source"] = kept(artifact.Reference)
|
||||
filled["digest"] = artifact.Digest
|
||||
// **And `${version}`, so a resource can name a place that is this build's alone**
|
||||
// (novox/hq ADR 0141, 04-ISSUES/142). A component is unpacked into a directory named
|
||||
@@ -363,3 +363,19 @@ func BundleLoads(m Manifest, a Artifact, run map[string]bool) []string {
|
||||
}
|
||||
return loads
|
||||
}
|
||||
|
||||
// kept is a bundle's or an archive's reference without the artifact store's address, if the builder wrote one (ADR 0155:
|
||||
// a resolved manifest names no installation). Only the address is read here: an `http://<host>/v2/<path>/blobs/<digest>`
|
||||
// becomes `artifact-store://<path>/blobs/<digest>`; a kept reference, or one in a registry seat's form (whose first part,
|
||||
// a seat's name, has no dot or port), is what it was. How the registry seats' forms are read is the registry clients'.
|
||||
func kept(reference string) string {
|
||||
if 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
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ func handedBy(m Manifest, archive Built) *Handed {
|
||||
return nil
|
||||
}
|
||||
dir := m.TemplatesDir()
|
||||
h := &Handed{Source: Recorded(archive.Reference), Digest: archive.Digest, Templates: map[string]string{}}
|
||||
h := &Handed{Source: kept(archive.Reference), Digest: archive.Digest, Templates: map[string]string{}}
|
||||
for _, r := range m.Resources {
|
||||
name, has, wrong := templateOf(m.Module, dir, r)
|
||||
if !has || wrong != "" {
|
||||
|
||||
@@ -39,7 +39,7 @@ var referenceForms = []string{"access", "backup", "bound", "collected", "config"
|
||||
"contribution", "dir", "machine", "port", "seat", "secret", "setting"}
|
||||
|
||||
// ReferenceFormSeat names the reference form a registry seat answers for a build's artifact,
|
||||
// `<seat>/<module>/<artifact>@sha256:<digest>` (novox/hq ADR 0605 §9; InRegistrySeat), as the reader capability
|
||||
// `<seat>/<module>/<artifact>@sha256:<digest>` (novox/hq ADR 0605 §9; the registry clients' reference forms), as the reader capability
|
||||
// `form:seat-reference` a node-engine built with this contract states (ADR 0618 §1, ADR 0624 §1): the builder writes it
|
||||
// into a build key only for the nodes whose engines state it. The older reference forms a node-engine read before any
|
||||
// stated what it reads are the baseline's; they are the builder's to name (delivery's artifact forms).
|
||||
@@ -56,8 +56,12 @@ var artifactReferenceForms = []string{ReferenceFormSeat}
|
||||
// (ManifestNeeds).
|
||||
const ReferenceFormSeatReach = "seat-reach"
|
||||
|
||||
// storeHolderSeat is the seat whose holder's own manifest needs form:seat-reach (novox/hq ADR 0632 §2): the one seat
|
||||
// name this contract reads a rule by, and the one seat name of the package-list test's exceptions (S-8).
|
||||
const storeHolderSeat = "container-image-registry"
|
||||
|
||||
// claimsAtMesh says whether a manifest claims seat at the mesh's scope (a claim with no scope is the seat's own). The
|
||||
// store holder's own manifest, a claim of containerImageRegistrySeat, needs form:seat-reach this way (ADR 0632 §2): read
|
||||
// store holder's own manifest, a claim of storeHolderSeat, needs form:seat-reach this way (ADR 0632 §2): read
|
||||
// on every node, it leaves an engine before the reading without the old way to find the store, so it is held from every
|
||||
// node whose engine does not state it.
|
||||
func claimsAtMesh(top map[string]any, seat string) bool {
|
||||
@@ -164,7 +168,7 @@ func ManifestNeeds(raw []byte) ([]string, error) {
|
||||
}
|
||||
used := map[string]bool{}
|
||||
textNeeds(raw, used)
|
||||
if claimsAtMesh(top, containerImageRegistrySeat) {
|
||||
if claimsAtMesh(top, storeHolderSeat) {
|
||||
used[ReadForm+ReferenceFormSeatReach] = true
|
||||
}
|
||||
if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) {
|
||||
|
||||
@@ -33,6 +33,6 @@ const (
|
||||
EngineSeat = "node-engine"
|
||||
NpmPackageRegistrySeat = "npm-package-registry"
|
||||
GoPackageRegistrySeat = "go-package-registry"
|
||||
ContainerImageRegistrySeat = containerImageRegistrySeat
|
||||
OciArtifactRegistrySeat = ociArtifactRegistrySeat
|
||||
ContainerImageRegistrySeat = "container-image-registry"
|
||||
OciArtifactRegistrySeat = "oci-artifact-registry"
|
||||
)
|
||||
|
||||
Vendored
+1
-3
@@ -1,6 +1,3 @@
|
||||
catalogue/artifact_sweep_seat.go: mesh-artifact-sweep
|
||||
catalogue/artifacts.go: container-image-registry
|
||||
catalogue/artifacts.go: oci-artifact-registry
|
||||
catalogue/environment_into.go: node-environment
|
||||
catalogue/environment_into.go: node-login-shell
|
||||
catalogue/environment_into.go: node-power
|
||||
@@ -16,6 +13,7 @@ catalogue/graphical_session.go: node-login-manager
|
||||
catalogue/graphical_session.go: node-notifier
|
||||
catalogue/graphical_session.go: node-secret-service
|
||||
catalogue/graphical_session.go: node-terminal-emulator
|
||||
catalogue/manifest_reads.go: container-image-registry
|
||||
catalogue/node_resolver.go: node-resolver
|
||||
catalogue/reads.go: node-reads
|
||||
catalogue/seat_contributions.go: node-backup
|
||||
|
||||
Reference in New Issue
Block a user