Files
mesh-controller/internal/builder/mirror_test.go
jschoubben 9f3790dcda Review: one local name is still a local name; a local name is unique; recovery knows it; recipes read as instructions; a tag before a digest; ask fails at once when nothing serves
A secrets object with one local name delivered no file. Two requirements could share
a local name. secret recover and the export could not tell two locals apart. The
recipe check missed continued lines and read heredoc bodies as bases. repo:tag@digest
kept the tag in the repository. ask now publishes mandatory, so a tool nothing serves
is said at once rather than after the wait.
2026-09-21 21:03:22 +02:00

222 lines
8.1 KiB
Go

package builder
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
// An upstream image is copied between registries, never through a machine's image store
// (novox/hq 04-ISSUES/046, ADR 0096): the index, every manifest it names, every blob — moved by
// digest, and the index put last under the module's repository.
func digestOf(b []byte) string {
sum := sha256.Sum256(b)
return "sha256:" + hex.EncodeToString(sum[:])
}
// anUpstreamRegistry serves one image as an index over two platforms, behind an anonymous bearer
// challenge the way the public hub does, and records what was fetched.
func anUpstreamRegistry(t *testing.T) (*httptest.Server, string, map[string][]byte) {
t.Helper()
blobs := map[string][]byte{}
manifests := map[string][]byte{}
put := func(kind, mediaType string, layer []byte) string {
config := []byte(`{"architecture":"` + kind + `"}`)
blobs[digestOf(config)] = config
blobs[digestOf(layer)] = layer
m, _ := json.Marshal(map[string]any{
"schemaVersion": 2, "mediaType": mediaType,
"config": map[string]any{"mediaType": "application/vnd.oci.image.config.v1+json", "digest": digestOf(config), "size": len(config)},
"layers": []map[string]any{{"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": digestOf(layer), "size": len(layer)}},
})
manifests[digestOf(m)] = m
return digestOf(m)
}
amd := put("amd64", mediaManifestOCI, []byte("amd64 layer bytes"))
arm := put("arm64", mediaManifestOCI, []byte("arm64 layer bytes"))
index, _ := json.Marshal(map[string]any{
"schemaVersion": 2, "mediaType": mediaIndexOCI,
"manifests": []map[string]any{
{"mediaType": mediaManifestOCI, "digest": amd, "size": len(manifests[amd]), "platform": map[string]string{"os": "linux", "architecture": "amd64"}},
{"mediaType": mediaManifestOCI, "digest": arm, "size": len(manifests[arm]), "platform": map[string]string{"os": "linux", "architecture": "arm64"}},
},
})
manifests["latest"] = index
manifests[digestOf(index)] = index
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/token" {
_, _ = w.Write([]byte(`{"token":"anonymous-token"}`))
return
}
if r.Header.Get("Authorization") != "Bearer anonymous-token" {
w.Header().Set("WWW-Authenticate", `Bearer realm="`+server.URL+`/token",service="test",scope="repository:library/thing:pull"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
switch {
case strings.HasPrefix(r.URL.Path, "/v2/library/thing/manifests/"):
ref := strings.TrimPrefix(r.URL.Path, "/v2/library/thing/manifests/")
body, ok := manifests[ref]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
var typed struct {
MediaType string `json:"mediaType"`
}
_ = json.Unmarshal(body, &typed)
w.Header().Set("Content-Type", typed.MediaType)
_, _ = w.Write(body)
case strings.HasPrefix(r.URL.Path, "/v2/library/thing/blobs/"):
body, ok := blobs[strings.TrimPrefix(r.URL.Path, "/v2/library/thing/blobs/")]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
_, _ = w.Write(body)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
return server, digestOf(index), blobs
}
// theMeshsRegistry accepts blobs and manifests the way a registry does, and remembers them.
type theMeshsRegistry struct {
mu sync.Mutex
blobs map[string][]byte
manifests map[string][]byte
uploads int
}
func (m *theMeshsRegistry) handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.mu.Lock()
defer m.mu.Unlock()
switch {
case r.Method == http.MethodHead && strings.Contains(r.URL.Path, "/blobs/"):
if _, ok := m.blobs[r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]]; ok {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNotFound)
}
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/blobs/uploads/"):
w.Header().Set("Location", strings.TrimSuffix(r.URL.Path, "/")+"/one")
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodPut && strings.Contains(r.URL.Path, "/blobs/uploads/"):
body, _ := readAll(r)
digest := r.URL.Query().Get("digest")
if digestOf(body) != digest {
w.WriteHeader(http.StatusBadRequest)
return
}
m.blobs[digest] = body
m.uploads++
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodPut && strings.Contains(r.URL.Path, "/manifests/"):
body, _ := readAll(r)
m.manifests[r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]] = body
w.WriteHeader(http.StatusCreated)
default:
w.WriteHeader(http.StatusNotFound)
}
})
}
func readAll(r *http.Request) ([]byte, error) {
var buf strings.Builder
b := make([]byte, 4096)
for {
n, err := r.Body.Read(b)
buf.Write(b[:n])
if err != nil {
break
}
}
return []byte(buf.String()), nil
}
func TestAnUpstreamIndexIsCopiedWholeIntoTheMeshsRegistry(t *testing.T) {
src, indexDigest, srcBlobs := anUpstreamRegistry(t)
defer src.Close()
dst := &theMeshsRegistry{blobs: map[string][]byte{}, manifests: map[string][]byte{}}
dstServer := httptest.NewServer(dst.handler())
defer dstServer.Close()
address := strings.TrimPrefix(dstServer.URL, "http://")
r := Registry{Address: address, HTTP: src.Client()}
from := strings.TrimPrefix(src.URL, "http://") + "/library/thing:latest"
reference, err := r.MirrorImage(context.Background(), from, "hello-web/server")
if err != nil {
t.Fatal(err)
}
// Pinned by the INDEX's digest under the module's own repository: what a machine fetches is
// the whole image, whatever its architecture.
if reference != address+"/hello-web/server@"+indexDigest {
t.Fatalf("pinned as %q, not the index under the module's repository", reference)
}
// Every blob of both platforms, moved by digest, and each only once.
if len(dst.blobs) != len(srcBlobs) || dst.uploads != len(srcBlobs) {
t.Fatalf("%d of %d blobs arrived in %d uploads", len(dst.blobs), len(srcBlobs), dst.uploads)
}
for digest, body := range srcBlobs {
if string(dst.blobs[digest]) != string(body) {
t.Fatalf("blob %s did not arrive intact", digest)
}
}
// Two manifests and the index, each under its digest.
if len(dst.manifests) != 3 {
t.Fatalf("expected two manifests and an index, got %d: %v", len(dst.manifests), dst.manifests)
}
if _, ok := dst.manifests[indexDigest]; !ok {
t.Fatal("the index was not put under its digest")
}
// Copied again, nothing is uploaded twice: blobs are content-named and already there.
if _, err := r.MirrorImage(context.Background(), from, "hello-web/server"); err != nil {
t.Fatal(err)
}
if dst.uploads != len(srcBlobs) {
t.Fatalf("a second copy uploaded blobs the registry already held: %d uploads", dst.uploads)
}
}
func TestAReferenceIsReadTheWayARuntimeReadsIt(t *testing.T) {
for ref, want := range map[string]upstream{
"alpine": {base: "https://registry-1.docker.io", repository: "library/alpine", reference: "latest"},
"alpine@sha256:abc": {base: "https://registry-1.docker.io", repository: "library/alpine", reference: "sha256:abc"},
"minio/minio:RELEASE.2025": {base: "https://registry-1.docker.io", repository: "minio/minio", reference: "RELEASE.2025"},
"quay.io/minio/mc@sha256:def": {base: "https://quay.io", repository: "minio/mc", reference: "sha256:def"},
"lscr.io/linuxserver/sonarr:4": {base: "https://lscr.io", repository: "linuxserver/sonarr", reference: "4"},
"localhost:5000/x/y:1": {base: "http://localhost:5000", repository: "x/y", reference: "1"},
} {
got, err := parseReference(ref)
if err != nil {
t.Fatalf("%s: %v", ref, err)
}
if got != want {
t.Errorf("%s: got %+v want %+v", ref, got, want)
}
}
}
// `repo:tag@digest` is what a runtime prints; the tag is not part of the repository (review C6).
func TestATagBeforeTheDigestIsNotPartOfTheRepository(t *testing.T) {
got, err := parseReference("quay.io/minio/mc:RELEASE.2025@sha256:abc")
if err != nil {
t.Fatal(err)
}
if got.repository != "minio/mc" || got.reference != "sha256:abc" {
t.Fatalf("got %+v", got)
}
}