the sandbox config section is werkdock, not bwrap (#19)

`bwrap` named the mechanism one layer below the tool that actually runs it: since
v1.0.0 Werkator does not invoke bwrap at all, it shells out to the werkdock CLI —
which made `bwrap.werkdock` a key naming its own executor.

The section is `werkdock` now and that key is `werkdock.binary`; BwrapConfig,
BwrapOverrides and BwrapBuildRunner follow the name. A file still writing `bwrap`
is read as before and warned about once per file, in `renameLegacySandbox` on the
raw map of every layer before merging — so nothing downstream knows two names, and
the old name is not a way around the pinning either. Renaming rather than refusing,
because the section lives in the machine configuration of every webspace instance,
which no repository tracks; the hard refusal belongs to the release that sets
ConfigVersions.FORMAT_BROKE_IN, where a file declaring no version can be caught
by name at all.

WERKATOR_SANDBOX in tools/remote follows, and still accepts `bwrap`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #19
This commit was merged in pull request #19.
This commit is contained in:
mi
2026-09-03 20:41:18 +02:00
co-authored by Claude Opus 5 mhoennig
parent 3ccc901d1b
commit 16543f038b
17 changed files with 340 additions and 120 deletions
+11 -10
View File
@@ -71,7 +71,7 @@ above, giving the precedence **branch > repo install > project**. It takes prece
everything that describes how this branch is built: the whole `builds` section — its own
definitions and its overrides of the definitions from the project config, with
`buildCommand`, `cleanCommand`, `artifactDirs`, log file names, and
`docker.image`/`dockerfile`/`context`/`env` and `bwrap.env` inside them. That is how a new configuration is tried out: change it on a branch, and
`docker.image`/`dockerfile`/`context`/`env` and `werkdock.env` inside them. That is how a new configuration is tried out: change it on a branch, and
no other branch's builds are affected.
The branch layer is used in both places where it matters: the watcher reads the committed
@@ -99,7 +99,7 @@ single branch may decide it:
- the repository-side settings: the whole `gitea`, `executor`, and `watcher` sections;
- the trust gate: `requirePullRequest`, and the Gitea status context: `statusContext`;
- the container sandbox policy: `docker.enabled`/`docker.network` and
`bwrap.enabled`/`bwrap.rootfs`/`bwrap.werkdock` — host-pinned as
`werkdock.enabled`/`werkdock.rootfs`/`werkdock.binary` — host-pinned as
long as only the host's configuration sets them, master-pinned once the committed
configuration does.
@@ -406,9 +406,9 @@ That is how a branch gets a build of its own without being built by the default
`activeWithin` (e.g. `24h`) additionally keeps only branches whose origin head commit is younger than the duration — useful to run a nightly deep check over all recently active branches.
Both parts combine as an intersection.
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` and `bwrap` with all their keys.
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` and `werkdock` with all their keys.
A definition carries the complete description of its build; unset keys fall back to `builds.default` and then to Werkator's own defaults.
`requirePullRequest`, `statusContext`, `docker.enabled`, `docker.network`, `bwrap.enabled`, `bwrap.rootfs`, and `bwrap.werkdock` are pinned (master-pinned, see [the branch layer](#the-branch-layer-a-branch-describes-its-own-ci)): they are read from the repo install/project config even when a branch sets them in its own committed config.
`requirePullRequest`, `statusContext`, `docker.enabled`, `docker.network`, `werkdock.enabled`, `werkdock.rootfs`, and `werkdock.binary` are pinned (master-pinned, see [the branch layer](#the-branch-layer-a-branch-describes-its-own-ci)): they are read from the repo install/project config even when a branch sets them in its own committed config.
Inheritance from `builds.default` covers the settings only — the `trigger` block says when and where *this* build runs and is never inherited.
Definitions are part of the branch layer: a branch may add its own and override those from the project config, for its own builds only.
Because the inheritance is applied after all layers are merged, a build a branch invents still inherits the host's `builds.default` — its sandbox policy included, which is what keeps the pinning effective for a build the host has never heard of.
@@ -464,22 +464,23 @@ Note that the rest of `.git` — including `.git/config` — is visible to build
The Docker socket is mounted into the container and `DOCKER_HOST`/`TESTCONTAINERS_*` variables are set, so Testcontainers-based builds work inside the container.
All Werkator containers carry `org.hoennig.werkator` labels; stale build containers of the repository are removed before the first Docker build after a restart.
### Notes on `builds.<name>.bwrap`
### Notes on `builds.<name>.werkdock`
With `bwrap.enabled`, Werkator runs the build in a bubblewrap sandbox instead of native execution.
With `werkdock.enabled`, Werkator runs the build in a bubblewrap sandbox instead of native execution.
This is the third runtime, for hosts without root and without a Docker daemon (e.g. Hostsharing managed webspaces); see `docs/plan/17-bwrap-build-runtime.md` and ADR 0008.
Since step 21 session C the sandbox is executed by the `werkdock` CLI (`bwrap.werkdock`, default: resolved via `PATH`) — Werkator no longer invokes `bwrap` itself; `bwrap` must be installed for werkdock.
Since step 21 session C the sandbox is executed by the `werkdock` CLI (`werkdock.binary`, default: resolved via `PATH`) — Werkator no longer invokes `bwrap` itself; `bwrap` must be installed for werkdock.
The section was called `bwrap` and its binary key `bwrap.werkdock` until v1.2.0; both are still read, with a warning naming the file, so an installation can be migrated at its next configuration edit rather than at the next update.
`werkdock doctor` checks the host's capability (it replaced the retired `tools/werkator-build-prerequisites.sh` in step 23).
`bwrap.rootfs` names the prepared root filesystem archive — a Debian-base rootfs with the build tools (JDK, git, locales, project-specific tooling) built elsewhere, since `debootstrap` is unavailable on the target.
`werkdock.rootfs` names the prepared root filesystem archive — a Debian-base rootfs with the build tools (JDK, git, locales, project-specific tooling) built elsewhere, since `debootstrap` is unavailable on the target.
It is a local path or an `http(s)` URL; a URL is downloaded once into `.git/werkator/buildenv/`.
Build the archive with `tools/build-bwrap-rootfs.sh` on any machine with Docker.
The archive is loaded once per source as the werkdock image `werkator-buildenv-<hash>` into werkdock's store (`$WERKDOCK_HOME`, default `~/.werkdock`) — shared by every repository of this OS user; the hash derives from the source string, so a changed `rootfs` loads a fresh image and stale ones can be removed from the store.
Per-repo Gradle caches persist in `.git/werkator/buildenv/home`, bound as `/root`.
`bwrap.env` adds environment variables inside the sandbox; the environment is otherwise cleared (docker semantics) — the server's environment does not leak in.
`werkdock.env` adds environment variables inside the sandbox; the environment is otherwise cleared (docker semantics) — the server's environment does not leak in.
Files created inside the sandbox are owned by the host user, because uid 0 maps back to the unprivileged webspace user.
`docker` and `bwrap` are mutually exclusive per branch: enabling both is rejected at start, not silently picked.
`docker` and `werkdock` are mutually exclusive per branch: enabling both is rejected at start, not silently picked.
Git works inside the sandbox exactly as inside the Docker container: the primary `.git` is mounted read-only with `.git/werkator/` masked, so builds can run read-only git commands but never reach the machine config or the control token.
## `.git/werkator/.werkator.yml` (not committed)
+2 -2
View File
@@ -185,7 +185,7 @@ The tarball unpacks to a `werkator/` directory, so it must not be extracted over
Rollback is the reverse: stop, remove the new directory (or jar), move `.bak` back, start.
`tools/remote --env-file .env.<instance> werkator instance-update` does the same sequence for any host, not only the webspace layout it was written for.
Three optional keys in the env file name what differs (see the script's header): `WERKATOR_REPO_DIR` (directory of the watched repository, which also names the systemd unit), `WERKATOR_INSTALL_DIR` (where the runtime bundle is unpacked), and `WERKATOR_SANDBOX` (`bwrap`, the default, or `docker` — a Docker host has no werkdock binary and no rootfs archive to upload).
Three optional keys in the env file name what differs (see the script's header): `WERKATOR_REPO_DIR` (directory of the watched repository, which also names the systemd unit), `WERKATOR_INSTALL_DIR` (where the runtime bundle is unpacked), and `WERKATOR_SANDBOX` (`werkdock`, the default, or `docker` — a Docker host has no werkdock binary and no rootfs archive to upload).
Their defaults are the layout `instance-install` creates, so an env file that names none of them behaves exactly as before.
The upload happens before the service is stopped and every artifact is checksum-verified after the transfer, so a dropped connection costs the transfer and not the running service.
@@ -329,7 +329,7 @@ Werkator runs as a systemd *user* service on the assigned localhost port ("eigen
Werkator is never built on the webspace: the runtime bundle and the werkdock binary are built locally and uploaded (ADR 0006).
All steps are driven by `tools/remote`; commands name their role — `instance-*` manages the installed Werkator, `repo-*` the repository it watches.
Each instance is a pair of files (step 23): a transport env file selected with `--env-file` (default `.env`), and a YAML fragment in the configuration schema, named by its `WERKATOR_INIT_CONFIG` key and installed remotely via `werkator init --apply` — e.g. `.env.mih34` + `.env.mih34.yml`, both gitignored.
The fragment carries the Werkator configuration (`server.port`, `publicBaseUrl`, systemd limits, `builds.default.bwrap.*`); the env file only says where and how to reach the host.
The fragment carries the Werkator configuration (`server.port`, `publicBaseUrl`, systemd limits, `builds.default.werkdock.*`); the env file only says where and how to reach the host.
```bash
tools/remote --env-file .env.mih34 werkator check-prerequisites # uploads werkdock, runs its doctor
@@ -0,0 +1,98 @@
> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
The build sandbox for hosts without Docker was configured as `bwrap`, named after the mechanism rather than after the thing Werkator runs.
Since step 21 session C (v1.0.0) Werkator does not invoke `bwrap` at all: it shells out to the [werkdock](https://git.javagil.de/mi/werkdock) CLI, which assembles the bubblewrap invocation and owns the image store.
The name outlived its truth, and the clearest symptom was the key `bwrap.werkdock` — a section naming its own executor.
It also leaked outward.
`tools/remote` gained a `WERKATOR_SANDBOX` key in PR#18 whose value had to be `bwrap` while the very thing it switches on is *uploading the werkdock binary*, and the question that prompted this PR — "is there also `WERKATOR_SANDBOX=werkdock`?" — is one nobody would ask about a name that matched.
## Non-Goals
- Refusing the old name. That belongs to the release which sets `ConfigVersions.FORMAT_BROKE_IN` (plan step 18): only there can a file that declares no version be caught by name at all, and only there is one migration asked of the operator instead of two.
- Renaming `tools/build-bwrap-rootfs.sh` or `docs/plan/17-bwrap-build-runtime.md`. The script really does build a bubblewrap rootfs, and plan documents are historic records.
- Touching ADR 0008, which decided the *runtime* and is a snapshot of that decision.
## The Scenarios
### Feature: the sandbox is named after the tool that runs it
#### Background
- `builds.<name>.werkdock` replaces `builds.<name>.bwrap`, and `werkdock.binary` replaces `bwrap.werkdock`.
- The pinned set is unchanged in meaning: `enabled`, `rootfs` and the binary stay host-pinned, under their new names.
#### Scenario#19.01: A configuration written for the old name keeps working
So that no installation has to be edited before it can be updated — the section lives in machine configurations that no repository tracks.
- **Given** a configuration writing `builds.default.bwrap` with `enabled`, `rootfs`, `werkdock` and `env`
- **When** the configuration is loaded
- **Then** the settings appear as `werkdock.enabled`, `werkdock.rootfs`, `werkdock.binary` and `werkdock.env`, and the file is named once in a warning
##### Verified by
- [ConfigLoaderTest — "the legacy bwrap section is read as werkdock, its werkdock key as binary"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
#### Scenario#19.02: The old name is not a way around the pinning
So that a branch cannot escape its sandbox by writing the section a branch is not allowed to write under its previous name.
- **Given** a host configuration with `werkdock.enabled: true` and a rootfs
- **When** a branch's committed config sets `bwrap.enabled: false` with a foreign rootfs
- **Then** the sandbox stays enabled and the host's rootfs is used
##### Verified by
- [ConfigLoaderTest — "a legacy bwrap section on a branch is pinned exactly like the new name"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
#### Scenario#19.03: The new name behaves exactly as the old one did
So that the rename is a rename, not a change of behavior.
- **Given** configurations using `werkdock` throughout
- **When** builds are dispatched, pinned keys stripped, and both sandboxes enabled at once
- **Then** the werkdock runner is selected, the branch cannot override the pinned keys, and enabling docker and werkdock together is rejected naming both
##### Verified by
- [DispatchingBuildRunnerTest — "runs in the werkdock sandbox when the branch enables it (and not Docker)"](../../src/test/kotlin/de/hoennig/werkator/build/DispatchingBuildRunnerTest.kt)
- [ConfigLoaderTest — "a branch cannot disable its werkdock sandbox …"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt) and "enabling both docker and werkdock on a build is rejected, not picked silently"
- [WerkdockBuildRunnerTest](../../src/test/kotlin/de/hoennig/werkator/build/WerkdockBuildRunnerTest.kt), unchanged in substance and renamed with the runner
## The Solution
`BwrapConfig``WerkdockConfig` (field `werkdock``binary`), `BwrapOverrides``WerkdockOverrides`, `BranchConfig.bwrap``.werkdock`, `BwrapBuildRunner``WerkdockBuildRunner`, and `PINNED_BWRAP_KEYS``PINNED_WERKDOCK_KEYS` with `binary` in place of `werkdock`.
The compatibility lives in exactly one function, `ConfigLoader.renameLegacySandbox`, applied in `loadFile` and `parseYaml` — the two places a raw layer enters — so it runs before merging, before pinning and before binding, and every consumer downstream knows one name.
That placement is what makes Scenario#19.02 hold without a second thought: the branch layer is normalised *before* `stripPinned` reads it, so the old name cannot smuggle a pinned key past a check that looks for the new one.
Where a file writes both sections, the explicit `werkdock` one wins, because it is the name that is meant.
The warning is emitted once per file (`warnedSections`), like the other section-level warnings, since the config is re-read on every poll cycle.
The version is bumped to 1.2.0 with a release note: the minor, because this changes the configuration schema, and a deployment must be identifiable as the one that introduced it.
## Open Questions
- **Should `WERKATOR_SANDBOX` keep accepting `bwrap`?** It does today, normalised on read and documented as the former name. The env files are local and gitignored, so this alias costs one line and can go whenever the config alias does.
## Additional Changes
- None beyond the rename and its documentation.
## Deployment note
The order matters, and only in one direction: deploy v1.2.0 to a host *before* rewriting its init fragment (`.env.<instance>.yml`) to the new key names.
A fragment carrying `werkdock:` applied by an older Werkator fails the fragment's strict schema validation — which is the safe outcome, but a failed `repo-init` nonetheless.
The reverse never breaks: v1.2.0 reads every existing `bwrap:` fragment and machine config as before.
## Prerequisite PRs
- [PR#18](2026-09-03-PR%2318-remote-host-layout.md) introduced `WERKATOR_SANDBOX`, whose value this PR renames.
## Follow-up PRs
- Plan step 18 (removing the legacy `branches` section) sets `ConfigVersions.FORMAT_BROKE_IN`; the `bwrap` alias should be dropped in the same release, refusing the key by name.