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