werkator build succeeded after 00:04
PUT /archive decided once, at the top level of the archive: a directory the image filled was refused, because binding it would hide what was inside. That was the right worry in the wrong place. Docker's clients put the whole path in the tar entries, so MariaDBContainer copying its configuration to /etc/mysql/conf.d arrives as a top-level "etc" — refused, and the container never started at all. The rule now descends instead of giving up: bind where the image has nothing, descend where it has a directory, bind the file where it has a file. The two trees meet at whatever depth they first differ, which is docker's merge; mariadb's config binds one file four levels down and /etc is left alone. The refusal that said werkdock "would have to hide them" is gone with it, because there is no longer a case where it would. Found by MariaDBContainer, which is now a test in werkprobe rather than something I ran once by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
391 lines
19 KiB
Markdown
391 lines
19 KiB
Markdown
# Changelog
|
|
|
|
Release notes for consumers of the `werkdock` CLI, Werkator first among them.
|
|
Each entry says what changed, what still works unchanged, and what a consumer
|
|
should adapt — and by when.
|
|
|
|
A release is a merge to main; there is no separate release step. So a branch
|
|
that changes behavior sets the version in `internal/cli/cli.go` (what
|
|
`werkdock version` prints) and titles its section here with that version and
|
|
the merge date, before it is merged. Nothing on main is ever "unreleased".
|
|
|
|
## 0.8.1 (2026-09-11)
|
|
|
|
Theme: a copy that stops one level too early. `MariaDBContainer` could not
|
|
start; anything that copies into a directory the image fills could not
|
|
either.
|
|
|
|
### Fixed
|
|
|
|
- **A copied tree is merged at whatever depth the two first differ**, not
|
|
refused at the top. 0.7.0 decided once, at the top level of the archive:
|
|
a directory the image filled was refused. Testcontainers copies
|
|
mariadb's configuration to `/etc/mysql/conf.d`, and docker's clients put
|
|
the whole path in the tar, so the top level was `etc` — refused, and the
|
|
container never started. The rule now descends: bind where the image has
|
|
nothing, descend where it has a directory, bind the file where it has a
|
|
file. Copying into `/etc/mysql/conf.d` now binds one file four levels
|
|
down and leaves `/etc` alone.
|
|
|
|
With it goes the 501 that said werkdock "would have to hide them": there
|
|
is no longer a case where it would.
|
|
|
|
### Unchanged
|
|
|
|
- Everything else about the endpoint, including what it still refuses: a
|
|
copy into a running instance, symlinks and device nodes, entries that
|
|
climb out of the destination, and archives above 64 entries.
|
|
|
|
## 0.8.0 (2026-09-11)
|
|
|
|
Theme: the quirk table, measured instead of assumed. Three of its four
|
|
entries were wrong, and each would have handed a client a port number that
|
|
nothing was listening on. A consumer running `redis`, `mariadb`, or `mysql`
|
|
through the API should take this release.
|
|
|
|
### Fixed
|
|
|
|
- **`redis`, `mariadb`, and `mysql` were published on the wrong port.**
|
|
All three were listed as moving their port through an environment
|
|
variable — `REDIS_PORT`, `MARIADB_TCP_PORT`, `MYSQL_TCP_PORT`. None of
|
|
the three images reads one: redis-server takes `--port`, and the two
|
|
database entrypoints read the port out of the server's own
|
|
configuration. Werkdock chose a free port, told nobody, and reported it.
|
|
Measured against the real images, not guessed: all three now move by a
|
|
command-line flag and were verified to answer on the port they report.
|
|
- **`redis` could not start at all.** Its entrypoint chowns its data
|
|
directory as root, which an unprivileged user namespace cannot do; it
|
|
now runs as `redis`, the same treatment `postgres` already had.
|
|
- **An image's `/run` was empty.** `/run` is the instance's own directory
|
|
so that sockets survive a stop, which meant nothing of the image's
|
|
showed through — and `mariadb` died with "Bind on unix socket: No such
|
|
file or directory" because `/run/mysqld` was missing. The instance's
|
|
`/run` is now seeded from the image's: directories, and files small
|
|
enough to be a word rather than data.
|
|
|
|
### New
|
|
|
|
- A quirk may move a port by a **command-line flag**, not only by an
|
|
environment variable, and does so only while the container still runs
|
|
the image's own program — werkdock does not append a flag to a command
|
|
a client chose. Where it cannot, the port is reported unmoved, as
|
|
before.
|
|
|
|
### Unchanged
|
|
|
|
- `postgres` was right and is untouched: it really does read `PGPORT`.
|
|
- Every CLI verb, every endpoint, and the instance directory format.
|
|
|
|
## 0.7.0 (2026-09-11)
|
|
|
|
Theme: a file into a container — `PUT /containers/{id}/archive`, the last
|
|
endpoint Testcontainers asks for that werkdock answered with a 501. Purely
|
|
additive; nothing that worked before behaves differently.
|
|
|
|
### New
|
|
|
|
- `PUT /containers/{id}/archive` unpacks a tar into an instance, which is
|
|
what `withCopyFileToContainer` and `MountableFile` send. An init script
|
|
copied to `/docker-entrypoint-initdb.d` is now run by the image's own
|
|
entrypoint, as on docker.
|
|
- The copy does **not** go into the image. A rootfs in the store is shared
|
|
by every instance of that image and bound read-only, so the tar is
|
|
unpacked into the instance directory and bound in — whole, where the
|
|
image has nothing at the destination, and file by file where it does, so
|
|
that the image's own files stay visible beside the copied ones. That is
|
|
docker's merge, reached by another road.
|
|
|
|
### Refused rather than ignored
|
|
|
|
- Copying into a **running** instance answers 501 with the reason: a
|
|
sandbox's mounts are fixed when it starts, and werkdock has no writable
|
|
container layer to put the file in. Copy before start, as
|
|
`withCopyFileToContainer` does, or write through a volume.
|
|
- Copying a **directory** onto one the image fills answers 501: the bind
|
|
would hide what is inside, and werkdock will not do that quietly.
|
|
- A symlink, hard link, device node, or fifo in the archive answers 400 —
|
|
each would have to mean something inside the sandbox that werkdock
|
|
cannot promise — as does an entry that climbs out of its destination.
|
|
- An archive of more than 64 entries answers 400: every entry may become a
|
|
bind, and a client that wants to populate a tree wants an image.
|
|
|
|
### Unchanged
|
|
|
|
- Every CLI verb, every other endpoint, and the instance directory format
|
|
(`config.json` stays version 1; a copy adds mounts to the spec it
|
|
already holds).
|
|
|
|
## 0.6.0 (2026-09-10)
|
|
|
|
Theme: the API socket — RFC 0005 milestone 3, and with it milestone 4:
|
|
Testcontainers starts a real PostgreSQL in werkdock. Purely additive;
|
|
no CLI verb changes behaviour.
|
|
|
|
### New
|
|
|
|
- `werkdock system service [--time D] [unix://PATH]` serves the Docker
|
|
Engine API subset a client actually uses, on
|
|
`$XDG_RUNTIME_DIR/werkdock.sock` by default (podman's verb and
|
|
podman's manners: optional, started when needed, `--time` to exit
|
|
when idle). A client is pointed at it with
|
|
`DOCKER_HOST=unix://<path>`.
|
|
- Testcontainers (validated with 1.19.3, `PostgreSQLContainer`) works
|
|
against it: pull, create, start, logs with the wait strategy, exec,
|
|
kill, remove, and the session cleanup. It needs
|
|
`TESTCONTAINERS_RYUK_DISABLED=true`, because Ryuk wants the API
|
|
socket inside a container of its own.
|
|
- The endpoints are those measured from a real run, written down in
|
|
`docs/testcontainers-api.md`. Everything outside the subset answers
|
|
**501 with the reason**, never 404, so a client that needs more says
|
|
so plainly.
|
|
- Instances carry docker labels (`config.json`), and the instance list
|
|
filters on them, which is how a client finds what it left behind.
|
|
- An exec is a record of its own with an id and an exit code, because
|
|
the API creates, starts, and inspects it in three calls.
|
|
|
|
### Refused rather than ignored
|
|
|
|
- `logs` parameters werkdock cannot honour say so with 501 and the
|
|
reason: `tail` (the output is streamed whole), `timestamps` and a
|
|
non-zero `since`/`until` (no clock is recorded beside the output).
|
|
What a client actually sends — `stdout`, `stderr`, `follow`,
|
|
`since=0`, `tail=all` — is served.
|
|
|
|
### Ports, the deliberate deviation
|
|
|
|
Werkdock shares the host's network and cannot map a port. Where a
|
|
client asks for a published port, werkdock therefore picks a free one
|
|
and tells the *service* to bind it, through the environment variable
|
|
the image honours (`PGPORT`, `MARIADB_TCP_PORT`, `MYSQL_TCP_PORT`,
|
|
`REDIS_PORT`). An image outside that small table keeps its own port,
|
|
which is right as long as one instance of it runs at a time; asking to
|
|
move it fails with the reason instead of silently reporting a port
|
|
nothing listens on. The same table carries the user an image needs when
|
|
its entrypoint expects to drop privileges itself (`postgres`), which
|
|
werkdock cannot do with one uid.
|
|
|
|
### Version reporting
|
|
|
|
`/version` and `/info` report the Docker Engine version whose API is
|
|
served (25.0.0), not werkdock's: clients compare that field against a
|
|
minimum, and 0.x fails it forever. Werkdock's own version is beside it
|
|
in `Components` and in `/info`'s `OperatingSystem`.
|
|
|
|
### For Werkator
|
|
|
|
- Nothing to change. The service is not started unless someone asks
|
|
for it, and no CLI verb behaves differently.
|
|
- New possibility: an integration test in a build sandbox can get a
|
|
real database. Bind the socket into the sandbox
|
|
(`-v $XDG_RUNTIME_DIR/werkdock.sock:/run/docker.sock`) and set
|
|
`DOCKER_HOST` and `TESTCONTAINERS_RYUK_DISABLED` in the build.
|
|
|
|
## 0.5.0 (2026-09-10)
|
|
|
|
Theme: the instance lifecycle — RFC 0005 milestone 2. A service image
|
|
runs in the background, can be stopped, restarted, queried, and talked
|
|
to. Additive for `run --rm`; two small semantic changes below.
|
|
|
|
### New
|
|
|
|
- `run -d` starts the instance under a background supervisor and prints
|
|
its 64-hex id; `--name NAME` names it (default: image basename plus six
|
|
characters of the id); `-i` passes stdin (no pty: `-t` is refused).
|
|
- `ps [-a] [-q] [--format FMT]` lists instances with docker's columns
|
|
(`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES`; PORTS stays
|
|
empty, the network is the host's); `stop [-t SECONDS]`, `kill [-s SIG]`,
|
|
`rm [-f]`, `wait`, `logs [-f]`, `create`, `start`, and `inspect` on an
|
|
instance id, unique prefix, or name. Each prints what docker prints
|
|
(the argument on success, `werkdock: …` and exit 125 per failure).
|
|
- `exec [-e K=V] [-w DIR] [-u USER] [-i] INSTANCE CMD...` runs a second
|
|
command in the instance's sandbox: same rootfs, mounts, uid, env,
|
|
`/run` and volumes; not the same pid namespace.
|
|
- docker's management form is accepted: `container ls|start|stop|...`,
|
|
`image ls|rm|pull|...`.
|
|
- State lives in `$WERKDOCK_HOME/instances/<id>/`: `config.json`
|
|
(`version: 1`), `state.json`, `stdout`, `stderr`, `run/`, `volumes/`.
|
|
A state left "running" by a dead supervisor (reboot, `kill -9`) is
|
|
repaired to exited 255 on the next read; an exited `--rm` instance
|
|
found that way is removed.
|
|
|
|
### Changed
|
|
|
|
- A `run` without `--rm` no longer fails ("persistent instances are not
|
|
implemented yet"); the instance stays for `ps -a`, `logs`, `start`, `rm`.
|
|
- The command is pid 1 of its sandbox (`bwrap --as-pid-1`), as in
|
|
docker, so `stop`'s SIGTERM can reach it. As in docker, an entrypoint
|
|
without a SIGTERM handler (`sh -c '…'`) is only ended by the SIGKILL
|
|
after the timeout, and orphaned children are its own to reap.
|
|
- A foreground `run` forwards the first SIGINT/SIGTERM/SIGHUP to the
|
|
command and kills it on the second, or after 10 seconds; bwrap runs in its own process
|
|
group, so Ctrl-C no longer kills bwrap (and with it the command)
|
|
outright. Werkator's builds see the same exit codes as before; a
|
|
`sh -c` build interrupted with SIGTERM now ends 10 seconds later, on
|
|
the SIGKILL, unless the script traps TERM.
|
|
- Anonymous volumes (the image's `VOLUME`s) and `/run` are directories
|
|
of the instance instead of tmpfs, so they survive `stop`/`start` and
|
|
are visible to `exec`. For a `--rm` run the difference is disk instead
|
|
of RAM for those paths, and nothing else.
|
|
|
|
### For Werkator
|
|
|
|
- Nothing to change: `run --rm …` behaves as before. The `ps` output
|
|
is new and Werkator does not parse it.
|
|
|
|
## 0.4.0 (2026-09-10)
|
|
|
|
Theme: service images — the first milestone of RFC 0005 (PostgreSQL in the
|
|
sandbox, Testcontainers as the destination). Additive; a `run` without the
|
|
new flags on an image without `USER`/`VOLUME` behaves exactly as before.
|
|
|
|
### New
|
|
|
|
- `run --user USER[:GROUP]` (`-u`) makes the caller appear as that user inside
|
|
the sandbox: a name is resolved in the image's `/etc/passwd` and `/etc/group`,
|
|
a number needs no entry. `HOME` follows the passwd entry when there is one.
|
|
Still one uid only — the caller is mapped to it; there is no second user.
|
|
- `run --userns=keep-id` (podman's spelling) maps the caller to their own uid
|
|
and gid. Other `--userns` modes are refused with the reason.
|
|
- An image's `USER` (OCI config) applies when neither flag is given, as with
|
|
podman; unknown names fail with `image USER: …`. Default stays uid 0.
|
|
- An image's `VOLUME` paths are mounted as anonymous tmpfs unless a `-v` or
|
|
`--mount` covers them, so `postgres:16` starts without knowing its data and
|
|
socket directories by heart.
|
|
- `/run` is a tmpfs in every sandbox, beside `/tmp` and `/root`. `/var/run`
|
|
points there in practically every image, and services keep pid files, locks
|
|
and sockets under it without declaring a `VOLUME` (`postgres` needs
|
|
`/var/run/postgresql` writable). A `--tmpfs`/`-v` on a path below `/run`
|
|
still works, it just lands inside the tmpfs.
|
|
- `import -c` accepts `USER user[:group]` and `VOLUME /path` (or a JSON array);
|
|
`inspect` shows `User` and `Volumes` in `Config`.
|
|
|
|
### For Werkator
|
|
|
|
- Nothing to change. Builds keep running as uid 0. If a build image ever sets
|
|
`USER`, that user applies; add `--user 0` to keep root.
|
|
|
|
### Podman before Docker
|
|
|
|
- Where the two differ, werkdock follows podman from now on (RFC 0005):
|
|
`--userns=keep-id` is the first case.
|
|
|
|
## 0.3.0 (2026-09-10)
|
|
|
|
Theme: `werkdock pull` — images come straight from a registry, no docker
|
|
on any machine needed (RFC 0002 level 2, RFC 0004). Additive; nothing a
|
|
consumer calls today changes.
|
|
|
|
### New
|
|
|
|
- `werkdock pull [--platform OS/ARCH] [REGISTRY/]REPOSITORY[:TAG]` fetches an
|
|
image and stores it exactly as `load -i` would: layers flattened, OCI
|
|
config kept, listed by `images` under the reference given. Prints
|
|
`Pulled image: REF`; exit 125 with the reason if the registry refuses,
|
|
the image already exists, or crane is missing. `--platform` defaults to
|
|
`linux/<host arch>`.
|
|
- The registry side is the `crane` binary
|
|
(github.com/google/go-containerregistry, one static executable, no
|
|
privileges). werkdock looks for it in `PATH`, then beside its own
|
|
binary, or at `WERKDOCK_CRANE`. A deployment on a managed webspace ships
|
|
it next to `werkdock`. Credentials come from `~/.docker/config.json`
|
|
(`DOCKER_CONFIG`), as with docker.
|
|
- `werkdock rmi IMAGE...` removes images (`docker rmi`): prints `Untagged: REF`
|
|
per image, tries every argument and exits 125 if one failed. Works on the
|
|
legacy flat layout too, so Werkator's pre-0.2.0 build image can be removed
|
|
with it. `pull` and `load` now name `rmi` in their "already exists" message.
|
|
- `werkdock images` takes docker's `--format`: a Go template over
|
|
`.Repository`, `.Tag`, `.ID`, `.CreatedAt`, `.CreatedSince`, `.Size`, or
|
|
`table [TEMPLATE]` for docker's column layout (`--format table` prints
|
|
`REPOSITORY TAG IMAGE ID CREATED SIZE`); `-q` prints IDs; an optional
|
|
`REPOSITORY[:TAG]` argument filters. Without flags the output is unchanged:
|
|
one short reference per line. `inspect` gains an `Id` field. The ID is
|
|
a hash of the image metadata, shared by tags of the same image; it is
|
|
not docker's config digest.
|
|
- `werkdock doctor` warns when crane is not in `PATH` (a warning, not a
|
|
failure: everything but `pull` works without it).
|
|
|
|
### For Werkator
|
|
|
|
- Optional: replace "download archive, `werkdock load -i`" with a single
|
|
`werkdock pull IMAGE` where the image is a public registry image; the
|
|
self-built archives keep going through `import`/`load`.
|
|
- Deployment: put `crane` beside `werkdock` if `pull` is to be used.
|
|
- Optional: retire old build images with `werkdock rmi` instead of deleting
|
|
store directories.
|
|
|
|
## 0.2.0 (2026-09-10)
|
|
|
|
Theme: the `run` surface, image names, and the image store move closer to
|
|
`docker run --rm` (RFC 0002, RFC 0004). Everything Werkator invokes today keeps working; two of its calls
|
|
now have a preferred docker-shaped form.
|
|
|
|
### Compatible — nothing to change
|
|
|
|
- `werkdock images` still prints one image per line and still prints a bare
|
|
name for images loaded without a tag. Werkator's exact-line existence check
|
|
(`image in loaded`) matches as before. Images that carry a tag other than
|
|
`latest` print as `repository:tag`.
|
|
- `werkdock load -i ARCHIVE --name NAME` still imports a rootfs archive; it
|
|
now additionally prints one note on stderr pointing to `werkdock import`.
|
|
Exit code and stdout are unchanged (`Loaded image: NAME` became
|
|
`Imported image: NAME` — adjust if the stdout line is parsed).
|
|
- `werkdock run --rm -v ... --tmpfs ... -e ... -w ... IMAGE CMD...` is
|
|
unchanged in syntax and mount order. Two semantic corrections match docker:
|
|
a bare `-e KEY` whose variable the host does not have is no longer passed as
|
|
`KEY=` (it is omitted); a missing `-w DIR` is created when it lies in a
|
|
writable area (tmpfs or a read-write bind) instead of failing with bwrap's
|
|
"Can't chdir".
|
|
- Existing stores: the flat layout `images/NAME/` written by earlier versions
|
|
is still read as `NAME:latest`. No reload after the upgrade; new imports land
|
|
in `images/REPOSITORY/TAG/`. The pre-existing `image.json` files lack the new
|
|
metadata fields and read back as an image without ENTRYPOINT/CMD/Env, which
|
|
is exactly what they were.
|
|
- `werkdock doctor`, `version`, exit codes (125 for werkdock's own errors,
|
|
the command's code otherwise): unchanged.
|
|
|
|
### Preferred new forms — adapt at the next convenient change
|
|
|
|
- `werkdock import ARCHIVE [REPOSITORY[:TAG]]` replaces `load -i --name` for
|
|
rootfs archives (`docker import` semantics). Plan: `load -i --name` stays
|
|
accepted until Werkator has switched, then it is removed together with the
|
|
stderr note.
|
|
- `werkdock load -i ARCHIVE` loads a `docker save` archive (legacy and OCI
|
|
layout) or a plain OCI image layout: layers flattened into one rootfs,
|
|
whiteouts applied, the OCI config kept as image metadata, the image named by
|
|
its RepoTags (every tag becomes a hardlinked copy). A multi-arch index picks
|
|
the host platform. This is the intended path for build environments from now
|
|
on: `docker build`, `docker save`, upload, `werkdock load` — with ENTRYPOINT,
|
|
ENV, and WORKDIR intact (RFC 0004). Layers compressed with zstd need the
|
|
`zstd` binary, as `import` does.
|
|
- Image names may carry a tag: `werkator-buildenv-<hash>` keeps working and
|
|
equals `werkator-buildenv-<hash>:latest`; a consumer may also use
|
|
`werkator-buildenv:<hash>` or a registry-style `git.javagil.de/mi/buildenv:<hash>`.
|
|
Name grammar: `[registry/]repository[:tag]`, repository components lowercase
|
|
`[a-z0-9]` with `.`, `_`, `-` between them, tag `[A-Za-z0-9_][A-Za-z0-9_.-]*`,
|
|
max 128 characters. The `.tmp` tag suffix is reserved.
|
|
- `import -c 'ENTRYPOINT [...]'`, `-c 'CMD ...'`, `-c 'ENV K=V'`,
|
|
`-c 'WORKDIR /d'`, `-c 'LABEL k=v'` store OCI metadata with the image;
|
|
`werkdock inspect IMAGE` prints it as a docker-shaped JSON array
|
|
(`RepoTags`, `Created`, `Architecture`, `Os`, `Config`, plus a `Werkdock`
|
|
section with `Source` and `RootFS`). A consumer that wants to check
|
|
existence like `docker image inspect` can use `inspect` (exit 125 when the
|
|
image is missing) instead of scanning `images`.
|
|
- `werkdock run IMAGE` without a command runs the image's ENTRYPOINT+CMD;
|
|
`--entrypoint PROGRAM` overrides ENTRYPOINT and drops CMD; positional
|
|
arguments replace CMD; `-e` and `-w` override the image's Env and WorkingDir.
|
|
- `--mount type=bind,src=SRC,dst=DEST[,readonly]` and
|
|
`--mount type=tmpfs,dst=DEST` are accepted beside `-v` and `--tmpfs`; all
|
|
three feed one ordered mount list (the git-metadata mask ordering still
|
|
holds across them).
|
|
- `--network host` (and `--net host`) is accepted as a no-op, so a build
|
|
script can pass a docker-compatible command line unchanged. Other modes fail
|
|
with: `network mode "X" is not available in unprivileged shared-webspace
|
|
environments; only "host" is supported`. `-p/--publish` remain refused.
|
|
|
|
### Incompatible
|
|
|
|
- None for the calls Werkator makes. For everyone else: an image name with an
|
|
uppercase letter or a leading `.`/`-`/`_` in the repository was accepted by
|
|
the old name check and is refused now (docker's grammar); `..`-style names
|
|
were refused before and still are.
|