From a1b383d4d88a4083a54a0c9778c18776d88b1bab Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 16 Dec 2025 12:59:38 -0800 Subject: [PATCH 01/15] test: containerd+erofs+stacker support This PR validates the end-to-end capability of stacker to build erofs OCI images that are compatible with containerd so that you can build and package self-compressed, runnable container images for use cases such as ML models. The alternative is the much slow tgz path. https://github.com/erofs/erofs-container-toolkit/blob/main/docs/native.md#enable-the-erofs-snapshotter-plugin Signed-off-by: Ramkumar Chinchani --- Makefile | 13 +- test/containerd-erofs.bats | 13 ++ test/data/config.toml | 260 +++++++++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 test/containerd-erofs.bats create mode 100644 test/data/config.toml diff --git a/Makefile b/Makefile index b1e2f7df..5721da80 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ TOOLS_D := $(HACK_D)/tools REGCLIENT := $(TOOLS_D)/bin/regctl REGCLIENT_VERSION := v0.5.1 SKOPEO = $(TOOLS_D)/bin/skopeo +CONTAINERD = $(TOOLS_D)/bin/containerd export SKOPEO_VERSION = 1.13.0 BATS = $(TOOLS_D)/bin/bats BATS_VERSION := v1.10.0 @@ -136,7 +137,7 @@ go-test: go tool cover -html coverage.txt -o $(HACK_D)/coverage.html .PHONY: download-tools -download-tools: $(GOLANGCI_LINT) $(REGCLIENT) $(ZOT) $(BATS) $(UMOCI) $(SKOPEO) +download-tools: $(GOLANGCI_LINT) $(REGCLIENT) $(ZOT) $(BATS) $(UMOCI) $(SKOPEO) $(CONTAINERD) $(GOLANGCI_LINT): @[ -x $(GOLANGCI_LINT) ] || \ @@ -170,6 +171,16 @@ $(SKOPEO): cd $(TOP_LEVEL); \ rm -rf $$tmpdir; +$(CONTAINERD): + @set -e; mkdir -p "$(TOOLS_D)/bin"; \ + tmpdir=$$(mktemp -d); \ + cd $$tmpdir; \ + wget https://github.com/containerd/containerd/releases/download/v2.1.4/containerd-2.1.4-linux-amd64.tar.gz; \ + tar xvf containerd-2.1.4-linux-amd64.tar.gz; \ + cp bin/containerd $(CONTAINERD); + cd $(TOP_LEVEL); \ + rm -rf $$tmpdir; + $(BATS): mkdir -p $(TOOLS_D)/bin rm -rf bats-core diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats new file mode 100644 index 00000000..c8b8eb8f --- /dev/null +++ b/test/containerd-erofs.bats @@ -0,0 +1,13 @@ +load helpers + +function setup() { + stacker_setup +} + +function teardown() { + cleanup +} + +@test "containerd with erofs support" { + ${ROOT_DIR}/hack/tools/bin/containerd -c ${ROOT_DIR}/test/data/config.toml +} diff --git a/test/data/config.toml b/test/data/config.toml new file mode 100644 index 00000000..2040b892 --- /dev/null +++ b/test/data/config.toml @@ -0,0 +1,260 @@ +version = 3 +root = '/var/lib/containerd' +state = '/run/containerd' +temp = '' +disabled_plugins = [] +required_plugins = [] +oom_score = 0 +imports = [] + +[grpc] + address = '/run/containerd/containerd.sock' + tcp_address = '' + tcp_tls_ca = '' + tcp_tls_cert = '' + tcp_tls_key = '' + uid = 0 + gid = 0 + max_recv_message_size = 16777216 + max_send_message_size = 16777216 + +[ttrpc] + address = '' + uid = 0 + gid = 0 + +[debug] + address = '' + uid = 0 + gid = 0 + level = '' + format = '' + +[metrics] + address = '' + grpc_histogram = false + +[plugins] + [plugins.'io.containerd.cri.v1.images'] + snapshotter = 'overlayfs' + disable_snapshot_annotations = true + discard_unpacked_layers = false + max_concurrent_downloads = 3 + concurrent_layer_fetch_buffer = 0 + image_pull_progress_timeout = '5m0s' + image_pull_with_sync_fs = false + stats_collect_period = 10 + use_local_image_pull = false + + [plugins.'io.containerd.cri.v1.images'.pinned_images] + sandbox = 'registry.k8s.io/pause:3.10' + + [plugins.'io.containerd.cri.v1.images'.registry] + config_path = '' + + [plugins.'io.containerd.cri.v1.images'.image_decryption] + key_model = 'node' + + [plugins.'io.containerd.cri.v1.runtime'] + enable_selinux = false + selinux_category_range = 1024 + max_container_log_line_size = 16384 + disable_apparmor = false + restrict_oom_score_adj = false + disable_proc_mount = false + unset_seccomp_profile = '' + tolerate_missing_hugetlb_controller = true + disable_hugetlb_controller = true + device_ownership_from_security_context = false + ignore_image_defined_volumes = false + netns_mounts_under_state_dir = false + enable_unprivileged_ports = true + enable_unprivileged_icmp = true + enable_cdi = true + cdi_spec_dirs = ['/etc/cdi', '/var/run/cdi'] + drain_exec_sync_io_timeout = '0s' + ignore_deprecation_warnings = [] + + [plugins.'io.containerd.cri.v1.runtime'.containerd] + default_runtime_name = 'runc' + ignore_blockio_not_enabled_errors = false + ignore_rdt_not_enabled_errors = false + + [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes] + [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc] + runtime_type = 'io.containerd.runc.v2' + runtime_path = '' + pod_annotations = [] + container_annotations = [] + privileged_without_host_devices = false + privileged_without_host_devices_all_devices_allowed = false + cgroup_writable = false + base_runtime_spec = '' + cni_conf_dir = '' + cni_max_conf_num = 0 + snapshotter = '' + sandboxer = 'podsandbox' + io_type = '' + + [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options] + BinaryName = '' + CriuImagePath = '' + CriuWorkPath = '' + IoGid = 0 + IoUid = 0 + NoNewKeyring = false + Root = '' + ShimCgroup = '' + + [plugins.'io.containerd.cri.v1.runtime'.cni] + bin_dir = '' + bin_dirs = ['/opt/cni/bin'] + conf_dir = '/etc/cni/net.d' + max_conf_num = 1 + setup_serially = false + conf_template = '' + ip_pref = '' + use_internal_loopback = false + + [plugins.'io.containerd.gc.v1.scheduler'] + pause_threshold = 0.02 + deletion_threshold = 0 + mutation_threshold = 100 + schedule_delay = '0s' + startup_delay = '100ms' + + [plugins.'io.containerd.grpc.v1.cri'] + disable_tcp_service = true + stream_server_address = '127.0.0.1' + stream_server_port = '0' + stream_idle_timeout = '4h0m0s' + enable_tls_streaming = false + + [plugins.'io.containerd.grpc.v1.cri'.x509_key_pair_streaming] + tls_cert_file = '' + tls_key_file = '' + + [plugins.'io.containerd.image-verifier.v1.bindir'] + bin_dir = '/opt/containerd/image-verifier/bin' + max_verifiers = 10 + per_verifier_timeout = '10s' + + [plugins.'io.containerd.internal.v1.opt'] + path = '/opt/containerd' + + [plugins.'io.containerd.internal.v1.tracing'] + + [plugins.'io.containerd.metadata.v1.bolt'] + content_sharing_policy = 'shared' + no_sync = false + + [plugins.'io.containerd.monitor.container.v1.restart'] + interval = '10s' + + [plugins.'io.containerd.monitor.task.v1.cgroups'] + no_prometheus = false + + [plugins.'io.containerd.nri.v1.nri'] + disable = false + socket_path = '/var/run/nri/nri.sock' + plugin_path = '/opt/nri/plugins' + plugin_config_path = '/etc/nri/conf.d' + plugin_registration_timeout = '5s' + plugin_request_timeout = '2s' + disable_connections = false + + [plugins.'io.containerd.runtime.v2.task'] + platforms = ['linux/amd64'] + + [plugins.'io.containerd.service.v1.diff-service'] + default = ['erofs', 'walking'] + sync_fs = false + + [plugins."io.containerd.differ.v1.erofs"] + mkfs_options = ["--sort=none"] + + [[plugins."io.containerd.transfer.v1.local".unpack_config]] + differ = "erofs" + platform = "linux/amd64" + snapshotter = "erofs" + layer_types = ["application/vnd.erofs"] + + [plugins.'io.containerd.service.v1.tasks-service'] + blockio_config_file = '' + rdt_config_file = '' + + [plugins.'io.containerd.shim.v1.manager'] + env = [] + + [plugins.'io.containerd.snapshotter.v1.blockfile'] + root_path = '' + scratch_file = '' + fs_type = '' + mount_options = [] + recreate_scratch = false + + [plugins.'io.containerd.snapshotter.v1.btrfs'] + root_path = '' + + [plugins.'io.containerd.snapshotter.v1.devmapper'] + root_path = '' + pool_name = '' + base_image_size = '' + async_remove = false + discard_blocks = false + fs_type = '' + fs_options = '' + + [plugins.'io.containerd.snapshotter.v1.erofs'] + root_path = '' + ovl_mount_options = [] + enable_fsverity = false + + [plugins.'io.containerd.snapshotter.v1.native'] + root_path = '' + + [plugins.'io.containerd.snapshotter.v1.overlayfs'] + root_path = '' + upperdir_label = false + sync_remove = false + slow_chown = false + mount_options = [] + + [plugins.'io.containerd.snapshotter.v1.zfs'] + root_path = '' + + [plugins.'io.containerd.tracing.processor.v1.otlp'] + + [plugins.'io.containerd.transfer.v1.local'] + max_concurrent_downloads = 3 + concurrent_layer_fetch_buffer = 0 + max_concurrent_uploaded_layers = 3 + check_platform_supported = false + config_path = '' + +[cgroup] + path = '' + +[timeouts] + 'io.containerd.timeout.bolt.open' = '0s' + 'io.containerd.timeout.cri.defercleanup' = '1m0s' + 'io.containerd.timeout.metrics.shimstats' = '2s' + 'io.containerd.timeout.shim.cleanup' = '5s' + 'io.containerd.timeout.shim.load' = '5s' + 'io.containerd.timeout.shim.shutdown' = '3s' + 'io.containerd.timeout.task.state' = '2s' + +[stream_processors] + [stream_processors.'io.containerd.ocicrypt.decoder.v1.tar'] + accepts = ['application/vnd.oci.image.layer.v1.tar+encrypted'] + returns = 'application/vnd.oci.image.layer.v1.tar' + path = 'ctd-decoder' + args = ['--decryption-keys-path', '/etc/containerd/ocicrypt/keys'] + env = ['OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf'] + + [stream_processors.'io.containerd.ocicrypt.decoder.v1.tar.gzip'] + accepts = ['application/vnd.oci.image.layer.v1.tar+gzip+encrypted'] + returns = 'application/vnd.oci.image.layer.v1.tar+gzip' + path = 'ctd-decoder' + args = ['--decryption-keys-path', '/etc/containerd/ocicrypt/keys'] + env = ['OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf'] From b4e109058049e25c8a03c03ca7fb67cb0046f5e0 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Wed, 8 Apr 2026 18:55:42 -0700 Subject: [PATCH 02/15] feat: add EROFS support for containerd and enhance testing Signed-off-by: Ramkumar Chinchani --- Makefile | 10 +- pkg/overlay/pack.go | 21 +++ pkg/types/layer_type.go | 8 ++ test/containerd-erofs.bats | 124 +++++++++++++++++- test/data/config.toml | 260 ------------------------------------- 5 files changed, 156 insertions(+), 267 deletions(-) delete mode 100644 test/data/config.toml diff --git a/Makefile b/Makefile index 5721da80..68213302 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ REGCLIENT := $(TOOLS_D)/bin/regctl REGCLIENT_VERSION := v0.5.1 SKOPEO = $(TOOLS_D)/bin/skopeo CONTAINERD = $(TOOLS_D)/bin/containerd +CTR = $(TOOLS_D)/bin/ctr export SKOPEO_VERSION = 1.13.0 BATS = $(TOOLS_D)/bin/bats BATS_VERSION := v1.10.0 @@ -174,11 +175,10 @@ $(SKOPEO): $(CONTAINERD): @set -e; mkdir -p "$(TOOLS_D)/bin"; \ tmpdir=$$(mktemp -d); \ - cd $$tmpdir; \ - wget https://github.com/containerd/containerd/releases/download/v2.1.4/containerd-2.1.4-linux-amd64.tar.gz; \ - tar xvf containerd-2.1.4-linux-amd64.tar.gz; \ - cp bin/containerd $(CONTAINERD); - cd $(TOP_LEVEL); \ + $(call dlbin,$$tmpdir/containerd.tar.gz,https://github.com/containerd/containerd/releases/download/v2.1.4/containerd-2.1.4-linux-$(GOARCH).tar.gz); \ + tar -xzf $$tmpdir/containerd.tar.gz -C $$tmpdir; \ + cp $$tmpdir/bin/containerd $(CONTAINERD); \ + cp $$tmpdir/bin/ctr $(CTR); \ rm -rf $$tmpdir; $(BATS): diff --git a/pkg/overlay/pack.go b/pkg/overlay/pack.go index 71a129a9..74bd1f86 100644 --- a/pkg/overlay/pack.go +++ b/pkg/overlay/pack.go @@ -34,6 +34,14 @@ import ( var tarEx sync.Mutex +func normalizeErofsLayerMediaType(mediaType string) string { + if strings.HasPrefix(mediaType, "application/vnd.stacker.image.layer.erofs") { + return types.ContainerdErofsLayerMediaType + } + + return mediaType +} + func safeOverlayName(d digest.Digest) string { // dirs used in overlay lowerdir args can't have : in them, so lets // sanitize it @@ -284,6 +292,9 @@ func generateBlob(layerType types.LayerType, contents string, ociDir string, sou if err != nil { return nil, "", "", err } + if layerType.Type == "erofs" { + mediaType = normalizeErofsLayerMediaType(mediaType) + } } return blob, mediaType, rootHash, nil } @@ -689,6 +700,16 @@ func unpackOne(l ispec.Descriptor, ociDir string, extractDir string) error { path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir) } + if l.MediaType == types.ContainerdErofsLayerMediaType { + fsi := stackerfs.New(fstypes.FilesystemType("erofs")) + if fsi == nil { + return errors.Errorf("failed to initialize erofs filesystem handler") + } + + return fsi.ExtractSingle( + path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir) + } + switch l.MediaType { case ispec.MediaTypeImageLayer, ispec.MediaTypeImageLayerGzip: tarEx.Lock() diff --git a/pkg/types/layer_type.go b/pkg/types/layer_type.go index dbab430a..c943c6a6 100644 --- a/pkg/types/layer_type.go +++ b/pkg/types/layer_type.go @@ -14,6 +14,12 @@ import ( var ErrEmptyLayers = errors.New("empty layers") +const ( + // ContainerdErofsLayerMediaType is the native EROFS OCI media type consumed + // by containerd's erofs snapshotter unpack configuration. + ContainerdErofsLayerMediaType = "application/vnd.erofs" +) + type LayerType struct { Type string Verity verity.VerityMetadata @@ -85,6 +91,8 @@ func NewLayerTypeManifest(manifest ispec.Manifest) (LayerType, error) { case erofs.GenerateErofsMediaType(erofs.LZ4Compression): fallthrough case erofs.GenerateErofsMediaType(erofs.ZstdCompression): + fallthrough + case ContainerdErofsLayerMediaType: return NewLayerType("erofs", verity.VerityMetadata(verityMetadataPresent)) case ispec.MediaTypeImageLayerGzip: fallthrough diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index c8b8eb8f..3ad31d97 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -5,9 +5,129 @@ function setup() { } function teardown() { + if [ -f "$TEST_TMPDIR/containerd.pid" ]; then + pid=$(cat "$TEST_TMPDIR/containerd.pid") + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi cleanup } -@test "containerd with erofs support" { - ${ROOT_DIR}/hack/tools/bin/containerd -c ${ROOT_DIR}/test/data/config.toml +function host_arch() { + case "$(uname -m)" in + x86_64) echo "amd64" ;; + aarch64) echo "arm64" ;; + *) + go env GOARCH + ;; + esac +} + +function write_containerd_config() { + local config_file="$1" + local arch + arch=$(host_arch) + + cat > "$config_file" < "$TEST_TMPDIR/containerd.log" 2>&1 & + echo $! > "$TEST_TMPDIR/containerd.pid" + + while [ "$n" -lt 30 ]; do + if "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" plugins ls >/dev/null 2>&1; then + return 0 + fi + + n=$((n+1)) + sleep 1 + done + + echo "containerd failed to start" >&3 + cat "$TEST_TMPDIR/containerd.log" >&3 + return 1 +} + +@test "stacker erofs image unpacks with containerd erofs snapshotter" { + require_privilege priv + + local containerd_bin="$ROOT_DIR/hack/tools/bin/containerd" + local ctr_bin="$ROOT_DIR/hack/tools/bin/ctr" + local manifest_digest + local image_ref + + [ -x "$containerd_bin" ] || skip "containerd test binary missing" + [ -x "$ctr_bin" ] || skip "ctr test binary missing" + + run modinfo erofs + [ "$status" -eq 0 ] || skip "missing erofs kernel module" + + cat > stacker.yaml <<"EOF" +test: + from: + type: oci + url: ${{BUSYBOX_OCI}} + run: | + echo hello > /hello +EOF + + stacker build --layer-type=erofs --substitute BUSYBOX_OCI=${BUSYBOX_OCI} + + manifest_digest=$(jq -r '.manifests[0].digest' oci/index.json | cut -d: -f2) + [ "$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" = "application/vnd.erofs" ] + + run start_containerd + [ "$status" -eq 0 ] + + run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" plugins ls + [ "$status" -eq 0 ] + echo "$output" | grep -E "io\.containerd\.snapshotter\.v1\s+erofs\s+.*\s+ok" + echo "$output" | grep -E "io\.containerd\.differ\.v1\s+erofs\s+.*\s+ok" + + tar -C oci -cf "$TEST_TMPDIR/stacker-erofs.oci.tar" . + + run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images import "$TEST_TMPDIR/stacker-erofs.oci.tar" + [ "$status" -eq 0 ] + + run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images ls -q + [ "$status" -eq 0 ] + image_ref=$(echo "$output" | grep test-erofs | head -n1) + [ -n "$image_ref" ] + + run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images unpack --snapshotter erofs "$image_ref" + [ "$status" -eq 0 ] + + run find "$TEST_TMPDIR/containerd-erofs" -type f -name layer.erofs + [ "$status" -eq 0 ] + [ -n "$output" ] } diff --git a/test/data/config.toml b/test/data/config.toml deleted file mode 100644 index 2040b892..00000000 --- a/test/data/config.toml +++ /dev/null @@ -1,260 +0,0 @@ -version = 3 -root = '/var/lib/containerd' -state = '/run/containerd' -temp = '' -disabled_plugins = [] -required_plugins = [] -oom_score = 0 -imports = [] - -[grpc] - address = '/run/containerd/containerd.sock' - tcp_address = '' - tcp_tls_ca = '' - tcp_tls_cert = '' - tcp_tls_key = '' - uid = 0 - gid = 0 - max_recv_message_size = 16777216 - max_send_message_size = 16777216 - -[ttrpc] - address = '' - uid = 0 - gid = 0 - -[debug] - address = '' - uid = 0 - gid = 0 - level = '' - format = '' - -[metrics] - address = '' - grpc_histogram = false - -[plugins] - [plugins.'io.containerd.cri.v1.images'] - snapshotter = 'overlayfs' - disable_snapshot_annotations = true - discard_unpacked_layers = false - max_concurrent_downloads = 3 - concurrent_layer_fetch_buffer = 0 - image_pull_progress_timeout = '5m0s' - image_pull_with_sync_fs = false - stats_collect_period = 10 - use_local_image_pull = false - - [plugins.'io.containerd.cri.v1.images'.pinned_images] - sandbox = 'registry.k8s.io/pause:3.10' - - [plugins.'io.containerd.cri.v1.images'.registry] - config_path = '' - - [plugins.'io.containerd.cri.v1.images'.image_decryption] - key_model = 'node' - - [plugins.'io.containerd.cri.v1.runtime'] - enable_selinux = false - selinux_category_range = 1024 - max_container_log_line_size = 16384 - disable_apparmor = false - restrict_oom_score_adj = false - disable_proc_mount = false - unset_seccomp_profile = '' - tolerate_missing_hugetlb_controller = true - disable_hugetlb_controller = true - device_ownership_from_security_context = false - ignore_image_defined_volumes = false - netns_mounts_under_state_dir = false - enable_unprivileged_ports = true - enable_unprivileged_icmp = true - enable_cdi = true - cdi_spec_dirs = ['/etc/cdi', '/var/run/cdi'] - drain_exec_sync_io_timeout = '0s' - ignore_deprecation_warnings = [] - - [plugins.'io.containerd.cri.v1.runtime'.containerd] - default_runtime_name = 'runc' - ignore_blockio_not_enabled_errors = false - ignore_rdt_not_enabled_errors = false - - [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes] - [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc] - runtime_type = 'io.containerd.runc.v2' - runtime_path = '' - pod_annotations = [] - container_annotations = [] - privileged_without_host_devices = false - privileged_without_host_devices_all_devices_allowed = false - cgroup_writable = false - base_runtime_spec = '' - cni_conf_dir = '' - cni_max_conf_num = 0 - snapshotter = '' - sandboxer = 'podsandbox' - io_type = '' - - [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options] - BinaryName = '' - CriuImagePath = '' - CriuWorkPath = '' - IoGid = 0 - IoUid = 0 - NoNewKeyring = false - Root = '' - ShimCgroup = '' - - [plugins.'io.containerd.cri.v1.runtime'.cni] - bin_dir = '' - bin_dirs = ['/opt/cni/bin'] - conf_dir = '/etc/cni/net.d' - max_conf_num = 1 - setup_serially = false - conf_template = '' - ip_pref = '' - use_internal_loopback = false - - [plugins.'io.containerd.gc.v1.scheduler'] - pause_threshold = 0.02 - deletion_threshold = 0 - mutation_threshold = 100 - schedule_delay = '0s' - startup_delay = '100ms' - - [plugins.'io.containerd.grpc.v1.cri'] - disable_tcp_service = true - stream_server_address = '127.0.0.1' - stream_server_port = '0' - stream_idle_timeout = '4h0m0s' - enable_tls_streaming = false - - [plugins.'io.containerd.grpc.v1.cri'.x509_key_pair_streaming] - tls_cert_file = '' - tls_key_file = '' - - [plugins.'io.containerd.image-verifier.v1.bindir'] - bin_dir = '/opt/containerd/image-verifier/bin' - max_verifiers = 10 - per_verifier_timeout = '10s' - - [plugins.'io.containerd.internal.v1.opt'] - path = '/opt/containerd' - - [plugins.'io.containerd.internal.v1.tracing'] - - [plugins.'io.containerd.metadata.v1.bolt'] - content_sharing_policy = 'shared' - no_sync = false - - [plugins.'io.containerd.monitor.container.v1.restart'] - interval = '10s' - - [plugins.'io.containerd.monitor.task.v1.cgroups'] - no_prometheus = false - - [plugins.'io.containerd.nri.v1.nri'] - disable = false - socket_path = '/var/run/nri/nri.sock' - plugin_path = '/opt/nri/plugins' - plugin_config_path = '/etc/nri/conf.d' - plugin_registration_timeout = '5s' - plugin_request_timeout = '2s' - disable_connections = false - - [plugins.'io.containerd.runtime.v2.task'] - platforms = ['linux/amd64'] - - [plugins.'io.containerd.service.v1.diff-service'] - default = ['erofs', 'walking'] - sync_fs = false - - [plugins."io.containerd.differ.v1.erofs"] - mkfs_options = ["--sort=none"] - - [[plugins."io.containerd.transfer.v1.local".unpack_config]] - differ = "erofs" - platform = "linux/amd64" - snapshotter = "erofs" - layer_types = ["application/vnd.erofs"] - - [plugins.'io.containerd.service.v1.tasks-service'] - blockio_config_file = '' - rdt_config_file = '' - - [plugins.'io.containerd.shim.v1.manager'] - env = [] - - [plugins.'io.containerd.snapshotter.v1.blockfile'] - root_path = '' - scratch_file = '' - fs_type = '' - mount_options = [] - recreate_scratch = false - - [plugins.'io.containerd.snapshotter.v1.btrfs'] - root_path = '' - - [plugins.'io.containerd.snapshotter.v1.devmapper'] - root_path = '' - pool_name = '' - base_image_size = '' - async_remove = false - discard_blocks = false - fs_type = '' - fs_options = '' - - [plugins.'io.containerd.snapshotter.v1.erofs'] - root_path = '' - ovl_mount_options = [] - enable_fsverity = false - - [plugins.'io.containerd.snapshotter.v1.native'] - root_path = '' - - [plugins.'io.containerd.snapshotter.v1.overlayfs'] - root_path = '' - upperdir_label = false - sync_remove = false - slow_chown = false - mount_options = [] - - [plugins.'io.containerd.snapshotter.v1.zfs'] - root_path = '' - - [plugins.'io.containerd.tracing.processor.v1.otlp'] - - [plugins.'io.containerd.transfer.v1.local'] - max_concurrent_downloads = 3 - concurrent_layer_fetch_buffer = 0 - max_concurrent_uploaded_layers = 3 - check_platform_supported = false - config_path = '' - -[cgroup] - path = '' - -[timeouts] - 'io.containerd.timeout.bolt.open' = '0s' - 'io.containerd.timeout.cri.defercleanup' = '1m0s' - 'io.containerd.timeout.metrics.shimstats' = '2s' - 'io.containerd.timeout.shim.cleanup' = '5s' - 'io.containerd.timeout.shim.load' = '5s' - 'io.containerd.timeout.shim.shutdown' = '3s' - 'io.containerd.timeout.task.state' = '2s' - -[stream_processors] - [stream_processors.'io.containerd.ocicrypt.decoder.v1.tar'] - accepts = ['application/vnd.oci.image.layer.v1.tar+encrypted'] - returns = 'application/vnd.oci.image.layer.v1.tar' - path = 'ctd-decoder' - args = ['--decryption-keys-path', '/etc/containerd/ocicrypt/keys'] - env = ['OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf'] - - [stream_processors.'io.containerd.ocicrypt.decoder.v1.tar.gzip'] - accepts = ['application/vnd.oci.image.layer.v1.tar+gzip+encrypted'] - returns = 'application/vnd.oci.image.layer.v1.tar+gzip' - path = 'ctd-decoder' - args = ['--decryption-keys-path', '/etc/containerd/ocicrypt/keys'] - env = ['OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf'] From 63c815079d9d7c82502bec57de810940dddaff45 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Wed, 8 Apr 2026 19:03:48 -0700 Subject: [PATCH 03/15] fix: update EROFS media type and containerd version in Makefile and tests Signed-off-by: Ramkumar Chinchani --- Makefile | 3 ++- pkg/types/layer_type.go | 2 +- test/containerd-erofs.bats | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 68213302..25d560e6 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ CTR = $(TOOLS_D)/bin/ctr export SKOPEO_VERSION = 1.13.0 BATS = $(TOOLS_D)/bin/bats BATS_VERSION := v1.10.0 +CONTAINERD_VERSION := v2.2.2 # OCI registry ZOT := $(TOOLS_D)/bin/zot ZOT_VERSION := v2.1.8 @@ -175,7 +176,7 @@ $(SKOPEO): $(CONTAINERD): @set -e; mkdir -p "$(TOOLS_D)/bin"; \ tmpdir=$$(mktemp -d); \ - $(call dlbin,$$tmpdir/containerd.tar.gz,https://github.com/containerd/containerd/releases/download/v2.1.4/containerd-2.1.4-linux-$(GOARCH).tar.gz); \ + $(call dlbin,$$tmpdir/containerd.tar.gz,https://github.com/containerd/containerd/releases/download/$(CONTAINERD_VERSION)/containerd-$(CONTAINERD_VERSION:v%=%)-linux-$(GOARCH).tar.gz); \ tar -xzf $$tmpdir/containerd.tar.gz -C $$tmpdir; \ cp $$tmpdir/bin/containerd $(CONTAINERD); \ cp $$tmpdir/bin/ctr $(CTR); \ diff --git a/pkg/types/layer_type.go b/pkg/types/layer_type.go index c943c6a6..8b27fcef 100644 --- a/pkg/types/layer_type.go +++ b/pkg/types/layer_type.go @@ -17,7 +17,7 @@ var ErrEmptyLayers = errors.New("empty layers") const ( // ContainerdErofsLayerMediaType is the native EROFS OCI media type consumed // by containerd's erofs snapshotter unpack configuration. - ContainerdErofsLayerMediaType = "application/vnd.erofs" + ContainerdErofsLayerMediaType = "vnd.erofs.layer.overlayfs.v1.erofs" ) type LayerType struct { diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index 3ad31d97..de08d6a4 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -46,7 +46,7 @@ state = '$TEST_TMPDIR/containerd-state' differ = "erofs" platform = "linux/$arch" snapshotter = "erofs" - layer_types = ["application/vnd.erofs"] + layer_types = ["vnd.erofs.layer.overlayfs.v1.erofs"] [plugins.'io.containerd.snapshotter.v1.erofs'] root_path = '$TEST_TMPDIR/containerd-erofs' @@ -104,7 +104,7 @@ EOF stacker build --layer-type=erofs --substitute BUSYBOX_OCI=${BUSYBOX_OCI} manifest_digest=$(jq -r '.manifests[0].digest' oci/index.json | cut -d: -f2) - [ "$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" = "application/vnd.erofs" ] + [ "$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" = "application/vnd.erofs.layer.overlayfs.v1.erofs" ] run start_containerd [ "$status" -eq 0 ] From 0f93c6b312e29aa4a263aff45b53267bd3b6502c Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Wed, 8 Apr 2026 21:17:32 -0700 Subject: [PATCH 04/15] fix: update media type check for EROFS layers in containerd tests Signed-off-by: Ramkumar Chinchani --- test/containerd-erofs.bats | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index de08d6a4..25db22fc 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -104,7 +104,10 @@ EOF stacker build --layer-type=erofs --substitute BUSYBOX_OCI=${BUSYBOX_OCI} manifest_digest=$(jq -r '.manifests[0].digest' oci/index.json | cut -d: -f2) - [ "$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" = "application/vnd.erofs.layer.overlayfs.v1.erofs" ] + # OCI format prefixes custom layer types with application/vnd.oci.image.layer. + mt="$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" + [ "$mt" = "application/vnd.oci.image.layer.vnd.erofs.layer.overlayfs.v1.erofs" ] || \ + [ "$mt" = "application/vnd.erofs.layer.overlayfs.v1.erofs" ] run start_containerd [ "$status" -eq 0 ] From b51d8ec28e6fc5880794fed2c3f938ff4c3af5ea Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Wed, 8 Apr 2026 21:51:15 -0700 Subject: [PATCH 05/15] fix: enhance media type validation for EROFS layers in containerd tests Signed-off-by: Ramkumar Chinchani --- test/containerd-erofs.bats | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index 25db22fc..20279371 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -104,10 +104,18 @@ EOF stacker build --layer-type=erofs --substitute BUSYBOX_OCI=${BUSYBOX_OCI} manifest_digest=$(jq -r '.manifests[0].digest' oci/index.json | cut -d: -f2) - # OCI format prefixes custom layer types with application/vnd.oci.image.layer. mt="$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" - [ "$mt" = "application/vnd.oci.image.layer.vnd.erofs.layer.overlayfs.v1.erofs" ] || \ - [ "$mt" = "application/vnd.erofs.layer.overlayfs.v1.erofs" ] + case "$mt" in + application/vnd.oci.image.layer.vnd.erofs.layer.overlayfs.v1.erofs|\ + application/vnd.oci.image.layer.v1.erofs|\ + application/vnd.oci.image.layer.v1+erofs|\ + application/vnd.erofs.layer.overlayfs.v1.erofs) + ;; + *) + echo "unexpected EROFS layer mediaType: $mt" >&3 + return 1 + ;; + esac run start_containerd [ "$status" -eq 0 ] From a95777845988f4b058870d175d8bf6154c511651 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Wed, 8 Apr 2026 22:44:57 -0700 Subject: [PATCH 06/15] fix: update EROFS layer media type validation in containerd tests Signed-off-by: Ramkumar Chinchani --- test/containerd-erofs.bats | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index 20279371..bc023f1e 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -109,7 +109,8 @@ EOF application/vnd.oci.image.layer.vnd.erofs.layer.overlayfs.v1.erofs|\ application/vnd.oci.image.layer.v1.erofs|\ application/vnd.oci.image.layer.v1+erofs|\ - application/vnd.erofs.layer.overlayfs.v1.erofs) + application/vnd.erofs.layer.overlayfs.v1.erofs|\ + vnd.erofs.layer.overlayfs.v1.erofs) ;; *) echo "unexpected EROFS layer mediaType: $mt" >&3 From 9e5fff028a65818ddfd92953fe6133fd5fa6b78f Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Wed, 8 Apr 2026 23:44:56 -0700 Subject: [PATCH 07/15] fix: enhance EROFS containerd configuration and ensure kernel module readiness --- test/containerd-erofs.bats | 112 +++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index bc023f1e..15fa0ee9 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -10,6 +10,7 @@ function teardown() { kill "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true fi + zot_teardown cleanup } @@ -25,6 +26,7 @@ function host_arch() { function write_containerd_config() { local config_file="$1" + local hosts_config_path="$2" local arch arch=$(host_arch) @@ -51,15 +53,41 @@ state = '$TEST_TMPDIR/containerd-state' [plugins.'io.containerd.snapshotter.v1.erofs'] root_path = '$TEST_TMPDIR/containerd-erofs' EOF + + if [ -n "$hosts_config_path" ]; then + cat >> "$config_file" < "$hosts_dir/hosts.toml" < "$TEST_TMPDIR/containerd.log" 2>&1 & echo $! > "$TEST_TMPDIR/containerd.pid" @@ -78,6 +106,17 @@ function start_containerd() { return 1 } +function ensure_erofs_ready() { + run modinfo erofs + [ "$status" -eq 0 ] || skip "missing erofs kernel module" + + run modprobe erofs + [ "$status" -eq 0 ] || skip "unable to load erofs kernel module" + + run grep -Eq '^nodev[[:space:]]+erofs$|[[:space:]]erofs$' /proc/filesystems + [ "$status" -eq 0 ] || skip "erofs filesystem is not available" +} + @test "stacker erofs image unpacks with containerd erofs snapshotter" { require_privilege priv @@ -89,8 +128,7 @@ function start_containerd() { [ -x "$containerd_bin" ] || skip "containerd test binary missing" [ -x "$ctr_bin" ] || skip "ctr test binary missing" - run modinfo erofs - [ "$status" -eq 0 ] || skip "missing erofs kernel module" + ensure_erofs_ready cat > stacker.yaml <<"EOF" test: @@ -119,7 +157,14 @@ EOF esac run start_containerd - [ "$status" -eq 0 ] + if [ "$status" -ne 0 ]; then + if grep -qE 'EROFS unsupported, please `modprobe erofs`|EROFS unsupported, please .*modprobe erofs' "$TEST_TMPDIR/containerd.log"; then + skip "erofs kernel support is unavailable" + fi + echo "containerd failed to start for unexpected reason" >&3 + cat "$TEST_TMPDIR/containerd.log" >&3 + return 1 + fi run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" plugins ls [ "$status" -eq 0 ] @@ -143,3 +188,62 @@ EOF [ "$status" -eq 0 ] [ -n "$output" ] } + +@test "stacker erofs image published to zot runs through containerd mirror" { + require_privilege priv + + local containerd_bin="$ROOT_DIR/hack/tools/bin/containerd" + local ctr_bin="$ROOT_DIR/hack/tools/bin/ctr" + local mirror_registry="docker.io" + local mirror_repo="stacker-erofs-mirror-${BATS_TEST_NUMBER}" + local mirror_ref="$mirror_registry/library/$mirror_repo:latest" + + [ -x "$containerd_bin" ] || skip "containerd test binary missing" + [ -x "$ctr_bin" ] || skip "ctr test binary missing" + [ -n "${ZOT_HOST}${ZOT_PORT}" ] || skip "zot env not configured" + + ensure_erofs_ready + + zot_setup + + cat > stacker.yaml <<"EOF" +test: + from: + type: oci + url: ${{BUSYBOX_OCI}} + run: | + echo hello-from-zot-mirror > /hello +EOF + + stacker build --layer-type=erofs --substitute BUSYBOX_OCI=${BUSYBOX_OCI} + stacker publish --skip-tls --url docker://${ZOT_HOST}:${ZOT_PORT} --image library/$mirror_repo --tag latest + + write_registry_mirror_hosts "$mirror_registry" + + run start_containerd_with_registry_config "$TEST_TMPDIR/certs.d" + if [ "$status" -ne 0 ]; then + if grep -qE 'EROFS unsupported, please `modprobe erofs`|EROFS unsupported, please .*modprobe erofs' "$TEST_TMPDIR/containerd.log"; then + skip "erofs kernel support is unavailable" + fi + echo "containerd failed to start for unexpected reason" >&3 + cat "$TEST_TMPDIR/containerd.log" >&3 + return 1 + fi + + run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" plugins ls + [ "$status" -eq 0 ] + echo "$output" | grep -E "io\.containerd\.snapshotter\.v1\s+erofs\s+.*\s+ok" + echo "$output" | grep -E "io\.containerd\.differ\.v1\s+erofs\s+.*\s+ok" + + # This image only exists in Zot; successful pull verifies mirror resolution. + run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images pull "$mirror_ref" + [ "$status" -eq 0 ] + + run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" run --rm --snapshotter erofs "$mirror_ref" erofs-mirror-test sh -ec "cat /hello" + [ "$status" -eq 0 ] + echo "$output" | grep -q "hello-from-zot-mirror" + + run find "$TEST_TMPDIR/containerd-erofs" -type f -name layer.erofs + [ "$status" -eq 0 ] + [ -n "$output" ] +} From f5d60e7e250537752848a6bb89b2cec1e5a6f9db Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 9 Apr 2026 07:40:56 -0700 Subject: [PATCH 08/15] fix: update EROFS layer media types and remove deprecated functions --- pkg/overlay/pack.go | 21 --------------------- pkg/types/layer_type.go | 8 -------- test/containerd-erofs.bats | 16 ++++++++++------ 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/pkg/overlay/pack.go b/pkg/overlay/pack.go index 74bd1f86..71a129a9 100644 --- a/pkg/overlay/pack.go +++ b/pkg/overlay/pack.go @@ -34,14 +34,6 @@ import ( var tarEx sync.Mutex -func normalizeErofsLayerMediaType(mediaType string) string { - if strings.HasPrefix(mediaType, "application/vnd.stacker.image.layer.erofs") { - return types.ContainerdErofsLayerMediaType - } - - return mediaType -} - func safeOverlayName(d digest.Digest) string { // dirs used in overlay lowerdir args can't have : in them, so lets // sanitize it @@ -292,9 +284,6 @@ func generateBlob(layerType types.LayerType, contents string, ociDir string, sou if err != nil { return nil, "", "", err } - if layerType.Type == "erofs" { - mediaType = normalizeErofsLayerMediaType(mediaType) - } } return blob, mediaType, rootHash, nil } @@ -700,16 +689,6 @@ func unpackOne(l ispec.Descriptor, ociDir string, extractDir string) error { path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir) } - if l.MediaType == types.ContainerdErofsLayerMediaType { - fsi := stackerfs.New(fstypes.FilesystemType("erofs")) - if fsi == nil { - return errors.Errorf("failed to initialize erofs filesystem handler") - } - - return fsi.ExtractSingle( - path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir) - } - switch l.MediaType { case ispec.MediaTypeImageLayer, ispec.MediaTypeImageLayerGzip: tarEx.Lock() diff --git a/pkg/types/layer_type.go b/pkg/types/layer_type.go index 8b27fcef..dbab430a 100644 --- a/pkg/types/layer_type.go +++ b/pkg/types/layer_type.go @@ -14,12 +14,6 @@ import ( var ErrEmptyLayers = errors.New("empty layers") -const ( - // ContainerdErofsLayerMediaType is the native EROFS OCI media type consumed - // by containerd's erofs snapshotter unpack configuration. - ContainerdErofsLayerMediaType = "vnd.erofs.layer.overlayfs.v1.erofs" -) - type LayerType struct { Type string Verity verity.VerityMetadata @@ -91,8 +85,6 @@ func NewLayerTypeManifest(manifest ispec.Manifest) (LayerType, error) { case erofs.GenerateErofsMediaType(erofs.LZ4Compression): fallthrough case erofs.GenerateErofsMediaType(erofs.ZstdCompression): - fallthrough - case ContainerdErofsLayerMediaType: return NewLayerType("erofs", verity.VerityMetadata(verityMetadataPresent)) case ispec.MediaTypeImageLayerGzip: fallthrough diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index 15fa0ee9..14cc8f45 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -48,7 +48,12 @@ state = '$TEST_TMPDIR/containerd-state' differ = "erofs" platform = "linux/$arch" snapshotter = "erofs" - layer_types = ["vnd.erofs.layer.overlayfs.v1.erofs"] + layer_types = [ + "application/vnd.stacker.image.layer.erofs", + "application/vnd.stacker.image.layer.erofs+lz4hc", + "application/vnd.stacker.image.layer.erofs+lz4", + "application/vnd.stacker.image.layer.erofs+zstd" + ] [plugins.'io.containerd.snapshotter.v1.erofs'] root_path = '$TEST_TMPDIR/containerd-erofs' @@ -144,11 +149,10 @@ EOF manifest_digest=$(jq -r '.manifests[0].digest' oci/index.json | cut -d: -f2) mt="$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" case "$mt" in - application/vnd.oci.image.layer.vnd.erofs.layer.overlayfs.v1.erofs|\ - application/vnd.oci.image.layer.v1.erofs|\ - application/vnd.oci.image.layer.v1+erofs|\ - application/vnd.erofs.layer.overlayfs.v1.erofs|\ - vnd.erofs.layer.overlayfs.v1.erofs) + application/vnd.stacker.image.layer.erofs|\ + application/vnd.stacker.image.layer.erofs+lz4hc|\ + application/vnd.stacker.image.layer.erofs+lz4|\ + application/vnd.stacker.image.layer.erofs+zstd) ;; *) echo "unexpected EROFS layer mediaType: $mt" >&3 From 54197d0eca90a92c8fff69cd0d6c5f920bd67f91 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 9 Apr 2026 08:21:48 -0700 Subject: [PATCH 09/15] fix: update ZOT_HOST to use 127.0.0.1 and enhance service readiness checks --- .github/workflows/build.yaml | 4 +-- .github/workflows/coverage.yaml | 2 +- test/helpers.bash | 59 +++++++++++++++------------------ test/setup_suite.bash | 2 +- 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 29dcfe26..fb578586 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -99,7 +99,7 @@ jobs: env: REGISTRY_SERVICE: ${{ inputs.registry-service }} REGISTRY_URL: localhost:5000 - ZOT_HOST: localhost + ZOT_HOST: 127.0.0.1 ZOT_PORT: 8080 - name: Show disk usage before running the tests if: always() @@ -110,7 +110,7 @@ jobs: env: REGISTRY_SERVICE: ${{ inputs.registry-service }} REGISTRY_URL: localhost:5000 - ZOT_HOST: localhost + ZOT_HOST: 127.0.0.1 ZOT_PORT: 8080 - name: Show disk usage after running the tests if: always() diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index be7dfa12..e84b5f37 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -99,7 +99,7 @@ jobs: env: REGISTRY_SERVICE: ${{ inputs.registry-service }} REGISTRY_URL: localhost:5000 - ZOT_HOST: localhost + ZOT_HOST: 127.0.0.1 ZOT_PORT: 8080 - name: Show disk usage after running the tests if: always() diff --git a/test/helpers.bash b/test/helpers.bash index 2939a8a3..3a584ff1 100644 --- a/test/helpers.bash +++ b/test/helpers.bash @@ -310,40 +310,35 @@ function start_zot { echo "zot is running at pid $pid" cat "$TEST_TMPDIR/zot.log" - # wait until service is up - count=5 - up=0 + # wait until registry API is reachable; this avoids localhost IPv6/IPv4 races. + ready=0 + for i in $(seq 1 60); do + if [ ! -d /proc/$pid ]; then + echo "zot failed to start or died" + cat "$TEST_TMPDIR/zot.log" >&3 || true + exit 1 + fi - while [[ $count -gt 0 ]]; do - if [ ! -d /proc/$pid ]; then - echo "zot failed to start or died" - exit 1 - fi - up=1 - # check if correct port is open - if ! nc -v -z "${ZOT_HOST}" "${ZOT_PORT}"; then - echo "no response from host:${ZOT_HOST} port:${ZOT_PORT}" >&3 - sleep 1 - count=$((count - 1)) - continue - fi - echo "Got response from host:${ZOT_HOST} on port:${ZOT_PORT}" >&3 - if [[ -n $ZOT_USE_TLS ]]; then - echo "testing zot at https://$ZOT_HOST:$ZOT_PORT" - curl -v --cacert $BATS_SUITE_TMPDIR/ca.crt -u "iam:careful" -f https://$ZOT_HOST:$ZOT_PORT/v2/ || up=0 - else - echo "testing zot at http://$ZOT_HOST:$ZOT_PORT" - curl -v -f http://$ZOT_HOST:$ZOT_PORT/v2/ || up=0 - fi + if [[ -n $ZOT_USE_TLS ]]; then + if curl -fsS --connect-timeout 1 --cacert "$BATS_SUITE_TMPDIR/ca.crt" -u "iam:careful" "https://${ZOT_HOST}:${ZOT_PORT}/v2/" >/dev/null; then + ready=1 + break + fi + else + if curl -fsS --connect-timeout 1 "http://${ZOT_HOST}:${ZOT_PORT}/v2/" >/dev/null; then + ready=1 + break + fi + fi - if [ $up -eq 1 ]; then break; fi - sleep 1 - count=$((count - 1)) - done - if [ $up -eq 0 ]; then - echo "Timed out waiting for zot" - exit 1 - fi + sleep 0.5 + done + + if [ $ready -eq 0 ]; then + echo "Timed out waiting for zot at ${ZOT_HOST}:${ZOT_PORT}" >&3 + cat "$TEST_TMPDIR/zot.log" >&3 || true + exit 1 + fi echo "# zot is up" >&3 # setup a OCI client diff --git a/test/setup_suite.bash b/test/setup_suite.bash index cb02fd3e..0c705676 100644 --- a/test/setup_suite.bash +++ b/test/setup_suite.bash @@ -28,7 +28,7 @@ function write_certs { -CAkey ca.key \ -CAcreateserial \ -out server.cert \ - -extfile <(echo subjectAltName = DNS:localhost) + -extfile <(echo subjectAltName = DNS:localhost,IP:127.0.0.1) openssl req \ -newkey rsa:2048 \ From 12e2a163f4db951685fb20402358ca76ec6e0cbe Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 9 Apr 2026 08:46:42 -0700 Subject: [PATCH 10/15] fix: update registry mirror hosts configuration and add Zot health check --- test/containerd-erofs.bats | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index 14cc8f45..cafc7b3e 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -74,10 +74,16 @@ function write_registry_mirror_hosts() { mkdir -p "$hosts_dir" cat > "$hosts_dir/hosts.toml" </dev/null 2>&1; then + break + fi + sleep 1 + done + curl -fsS "http://${ZOT_HOST}:${ZOT_PORT}/v2/" >/dev/null || { + echo "zot health check failed" >&3 + return 1 + } + cat > stacker.yaml <<"EOF" test: from: From beded5f81d8f67e49a5c1655b3e54db06f34a9a4 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 4 Aug 2026 20:45:57 -0700 Subject: [PATCH 11/15] fix: the containerd deps Signed-off-by: Ramkumar Chinchani --- install-build-deps.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install-build-deps.sh b/install-build-deps.sh index 5cbc2290..6873b165 100755 --- a/install-build-deps.sh +++ b/install-build-deps.sh @@ -4,10 +4,12 @@ set -o errexit installdeps_fedora() { sudo dnf install \ + kubernetes-cni \ jq \ lxc-devel \ libcap-devel \ - libacl-devel + libacl-devel \ + runc # skopeo deps sudo dnf install \ gpgme-devel \ @@ -24,6 +26,7 @@ installdeps_fedora() { COMMON_DEBS=( apache2-utils build-essential + containernetworking-plugins cryptsetup-bin curl erofsfuse @@ -48,6 +51,7 @@ COMMON_DEBS=( parallel pkg-config psmisc + runc shellcheck squashfs-tools squashfuse From dc1bf880cde0107822443b8f33099b05199487ee Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 4 Aug 2026 20:59:44 -0700 Subject: [PATCH 12/15] fix: generate containerd config for erofs tests --- stacker.yaml | 6 ++++++ test/containerd-erofs.bats | 40 +++++++++++++++++--------------------- 2 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 stacker.yaml diff --git a/stacker.yaml b/stacker.yaml new file mode 100644 index 00000000..d4a483c2 --- /dev/null +++ b/stacker.yaml @@ -0,0 +1,6 @@ +test: + from: + type: oci + url: ${{BUSYBOX_OCI}} + run: | + echo hello > /hello diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index cafc7b3e..e9c54d96 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -27,24 +27,25 @@ function host_arch() { function write_containerd_config() { local config_file="$1" local hosts_config_path="$2" + local containerd_bin="$ROOT_DIR/hack/tools/bin/containerd" local arch arch=$(host_arch) - cat > "$config_file" < "$config_file" -[plugins.'io.containerd.service.v1.diff-service'] - default = ["erofs", "walking"] + # Keep the generated defaults and isolate only the state used by this test. + sed -i \ + -e "s|^root = .*|root = '$TEST_TMPDIR/containerd-root'|" \ + -e "s|^state = .*|state = '$TEST_TMPDIR/containerd-state'|" \ + -e "s|^ address = '/run/containerd/containerd.sock'$| address = '$TEST_TMPDIR/containerd.sock'|" \ + -e "/io.containerd.service.v1.diff-service/,/^ \[plugins\./ s|^ default = .*| default = ['erofs', 'walking']|" \ + -e "/io.containerd.differ.v1.erofs/,/^ \[plugins\./ s|^ mkfs_options = .*| mkfs_options = ['--sort=none']|" \ + -e "/io.containerd.snapshotter.v1.erofs/,/^ \[plugins\./ s|^ root_path = .*| root_path = '$TEST_TMPDIR/containerd-erofs'|" \ + "$config_file" -[plugins."io.containerd.differ.v1.erofs"] - mkfs_options = ["--sort=none"] + cat >> "$config_file" <> "$config_file" < Date: Tue, 4 Aug 2026 22:19:32 -0700 Subject: [PATCH 13/15] fix: rewrite generated containerd socket address Signed-off-by: Ramkumar Chinchani --- test/containerd-erofs.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index e9c54d96..72e1b4e4 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -37,7 +37,7 @@ function write_containerd_config() { sed -i \ -e "s|^root = .*|root = '$TEST_TMPDIR/containerd-root'|" \ -e "s|^state = .*|state = '$TEST_TMPDIR/containerd-state'|" \ - -e "s|^ address = '/run/containerd/containerd.sock'$| address = '$TEST_TMPDIR/containerd.sock'|" \ + -e "s|^\([[:space:]]*\)address = '/run/containerd/containerd.sock'$|\1address = '$TEST_TMPDIR/containerd.sock'|" \ -e "/io.containerd.service.v1.diff-service/,/^ \[plugins\./ s|^ default = .*| default = ['erofs', 'walking']|" \ -e "/io.containerd.differ.v1.erofs/,/^ \[plugins\./ s|^ mkfs_options = .*| mkfs_options = ['--sort=none']|" \ -e "/io.containerd.snapshotter.v1.erofs/,/^ \[plugins\./ s|^ root_path = .*| root_path = '$TEST_TMPDIR/containerd-erofs'|" \ From 8583ecffa21db392910dca689ebc43fa543b198d Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 4 Aug 2026 22:44:15 -0700 Subject: [PATCH 14/15] fix: shorten containerd test socket path --- test/containerd-erofs.bats | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index 72e1b4e4..463f04bd 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -2,6 +2,7 @@ load helpers function setup() { stacker_setup + CONTAINERD_ADDRESS="/tmp/stacker-containerd-${BATS_TEST_NUMBER}-$$.sock" } function teardown() { @@ -10,6 +11,7 @@ function teardown() { kill "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true fi + rm -f "$CONTAINERD_ADDRESS" "$CONTAINERD_ADDRESS.ttrpc" zot_teardown cleanup } @@ -37,7 +39,7 @@ function write_containerd_config() { sed -i \ -e "s|^root = .*|root = '$TEST_TMPDIR/containerd-root'|" \ -e "s|^state = .*|state = '$TEST_TMPDIR/containerd-state'|" \ - -e "s|^\([[:space:]]*\)address = '/run/containerd/containerd.sock'$|\1address = '$TEST_TMPDIR/containerd.sock'|" \ + -e "s|^\([[:space:]]*\)address = '/run/containerd/containerd.sock'$|\1address = '$CONTAINERD_ADDRESS'|" \ -e "/io.containerd.service.v1.diff-service/,/^ \[plugins\./ s|^ default = .*| default = ['erofs', 'walking']|" \ -e "/io.containerd.differ.v1.erofs/,/^ \[plugins\./ s|^ mkfs_options = .*| mkfs_options = ['--sort=none']|" \ -e "/io.containerd.snapshotter.v1.erofs/,/^ \[plugins\./ s|^ root_path = .*| root_path = '$TEST_TMPDIR/containerd-erofs'|" \ @@ -100,7 +102,7 @@ function start_containerd_with_registry_config() { echo $! > "$TEST_TMPDIR/containerd.pid" while [ "$n" -lt 30 ]; do - if "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" plugins ls >/dev/null 2>&1; then + if "$ctr_bin" --address "$CONTAINERD_ADDRESS" plugins ls >/dev/null 2>&1; then return 0 fi @@ -172,22 +174,22 @@ EOF return 1 fi - run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" plugins ls + run "$ctr_bin" --address "$CONTAINERD_ADDRESS" plugins ls [ "$status" -eq 0 ] echo "$output" | grep -E "io\.containerd\.snapshotter\.v1\s+erofs\s+.*\s+ok" echo "$output" | grep -E "io\.containerd\.differ\.v1\s+erofs\s+.*\s+ok" tar -C oci -cf "$TEST_TMPDIR/stacker-erofs.oci.tar" . - run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images import "$TEST_TMPDIR/stacker-erofs.oci.tar" + run "$ctr_bin" --address "$CONTAINERD_ADDRESS" images import "$TEST_TMPDIR/stacker-erofs.oci.tar" [ "$status" -eq 0 ] - run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images ls -q + run "$ctr_bin" --address "$CONTAINERD_ADDRESS" images ls -q [ "$status" -eq 0 ] image_ref=$(echo "$output" | grep test-erofs | head -n1) [ -n "$image_ref" ] - run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images unpack --snapshotter erofs "$image_ref" + run "$ctr_bin" --address "$CONTAINERD_ADDRESS" images unpack --snapshotter erofs "$image_ref" [ "$status" -eq 0 ] run find "$TEST_TMPDIR/containerd-erofs" -type f -name layer.erofs @@ -248,16 +250,16 @@ EOF return 1 fi - run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" plugins ls + run "$ctr_bin" --address "$CONTAINERD_ADDRESS" plugins ls [ "$status" -eq 0 ] echo "$output" | grep -E "io\.containerd\.snapshotter\.v1\s+erofs\s+.*\s+ok" echo "$output" | grep -E "io\.containerd\.differ\.v1\s+erofs\s+.*\s+ok" # This image only exists in Zot; successful pull verifies mirror resolution. - run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" images pull "$mirror_ref" + run "$ctr_bin" --address "$CONTAINERD_ADDRESS" images pull "$mirror_ref" [ "$status" -eq 0 ] - run "$ctr_bin" --address "$TEST_TMPDIR/containerd.sock" run --rm --snapshotter erofs "$mirror_ref" erofs-mirror-test sh -ec "cat /hello" + run "$ctr_bin" --address "$CONTAINERD_ADDRESS" run --rm --snapshotter erofs "$mirror_ref" erofs-mirror-test sh -ec "cat /hello" [ "$status" -eq 0 ] echo "$output" | grep -q "hello-from-zot-mirror" From f3897dd21ee1529f43e8a521fce4d29cb0b7934e Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 4 Aug 2026 23:38:55 -0700 Subject: [PATCH 15/15] fix: align EROFS media type with containerd 2.3 Signed-off-by: Ramkumar Chinchani --- Makefile | 2 +- pkg/overlay/pack.go | 15 +++++++++++++++ pkg/types/layer_type.go | 5 +++++ pkg/types/types_test.go | 16 ++++++++++++++++ test/containerd-erofs.bats | 19 ++----------------- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 25d560e6..a4080ff1 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ CTR = $(TOOLS_D)/bin/ctr export SKOPEO_VERSION = 1.13.0 BATS = $(TOOLS_D)/bin/bats BATS_VERSION := v1.10.0 -CONTAINERD_VERSION := v2.2.2 +CONTAINERD_VERSION := v2.3.3 # OCI registry ZOT := $(TOOLS_D)/bin/zot ZOT_VERSION := v2.1.8 diff --git a/pkg/overlay/pack.go b/pkg/overlay/pack.go index 71a129a9..02f707a2 100644 --- a/pkg/overlay/pack.go +++ b/pkg/overlay/pack.go @@ -284,6 +284,11 @@ func generateBlob(layerType types.LayerType, contents string, ociDir string, sou if err != nil { return nil, "", "", err } + // LZ4/Zstd selected by mkfs.erofs is filesystem-internal compression; + // containerd suffixes describe an additional wrapper around the blob. + if layerType.Type == "erofs" { + mediaType = types.ContainerdErofsLayerMediaType + } } return blob, mediaType, rootHash, nil } @@ -684,6 +689,16 @@ func unpackOne(l ispec.Descriptor, ociDir string, extractDir string) error { return nil } + if l.MediaType == types.ContainerdErofsLayerMediaType { + fsi := stackerfs.New(fstypes.FilesystemType("erofs")) + if fsi == nil { + return errors.Errorf("failed to initialize erofs filesystem handler") + } + + return fsi.ExtractSingle( + path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir) + } + if fsi := stackerfs.NewFromMediaType(l.MediaType); fsi != nil { return fsi.ExtractSingle( path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir) diff --git a/pkg/types/layer_type.go b/pkg/types/layer_type.go index dbab430a..860a6965 100644 --- a/pkg/types/layer_type.go +++ b/pkg/types/layer_type.go @@ -14,6 +14,9 @@ import ( var ErrEmptyLayers = errors.New("empty layers") +// ContainerdErofsLayerMediaType identifies an unwrapped native EROFS blob. +const ContainerdErofsLayerMediaType = "application/vnd.erofs.layer.v1" + type LayerType struct { Type string Verity verity.VerityMetadata @@ -85,6 +88,8 @@ func NewLayerTypeManifest(manifest ispec.Manifest) (LayerType, error) { case erofs.GenerateErofsMediaType(erofs.LZ4Compression): fallthrough case erofs.GenerateErofsMediaType(erofs.ZstdCompression): + fallthrough + case ContainerdErofsLayerMediaType: return NewLayerType("erofs", verity.VerityMetadata(verityMetadataPresent)) case ispec.MediaTypeImageLayerGzip: fallthrough diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go index c741150f..4a04b15d 100644 --- a/pkg/types/types_test.go +++ b/pkg/types/types_test.go @@ -4,6 +4,8 @@ import ( "os" "reflect" "testing" + + ispec "github.com/opencontainers/image-spec/specs-go/v1" ) func parse(t *testing.T, content string) *Stackerfile { @@ -51,6 +53,20 @@ func TestDockerFrom(t *testing.T) { } } +func TestNewLayerTypeManifestContainerdErofs(t *testing.T) { + manifest := ispec.Manifest{ + Layers: []ispec.Descriptor{{MediaType: ContainerdErofsLayerMediaType}}, + } + + layerType, err := NewLayerTypeManifest(manifest) + if err != nil { + t.Fatalf("failed to parse containerd EROFS layer type: %s", err) + } + if layerType.Type != "erofs" { + t.Fatalf("expected erofs layer type, got %s", layerType.Type) + } +} + func TestDependencyOrder(t *testing.T) { content := `first: from: diff --git a/test/containerd-erofs.bats b/test/containerd-erofs.bats index 463f04bd..9ec317ac 100644 --- a/test/containerd-erofs.bats +++ b/test/containerd-erofs.bats @@ -51,12 +51,7 @@ function write_containerd_config() { differ = "erofs" platform = "linux/$arch" snapshotter = "erofs" - layer_types = [ - "application/vnd.stacker.image.layer.erofs", - "application/vnd.stacker.image.layer.erofs+lz4hc", - "application/vnd.stacker.image.layer.erofs+lz4", - "application/vnd.stacker.image.layer.erofs+zstd" - ] + layer_types = ["application/vnd.erofs.layer.v1"] EOF if [ -n "$hosts_config_path" ]; then @@ -152,17 +147,7 @@ EOF manifest_digest=$(jq -r '.manifests[0].digest' oci/index.json | cut -d: -f2) mt="$(jq -r '.layers[0].mediaType' "oci/blobs/sha256/$manifest_digest")" - case "$mt" in - application/vnd.stacker.image.layer.erofs|\ - application/vnd.stacker.image.layer.erofs+lz4hc|\ - application/vnd.stacker.image.layer.erofs+lz4|\ - application/vnd.stacker.image.layer.erofs+zstd) - ;; - *) - echo "unexpected EROFS layer mediaType: $mt" >&3 - return 1 - ;; - esac + [ "$mt" = "application/vnd.erofs.layer.v1" ] run start_containerd if [ "$status" -ne 0 ]; then