diff --git a/go/catalogue/artifact_sweep_seat.go b/go/catalogue/artifact_sweep_seat.go deleted file mode 100644 index 752852c..0000000 --- a/go/catalogue/artifact_sweep_seat.go +++ /dev/null @@ -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" diff --git a/go/catalogue/artifacts.go b/go/catalogue/artifacts.go index aa5009b..96a223b 100644 --- a/go/catalogue/artifacts.go +++ b/go/catalogue/artifacts.go @@ -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 `/@sha256:…` is kept as its path; an -// archive it named `http:///v2//blobs/` 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: `/@` -// 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 == 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): `//@sha256:`, 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, `//@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. -// -// It also reads the holder's own naming of an upstream copy through container-image-registry, -// `container-image-registry/upstream//@sha256:`, 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//@`, 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//@sha256:` (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 -} diff --git a/go/catalogue/artifacts_test.go b/go/catalogue/artifacts_test.go index 252ca22..1f1666f 100644 --- a/go/catalogue/artifacts_test.go +++ b/go/catalogue/artifacts_test.go @@ -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) - } - } -} diff --git a/go/catalogue/build.go b/go/catalogue/build.go index 975f461..4885846 100644 --- a/go/catalogue/build.go +++ b/go/catalogue/build.go @@ -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:///v2//blobs/` +// becomes `artifact-store:///blobs/`; 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 +} diff --git a/go/catalogue/handed.go b/go/catalogue/handed.go index e2853b4..cd34b1b 100644 --- a/go/catalogue/handed.go +++ b/go/catalogue/handed.go @@ -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 != "" { diff --git a/go/catalogue/manifest_reads.go b/go/catalogue/manifest_reads.go index 0bbd68b..fb9add9 100644 --- a/go/catalogue/manifest_reads.go +++ b/go/catalogue/manifest_reads.go @@ -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, -// `//@sha256:` (novox/hq ADR 0605 §9; InRegistrySeat), as the reader capability +// `//@sha256:` (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) { diff --git a/go/catalogue/test_seats_test.go b/go/catalogue/test_seats_test.go index 95a2a46..06ca6ad 100644 --- a/go/catalogue/test_seats_test.go +++ b/go/catalogue/test_seats_test.go @@ -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" ) diff --git a/go/testdata/seat-names.txt b/go/testdata/seat-names.txt index aef59a4..7892fbc 100644 --- a/go/testdata/seat-names.txt +++ b/go/testdata/seat-names.txt @@ -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