Repository navigation
feat: set env, user and ports, own layer entries, read files from images - #12
Merged
ItamarZand88 merged 13 commits intoSep 29, 2026
Conversation
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.
|
…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.
Contributor
Author
|
@greptile review |
… extraction A symlink loop or similar under a whiteout leaves no directory the marker could clear, so extraction warns and continues.
Contributor
Author
|
@greptile review |
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.
Contributor
Author
|
@greptile review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dockdash can now build an image FROM any base plus one layer whose entries have an explicit owner and mode, set
ENV,USERandEXPOSEon 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 aRUNstep, so nothing ever executes inside the base.Step-by-step flow when you derive an image from a base:
Image::read_file("/etc/passwd")reads the base's copy from its layers, top down, following OCI whiteouts, without extracting anything.LayerBuilder::data_with_ownerandempty_directorywrite the new entries with the uid, gid and mode you pass, into the tar headers the runtime applies on unpack.ImageBuilder::env,userandexpose_portmerge onto the base config the way DockerfileENV,USERandEXPOSEdo.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)andLayerBuilder::empty_directory(path, mode, uid, gid).data()keeps writing root-owned0o644entries.empty_directorymust 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)returnsOk(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, sounpack_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 importtest_utilsrequire thetest-utilsfeature, so a plaincargo testcompiles.libcbecomes a direct unix dependency, only forELOOP:ErrorKind::FilesystemLoopis not stable yet. It was already in the lockfile.How I tested
python:3.14-slim,node:22-slim,ubi9-minimaland a distroless base.read_filematcheddocker cpbyte for byte;docker run --entrypoint idprinted the uid-1000 user from the merged/etc/passwd;statshowed the directory at1000:1000 0700and the binary at0:0 0755; the config kept the basePATH, hadCmdnull andExposedPortsas an OCI object.cargo test --libcovers 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.cargo test --all-features --test base_image_layer_testsderives fromalpine, pushes to a localregistry: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.test_push_with_basic_authenticationfails locally with a 401 on pull, identically onmain, so it is unrelated.read_fileloads each layer blob into memory, asextractdoes.