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/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
+}