Skip to content

feat: set env, user and ports, own layer entries, read files from images - #12

Merged
ItamarZand88 merged 13 commits into
mainfrom
itamar/alien-1062-remote-sandbox-a-custom-image-runs-on-gcp-phase-b
Sep 29, 2026
Merged

ItamarZand88 merged 13 commits into
mainfrom
itamar/alien-1062-remote-sandbox-a-custom-image-runs-on-gcp-phase-b

Conversation

@ItamarZand88

@ItamarZand88 ItamarZand88 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

dockdash can now build an image FROM any base plus one layer whose entries have an explicit owner and mode, set ENV, USER and EXPOSE on top of the base config, and read a single file out of an image's layers. Together that covers a Dockerfile like "copy a merged /etc/passwd, a root-owned binary and a user-owned directory onto the base, then set env, user and ports" without a RUN step, so nothing ever executes inside the base.

Step-by-step flow when you derive an image from a base:

  1. Image::read_file("/etc/passwd") reads the base's copy from its layers, top down, following OCI whiteouts, without extracting anything.
  2. LayerBuilder::data_with_owner and empty_directory write the new entries with the uid, gid and mode you pass, into the tar headers the runtime applies on unpack.
  3. ImageBuilder::env, user and expose_port merge onto the base config the way Dockerfile ENV, USER and EXPOSE do.

This PR changes dockdash from "root-owned files on a base, config limited to entrypoint/cmd/workdir" to "any owner and mode, plus env, user and ports, and a way to read what the base already has".

What I did

  • ImageBuilder::env(key, value) replaces a base variable with the same name and keeps the rest; user(..) overrides the base user; expose_port("8080/tcp") adds to the base's ports. On a scratch image they set the config directly. build() rejects an env name that is empty or contains =.
  • LayerBuilder::data_with_owner(path, content, mode, uid, gid) and LayerBuilder::empty_directory(path, mode, uid, gid). data() keeps writing root-owned 0o644 entries. empty_directory must come before any entry at or under its path, so call order cannot change the layer behind one cache key. The owner goes into the layer's cache key only when set, so layers built without an owner keep their key.
  • Image::read_file(path) returns Ok(None) when the file is absent or whited out. It honours .wh.<name>, opaque .wh..wh..opq, and a parent replaced by a non-directory, which hides the subtree even if the directory is recreated later in that layer. A whiteout never hides an entry in its own layer, and a path repeated in one layer resolves to its last entry, as on extraction. It refuses directories, symlinks, paths under a link and paths with .. rather than following them.

Files touched

  • src/image.rs: the config setters, read_file, and unit tests for both. Extraction also keeps whiteouts inside the target: whiteouts delete rather than unpack, so unpack_in's traversal check never covered them. A whiteout is skipped when its path contains .., its name is not a single file name (.wh..., .wh.), or its parent resolves outside the target through a symlink. It is also skipped when the parent is missing, not a directory, or a symlink loop, since there is nothing to clear. Any other error resolving the parent fails extraction rather than leaving lower-layer files behind.
  • src/layer.rs: data_with_owner, empty_directory, owner in the cache key, and tests on the written tar headers.
  • tests/base_image_layer_tests.rs: the end-to-end case against a public base and a local registry.
  • Cargo.toml: the two tests that import test_utils require the test-utils feature, so a plain cargo test compiles. libc becomes a direct unix dependency, only for ELOOP: ErrorKind::FilesystemLoop is not stable yet. It was already in the lockfile.

How I tested

  • Manually: built derived images on python:3.14-slim, node:22-slim, ubi9-minimal and a distroless base. read_file matched docker cp byte for byte; docker run --entrypoint id printed the uid-1000 user from the merged /etc/passwd; stat showed the directory at 1000:1000 0700 and the binary at 0:0 0755; the config kept the base PATH, had Cmd null and ExposedPorts as an OCI object.
  • Unit tests: cargo test --lib covers the env merge and name check, scratch-image config, every whiteout case above and the last-entry rule. Layer tests read back uid, gid and mode from the tar and check that the owner changes the cache key.
  • Integration: cargo test --all-features --test base_image_layer_tests derives from alpine, pushes to a local registry:2, pulls it back and checks the config and top-layer headers. Needs Docker, Docker Hub access and --features test-utils, so CI does not run it.
  • Anything I couldn't test: test_push_with_basic_authentication fails locally with a 401 on pull, identically on main, so it is unrelated. read_file loads each layer blob into memory, as extract does.

ImageBuilder gains env(), user() and expose_port(), merged onto the base
config the way Dockerfile ENV, USER and EXPOSE are. LayerBuilder gains
data_with_owner() and empty_directory() for entries with an explicit
uid, gid and mode; the owner is part of the layer's input cache key.
Image::read_file() reads one file from the layers top-down, honouring
whiteouts, so a caller can derive files such as /etc/passwd from a base
without running anything inside it.
Tar extraction keeps the last entry for a repeated path, so read_file
does the same. The whiteout tests move into the library's unit tests,
which CI runs.
…names

Ancestor entries in a layer now resolve last-entry-wins like the target,
whiteout names compare as raw bytes so non-UTF-8 names still match, and
build() rejects an env name that is empty or contains '='. The derived
image test uses neutral user and directory names.
@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Adds dependency and changes layer and image handling code.

The PR appears safe to merge; no new actionable issue or outstanding previous finding was identified.

Summary

The PR adds image configuration setters, owner-aware layer entries, and file reads from merged image layers. The latest change makes whiteout-parent resolution fail extraction on unexpected filesystem errors while continuing to skip absent parents and symlink loops.

Reviews (4) · Last reviewed commit: "fix: fail extraction when a whiteout par..."

Comment thread tests/base_image_layer_tests.rs
Comment thread src/image.rs Outdated
Comment thread src/image.rs Outdated
Comment thread src/layer.rs
…omponents

A file whose parent is replaced by a non-directory later in the same
layer is hidden, and a path with a '..' component is refused, as
extraction refuses it. empty_directory fails when the layer already has
a directory entry at that path, so the entry order cannot change the
tar under one cache key. The tests that need the test-utils feature
declare it, so a default cargo test compiles.
…g whiteouts

A non-directory parent entry hides the file even if a directory is
recreated later in the layer. Extraction runs whiteout paths through the
same normalization and skips any with '..', since unpack_in never checks
a whiteout. empty_directory also refuses a path that directory() or a
file already covers.
…rents

Whiteout parents are canonicalized and skipped when they resolve outside
the target directory, the check unpack_in applies to regular entries.
…ctory

A whiteout name of '', '.', '..' or a drive prefix would make the join
point at the parent or elsewhere, so it is skipped. A whiteout under a
path that is not a directory has nothing to remove. The target directory
is canonicalized once per layer.
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

Comment thread src/image.rs
… extraction

A symlink loop or similar under a whiteout leaves no directory the
marker could clear, so extraction warns and continues.
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

Comment thread src/image.rs Outdated
Only a missing parent, a non-directory or a symlink loop means there is
nothing for the whiteout to clear. Other errors, such as a permission
failure, would leave lower-layer files behind, so extraction fails.
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

@ItamarZand88
ItamarZand88 merged commit 5d69dc3 into main Sep 29, 2026
6 checks passed
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