Merge pull request #7 from mhoennig/werkator-consumes-werkdock
BwrapBuildRunner delegates to werkdock; webspace install path replaces the self-build prototype
This commit is contained in:
+11
-10
@@ -91,7 +91,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` — host-pinned as
|
||||
`bwrap.enabled`/`bwrap.rootfs`/`bwrap.werkdock` — host-pinned as
|
||||
long as only the host's configuration sets them, master-pinned once the committed
|
||||
configuration does.
|
||||
|
||||
@@ -353,7 +353,7 @@ Both parts combine as an intersection.
|
||||
|
||||
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` and `bwrap` 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`, and `bwrap.rootfs` 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`, `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.
|
||||
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.
|
||||
@@ -411,16 +411,17 @@ All Werkator containers carry `org.hoennig.werkator` labels; stale build contain
|
||||
|
||||
### Notes on `builds.<name>.bwrap`
|
||||
|
||||
With `bwrap.enabled`, Werkator shells out to the `bwrap` CLI (bubblewrap) 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 0007.
|
||||
`bwrap` must be on the `PATH`.
|
||||
With `bwrap.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.
|
||||
`werkdock doctor` checks the host's capability, superseding `tools/werkator-build-prerequisites.sh`.
|
||||
|
||||
`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.
|
||||
It is a local path or an `http(s)` URL; a URL is downloaded once.
|
||||
Build the archive with `tools/build-bwrap-rootfs.sh` on any machine with Docker; verify the host's user-namespace capability first with `tools/werkator-build-prerequisites.sh`.
|
||||
The archive is unpacked on demand (`tar --no-same-owner`) into `.git/werkator/buildenv/<envKey>/rootfs`, shared across all branch worktrees like the Docker Gradle cache volume; `<envKey>` derives from a hash of the source, so a changed `rootfs` unpacks a fresh environment and stale ones can be pruned.
|
||||
Per-branch Gradle caches persist in `.git/werkator/buildenv/home`, bound as `/root`.
|
||||
`bwrap.env` adds environment variables inside the sandbox.
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -261,3 +261,31 @@ All nginx and certificate failures are non-fatal warnings — the plain HTTP ser
|
||||
The nginx container cannot reach `localhost` of the host, so the proxy upstream defaults to `serverName`; set `server.nginx.upstreamHost` if the host is reachable under a different name from inside containers.
|
||||
With the managed nginx, set `server.bindAddress: 0.0.0.0` explicitly (or an address reachable from the Docker network) — the default `127.0.0.1` makes Werkator unreachable for the proxy container.
|
||||
See [configuration.md](configuration.md) for all `server.nginx.*` keys.
|
||||
|
||||
## Hostsharing Managed Webspace
|
||||
|
||||
The third deployment variant (plan step 21, verified live on a real webspace): no root, no Docker daemon, no own reverse proxy.
|
||||
Werkator runs as a systemd *user* service on the assigned localhost port ("eigener Serverdienst"), the platform's managed Apache terminates TLS and proxies via `.htaccess`, and builds run in the bubblewrap sandbox executed by the [werkdock](../werkdock/README.md) CLI (ADR 0008, step 21 session C).
|
||||
|
||||
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`, configured through the `.env` file in the repository root; commands name their role — `instance-*` manages the installed Werkator, `repo-*` the repository it watches.
|
||||
|
||||
```bash
|
||||
tools/remote werkator check-prerequisites # bwrap capability, disk and quota headroom
|
||||
tools/remote werkator instance-install # upload + unpack the runtime bundle and werkdock
|
||||
tools/remote werkator repo-init # clone the watched repo, init, rootfs archive, bwrap config
|
||||
tools/remote werkator instance-start # server config, Apache proxy, systemd user unit
|
||||
tools/remote port-forward start # browser tunnel while no public domain is set up
|
||||
```
|
||||
|
||||
Layout on the host: the watched repository at `$WERKATOR_PATH/werkator/`, the unpacked runtime at `$WERKATOR_PATH/.werkator/werkator/`, the werkdock binary at `$WERKATOR_PATH/.werkator/bin/werkdock`.
|
||||
The rootfs archive is loaded once per source into werkdock's image store (`~/.werkdock`), shared by every repository of the user.
|
||||
Fill `git.account`/`git.token` in the machine config when the origin is private, and make the user's services survive logout with `loginctl enable-linger`.
|
||||
|
||||
Updates are one command, refused while a build runs (`FORCE=1` overrides):
|
||||
|
||||
```bash
|
||||
tools/remote werkator instance-update
|
||||
```
|
||||
|
||||
The previous runtime stays as `.werkator/werkator.prev` for one deployment as the rollback asset.
|
||||
|
||||
@@ -64,13 +64,16 @@ A docker-like CLI over `bwrap`, filesystem isolation only.
|
||||
- Own docs, plan, and ADRs under `werkdock/` from the start, so the later repository split is a directory move; the Werkator side only keeps what is Werkator-specific (the git-metadata mounts of step 16 and the config pinning).
|
||||
- Keep `werkdock/` self-contained: no imports from Werkator code, no Gradle coupling to the Werkator build — it must build and test on its own.
|
||||
|
||||
### C — Werkator consumes Werkdock (this repo, after B)
|
||||
### C — Werkator consumes Werkdock (this repo, after B; implemented 2026-09-01 on branch `werkator-consumes-werkdock`)
|
||||
|
||||
- `BwrapBuildRunner` shells out to `werkdock run` instead of assembling the raw `bwrap` argv — same pattern as git and docker: CLI, no library.
|
||||
- Config keys (`bwrap.enabled`, `bwrap.rootfs`) and their pinning stay as they are; only the executor behind them changes.
|
||||
- Decide in the step: whether the git-metadata mounts stay Werkator-side (passed as extra `--bind`/`--tmpfs` options to `werkdock run`) or become a Werkdock feature; the secrets-masking of `.git/werkator/` must hold either way.
|
||||
One key was added: `bwrap.werkdock` (the executing binary, default via PATH) — pinned like the rest of the sandbox policy, since a branch must not substitute the executing binary.
|
||||
- Decided: the git-metadata mounts stay Werkator-side, passed as `-v …:ro` / `--tmpfs` / `-v` options whose flag order werkdock preserves (it grew `--tmpfs` and an ordered mount list for exactly this); the secrets-masking of `.git/werkator/` holds unchanged.
|
||||
- Decided: the rootfs archive becomes a werkdock *image* (`werkator-buildenv-<source-hash>`, checked via `werkdock images`, loaded via `werkdock load`) in werkdock's own store — shared across every repository of the OS user, which resolves step 22's buildenv-sharing question; only the URL download cache and the persistent `/root` toolchain home stay under `.git/werkator/buildenv/`.
|
||||
- Consequence of werkdock's `--clearenv`: the server environment no longer leaks into builds, and the runner's TMPDIR workaround is gone.
|
||||
|
||||
### D — The Managed-Webspace install path (this repo, independent of B/C)
|
||||
### D — The Managed-Webspace install path (this repo, independent of B/C; implemented 2026-09-01 on branch `werkator-consumes-werkdock`)
|
||||
|
||||
Bring intent 1 to the webspace: build locally, install the bundle — Werkator never builds itself on the target.
|
||||
|
||||
@@ -90,6 +93,9 @@ Bring intent 1 to the webspace: build locally, install the bundle — Werkator n
|
||||
Three defects found and fixed on the way: unanchored tar excludes dropped the Go stdlib's `sys` directory from the archive, pam_tmpdir's `TMPDIR` leaked into the sandbox (Werkator-side fix; Werkdock is immune via `--clearenv`), and non-report artifacts were stored below `reports/` and invisible in the UI.
|
||||
The image was then trimmed (headless JDK, en/de locales only, no man/doc/apt-lists): 351 MB compressed — smaller than the original JDK-only archive despite carrying Go and Node.
|
||||
All rollback assets on mih34 are removed; the PR for this branch is prepared (PR-doc with `PR#000` placeholder) and will be opened later.
|
||||
- 2026-09-01, session C deployed to mih34: the werkdock binary sits at `.werkator/bin/werkdock`, the machine config names it in `bwrap.werkdock`, the runtime bundle carries the delegating runner, and the TMPDIR workaround left the machine config (obsolete under werkdock's clearenv).
|
||||
- 2026-09-01, session D done and live-verified on mih34: `tools/remote` reworked to role-named commands (`instance-install`/`instance-update`/`instance-start` for the builder, `repo-init` for the built; the retired `install`/`build`/`start` fail loudly naming their successors); the self-build, the repo clone for it, and the GitHub-key step are gone — the instance installs from locally built artifacts (bundle + werkdock), the watched repo clones anonymously via https.
|
||||
`instance-update` refuses to swap under a running build, `repo-init` is idempotent (checksum-skipped rootfs upload; the machine-config guard whose indentation mismatch once appended nine duplicate bwrap blocks is fixed); `docs/deployment.md` gained the Managed Webspace as the third variant, written from the verified setup.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
> **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
|
||||
|
||||
PR #6 grew Werkdock as a standalone sandbox CLI, but `BwrapBuildRunner` still assembled its own raw `bwrap` invocation — the extraction was only half done, and the two implementations could drift.
|
||||
Separately, the webspace deployment path in `tools/remote` still followed the original self-build prototype: clone Werkator's own repository onto the target and build it there, which is exactly the pattern ADR 0006 rejected ("build locally, install the bundle") and step 21 set out to correct.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- `tools/remote`'s configuration-writing duplication (heredocs/`sed` into the machine config) — that is step 23, PR #8/#9.
|
||||
- Multi-repository support for one Werkator instance (step 22, PR #10).
|
||||
- RFC 0002 levels 2/3 and RFC 0003 (composable toolchain mounts) stay deferred/candidate.
|
||||
|
||||
## The Scenarios
|
||||
|
||||
### Feature: `BwrapBuildRunner` delegates to Werkdock
|
||||
|
||||
#### Scenario#7.01: A build runs through `werkdock run` instead of a raw `bwrap` invocation
|
||||
|
||||
So that Werkator and Werkdock never carry two implementations of the same sandbox invocation.
|
||||
|
||||
- **Given** `bwrap.enabled` and a configured rootfs archive
|
||||
- **When** a build needs the sandbox
|
||||
- **Then** `BwrapBuildRunner` loads the image via `werkdock images`/`werkdock load` (once, keyed by `imageName(rootfs)` = `werkator-buildenv-<hash-of-source>`) and runs the build via `werkdock run --rm`
|
||||
- **and** the configured `bwrap.werkdock` binary (default: `werkdock` via `PATH`) is what gets invoked.
|
||||
|
||||
##### Verified by
|
||||
|
||||
- [assembles the exact werkdock run command for a loaded image](../../src/test/kotlin/de/hoennig/werkator/build/BwrapBuildRunnerTest.kt)
|
||||
- [loads the image once when werkdock does not know it yet](../../src/test/kotlin/de/hoennig/werkator/build/BwrapBuildRunnerTest.kt)
|
||||
- [does not load an image werkdock already has](../../src/test/kotlin/de/hoennig/werkator/build/BwrapBuildRunnerTest.kt)
|
||||
- [uses the configured werkdock binary path](../../src/test/kotlin/de/hoennig/werkator/build/BwrapBuildRunnerTest.kt)
|
||||
|
||||
#### Scenario#7.02: The git-metadata mask survives the move to ordered `-v`/`--tmpfs` flags
|
||||
|
||||
So that secrets stay outside the sandbox exactly as before, now expressed as flag order instead of an internal mount list.
|
||||
|
||||
- **Given** a worktree build
|
||||
- **When** the invocation is assembled
|
||||
- **Then** `.git` is bound read-only, then `.git/werkator/` is masked with `--tmpfs`, then the worktree's admin dir is bound read-write, in that exact order
|
||||
- **and** Werkdock's `Mount` list (replacing the earlier unordered `Bind` list) preserves the order flags were given in.
|
||||
|
||||
##### Verified by
|
||||
|
||||
- [exposes git metadata read-only with the werkator dir masked, in mount order](../../src/test/kotlin/de/hoennig/werkator/build/BwrapBuildRunnerTest.kt)
|
||||
- [mounts no git metadata when the workspace is not a worktree](../../src/test/kotlin/de/hoennig/werkator/build/BwrapBuildRunnerTest.kt)
|
||||
- [TestParseRunKeepsMountFlagOrderAcrossVolumeAndTmpfs](../../werkdock/internal/cli/run_test.go)
|
||||
|
||||
#### Scenario#7.03: Werkdock gained what the delegation needed
|
||||
|
||||
So that `images`/`:rw` were built because Werkator's runner needed them, not speculatively.
|
||||
|
||||
- **Given** the new `werkdock images` verb and `:rw` volume option
|
||||
- **When** the runner checks whether an image is already loaded, or mounts the admin dir read-write
|
||||
- **Then** `images` lists loaded image names (one per line, `docker images --format` shaped) and `-v src:dst:rw` is accepted alongside the existing `:ro`.
|
||||
|
||||
##### Verified by
|
||||
|
||||
- [TestListNamesLoadedImagesAndIgnoresTmpLeftovers](../../werkdock/internal/store/store_test.go)
|
||||
- [TestParseRunAcceptsTheExplicitRwVolumeOption](../../werkdock/internal/cli/run_test.go)
|
||||
- [TestImageNameFromArchive](../../werkdock/internal/store/store_test.go)
|
||||
|
||||
#### Scenario#7.04: The TMPDIR workaround is gone because it is now structurally impossible
|
||||
|
||||
So that the fix and its own workaround do not both linger in the codebase.
|
||||
|
||||
- **Given** Werkdock's `--clearenv`
|
||||
- **When** a build runs in the sandbox
|
||||
- **Then** no host `TMPDIR`/`TMP` reaches the sandboxed process at all, so `BwrapBuildRunner`'s earlier explicit `--setenv TMPDIR /tmp` workaround (PR #4) is removed as dead code, not merely redundant.
|
||||
|
||||
##### Verified by
|
||||
|
||||
- [adds bwrap env after the branch environment](../../src/test/kotlin/de/hoennig/werkator/build/BwrapBuildRunnerTest.kt)
|
||||
- [TestArgvAssemblesTheHardenedInvocation](../../werkdock/internal/engine/bwrap_test.go)
|
||||
|
||||
### Feature: the webspace install path replaces the self-build prototype
|
||||
|
||||
#### Scenario#7.05: `tools/remote` separates the builder role from the built (watched) repository role
|
||||
|
||||
So that "build Werkator on the webspace" and "Werkator watches a repository on the webspace" are never conflated again.
|
||||
|
||||
- **Given** a Managed Webspace target
|
||||
- **When** the wrapper manages the Werkator runtime versus a repository Werkator watches
|
||||
- **Then** `instance-install`/`instance-update`/`instance-start` install and run the Werkator **builder** binary+bundle
|
||||
- **and** `repo-init` prepares a repository to be **built by** that instance
|
||||
- **and** the retired `install`/`build`/`start` commands fail loudly, naming their successors, instead of silently doing the old thing.
|
||||
|
||||
##### Verified by
|
||||
|
||||
- manual invocation of the retired commands on mih34 (shell script; no automated test harness for `tools/remote`)
|
||||
|
||||
#### Scenario#7.06: The self-build prototype is gone
|
||||
|
||||
So that Werkator is never again built by checking out its own source onto the target and compiling there.
|
||||
|
||||
- **Given** the old prototype cloned werkator's own repository onto the webspace and built it in place
|
||||
- **When** an instance is installed or updated now
|
||||
- **Then** the runtime bundle is built locally and transported (`instance-install`/`instance-update`), never cloned-and-built on the target.
|
||||
|
||||
##### Verified by
|
||||
|
||||
- live run on mih34: `instance-update` against a runtime bundle built locally
|
||||
|
||||
## The Solution
|
||||
|
||||
`BwrapBuildRunner.invocation()` no longer builds a `bwrap` argv; it shells out to the `werkdock` binary named by `bwrap.werkdock` (a new pinned config key, alongside `bwrap.enabled`/`bwrap.rootfs`) for `images`, `load`, and `run --rm`.
|
||||
`werkdock/internal/engine/engine.go` was rewritten from an unordered `Bind` list to an ordered `Mount` list (`MountBind`/`MountRoBind`/`MountTmpfs`) specifically so the CLI's `-v`/`--tmpfs` flag order — which the git-metadata mask depends on — survives into the sandbox invocation unchanged.
|
||||
`werkdock/internal/cli/images.go` is new; `run.go`'s volume parsing gained the `:rw` option.
|
||||
`tools/remote` was reorganized around two roles instead of one flat command list: builder lifecycle (`instance-install`/`instance-update`/`instance-start`) versus watched-repository lifecycle (`repo-init`); the old `install`/`build`/`start` now `die` with the successor's name.
|
||||
`require_idle()`/`FORCE=1` guards a runtime swap against a build in progress.
|
||||
|
||||
## Additional Changes
|
||||
|
||||
- `docs/deployment.md`: the webspace section now describes the role-separated commands.
|
||||
- `docs/configuration.md` and `AGENTS.md`: `bwrap.werkdock` documented as a fourth pinned bwrap key.
|
||||
- Step 21 plan: sessions C and D marked done with live-verification notes.
|
||||
|
||||
## Prerequisite PRs
|
||||
|
||||
- PR #6 (Werkdock bootstrap) — this PR is the consumer of the CLI it built.
|
||||
|
||||
## Follow-up PRs
|
||||
|
||||
- PR #8/#9: `tools/remote`'s remaining configuration-writing duplication with `werkator init` is resolved next (step 23).
|
||||
- PR #10: multi-repository support for one Werkator instance (step 22).
|
||||
Reference in New Issue
Block a user