Skip to content

image: resolve --image via the Docker daemon API instead of the CLI#74

Merged
congwang-mk merged 1 commit into
mainfrom
drop-docker-dep
Jun 1, 2026
Merged

image: resolve --image via the Docker daemon API instead of the CLI#74
congwang-mk merged 1 commit into
mainfrom
drop-docker-dep

Conversation

@congwang-mk
Copy link
Copy Markdown
Contributor

@congwang-mk congwang-mk commented May 28, 2026

Summary

  • --image takes a local Docker image reference again (python:3.12-slim, a digest, or an image id), not an archive path.
  • Sandlock talks to the Docker daemon directly through its HTTP API (the bollard crate) instead of shelling out to the docker CLI. The old path ran docker create + docker export + docker inspect + docker rm as subprocesses; those are gone, along with the tar subprocess that piped the export. The docker binary no longer needs to be on PATH.
  • A running daemon with an accessible socket is still required. --image pings the daemon up front and fails early with a clear message when it is unreachable, rather than partway through sandbox setup.
  • Image lookup is local only: inspect_image never pulls from a registry, and a missing image surfaces the daemon's 404 as image not found in local Docker storage.
  • Rootfs is materialized the same way docker export works: a throwaway stopped container is created, its flattened filesystem is streamed out with export_container into a temp tar (bounded memory, never held whole), then unpacked. The container is always removed afterward, even when the export fails.
  • Because docker export yields an already merged filesystem, the per-layer, gzip, and AUFS/OCI whiteout handling is gone. Only the hard-link fixups remain (the tar crate resolves link targets against the process cwd rather than the destination root).
  • Cache key is the image content id, stored at ~/.cache/sandlock/images/<image-id>/rootfs/ with a .complete marker distinguishing a finished extraction from an interrupted one. Warm-cache invocations skip the daemon export entirely.
  • Deps: drop flate2 and sha2; add bollard and futures-util; enable tokio's fs feature.

Also works with Podman: point DOCKER_HOST at podman system service's socket, since bollard speaks the Docker API.

Builds on #73.

Test plan

  • cargo test -p sandlock-core image:: (5 image unit tests: hard-link forward references, regular files, default cmd, id sanitization) all green
  • cargo test -p sandlock-cli (26 tests) green
  • Manual: sandlock run --image python:3.11-slim -- python3 -c 'import sys; print(sys.version.split()[0])' returns 3.11.15
  • Manual: sandlock run --image ubuntu:22.04 -- cat /etc/os-release returns Ubuntu 22.04.5 LTS
  • Warm cache hit on second invocation (~0.19s, skips export)
  • Missing image gives a clear local-storage 404 error
  • Unreachable daemon (DOCKER_HOST pointed at a bogus socket) fails early before any sandbox setup

Sandlock used to shell out to the `docker` CLI on every --image
invocation: `docker create` + `docker export` to dump the container's
rootfs, `docker inspect` to read Entrypoint/Cmd, and `docker rm` to
clean up. That made the `docker` executable a hard runtime dependency
and meant parsing its textual output.

Talk to the Docker daemon directly through its HTTP API (the bollard
crate) instead. The --image flag still takes a local image reference
(python:3.12-slim, a digest, an image id); only the mechanism changes.
The `docker` binary no longer needs to be on PATH, though a running
daemon with an accessible socket is still required.

extract() and inspect_cmd() are now async (callers already run inside
the tokio runtime). Both connect to the local daemon and ping it up
front, so --image fails early with a clear message when the daemon is
unreachable rather than partway through sandbox setup. Image lookup is
local only: inspect_image never pulls, and a missing image surfaces the
daemon's 404 as "image not found in local Docker storage".

Rootfs materialization mirrors the old create+export path: a throwaway
stopped container is created from the image, its flattened filesystem is
streamed out with export_container into a temp tar (bounded memory,
never held whole), then unpacked. The container is always removed
afterward, even when the export fails. Because docker export yields an
already merged filesystem, all the per-layer, gzip, and AUFS/OCI
whiteout handling is gone; only the hard-link fixups remain, since the
tar crate resolves link targets against the process cwd rather than the
destination root.

Extracted rootfs is cached at
~/.cache/sandlock/images/<image-id>/rootfs/, keyed by the image content
id, with a .complete marker distinguishing a finished extraction from an
interrupted one. Warm-cache invocations skip the daemon export entirely.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk congwang-mk changed the title Parse OCI / docker-save archives in-process, drop docker binary dep image: resolve --image via the Docker daemon API instead of the CLI Jun 1, 2026
@congwang-mk congwang-mk merged commit 26a41c4 into main Jun 1, 2026
9 checks passed
@congwang-mk congwang-mk deleted the drop-docker-dep branch June 1, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant