From eb9b4b99743bbaf351cf0a033740735b21b7f081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 09:44:04 -0400 Subject: [PATCH 1/5] Rewrite Plan F to match docker::_prepare_layers actual output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original plan assumed _prepare_layers leaves layer tarballs in the cwd and designed a per-layer ZFS clone chain with manual whiteout handling. Reality: _prepare_layers untars each layer into a numbered directory and runs enroot-aufs2ovlfs to convert whiteouts in-place. After it returns, directories 0/, 1/, ..., N/ are extracted, whiteout-converted layer trees ready for an overlayfs lowerdir mount. The revised plan reuses the dir backend's existing single-pass overlay+tar-pipe mechanism unchanged, only redirecting the destination from a directory to a ZFS clone's mountpoint that we create outside the user namespace (per Plan E's userns-zfs lesson). No new C helpers, no per-layer chain, no manual whiteout merge — the kernel's overlay support is still the merge engine. zfs::ensure_template_from_target is added as the atomic-template lifecycle helper, separating the create-tmp/promote/snapshot/lock machinery from the content-fill mechanism (which docker::load provides via the existing nsenter+overlay+tar-pipe). Signed-off-by: Patrick Sodré --- doc/plans/2026-04-29-zfs-f-docker-load.md | 481 +++++++++++----------- 1 file changed, 238 insertions(+), 243 deletions(-) diff --git a/doc/plans/2026-04-29-zfs-f-docker-load.md b/doc/plans/2026-04-29-zfs-f-docker-load.md index d5803d4..32e4cdb 100644 --- a/doc/plans/2026-04-29-zfs-f-docker-load.md +++ b/doc/plans/2026-04-29-zfs-f-docker-load.md @@ -2,221 +2,155 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** When `ENROOT_STORAGE_BACKEND=zfs`, `enroot load docker://...` stops requiring `ENROOT_NATIVE_OVERLAYFS=y`. Layers are stacked as a chain of ZFS clones (mirroring Docker's own `zfs` storage driver): each layer is `zfs clone parent@done`, the layer tarball is extracted into the clone with whiteout handling, then `zfs snapshot @done`. The leaf snapshot becomes the cached template's `@pristine`, and a clone of that becomes the user's container. +**Goal:** When `ENROOT_STORAGE_BACKEND=zfs`, `enroot load docker://...` no longer requires `ENROOT_NATIVE_OVERLAYFS=y`. The merged image is materialized into a ZFS template dataset (cached by image config digest) and the user's container is a `zfs clone` of it. Default `dir` backend behavior is preserved byte-for-byte and still requires `ENROOT_NATIVE_OVERLAYFS=y`. -**Architecture:** Add `zfs::stack_layers` that takes the same layer-tarball list `docker::_prepare_layers` produces and replays it onto a fresh chain of ZFS datasets. Modify `docker::load` to dispatch on backend: existing `enroot-mksquashovlfs` path stays for the `dir` backend; ZFS path uses `zfs::stack_layers` and skips the `ENROOT_NATIVE_OVERLAYFS=y` precondition. +**Architecture:** `docker::_prepare_layers` already does the heavy lifting — for each layer it `mkdir N`, untars the layer's tarball into directory `N/`, and runs `enroot-aufs2ovlfs N` to convert AUFS-style whiteouts to overlayfs whiteouts. After it returns, the cwd contains directories `0/` (synthetic config layer with `/etc/{rc,fstab,environment}` from `docker::configure`) and `1/` … `N/` (extracted, whiteout-converted layer trees). The dir-backend `docker::load` then runs `enroot-nsenter --user --remap-root` + `mount -t overlay lowerdir=0:1:…:N rootfs` and tar-pipes the merged view into `${name}/`. -**Depends on:** Plan A. - -**Prerequisite host setup:** Same as Plan A. Test image: `alpine` from Docker Hub (small, exercises whiteouts via standard Docker tooling). +The ZFS path reuses that exact merge logic — the only thing that changes is the *destination* of the tar-pipe: instead of writing into a regular directory under `${ENROOT_DATA_PATH}`, we write into the mountpoint of a freshly-created ZFS template dataset, then snapshot `@pristine` and clone for the user. The clone (and its mountpoint resolution) happens **outside** the user namespace because zfs(8) cannot enumerate datasets from inside one (see Plan E for the full background). ---- +This single-pass approach is simpler than the per-layer clone chain originally sketched, and it keeps the existing `_prepare_layers` + `enroot-aufs2ovlfs` machinery as-is. -## Files +**Depends on:** Plan A. -- **Modify:** `src/storage_zfs.sh` — add `zfs::stack_layers`, `zfs::extract_layer_tarball`. -- **Modify:** `src/docker.sh:488-548` (`docker::load`) — backend dispatch. +**Prerequisite host setup:** Same as Plan A. Test images: `docker://alpine` (single layer, fast), `docker://debian:slim` (multi-layer, exercises whiteouts). --- -### Task 1: Add `zfs::extract_layer_tarball` (whiteout-aware) - -Docker layer tarballs use AUFS-style whiteouts: `.wh.foo` means "delete `foo` from lower layers"; `.wh..wh..opq` in a directory means "ignore everything from lower layers in this directory." When stacking with overlayfs, the kernel handles these. With ZFS clones, we replicate the semantics manually. - -**Files:** -- Modify: `src/storage_zfs.sh` (append) - -- [ ] **Step 1.1: Add the helper** - -Append to `src/storage_zfs.sh`: - -```bash -# Extracts a single Docker layer tarball into the given mountpoint with -# whiteout/opaque-marker handling, suitable for use on top of a parent layer's -# ZFS clone. Handles both compressed and uncompressed tarballs. -zfs::extract_layer_tarball() { - local -r tarball="$1" mountpoint="$2" - - common::checkcmd tar awk find - - # Pass 1: extract everything except whiteout markers. - tar --numeric-owner -C "${mountpoint}" -xpf "${tarball}" \ - --exclude='.wh.*' --exclude='.wh..wh..opq' 2> /dev/null || \ - tar --numeric-owner -C "${mountpoint}" -xpf "${tarball}" \ - --exclude='.wh.*' --exclude='.wh..wh..opq' # surface real errors on retry - - # Pass 2: list whiteout markers and apply them to the tree. - tar -tf "${tarball}" 2> /dev/null | awk ' - /\/\.wh\..*$/ { sub(/\.wh\.([^/]+)$/, "\\1"); print "del\t"$0; next } - /^\.wh\..*$/ { sub(/^\.wh\./, ""); print "del\t"$0; next } - /\.wh\..wh..opq$/ { sub(/\.wh\..wh..opq$/, ""); print "opq\t"$0; next } - ' | while IFS=$'\t' read -r kind path; do - case "${kind}" in - del) - rm -rf "${mountpoint}/${path}" 2> /dev/null || : - ;; - opq) - # Opaque dir: clear everything in this directory from lower layers. - # The directory itself was already created by this layer's pass 1 - # (or already exists from a parent). Remove all children, then let - # pass 1's content (which we've already laid down) repopulate. - find "${mountpoint}/${path}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2> /dev/null || : - # Re-extract the directory's contents from this tar without the wh markers. - tar --numeric-owner -C "${mountpoint}" -xpf "${tarball}" \ - --exclude='.wh.*' --exclude='.wh..wh..opq' \ - "${path}" 2> /dev/null || : - ;; - esac - done -} -``` +## Files -- [ ] **Step 1.2: Verify with a hand-built tarball** +- **Modify:** `src/storage_zfs.sh` — add `zfs::ensure_template_from_target`, a sibling of `zfs::ensure_template` that creates the `.tmp` dataset, hands its mountpoint to a caller-supplied filler, then renames/snapshots it. +- **Modify:** `src/docker.sh` (`docker::load` at lines 488–548) — relax the `ENROOT_NATIVE_OVERLAYFS=y` precondition when ZFS is enabled; on the ZFS path, redirect the existing tar-pipe destination from `${name}/` to the template clone's mountpoint and clone for the user. +- **Modify:** `doc/zfs.md` — status note and a sentence about the lifted precondition. -```sh -mkdir /tmp/wh-test && cd /tmp/wh-test -mkdir -p layer1/usr/bin layer2 -echo "old" > layer1/usr/bin/foo -echo "x" > layer1/keep_me -( cd layer2 && touch usr/bin/.wh.foo ) # delete /usr/bin/foo -tar --numeric-owner -C layer1 -cf l1.tar . -tar --numeric-owner -C layer2 -cf l2.tar . - -mkdir target -tar -C target -xpf l1.tar -ls target/usr/bin/foo # exists - -bash -c 'source ${ENROOT_LIBRARY_PATH}/common.sh - source ${ENROOT_LIBRARY_PATH}/storage_zfs.sh - zfs::extract_layer_tarball /tmp/wh-test/l2.tar /tmp/wh-test/target' -ls /tmp/wh-test/target/usr/bin/foo 2>&1 | grep -q "No such" && echo "deleted OK" -ls /tmp/wh-test/target/keep_me && echo "preserved OK" -cd && rm -rf /tmp/wh-test -``` +`docker::_prepare_layers`, `docker::configure`, and the existing dir-backend overlay path are **not** modified. -Expected: `deleted OK` and `preserved OK`. +--- -- [ ] **Step 1.3: Commit** +### Task 1: Add `zfs::ensure_template_from_target` -```sh -git add src/storage_zfs.sh -git commit -s -m "Add zfs::extract_layer_tarball with whiteout/opaque handling" -``` +A generic atomic-template-fill helper. Unlike `zfs::ensure_template` (which extracts a `.sqsh` itself), this one: ---- +1. Returns the cached template name immediately if `@pristine` already exists. +2. Otherwise creates `