treewide: replace rules_docker with rules_oci

rules_docker is not maintained anymore and recommends migration to
rules_oci

Change-Id: I089f3cf44888b3c3c0baa2c84a319b04b1a7dec4
Reviewed-on: https://review.monogon.dev/c/monogon/+/2712
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/BUILD.bazel b/BUILD.bazel
index eb77d5e..3a94059 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,5 +1,4 @@
 load("@bazel_gazelle//:def.bzl", "gazelle")
-load("@io_bazel_rules_go//go:def.bzl", "go_path")
 load("@bazeldnf//:def.bzl", "bazeldnf")
 
 # gazelle:prefix source.monogon.dev
diff --git a/WORKSPACE b/WORKSPACE
index 159c580..41087a0 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -318,28 +318,57 @@
 
 register_toolchains("//:host_python")
 
-# same for gvisor/rules_docker.
+http_archive(
+    name = "aspect_bazel_lib",
+    sha256 = "bda4a69fa50411b5feef473b423719d88992514d259dadba7d8218a1d02c7883",
+    strip_prefix = "bazel-lib-2.3.0",
+    url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.3.0/bazel-lib-v2.3.0.tar.gz",
+)
+
+load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains")
+
+# Required bazel-lib dependencies
+
+aspect_bazel_lib_dependencies()
+
+# Register bazel-lib toolchains
+
+aspect_bazel_lib_register_toolchains()
 
 http_archive(
-    name = "io_bazel_rules_docker",
-    sha256 = "b1e80761a8a8243d03ebca8845e9cc1ba6c82ce7c5179ce2b295cd36f7e394bf",
-    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.25.0/rules_docker-v0.25.0.tar.gz"],
+    name = "rules_oci",
+    sha256 = "58b7a175ee90c12583afeca388523adf6a4e5a0528f330b41c302b91a4d6fc06",
+    strip_prefix = "rules_oci-1.6.0",
+    url = "https://github.com/bazel-contrib/rules_oci/releases/download/v1.6.0/rules_oci-v1.6.0.tar.gz",
 )
 
-load(
-    "@io_bazel_rules_docker//repositories:repositories.bzl",
-    container_repositories = "repositories",
+load("@rules_oci//oci:dependencies.bzl", "rules_oci_dependencies")
+
+rules_oci_dependencies()
+
+load("@rules_oci//oci:repositories.bzl", "LATEST_CRANE_VERSION", "oci_register_toolchains")
+
+oci_register_toolchains(
+    name = "oci",
+    crane_version = LATEST_CRANE_VERSION,
+    # Uncommenting the zot toolchain will cause it to be used instead of crane for some tasks.
+    # Note that it does not support docker-format images.
+    # zot_version = LATEST_ZOT_VERSION,
 )
 
-container_repositories()
+# You can pull your base images using oci_pull like this:
+load("@rules_oci//oci:pull.bzl", "oci_pull")
 
-load(
-    "@io_bazel_rules_docker//go:image.bzl",
-    go_image_repos = "repositories",
+oci_pull(
+    name = "distroless_base",
+    digest = "sha256:6c1e34e2f084fe6df17b8bceb1416f1e11af0fcdb1cef11ee4ac8ae127cb507c",
+    image = "gcr.io/distroless/base",
+    platforms = [
+        "linux/amd64",
+        "linux/arm64",
+    ],
 )
 
-go_image_repos()
-
 # Derived from Mozilla NSS, currently needed for containerd to be able to pull images
 http_file(
     name = "cacerts",
diff --git a/build/platforms/BUILD.bazel b/build/platforms/BUILD.bazel
index 4025c0a..a238039 100644
--- a/build/platforms/BUILD.bazel
+++ b/build/platforms/BUILD.bazel
@@ -5,6 +5,7 @@
         "@platforms//os:linux",
         "@platforms//cpu:x86_64",
     ],
+    visibility = ["//visibility:public"],
 )
 
 # EFI preboot environment for x86_64 machines.
@@ -26,4 +27,5 @@
         "@platforms//cpu:x86_64",
         "//build/platforms/linkmode:musl-static",
     ],
+    visibility = ["//visibility:public"],
 )
diff --git a/cloud/BUILD.bazel b/cloud/BUILD.bazel
index 0e44a88..965c100 100644
--- a/cloud/BUILD.bazel
+++ b/cloud/BUILD.bazel
@@ -1,19 +1,58 @@
-load("@io_bazel_rules_docker//container:bundle.bzl", "container_bundle")
-load("@io_bazel_rules_docker//contrib:push-all.bzl", "container_push")
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
 
-container_bundle(
-    name = "cloud_containers",
-    images = {
-        "gcr.io/monogon-infra/cloud/apigw:{STABLE_MONOGON_cloud_version}": "//cloud/apigw:apigw_container",
-        "gcr.io/monogon-infra/cloud/shepherd/equinix:{STABLE_MONOGON_cloud_version}": "//cloud/shepherd/provider/equinix:equinix_container",
-        "gcr.io/monogon-infra/cloud/bmsrv:{STABLE_MONOGON_cloud_version}": "//cloud/bmaas/server/cmd:cmd_container",
-        "gcr.io/monogon-infra/cloud/scruffy:{STABLE_MONOGON_cloud_version}": "//cloud/bmaas/scruffy/cmd:cmd_container",
-        "gcr.io/monogon-infra/cloud/shepherd/mini:{STABLE_MONOGON_cloud_version}": "//cloud/shepherd/mini:mini_container",
-    },
+write_file(
+    name = "tags_tmpl",
+    out = "tags.txt.tmpl",
+    content = [
+        "BUILD_VERSION",
+    ],
 )
 
-container_push(
-    name = "push",
-    bundle = ":cloud_containers",
-    format = "Docker",
+load("@aspect_bazel_lib//lib:expand_template.bzl", "expand_template")
+
+# Use the value of --embed_label under --stamp, otherwise use a deterministic constant
+# value to ensure cache hits for actions that depend on this.
+expand_template(
+    name = "stamped",
+    out = "_stamped.tags.txt",
+    stamp_substitutions = {"BUILD_VERSION": "{{STABLE_MONOGON_cloud_version}}"},
+    substitutions = {"BUILD_VERSION": "0.0.0"},
+    template = "tags_tmpl",
+)
+
+load("@rules_oci//oci:defs.bzl", "oci_push")
+
+oci_push(
+    name = "apigw_image",
+    image = "//cloud/apigw:apigw_image",
+    remote_tags = ":stamped",
+    repository = "gcr.io/monogon-infra/cloud/apigw",
+)
+
+oci_push(
+    name = "shepherd_equinix",
+    image = "//cloud/shepherd/provider/equinix:equinix_image",
+    remote_tags = ":stamped",
+    repository = "gcr.io/monogon-infra/cloud/shepherd/equinix",
+)
+
+oci_push(
+    name = "bmsrv",
+    image = "//cloud/bmaas/server/cmd:cmd_image",
+    remote_tags = ":stamped",
+    repository = "gcr.io/monogon-infra/cloud/bmsrv",
+)
+
+oci_push(
+    name = "scruffy",
+    image = "//cloud/bmaas/scruffy/cmd:cmd_image",
+    remote_tags = ":stamped",
+    repository = "gcr.io/monogon-infra/cloud/scruffy",
+)
+
+oci_push(
+    name = "shepherd_mini",
+    image = "//cloud/shepherd/mini:mini_image",
+    remote_tags = ":stamped",
+    repository = "gcr.io/monogon-infra/cloud/shepherd/mini",
 )
diff --git a/cloud/apigw/BUILD.bazel b/cloud/apigw/BUILD.bazel
index ee3b409..a570529 100644
--- a/cloud/apigw/BUILD.bazel
+++ b/cloud/apigw/BUILD.bazel
@@ -1,6 +1,4 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("@io_bazel_rules_docker//container:container.bzl", "container_image")
-load("//build/static_binary_tarball:def.bzl", "static_binary_tarball")
 
 go_library(
     name = "apigw_lib",
@@ -19,15 +17,19 @@
     visibility = ["//visibility:public"],
 )
 
-static_binary_tarball(
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
     name = "apigw_layer",
-    executable = ":apigw",
+    srcs = [":apigw"],
 )
 
-container_image(
-    name = "apigw_container",
-    base = "@go_image_base//image",
-    entrypoint = ["/app/cloud/apigw/apigw_/apigw"],
+load("@rules_oci//oci:defs.bzl", "oci_image")
+
+oci_image(
+    name = "apigw_image",
+    base = "@distroless_base",
+    entrypoint = ["/apigw"],
     tars = [":apigw_layer"],
     visibility = ["//visibility:public"],
     workdir = "/app",
diff --git a/cloud/bmaas/scruffy/cmd/BUILD.bazel b/cloud/bmaas/scruffy/cmd/BUILD.bazel
index cf0391b..1a372e7 100644
--- a/cloud/bmaas/scruffy/cmd/BUILD.bazel
+++ b/cloud/bmaas/scruffy/cmd/BUILD.bazel
@@ -1,6 +1,4 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("@io_bazel_rules_docker//container:container.bzl", "container_image")
-load("//build/static_binary_tarball:def.bzl", "static_binary_tarball")
 
 go_library(
     name = "cmd_lib",
@@ -19,15 +17,19 @@
     visibility = ["//visibility:public"],
 )
 
-static_binary_tarball(
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
     name = "cmd_layer",
-    executable = ":cmd",
+    srcs = [":cmd"],
 )
 
-container_image(
-    name = "cmd_container",
-    base = "@go_image_base//image",
-    entrypoint = ["/app/cloud/bmaas/scruffy/cmd/cmd_/cmd"],
+load("@rules_oci//oci:defs.bzl", "oci_image")
+
+oci_image(
+    name = "cmd_image",
+    base = "@distroless_base",
+    entrypoint = ["/cmd"],
     tars = [":cmd_layer"],
     visibility = ["//visibility:public"],
     workdir = "/app",
diff --git a/cloud/bmaas/server/cmd/BUILD.bazel b/cloud/bmaas/server/cmd/BUILD.bazel
index 50184b9..958f2c4 100644
--- a/cloud/bmaas/server/cmd/BUILD.bazel
+++ b/cloud/bmaas/server/cmd/BUILD.bazel
@@ -1,5 +1,4 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("@io_bazel_rules_docker//container:container.bzl", "container_image")
 load("//build/static_binary_tarball:def.bzl", "static_binary_tarball")
 
 go_library(
@@ -20,15 +19,19 @@
     visibility = ["//visibility:public"],
 )
 
-static_binary_tarball(
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
     name = "cmd_layer",
-    executable = ":cmd",
+    srcs = [":cmd"],
 )
 
-container_image(
-    name = "cmd_container",
-    base = "@go_image_base//image",
-    entrypoint = ["/app/cloud/bmaas/server/cmd/cmd_/cmd"],
+load("@rules_oci//oci:defs.bzl", "oci_image")
+
+oci_image(
+    name = "cmd_image",
+    base = "@distroless_base",
+    entrypoint = ["/cmd"],
     tars = [":cmd_layer"],
     visibility = ["//visibility:public"],
     workdir = "/app",
diff --git a/cloud/shepherd/mini/BUILD.bazel b/cloud/shepherd/mini/BUILD.bazel
index eb949ee..5587d1e 100644
--- a/cloud/shepherd/mini/BUILD.bazel
+++ b/cloud/shepherd/mini/BUILD.bazel
@@ -1,6 +1,4 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("@io_bazel_rules_docker//container:container.bzl", "container_image")
-load("//build/static_binary_tarball:def.bzl", "static_binary_tarball")
 
 go_library(
     name = "mini_lib",
@@ -30,15 +28,19 @@
     visibility = ["//visibility:public"],
 )
 
-static_binary_tarball(
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
     name = "mini_layer",
-    executable = ":mini",
+    srcs = [":mini"],
 )
 
-container_image(
-    name = "mini_container",
-    base = "@go_image_base//image",
-    entrypoint = ["/app/cloud/shepherd/mini/mini_/mini"],
+load("@rules_oci//oci:defs.bzl", "oci_image")
+
+oci_image(
+    name = "mini_image",
+    base = "@distroless_base",
+    entrypoint = ["/mini"],
     tars = [
         ":mini_layer",
         "//cloud/takeover:takeover_layer",
diff --git a/cloud/shepherd/provider/equinix/BUILD.bazel b/cloud/shepherd/provider/equinix/BUILD.bazel
index 3363d7f..2ea4ee7 100644
--- a/cloud/shepherd/provider/equinix/BUILD.bazel
+++ b/cloud/shepherd/provider/equinix/BUILD.bazel
@@ -1,6 +1,4 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
-load("@io_bazel_rules_docker//container:container.bzl", "container_image")
-load("//build/static_binary_tarball:def.bzl", "static_binary_tarball")
 
 go_library(
     name = "equinix_lib",
@@ -59,15 +57,19 @@
     visibility = ["//visibility:public"],
 )
 
-static_binary_tarball(
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
     name = "equinix_layer",
-    executable = ":equinix",
+    srcs = [":equinix"],
 )
 
-container_image(
-    name = "equinix_container",
-    base = "@go_image_base//image",
-    entrypoint = ["/app/cloud/shepherd/provider/equinix/equinix_/equinix"],
+load("@rules_oci//oci:defs.bzl", "oci_image")
+
+oci_image(
+    name = "equinix_image",
+    base = "@distroless_base",
+    entrypoint = ["/equinix"],
     tars = [
         ":equinix_layer",
         "//cloud/takeover:takeover_layer",
diff --git a/metropolis/node/BUILD.bazel b/metropolis/node/BUILD.bazel
index ba5103c..b1173db 100644
--- a/metropolis/node/BUILD.bazel
+++ b/metropolis/node/BUILD.bazel
@@ -83,7 +83,7 @@
         "//metropolis/node/kubernetes/containerd:cnispec.gojson": "/containerd/conf/cnispec.gojson",
 
         # Containerd preseed bundles
-        "//metropolis/test/e2e/preseedtest:preseedtest_image.tar": "/containerd/preseed/k8s.io/preseedtest.tar",
+        "//metropolis/test/e2e/preseedtest:preseedtest_tarball": "/containerd/preseed/k8s.io/preseedtest.tar",
 
         # CNI Plugins
         "@com_github_containernetworking_plugins//plugins/main/loopback": "/containerd/bin/cni/loopback",
diff --git a/metropolis/pkg/localregistry/BUILD.bazel b/metropolis/pkg/localregistry/BUILD.bazel
index 636bc57..4281ac1 100644
--- a/metropolis/pkg/localregistry/BUILD.bazel
+++ b/metropolis/pkg/localregistry/BUILD.bazel
@@ -8,6 +8,8 @@
     deps = [
         "//metropolis/pkg/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",
diff --git a/metropolis/pkg/localregistry/def.bzl b/metropolis/pkg/localregistry/def.bzl
index 061a63d..c5fc560 100644
--- a/metropolis/pkg/localregistry/def.bzl
+++ b/metropolis/pkg/localregistry/def.bzl
@@ -1,28 +1,19 @@
-load("@io_bazel_rules_docker//container:providers.bzl", "ImageInfo")
+#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_info = i[ImageInfo].container_parts
-        referenced.append(image_info['config'])
-        referenced.append(image_info['config_digest'])
+        image_file = i.files.to_list()[0]
         image = struct(
             name = i.label.package + "/" + i.label.name,
-            config = struct(
-                file_path = image_info['config'].short_path,
-                digest_path = image_info['config_digest'].short_path,
-            ),
-            layers = [],
+            path = image_file.short_path,
         )
-        for layer in zip(image_info['zipped_layer'], image_info['blobsum']):
-            referenced.append(layer[0])
-            referenced.append(layer[1])
-            image.layers.append(struct(file_path = layer[0].short_path, digest_path=layer[1].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]))]
 
@@ -36,9 +27,9 @@
         "images": attr.label_list(
             mandatory = True,
             doc = """
-                List of images (with ImageInfo provider) to be served from the local registry.
+                List of images to be served from the local registry.
             """,
-           providers = [ImageInfo],
+           providers = [],
         ),
     },
 )
diff --git a/metropolis/pkg/localregistry/localregistry.go b/metropolis/pkg/localregistry/localregistry.go
index e40fb1b..885259b 100644
--- a/metropolis/pkg/localregistry/localregistry.go
+++ b/metropolis/pkg/localregistry/localregistry.go
@@ -10,10 +10,13 @@
 	"log"
 	"net/http"
 	"os"
+	"path/filepath"
 	"regexp"
 	"strconv"
 
 	"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"
@@ -33,55 +36,76 @@
 	contentLength int64
 }
 
-func blobFromBazel(s *Server, bd *spec.BlobDescriptor, mediaType string) (distribution.Descriptor, error) {
-	digestRaw, err := os.ReadFile(bd.DigestPath)
+func manifestDescriptorFromBazel(image *spec.Image) (manifestlist.ManifestDescriptor, error) {
+	indexPath := filepath.Join(image.Path, "index.json")
+
+	manifestListRaw, err := os.ReadFile(indexPath)
 	if err != nil {
-		return distribution.Descriptor{}, fmt.Errorf("while opening digest file: %w", err)
+		return manifestlist.ManifestDescriptor{}, fmt.Errorf("while opening manifest list file: %w", err)
 	}
-	stat, err := os.Stat(bd.FilePath)
-	if err != nil {
-		return distribution.Descriptor{}, fmt.Errorf("while stat'ing blob 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)
 	}
-	digest := digest.Digest("sha256:" + string(digestRaw))
-	s.blobs[digest] = blobMeta{filePath: bd.FilePath, mediaType: mediaType, contentLength: stat.Size()}
-	return distribution.Descriptor{
-		MediaType: mediaType,
-		Size:      stat.Size(),
-		Digest:    digest,
-	}, nil
+
+	if len(imageManifestList.Manifests) != 1 {
+		return manifestlist.ManifestDescriptor{}, fmt.Errorf("unexpected manifest list length > 1")
+	}
+
+	return imageManifestList.Manifests[0], nil
 }
 
-func FromBazelManifest(m []byte) (*Server, error) {
-	var manifest spec.Manifest
-	if err := prototext.Unmarshal(m, &manifest); err != nil {
+func manifestFromBazel(s *Server, image *spec.Image, md manifestlist.ManifestDescriptor) (ocischema.Manifest, error) {
+	manifestPath := filepath.Join(image.Path, "blobs", md.Digest.Algorithm().String(), md.Digest.Hex())
+	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) {
+	path := filepath.Join(image.Path, "blobs", dd.Digest.Algorithm().String(), dd.Digest.Hex())
+	s.blobs[dd.Digest] = blobMeta{filePath: path, mediaType: dd.MediaType, contentLength: dd.Size}
+}
+
+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 manifest.Images {
-		imageManifest := schema2.Manifest{
-			Versioned: schema2.SchemaVersion,
-		}
-		var err error
-		imageManifest.Config, err = blobFromBazel(&s, i.Config, schema2.MediaTypeImageConfig)
+	for _, i := range bazelManifest.Images {
+		md, err := manifestDescriptorFromBazel(i)
 		if err != nil {
-			return nil, fmt.Errorf("while creating blob spec for %q: %w", i.Name, err)
+			return nil, err
 		}
-		for _, l := range i.Layers {
-			ml, err := blobFromBazel(&s, l, schema2.MediaTypeLayer)
-			if err != nil {
-				return nil, fmt.Errorf("while creating blob spec for %q: %w", i.Name, err)
-			}
-			imageManifest.Layers = append(imageManifest.Layers, ml)
-		}
-		s.manifests[i.Name], err = json.Marshal(imageManifest)
+
+		addBazelBlobFromDescriptor(&s, i, md.Descriptor)
+
+		m, err := manifestFromBazel(&s, i, md)
 		if err != nil {
-			return nil, fmt.Errorf("while marshaling image %q manifest: %w", i.Name, err)
+			return nil, err
 		}
-		// For Digest lookups
-		s.manifests[string(digest.Canonical.FromBytes(s.manifests[i.Name]))] = s.manifests[i.Name]
+
+		addBazelBlobFromDescriptor(&s, i, m.Config)
+		for _, l := range m.Layers {
+			addBazelBlobFromDescriptor(&s, i, l)
+		}
 	}
 	return &s, nil
 }
diff --git a/metropolis/pkg/localregistry/spec/manifest.proto b/metropolis/pkg/localregistry/spec/manifest.proto
index b4daeab..b28c8b7 100644
--- a/metropolis/pkg/localregistry/spec/manifest.proto
+++ b/metropolis/pkg/localregistry/spec/manifest.proto
@@ -4,23 +4,12 @@
 
 option go_package = "source.monogon.dev/metropolis/pkg/localregistry/spec";
 
-// BlobDescriptor is metadata for a single registry blob. Analogous to a
-// distribution.Descriptor in Go.
-message BlobDescriptor {
-    // Path to the file in the build directory.
-    string file_path = 1;
-    // Path to a file containing the SHA256 digest of the blob.
-    string digest_path = 2;
-}
-
 // Single image metadata
 message Image {
     // Name of the image (no domain or tag, just slash-separated path)
     string name = 1;
-    // Config blob
-    BlobDescriptor config = 2;
-    // Layer blobs in order
-    repeated BlobDescriptor layers = 3;
+    // Path to the image
+    string path = 2;
 }
 
 // Main message
diff --git a/metropolis/test/e2e/BUILD.bazel b/metropolis/test/e2e/BUILD.bazel
index b3b2625..1e21035 100644
--- a/metropolis/test/e2e/BUILD.bazel
+++ b/metropolis/test/e2e/BUILD.bazel
@@ -21,7 +21,7 @@
     name = "testimages_manifest",
     images = [
         "//metropolis/test/e2e/selftest:selftest_image",
-        "//metropolis/vm/smoketest:smoketest_container",
+        "//metropolis/vm/smoketest:smoketest_image",
     ],
 )
 
diff --git a/metropolis/test/e2e/preseedtest/BUILD.bazel b/metropolis/test/e2e/preseedtest/BUILD.bazel
index 8f13e23..864ba59 100644
--- a/metropolis/test/e2e/preseedtest/BUILD.bazel
+++ b/metropolis/test/e2e/preseedtest/BUILD.bazel
@@ -1,16 +1,50 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-load("@io_bazel_rules_docker//go:image.bzl", "go_image")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 
 go_library(
-    name = "preseedtest",
+    name = "preseedtest_lib",
     srcs = ["main.go"],
     importpath = "source.monogon.dev/metropolis/test/e2e/preseedtest",
     visibility = ["//visibility:private"],
 )
 
-go_image(
-    name = "preseedtest_image",
-    embed = [":preseedtest"],
+go_binary(
+    name = "preseedtest",
+    embed = [":preseedtest_lib"],
     pure = "on",
+    visibility = ["//visibility:private"],
+)
+
+load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_binary")
+
+platform_transition_binary(
+    name = "preseedtest_transitioned",
+    binary = ":preseedtest",
+    target_platform = "//build/platforms:linux_amd64_static",
+    visibility = ["//visibility:private"],
+)
+
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
+    name = "preseedtest_layer",
+    srcs = [":preseedtest_transitioned"],
+    visibility = ["//visibility:private"],
+)
+
+load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball")
+
+oci_image(
+    name = "preseedtest_image",
+    base = "@distroless_base",
+    entrypoint = ["/preseedtest"],
+    tars = [":preseedtest_layer"],
+    visibility = ["//visibility:private"],
+    workdir = "/app",
+)
+
+oci_tarball(
+    name = "preseedtest_tarball",
+    image = ":preseedtest_image",
+    repo_tags = ["bazel/metropolis/test/e2e/preseedtest:preseedtest_image"],
     visibility = ["//metropolis/node:__pkg__"],
 )
diff --git a/metropolis/test/e2e/selftest/BUILD.bazel b/metropolis/test/e2e/selftest/BUILD.bazel
index 7560790..0990150 100644
--- a/metropolis/test/e2e/selftest/BUILD.bazel
+++ b/metropolis/test/e2e/selftest/BUILD.bazel
@@ -1,16 +1,43 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-load("@io_bazel_rules_docker//go:image.bzl", "go_image")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 
 go_library(
-    name = "selftest",
+    name = "selftest_lib",
     srcs = ["main.go"],
     importpath = "source.monogon.dev/metropolis/test/e2e/selftest",
     visibility = ["//visibility:private"],
 )
 
-go_image(
-    name = "selftest_image",
-    embed = [":selftest"],
+go_binary(
+    name = "selftest",
+    embed = [":selftest_lib"],
     pure = "on",
+    visibility = ["//visibility:private"],
+)
+
+load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_binary")
+
+platform_transition_binary(
+    name = "selftest_transitioned",
+    binary = ":selftest",
+    target_platform = "//build/platforms:linux_amd64_static",
+    visibility = ["//visibility:private"],
+)
+
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
+    name = "selftest_layer",
+    srcs = [":selftest_transitioned"],
+    visibility = ["//visibility:private"],
+)
+
+load("@rules_oci//oci:defs.bzl", "oci_image")
+
+oci_image(
+    name = "selftest_image",
+    base = "@distroless_base",
+    entrypoint = ["/selftest"],
+    tars = [":selftest_layer"],
     visibility = ["//metropolis/test/e2e:__pkg__"],
+    workdir = "/app",
 )
diff --git a/metropolis/vm/smoketest/BUILD.bazel b/metropolis/vm/smoketest/BUILD.bazel
index 66e0736..1dce6e4 100644
--- a/metropolis/vm/smoketest/BUILD.bazel
+++ b/metropolis/vm/smoketest/BUILD.bazel
@@ -1,7 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("@io_bazel_rules_docker//container:container.bzl", "container_image")
 load("//metropolis/node/build:def.bzl", "node_initramfs")
-load("//build/static_binary_tarball:def.bzl", "static_binary_tarball")
 
 go_library(
     name = "smoketest_lib",
@@ -28,18 +26,33 @@
         "@qemu//:qemu-x86_64-softmmu",
     ],
     embed = [":smoketest_lib"],
-    visibility = ["//visibility:public"],
+    pure = "on",
+    visibility = ["//visibility:private"],
 )
 
-static_binary_tarball(
+load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_binary")
+
+platform_transition_binary(
+    name = "smoketest_transitioned",
+    binary = ":smoketest",
+    target_platform = "//build/platforms:linux_amd64_static",
+    visibility = ["//visibility:private"],
+)
+
+load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
+
+pkg_tar(
     name = "smoketest_layer",
-    executable = ":smoketest",
+    srcs = [":smoketest_transitioned"],
+    visibility = ["//visibility:private"],
 )
 
-container_image(
-    name = "smoketest_container",
-    base = "@go_image_base//image",
-    entrypoint = ["/app/metropolis/vm/smoketest/smoketest_/smoketest"],
+load("@rules_oci//oci:defs.bzl", "oci_image")
+
+oci_image(
+    name = "smoketest_image",
+    base = "@distroless_base",
+    entrypoint = ["/smoketest"],
     tars = [":smoketest_layer"],
     visibility = ["//visibility:public"],
     workdir = "/app",
diff --git a/third_party/linux/def.bzl b/third_party/linux/def.bzl
index 885e329..715d058 100644
--- a/third_party/linux/def.bzl
+++ b/third_party/linux/def.bzl
@@ -36,9 +36,6 @@
     "@io_bazel_rules_go//go/config:pure": True,
     "@io_bazel_rules_go//go/config:static": True,
 
-    "@io_bazel_rules_docker//platforms:image_transition_cpu": "@platforms//cpu:x86_64",
-    "@io_bazel_rules_docker//platforms:image_transition_os": "@platforms//os:linux",
-
     # Note: this toolchain is not actually used to perform the build.
     "//command_line_option:platforms": "//build/platforms:linux_amd64_static",
 }