treewide: introduce osbase package and move things around
All except localregistry moved from metropolis/pkg to osbase,
localregistry moved to metropolis/test as its only used there anyway.
Change-Id: If1a4bf377364bef0ac23169e1b90379c71b06d72
Reviewed-on: https://review.monogon.dev/c/monogon/+/3079
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/test/localregistry/BUILD.bazel b/metropolis/test/localregistry/BUILD.bazel
new file mode 100644
index 0000000..548960a
--- /dev/null
+++ b/metropolis/test/localregistry/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "localregistry",
+ srcs = ["localregistry.go"],
+ importpath = "source.monogon.dev/metropolis/test/localregistry",
+ 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",
+ "@io_bazel_rules_go//go/runfiles:go_default_library",
+ "@org_golang_google_protobuf//encoding/prototext",
+ ],
+)
diff --git a/metropolis/test/localregistry/def.bzl b/metropolis/test/localregistry/def.bzl
new file mode 100644
index 0000000..c5fc560
--- /dev/null
+++ b/metropolis/test/localregistry/def.bzl
@@ -0,0 +1,35 @@
+#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]
+ image = struct(
+ name = i.label.package + "/" + i.label.name,
+ path = image_file.short_path,
+ )
+ referenced.append(image_file)
+ images.append(image)
+
+ ctx.actions.write(manifest_out, proto.encode_text(struct(images = images)))
+ return [DefaultInfo(runfiles = ctx.runfiles(files = referenced), files = depset([manifest_out]))]
+
+
+localregistry_manifest = rule(
+ implementation = _localregistry_manifest_impl,
+ doc = """
+ Builds a manifest for serving images directly from the build files.
+ """,
+ attrs = {
+ "images": attr.label_list(
+ mandatory = True,
+ doc = """
+ List of images to be served from the local registry.
+ """,
+ providers = [],
+ ),
+ },
+)
diff --git a/metropolis/test/localregistry/localregistry.go b/metropolis/test/localregistry/localregistry.go
new file mode 100644
index 0000000..120eb61
--- /dev/null
+++ b/metropolis/test/localregistry/localregistry.go
@@ -0,0 +1,172 @@
+// Package localregistry implements a read-only OCI Distribution / Docker
+// V2 container image registry backed by local layers.
+package localregistry
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+
+ "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"
+)
+
+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) {
+ var bazelManifest spec.Manifest
+ if err := prototext.Unmarshal(mb, &bazelManifest); err != nil {
+ log.Fatalf("failed to parse manifest: %v", err)
+ }
+ s := Server{
+ manifests: make(map[string][]byte),
+ blobs: make(map[digest.Digest]blobMeta),
+ }
+ for _, i := range bazelManifest.Images {
+ md, err := manifestDescriptorFromBazel(i)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := addBazelBlobFromDescriptor(&s, i, md.Descriptor); err != nil {
+ return nil, err
+ }
+
+ m, err := manifestFromBazel(&s, i, md)
+ if err != nil {
+ return nil, 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
+ }
+ }
+ }
+ 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)
+ }
+}
diff --git a/metropolis/test/localregistry/spec/BUILD.bazel b/metropolis/test/localregistry/spec/BUILD.bazel
new file mode 100644
index 0000000..71253d1
--- /dev/null
+++ b/metropolis/test/localregistry/spec/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+ name = "spec_proto",
+ srcs = ["manifest.proto"],
+ visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+ name = "spec_go_proto",
+ importpath = "source.monogon.dev/metropolis/test/localregistry/spec",
+ proto = ":spec_proto",
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "spec",
+ embed = [":spec_go_proto"],
+ importpath = "source.monogon.dev/metropolis/test/localregistry/spec",
+ visibility = ["//visibility:public"],
+)
diff --git a/metropolis/test/localregistry/spec/gomod-generated-placeholder.go b/metropolis/test/localregistry/spec/gomod-generated-placeholder.go
new file mode 100644
index 0000000..f09cd57
--- /dev/null
+++ b/metropolis/test/localregistry/spec/gomod-generated-placeholder.go
@@ -0,0 +1 @@
+package spec
diff --git a/metropolis/test/localregistry/spec/manifest.proto b/metropolis/test/localregistry/spec/manifest.proto
new file mode 100644
index 0000000..bb53581
--- /dev/null
+++ b/metropolis/test/localregistry/spec/manifest.proto
@@ -0,0 +1,19 @@
+syntax = "proto3";
+
+package monogon.metropolis.pkg.localregistry;
+
+option go_package = "source.monogon.dev/metropolis/test/localregistry/spec";
+
+// 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;
+}
+
+// Main message
+message Manifest {
+ // List of images for the local registry
+ repeated Image images = 1;
+}
\ No newline at end of file