m/test/localregistry: use osbase/oci/registry

This replaces the localregistry implementation with a small wrapper
around the new registry package.

The images attribute of the Bazel rule was changed from a list to a
dict, which makes the repository and tag independent from the file path.

Change-Id: I1f6213dd67f7bdcf2373fe136958caa68b9f4d10
Reviewed-on: https://review.monogon.dev/c/monogon/+/4089
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/build/bazel/go.MODULE.bazel b/build/bazel/go.MODULE.bazel
index 1756ef7..120f4c8 100644
--- a/build/bazel/go.MODULE.bazel
+++ b/build/bazel/go.MODULE.bazel
@@ -24,7 +24,6 @@
     "com_github_coreos_go_semver",
     "com_github_corverroos_commentwrap",
     "com_github_diskfs_go_diskfs",
-    "com_github_docker_distribution",
     "com_github_gdamore_tcell_v2",
     "com_github_go_delve_delve",
     "com_github_golang_migrate_migrate_v4",
diff --git a/go.mod b/go.mod
index fa17039..b4b41ab 100644
--- a/go.mod
+++ b/go.mod
@@ -65,7 +65,6 @@
 	github.com/coreos/go-semver v0.3.1
 	github.com/corverroos/commentwrap v0.0.0-20191204065359-2926638be44c
 	github.com/diskfs/go-diskfs v1.2.0
-	github.com/docker/distribution v2.8.2+incompatible
 	github.com/gdamore/tcell/v2 v2.7.4
 	github.com/go-delve/delve v1.24.0
 	github.com/golang-migrate/migrate/v4 v4.15.2
diff --git a/go.sum b/go.sum
index baddf28..5ea0d0f 100644
--- a/go.sum
+++ b/go.sum
@@ -1950,8 +1950,6 @@
 github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
-github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
diff --git a/metropolis/test/e2e/BUILD.bazel b/metropolis/test/e2e/BUILD.bazel
index 85ef13a..dc43be6 100644
--- a/metropolis/test/e2e/BUILD.bazel
+++ b/metropolis/test/e2e/BUILD.bazel
@@ -2,12 +2,12 @@
 
 localregistry_manifest(
     name = "testimages_manifest",
-    images = [
-        "//metropolis/test/e2e/selftest:selftest_image",
-        "//metropolis/test/e2e/persistentvolume:persistentvolume_image",
-        "//metropolis/test/e2e/httpserver:httpserver_image",
-        "//metropolis/test/e2e/connectivity/agent:agent_image",
-    ],
+    images = {
+        "selftest:latest": "//metropolis/test/e2e/selftest:selftest_image",
+        "persistentvolume:latest": "//metropolis/test/e2e/persistentvolume:persistentvolume_image",
+        "httpserver:latest": "//metropolis/test/e2e/httpserver:httpserver_image",
+        "connectivity/agent:latest": "//metropolis/test/e2e/connectivity/agent:agent_image",
+    },
     visibility = [
         "//metropolis/test/e2e/suites:__subpackages__",
     ],
diff --git a/metropolis/test/e2e/connectivity/connectivity.go b/metropolis/test/e2e/connectivity/connectivity.go
index 070bacc..7ecbb08 100644
--- a/metropolis/test/e2e/connectivity/connectivity.go
+++ b/metropolis/test/e2e/connectivity/connectivity.go
@@ -218,7 +218,7 @@
 			Spec: corev1.PodSpec{
 				Containers: []corev1.Container{{
 					Name:  "connectivitytester",
-					Image: "test.monogon.internal/metropolis/test/e2e/connectivity/agent/agent_image",
+					Image: "test.monogon.internal/connectivity/agent:latest",
 					Stdin: true,
 				}},
 				EnableServiceLinks: ptr.To(false),
diff --git a/metropolis/test/e2e/suites/kubernetes/kubernetes_helpers.go b/metropolis/test/e2e/suites/kubernetes/kubernetes_helpers.go
index 1f45fab..02eddfd 100644
--- a/metropolis/test/e2e/suites/kubernetes/kubernetes_helpers.go
+++ b/metropolis/test/e2e/suites/kubernetes/kubernetes_helpers.go
@@ -78,7 +78,7 @@
 						{
 							Name:            "test",
 							ImagePullPolicy: corev1.PullIfNotPresent,
-							Image:           "test.monogon.internal/metropolis/test/e2e/httpserver/httpserver_image",
+							Image:           "test.monogon.internal/httpserver:latest",
 							LivenessProbe: &corev1.Probe{
 								ProbeHandler: corev1.ProbeHandler{
 									HTTPGet: &corev1.HTTPGetAction{Port: intstr.FromInt(8080)},
@@ -130,7 +130,7 @@
 						{
 							Name:            "test",
 							ImagePullPolicy: corev1.PullIfNotPresent,
-							Image:           "test.monogon.internal/metropolis/test/e2e/selftest/selftest_image",
+							Image:           "test.monogon.internal/selftest:latest",
 						},
 					},
 					RestartPolicy: corev1.RestartPolicyOnFailure,
@@ -191,7 +191,7 @@
 						{
 							Name:            "test",
 							ImagePullPolicy: corev1.PullIfNotPresent,
-							Image:           "test.monogon.internal/metropolis/test/e2e/persistentvolume/persistentvolume_image",
+							Image:           "test.monogon.internal/persistentvolume:latest",
 							Args:            []string{"-runtimeclass", runtimeClass},
 							VolumeMounts: []corev1.VolumeMount{
 								{
diff --git a/metropolis/test/launch/BUILD.bazel b/metropolis/test/launch/BUILD.bazel
index 166ba3a..492fdd4 100644
--- a/metropolis/test/launch/BUILD.bazel
+++ b/metropolis/test/launch/BUILD.bazel
@@ -49,8 +49,8 @@
         "//metropolis/node/core/rpc/resolver",
         "//metropolis/proto/api",
         "//metropolis/proto/common",
-        "//metropolis/test/localregistry",
         "//osbase/logbuffer",
+        "//osbase/oci/registry",
         "//osbase/test/qemu",
         "@com_github_cenkalti_backoff_v4//:backoff",
         "@com_github_kballard_go_shellquote//:go-shellquote",
diff --git a/metropolis/test/launch/cluster.go b/metropolis/test/launch/cluster.go
index d8eb8cd..5ad4121 100644
--- a/metropolis/test/launch/cluster.go
+++ b/metropolis/test/launch/cluster.go
@@ -51,7 +51,7 @@
 	"source.monogon.dev/metropolis/node"
 	"source.monogon.dev/metropolis/node/core/rpc"
 	"source.monogon.dev/metropolis/node/core/rpc/resolver"
-	"source.monogon.dev/metropolis/test/localregistry"
+	"source.monogon.dev/osbase/oci/registry"
 	"source.monogon.dev/osbase/test/qemu"
 )
 
@@ -583,7 +583,7 @@
 	// Optional local registry which will be made available to the cluster to
 	// pull images from. This is a more efficient alternative to preseeding all
 	// images used for testing.
-	LocalRegistry *localregistry.Server
+	LocalRegistry *registry.Server
 
 	// InitialClusterConfiguration will be passed to the first node when creating the
 	// cluster, and defines some basic properties of the cluster. If not specified,
diff --git a/metropolis/test/localregistry/BUILD.bazel b/metropolis/test/localregistry/BUILD.bazel
index fa1f229..a5621fc 100644
--- a/metropolis/test/localregistry/BUILD.bazel
+++ b/metropolis/test/localregistry/BUILD.bazel
@@ -7,12 +7,8 @@
     visibility = ["//visibility:public"],
     deps = [
         "//metropolis/test/localregistry/spec",
-        "@com_github_docker_distribution//:distribution",
-        "@com_github_docker_distribution//manifest/manifestlist",
-        "@com_github_docker_distribution//manifest/ocischema",
-        "@com_github_docker_distribution//manifest/schema2",
-        "@com_github_docker_distribution//reference",
-        "@com_github_opencontainers_go_digest//:go-digest",
+        "//osbase/oci",
+        "//osbase/oci/registry",
         "@io_bazel_rules_go//go/runfiles",
         "@org_golang_google_protobuf//encoding/prototext",
     ],
diff --git a/metropolis/test/localregistry/def.bzl b/metropolis/test/localregistry/def.bzl
index 289dc34..3b8e378 100644
--- a/metropolis/test/localregistry/def.bzl
+++ b/metropolis/test/localregistry/def.bzl
@@ -1,14 +1,14 @@
-#load("@io_bazel_rules_docker//container:providers.bzl", "ImageInfo")
-
 def _localregistry_manifest_impl(ctx):
     manifest_out = ctx.actions.declare_file(ctx.label.name + ".prototxt")
 
     images = []
     referenced = [manifest_out]
-    for i in ctx.attr.images:
-        image_file = i.files.to_list()[0]
+    for key, label in ctx.attr.images.items():
+        image_file = label[DefaultInfo].files.to_list()[0]
+        repository, _, tag = key.partition(":")
         image = struct(
-            name = i.label.package + "/" + i.label.name,
+            repository = repository,
+            tag = tag,
             path = image_file.short_path,
         )
         referenced.append(image_file)
@@ -23,10 +23,12 @@
         Builds a manifest for serving images directly from the build files.
     """,
     attrs = {
-        "images": attr.label_list(
+        "images": attr.string_keyed_label_dict(
             mandatory = True,
             doc = """
-                List of images to be served from the local registry.
+                Images to be served from the local registry.
+                The key defines the repository and tag, separated by ':'.
+                The value is a label which must contain an OCI layout directory.
             """,
             providers = [],
         ),
diff --git a/metropolis/test/localregistry/localregistry.go b/metropolis/test/localregistry/localregistry.go
index 1f648e0..694476e 100644
--- a/metropolis/test/localregistry/localregistry.go
+++ b/metropolis/test/localregistry/localregistry.go
@@ -6,170 +6,36 @@
 package localregistry
 
 import (
-	"bytes"
-	"encoding/json"
 	"fmt"
-	"io"
-	"log"
-	"net/http"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strconv"
+	"path"
 
 	"github.com/bazelbuild/rules_go/go/runfiles"
-	"github.com/docker/distribution"
-	"github.com/docker/distribution/manifest/manifestlist"
-	"github.com/docker/distribution/manifest/ocischema"
-	"github.com/docker/distribution/manifest/schema2"
-	"github.com/docker/distribution/reference"
-	"github.com/opencontainers/go-digest"
 	"google.golang.org/protobuf/encoding/prototext"
 
 	"source.monogon.dev/metropolis/test/localregistry/spec"
+	"source.monogon.dev/osbase/oci"
+	"source.monogon.dev/osbase/oci/registry"
 )
 
-type Server struct {
-	manifests map[string][]byte
-	blobs     map[digest.Digest]blobMeta
-}
-
-type blobMeta struct {
-	filePath      string
-	mediaType     string
-	contentLength int64
-}
-
-func manifestDescriptorFromBazel(image *spec.Image) (manifestlist.ManifestDescriptor, error) {
-	indexPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "index.json"))
-	if err != nil {
-		return manifestlist.ManifestDescriptor{}, fmt.Errorf("while locating manifest list file: %w", err)
-	}
-
-	manifestListRaw, err := os.ReadFile(indexPath)
-	if err != nil {
-		return manifestlist.ManifestDescriptor{}, fmt.Errorf("while opening manifest list file: %w", err)
-	}
-
-	var imageManifestList manifestlist.ManifestList
-	if err := json.Unmarshal(manifestListRaw, &imageManifestList); err != nil {
-		return manifestlist.ManifestDescriptor{}, fmt.Errorf("while unmarshaling manifest list for %q: %w", image.Name, err)
-	}
-
-	if len(imageManifestList.Manifests) != 1 {
-		return manifestlist.ManifestDescriptor{}, fmt.Errorf("unexpected manifest list length > 1")
-	}
-
-	return imageManifestList.Manifests[0], nil
-}
-
-func manifestFromBazel(s *Server, image *spec.Image, md manifestlist.ManifestDescriptor) (ocischema.Manifest, error) {
-	manifestPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", md.Digest.Algorithm().String(), md.Digest.Hex()))
-	if err != nil {
-		return ocischema.Manifest{}, fmt.Errorf("while locating manifest file: %w", err)
-	}
-	manifestRaw, err := os.ReadFile(manifestPath)
-	if err != nil {
-		return ocischema.Manifest{}, fmt.Errorf("while opening manifest file: %w", err)
-	}
-
-	var imageManifest ocischema.Manifest
-	if err := json.Unmarshal(manifestRaw, &imageManifest); err != nil {
-		return ocischema.Manifest{}, fmt.Errorf("while unmarshaling manifest for %q: %w", image.Name, err)
-	}
-
-	// For Digest lookups
-	s.manifests[image.Name] = manifestRaw
-	s.manifests[md.Digest.String()] = manifestRaw
-
-	return imageManifest, nil
-}
-
-func addBazelBlobFromDescriptor(s *Server, image *spec.Image, dd distribution.Descriptor) error {
-	path, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", dd.Digest.Algorithm().String(), dd.Digest.Hex()))
-	if err != nil {
-		return fmt.Errorf("while locating blob: %w", err)
-	}
-	s.blobs[dd.Digest] = blobMeta{filePath: path, mediaType: dd.MediaType, contentLength: dd.Size}
-	return nil
-}
-
-func FromBazelManifest(mb []byte) (*Server, error) {
+func FromBazelManifest(mb []byte) (*registry.Server, error) {
 	var bazelManifest spec.Manifest
 	if err := prototext.Unmarshal(mb, &bazelManifest); err != nil {
-		log.Fatalf("failed to parse manifest: %v", err)
+		return nil, fmt.Errorf("failed to parse manifest: %w", err)
 	}
-	s := Server{
-		manifests: make(map[string][]byte),
-		blobs:     make(map[digest.Digest]blobMeta),
-	}
+	s := registry.NewServer()
 	for _, i := range bazelManifest.Images {
-		md, err := manifestDescriptorFromBazel(i)
+		resolvedPath, err := runfiles.Rlocation(path.Join("_main", i.Path))
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("failed to resolve image path %q: %w", i.Path, err)
 		}
-
-		if err := addBazelBlobFromDescriptor(&s, i, md.Descriptor); err != nil {
-			return nil, err
-		}
-
-		m, err := manifestFromBazel(&s, i, md)
+		image, err := oci.ReadLayout(resolvedPath)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("failed to read image from %q: %w", i.Path, err)
 		}
-
-		if err := addBazelBlobFromDescriptor(&s, i, m.Config); err != nil {
-			return nil, err
-		}
-		for _, l := range m.Layers {
-			if err := addBazelBlobFromDescriptor(&s, i, l); err != nil {
-				return nil, err
-			}
+		err = s.AddImage(i.Repository, i.Tag, image)
+		if err != nil {
+			return nil, fmt.Errorf("failed to add image: %w", err)
 		}
 	}
-	return &s, nil
-}
-
-var (
-	versionCheckEp = regexp.MustCompile(`^/v2/$`)
-	manifestEp     = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")$")
-	blobEp         = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/blobs/(" + digest.DigestRegexp.String() + ")$")
-)
-
-func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
-	if req.Method != http.MethodGet && req.Method != http.MethodHead {
-		w.WriteHeader(http.StatusMethodNotAllowed)
-		fmt.Fprintf(w, "Registry is read-only, only GET and HEAD are allowed\n")
-		return
-	}
-	w.Header().Set("Content-Type", "application/json")
-	if versionCheckEp.MatchString(req.URL.Path) {
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, "{}")
-		return
-	} else if matches := manifestEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
-		name := matches[1]
-		manifest, ok := s.manifests[name]
-		if !ok {
-			w.WriteHeader(http.StatusNotFound)
-			fmt.Fprintf(w, "Image not found")
-			return
-		}
-		w.Header().Set("Content-Type", schema2.MediaTypeManifest)
-		w.Header().Set("Content-Length", strconv.FormatInt(int64(len(manifest)), 10))
-		w.WriteHeader(http.StatusOK)
-		io.Copy(w, bytes.NewReader(manifest))
-	} else if matches := blobEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
-		bm, ok := s.blobs[digest.Digest(matches[2])]
-		if !ok {
-			w.WriteHeader(http.StatusNotFound)
-			fmt.Fprintf(w, "Blob not found")
-			return
-		}
-		w.Header().Set("Content-Type", bm.mediaType)
-		w.Header().Set("Content-Length", strconv.FormatInt(bm.contentLength, 10))
-		http.ServeFile(w, req, bm.filePath)
-	} else {
-		w.WriteHeader(http.StatusNotFound)
-	}
+	return s, nil
 }
diff --git a/metropolis/test/localregistry/spec/manifest.proto b/metropolis/test/localregistry/spec/manifest.proto
index d9cbeab..529c013 100644
--- a/metropolis/test/localregistry/spec/manifest.proto
+++ b/metropolis/test/localregistry/spec/manifest.proto
@@ -6,14 +6,16 @@
 
 // Single image metadata
 message Image {
-    // Name of the image (no domain or tag, just slash-separated path)
-    string name = 1;
-    // Path to the image
-    string path = 2;
+    // Repository where the image is served
+    string repository = 1;
+    // Tag where the image is served
+    string tag = 2;
+    // Path to the OCI layout directory containing the image
+    string path = 3;
 }
 
 // Main message
 message Manifest {
     // List of images for the local registry
     repeated Image images = 1;
-}
\ No newline at end of file
+}