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
@@ -43,17 +43,17 @@ class ProcessBuildRunner : BuildRunner {
}
/**
* Selects the runtime per branch: Docker when `branches.<name>.docker.enabled`,
* bubblewrap when `branches.<name>.bwrap.enabled`, native shell execution otherwise
* (the unchanged default). Docker and bwrap are mutually exclusive per branch and are
* rejected together at config load, so the branch order here never has to "pick".
* Selects the runtime per branch: Docker when `builds.<name>.docker.enabled`, the
* werkdock sandbox when `builds.<name>.werkdock.enabled`, native shell execution
* otherwise (the unchanged default). Docker and werkdock are mutually exclusive per
* branch and are rejected together at config load, so the order here never has to "pick".
*/
@Primary
@Component
class DispatchingBuildRunner(
private val processBuildRunner: ProcessBuildRunner,
private val dockerBuildRunner: DockerBuildRunner,
private val bwrapBuildRunner: BwrapBuildRunner,
private val werkdockBuildRunner: WerkdockBuildRunner,
) : BuildRunner {
override fun start(
command: String,
@@ -66,7 +66,7 @@ class DispatchingBuildRunner(
val runner =
when {
branchConfig.docker.enabled -> dockerBuildRunner
branchConfig.bwrap.enabled -> bwrapBuildRunner
branchConfig.werkdock.enabled -> werkdockBuildRunner
else -> processBuildRunner
}
return runner.start(command, workingDir, environment, repoDir, branchConfig, onAuxProcess)
@@ -1,7 +1,7 @@
package de.hoennig.werkator.build
import de.hoennig.werkator.config.BranchConfig
import de.hoennig.werkator.config.BwrapConfig
import de.hoennig.werkator.config.WerkdockConfig
import de.hoennig.werkator.git.GitCommandRunner
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@@ -13,7 +13,7 @@ import java.security.MessageDigest
* Runs build commands inside a bubblewrap user-namespace sandbox (Step 17 / ADR 0008),
* for hosts without root and without a Docker daemon (e.g. Hostsharing managed
* webspaces). Since step 21 session C it no longer assembles the raw `bwrap` argv:
* it shells out to the `werkdock` CLI (`bwrap.werkdock`, default via PATH) the same
* it shells out to the `werkdock` CLI (`werkdock.binary`, default via PATH) the same
* pattern as git and docker, CLI, no library.
*
* The rootfs archive becomes a werkdock *image*, loaded once per source
@@ -36,10 +36,10 @@ import java.security.MessageDigest
* native builds.
*/
@Component
class BwrapBuildRunner(
class WerkdockBuildRunner(
private val commandRunner: GitCommandRunner,
) : BuildRunner {
private val log = LoggerFactory.getLogger(BwrapBuildRunner::class.java)
private val log = LoggerFactory.getLogger(WerkdockBuildRunner::class.java)
/** Replaceable process launcher so unit tests can capture the assembled `werkdock` argv. */
internal var processStarter: (List<String>, Path) -> Process = { command, dir ->
@@ -54,14 +54,14 @@ class BwrapBuildRunner(
branchConfig: BranchConfig,
onAuxProcess: (Process) -> Unit,
): Process {
val bwrap = branchConfig.bwrap
require(bwrap.rootfs.isNotBlank()) { "branches.<name>.bwrap.rootfs must be set when bwrap.enabled is true" }
val werkdock = bwrap.werkdock.ifBlank { "werkdock" }
val image = imageName(bwrap.rootfs)
ensureImage(werkdock, image, bwrap, repoDir, onAuxProcess)
val sandbox = branchConfig.werkdock
require(sandbox.rootfs.isNotBlank()) { "builds.<name>.werkdock.rootfs must be set when werkdock.enabled is true" }
val werkdock = sandbox.binary.ifBlank { "werkdock" }
val image = imageName(sandbox.rootfs)
ensureImage(werkdock, image, sandbox, repoDir, onAuxProcess)
val homeDir = repoDir.resolve(BUILDENV_DIR).resolve(HOME_DIR)
Files.createDirectories(homeDir)
val args = invocation(command, workingDir, environment, repoDir, bwrap, werkdock, image, homeDir)
val args = invocation(command, workingDir, environment, repoDir, sandbox, werkdock, image, homeDir)
return processStarter(args, repoDir)
}
@@ -73,7 +73,7 @@ class BwrapBuildRunner(
private fun ensureImage(
werkdock: String,
image: String,
bwrap: BwrapConfig,
sandbox: WerkdockConfig,
repoDir: Path,
onAuxProcess: (Process) -> Unit,
) {
@@ -81,10 +81,10 @@ class BwrapBuildRunner(
if (image in loaded) {
return
}
val envDir = repoDir.resolve(BUILDENV_DIR).resolve(sourceKey(bwrap.rootfs))
val envDir = repoDir.resolve(BUILDENV_DIR).resolve(sourceKey(sandbox.rootfs))
Files.createDirectories(envDir)
val archive = localArchive(bwrap.rootfs, envDir, repoDir, onAuxProcess)
log.info("loading build environment {} as werkdock image {}", bwrap.rootfs, image)
val archive = localArchive(sandbox.rootfs, envDir, repoDir, onAuxProcess)
log.info("loading build environment {} as werkdock image {}", sandbox.rootfs, image)
commandRunner.runOrThrow(
listOf(werkdock, "load", "-i", archive, "--name", image),
repoDir,
@@ -93,7 +93,7 @@ class BwrapBuildRunner(
}
/**
* Resolves [BwrapConfig.rootfs] to a local archive path: a bare or `file:` path is
* Resolves [WerkdockConfig.rootfs] to a local archive path: a bare or `file:` path is
* used as-is; an `http(s)` URL is downloaded once into the buildenv cache.
*/
private fun localArchive(
@@ -123,7 +123,7 @@ class BwrapBuildRunner(
workspace: Path,
environment: Map<String, String>,
repoDir: Path,
bwrap: BwrapConfig,
sandbox: WerkdockConfig,
werkdock: String,
image: String,
homeDir: Path,
@@ -148,7 +148,7 @@ class BwrapBuildRunner(
for ((key, value) in environment) {
args += listOf("-e", "$key=$value")
}
for ((key, value) in bwrap.env) {
for ((key, value) in sandbox.env) {
args += listOf("-e", "$key=$value")
}
args += listOf("-w", "$workspaceAbs")
@@ -242,12 +242,13 @@ class InitCommand(
context: "." # Docker build context used with dockerfile
network: "" # Docker network mode for the build container; empty = Docker default (pinned)
env: {} # additional environment variables set inside the build container
# bubblewrap user-namespace sandbox — for hosts without root and without a
# Docker daemon (e.g. Hostsharing managed webspaces). Mutually exclusive with docker.
bwrap:
enabled: false # run clean/build in a bwrap sandbox instead of natively (pinned)
# werkdock sandbox (bubblewrap user namespace) — for hosts without root and
# without a Docker daemon (e.g. Hostsharing managed webspaces). Mutually
# exclusive with docker. Called bwrap before v1.2.0, still read under that name.
werkdock:
enabled: false # run clean/build in the sandbox instead of natively (pinned)
rootfs: "" # prepared rootfs archive (path or URL); required when enabled (pinned)
werkdock: werkdock # the werkdock CLI executing the sandbox; default resolves via PATH (pinned)
binary: werkdock # the werkdock CLI executing the sandbox; default resolves via PATH (pinned)
env: {} # additional environment variables set inside the sandbox
# Gitea check this build reports as; empty uses gitea.statusContext.
# Two builds of one commit under the same context overwrite each other.
@@ -42,8 +42,8 @@ data class BuildDefinition(
val statusContext: String? = null,
/** Overrides of the docker settings; null inherits them. */
val docker: DockerOverrides? = null,
/** Overrides of the bwrap settings; null inherits them. */
val bwrap: BwrapOverrides? = null,
/** Overrides of the werkdock settings; null inherits them. */
val werkdock: WerkdockOverrides? = null,
) {
/** The settings this build runs with: [branchConfig] with this definition applied; unset values fall through. */
fun applyTo(branchConfig: BranchConfig): BranchConfig =
@@ -64,12 +64,12 @@ data class BuildDefinition(
network = docker?.network ?: branchConfig.docker.network,
env = docker?.env ?: branchConfig.docker.env,
),
bwrap =
branchConfig.bwrap.copy(
enabled = bwrap?.enabled ?: branchConfig.bwrap.enabled,
rootfs = bwrap?.rootfs ?: branchConfig.bwrap.rootfs,
werkdock = bwrap?.werkdock ?: branchConfig.bwrap.werkdock,
env = bwrap?.env ?: branchConfig.bwrap.env,
werkdock =
branchConfig.werkdock.copy(
enabled = werkdock?.enabled ?: branchConfig.werkdock.enabled,
rootfs = werkdock?.rootfs ?: branchConfig.werkdock.rootfs,
binary = werkdock?.binary ?: branchConfig.werkdock.binary,
env = werkdock?.env ?: branchConfig.werkdock.env,
),
)
@@ -169,13 +169,13 @@ data class DockerOverrides(
val env: Map<String, String>? = null,
)
/** Nullable bubblewrap overrides of a [BuildDefinition]; null values inherit the branch's setting. */
data class BwrapOverrides(
/** Run the build in the bwrap sandbox instead of natively. Pinned — a branch must not escape its sandbox. */
/** Nullable werkdock overrides of a [BuildDefinition]; null values inherit the branch's setting. */
data class WerkdockOverrides(
/** Run the build in the sandbox instead of natively. Pinned — a branch must not escape its sandbox. */
val enabled: Boolean? = null,
/** Rootfs archive source. Pinned — a branch must not substitute a foreign rootfs. */
val rootfs: String? = null,
/** The werkdock CLI executing the sandbox. Pinned — a branch must not substitute the executing binary. */
val werkdock: String? = null,
val binary: String? = null,
val env: Map<String, String>? = null,
)
@@ -201,10 +201,10 @@ class ConfigLoader(
val strippedDocker = docker.toMutableMap().apply { PINNED_DOCKER_KEYS.forEach { remove(it) } }
if (strippedDocker.isEmpty()) result.remove("docker") else result["docker"] = strippedDocker
}
val bwrap = entry["bwrap"] as? Map<String, Any?>
if (bwrap != null) {
val strippedBwrap = bwrap.toMutableMap().apply { PINNED_BWRAP_KEYS.forEach { remove(it) } }
if (strippedBwrap.isEmpty()) result.remove("bwrap") else result["bwrap"] = strippedBwrap
val werkdock = entry["werkdock"] as? Map<String, Any?>
if (werkdock != null) {
val strippedWerkdock = werkdock.toMutableMap().apply { PINNED_WERKDOCK_KEYS.forEach { remove(it) } }
if (strippedWerkdock.isEmpty()) result.remove("werkdock") else result["werkdock"] = strippedWerkdock
}
return result
}
@@ -481,14 +481,62 @@ class ConfigLoader(
private fun loadFile(file: File): Map<String, Any?> {
if (!file.exists()) return emptyMap()
@Suppress("UNCHECKED_CAST")
return yaml.readValue(file, Map::class.java) as Map<String, Any?>
return renameLegacySandbox(yaml.readValue(file, Map::class.java) as Map<String, Any?>, file.toString())
}
/** Parses a `.werkator.yml` read from git (not from disk); blank or null yields no layer. */
private fun parseYaml(text: String?): Map<String, Any?> {
if (text.isNullOrBlank()) return emptyMap()
@Suppress("UNCHECKED_CAST")
return yaml.readValue(text, Map::class.java) as? Map<String, Any?> ?: emptyMap()
val raw = yaml.readValue(text, Map::class.java) as? Map<String, Any?> ?: emptyMap()
return renameLegacySandbox(raw, "the branch configuration")
}
/**
* Reads the pre-PR#19 `bwrap` section under its new name `werkdock`, including its
* `werkdock` key which is `binary` now. Done on the raw map of every layer, before
* merging, so nothing downstream — merging, pinning, binding — knows two names.
*
* Renaming rather than rejecting: the section is written in the machine configuration
* of every webspace instance, which no repository tracks. The warning is what makes
* the old name go away; 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.
*/
@Suppress("UNCHECKED_CAST")
private fun renameLegacySandbox(
raw: Map<String, Any?>,
source: String,
): Map<String, Any?> {
var renamed = false
val result =
raw.mapValues { (section, value) ->
if (section != "builds" && section != "branches") {
return@mapValues value
}
val entries = value as? Map<String, Any?> ?: return@mapValues value
entries.mapValues inner@{ (_, entry) ->
val settings = entry as? Map<String, Any?> ?: return@inner entry
val legacy = settings["bwrap"] as? Map<String, Any?> ?: return@inner entry
renamed = true
val moved =
legacy.mapKeys { (key, _) -> if (key == "werkdock") "binary" else key }
val existing = settings["werkdock"] as? Map<String, Any?> ?: emptyMap()
settings.toMutableMap().apply {
remove("bwrap")
// an explicit werkdock section wins: the new name is the one meant
put("werkdock", moved + existing)
}
}
}
if (renamed && warnedSections.add("bwrap-renamed:$source")) {
log.warn(
"reading the 'bwrap' section of {} as 'werkdock' (and 'bwrap.werkdock' as 'werkdock.binary'); " +
"rename it — the old name goes away with the next breaking configuration change",
source,
)
}
return result
}
@Suppress("UNCHECKED_CAST")
@@ -550,8 +598,8 @@ class ConfigLoader(
/** `docker` keys a branch must never override: the sandbox policy. */
private val PINNED_DOCKER_KEYS = setOf("enabled", "network")
/** `bwrap` keys a branch must never override: the sandbox policy (Step 17) and its executing binary. */
private val PINNED_BWRAP_KEYS = setOf("enabled", "rootfs", "werkdock")
/** `werkdock` keys a branch must never override: the sandbox policy (Step 17) and its executing binary. */
private val PINNED_WERKDOCK_KEYS = setOf("enabled", "rootfs", "binary")
/**
* The one key of a build definition that says *when* and *for which branches* it
@@ -38,9 +38,9 @@ data class WerkatorConfig(
): BranchConfig {
val branchConfig = branches[branch] ?: branches["default"] ?: BranchConfig()
val settings = effectiveBuildDefinitions()[build]?.applyTo(branchConfig) ?: branchConfig
if (settings.docker.enabled && settings.bwrap.enabled) {
if (settings.docker.enabled && settings.werkdock.enabled) {
throw IllegalArgumentException(
"builds.$build on '$branch' enables both docker and bwrap; a build runs in exactly one sandbox. " +
"builds.$build on '$branch' enables both docker and werkdock; a build runs in exactly one sandbox. " +
"Disable one of them.",
)
}
@@ -179,17 +179,22 @@ data class BranchConfig(
val statusContext: String = "",
val autoBuild: AutoBuildConfig = AutoBuildConfig(),
val docker: DockerConfig = DockerConfig(),
/** bubblewrap user-namespace sandbox; mutually exclusive with [docker]. */
val bwrap: BwrapConfig = BwrapConfig(),
/** werkdock sandbox (bubblewrap user namespace); mutually exclusive with [docker]. */
val werkdock: WerkdockConfig = WerkdockConfig(),
)
/**
* bubblewrap build sandbox (Step 17): runs the build in an unprivileged user namespace
* with a prepared Debian root filesystem. For hosts without root and without a Docker
* daemon (e.g. Hostsharing managed webspaces); see `docs/plan/17-bwrap-build-runtime.md`.
* The werkdock build sandbox (Step 17, executed by the werkdock CLI since step 21):
* runs the build in an unprivileged bubblewrap user namespace over a prepared Debian
* root filesystem. For hosts without root and without a Docker daemon (e.g. Hostsharing
* managed webspaces); see `docs/plan/17-bwrap-build-runtime.md`.
*
* The section was called `bwrap` until PR#19 and is still read under that name, with a
* warning: `bwrap` named the mechanism one layer below the tool that actually runs it,
* which made `bwrap.werkdock` the key naming its own executor.
*/
data class BwrapConfig(
/** Run the clean and build commands in a bwrap sandbox instead of natively. */
data class WerkdockConfig(
/** Run the clean and build commands in the sandbox instead of natively. */
val enabled: Boolean = false,
/**
* Path or URL of the prepared rootfs archive (e.g. `werkator-buildenv-trixie-java21.tar.zst`),
@@ -200,8 +205,9 @@ data class BwrapConfig(
/**
* The werkdock CLI executing the sandbox (step 21 session C); empty or the default
* resolves via PATH. Pinned — a branch must not substitute the executing binary.
* Was `bwrap.werkdock` until PR#19.
*/
val werkdock: String = "werkdock",
val binary: String = "werkdock",
/** Additional environment variables set inside the sandbox. */
val env: Map<String, String> = emptyMap(),
)
@@ -7,6 +7,16 @@
<div th:replace="~{fragments :: nav(${view})}"></div>
<div class="panel release-notes">
<h2>v1.2.0 <span class="muted">— 2026-09-03</span></h2>
<ul>
<li>The build sandbox for hosts without Docker is configured as <code>werkdock</code> now,
not <code>bwrap</code> (PR#19), and its <code>bwrap.werkdock</code> key — which named its
own executor — is <code>werkdock.binary</code>. The old section is still read, with a
warning naming the file, so no installation has to be changed before its next
configuration edit. <code>bwrap</code> named the mechanism one layer below the tool that
actually runs it: builds have been executed by the werkdock CLI since v1.0.0.</li>
</ul>
<h2>v1.1.2 <span class="muted">— 2026-09-03</span></h2>
<ul>
<li>On a Hostsharing Managed Webspace, an <code>instance-update</code> restart no longer looks
@@ -1,8 +1,8 @@
package de.hoennig.werkator.build
import de.hoennig.werkator.config.BranchConfig
import de.hoennig.werkator.config.BwrapConfig
import de.hoennig.werkator.config.DockerConfig
import de.hoennig.werkator.config.WerkdockConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.Called
@@ -15,13 +15,13 @@ import java.nio.file.Paths
class DispatchingBuildRunnerTest : FunSpec() {
private val processBuildRunner = mockk<ProcessBuildRunner>()
private val dockerBuildRunner = mockk<DockerBuildRunner>()
private val bwrapBuildRunner = mockk<BwrapBuildRunner>()
private val dispatcher = DispatchingBuildRunner(processBuildRunner, dockerBuildRunner, bwrapBuildRunner)
private val werkdockBuildRunner = mockk<WerkdockBuildRunner>()
private val dispatcher = DispatchingBuildRunner(processBuildRunner, dockerBuildRunner, werkdockBuildRunner)
private val process = mockk<Process>()
private val dir = Paths.get(".")
init {
beforeEach { clearMocks(processBuildRunner, dockerBuildRunner, bwrapBuildRunner) }
beforeEach { clearMocks(processBuildRunner, dockerBuildRunner, werkdockBuildRunner) }
test("runs natively by default") {
val branchConfig = BranchConfig()
@@ -30,7 +30,7 @@ class DispatchingBuildRunnerTest : FunSpec() {
dispatcher.start("cmd", dir, emptyMap(), dir, branchConfig) shouldBe process
verify { dockerBuildRunner wasNot Called }
verify { bwrapBuildRunner wasNot Called }
verify { werkdockBuildRunner wasNot Called }
}
test("runs in Docker when the branch enables it") {
@@ -40,12 +40,12 @@ class DispatchingBuildRunnerTest : FunSpec() {
dispatcher.start("cmd", dir, emptyMap(), dir, branchConfig) shouldBe process
verify { processBuildRunner wasNot Called }
verify { bwrapBuildRunner wasNot Called }
verify { werkdockBuildRunner wasNot Called }
}
test("runs in bwrap when the branch enables it (and not Docker)") {
val branchConfig = BranchConfig(bwrap = BwrapConfig(enabled = true, rootfs = "/srv/buildenv.tar.zst"))
every { bwrapBuildRunner.start("cmd", dir, emptyMap(), dir, branchConfig) } returns process
test("runs in the werkdock sandbox when the branch enables it (and not Docker)") {
val branchConfig = BranchConfig(werkdock = WerkdockConfig(enabled = true, rootfs = "/srv/buildenv.tar.zst"))
every { werkdockBuildRunner.start("cmd", dir, emptyMap(), dir, branchConfig) } returns process
dispatcher.start("cmd", dir, emptyMap(), dir, branchConfig) shouldBe process
@@ -1,7 +1,7 @@
package de.hoennig.werkator.build
import de.hoennig.werkator.config.BranchConfig
import de.hoennig.werkator.config.BwrapConfig
import de.hoennig.werkator.config.WerkdockConfig
import de.hoennig.werkator.git.GitCommandResult
import de.hoennig.werkator.git.GitCommandRunner
import io.kotest.assertions.throwables.shouldThrow
@@ -15,20 +15,20 @@ import io.mockk.verify
import java.nio.file.Files
import java.nio.file.Path
class BwrapBuildRunnerTest : FunSpec() {
class WerkdockBuildRunnerTest : FunSpec() {
private val commandRunner = mockk<GitCommandRunner>()
private lateinit var runner: BwrapBuildRunner
private lateinit var runner: WerkdockBuildRunner
private lateinit var repoDir: Path
private lateinit var workspace: Path
private val captured = mutableListOf<List<String>>()
private fun bwrapBranchConfig(
private fun werkdockBranchConfig(
rootfs: String = "/srv/buildenv.tar.zst",
env: Map<String, String> = emptyMap(),
): BranchConfig =
BranchConfig(
bwrap =
BwrapConfig(
werkdock =
WerkdockConfig(
enabled = true,
rootfs = rootfs,
env = env,
@@ -52,9 +52,9 @@ class BwrapBuildRunnerTest : FunSpec() {
beforeEach {
clearMocks(commandRunner)
captured.clear()
repoDir = Files.createTempDirectory("werkator-bwrap-runner")
repoDir = Files.createTempDirectory("werkator-werkdock-runner")
workspace = repoDir.resolve("workspace")
runner = BwrapBuildRunner(commandRunner)
runner = WerkdockBuildRunner(commandRunner)
runner.processStarter = { command, _ ->
captured += command
ProcessBuilder("true").start()
@@ -64,7 +64,7 @@ class BwrapBuildRunnerTest : FunSpec() {
test("assembles the exact werkdock run command for a loaded image") {
givenImageLoaded()
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, bwrapBranchConfig())
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, werkdockBranchConfig())
captured.single() shouldBe
listOf(
@@ -99,7 +99,7 @@ class BwrapBuildRunnerTest : FunSpec() {
)
} returns GitCommandResult(0, "", "")
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, bwrapBranchConfig())
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, werkdockBranchConfig())
verify {
commandRunner.runOrThrow(
@@ -114,7 +114,7 @@ class BwrapBuildRunnerTest : FunSpec() {
test("does not load an image werkdock already has") {
givenImageLoaded()
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, bwrapBranchConfig())
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, werkdockBranchConfig())
verify(exactly = 0) { commandRunner.runOrThrow(match { "load" in it }, any(), any(), any()) }
}
@@ -124,7 +124,7 @@ class BwrapBuildRunnerTest : FunSpec() {
GitCommandResult(0, imageName() + "\n", "")
val branchConfig =
BranchConfig(
bwrap = BwrapConfig(enabled = true, rootfs = "/srv/buildenv.tar.zst", werkdock = "/opt/bin/werkdock"),
werkdock = WerkdockConfig(enabled = true, rootfs = "/srv/buildenv.tar.zst", binary = "/opt/bin/werkdock"),
)
runner.start("./gradlew test", workspace, emptyMap(), repoDir, branchConfig)
@@ -136,7 +136,7 @@ class BwrapBuildRunnerTest : FunSpec() {
givenImageLoaded()
val relativeWorkspace = repoDir.relativize(workspace)
runner.start("./gradlew test", relativeWorkspace, mapOf("branch" to "main"), repoDir, bwrapBranchConfig())
runner.start("./gradlew test", relativeWorkspace, mapOf("branch" to "main"), repoDir, werkdockBranchConfig())
val args = captured.single()
val absolute = workspace.toAbsolutePath().normalize().toString()
@@ -145,7 +145,7 @@ class BwrapBuildRunnerTest : FunSpec() {
args.count { it == "$absolute:$absolute" } shouldBe 1
}
test("adds bwrap env after the branch environment") {
test("adds the sandbox env after the branch environment") {
givenImageLoaded()
runner.start(
@@ -153,7 +153,7 @@ class BwrapBuildRunnerTest : FunSpec() {
workspace,
mapOf("branch" to "main"),
repoDir,
bwrapBranchConfig(env = mapOf("FOO" to "bar")),
werkdockBranchConfig(env = mapOf("FOO" to "bar")),
)
val args = captured.single()
@@ -171,7 +171,7 @@ class BwrapBuildRunnerTest : FunSpec() {
Files.writeString(workspace.resolve(".git"), "gitdir: $adminDir\n")
givenImageLoaded()
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, bwrapBranchConfig())
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, werkdockBranchConfig())
val args = captured.single()
args[args.indexOf("$gitDir:$gitDir:ro") - 1] shouldBe "-v"
@@ -190,7 +190,7 @@ class BwrapBuildRunnerTest : FunSpec() {
givenImageLoaded()
Files.createDirectories(workspace)
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, bwrapBranchConfig())
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, werkdockBranchConfig())
val args = captured.single()
val gitDir = repoDir.resolve(".git")
@@ -199,21 +199,21 @@ class BwrapBuildRunnerTest : FunSpec() {
}
test("fails without a configured rootfs") {
val branchConfig = BranchConfig(bwrap = BwrapConfig(enabled = true))
val branchConfig = BranchConfig(werkdock = WerkdockConfig(enabled = true))
val exception =
shouldThrow<IllegalArgumentException> {
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, branchConfig)
}
exception.message shouldContain "bwrap.rootfs"
exception.message shouldContain "werkdock.rootfs"
}
test("downloads a URL rootfs once before loading it") {
val url = "https://example.test/buildenv.tar.zst"
val downloadTarget =
repoDir
.resolve(BwrapBuildRunner.BUILDENV_DIR)
.resolve(WerkdockBuildRunner.BUILDENV_DIR)
.resolve(url.sha12())
.resolve("buildenv.tar.zst")
givenImageMissing()
@@ -222,7 +222,7 @@ class BwrapBuildRunnerTest : FunSpec() {
every { commandRunner.runOrThrow(match { "load" in it }, any(), any(), any()) } returns
GitCommandResult(0, "", "")
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, bwrapBranchConfig(rootfs = url))
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, werkdockBranchConfig(rootfs = url))
verify {
commandRunner.runOrThrow(listOf("curl", "-fsSL", "-o", downloadTarget.toString(), url), repoDir, any(), any())
@@ -496,7 +496,7 @@ class ConfigLoaderTest : FunSpec() {
settings.docker.image shouldBe "attacker-image"
}
test("a branch cannot disable its bwrap sandbox or substitute a foreign rootfs through a build definition") {
test("the legacy bwrap section is read as werkdock, its werkdock key as binary") {
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
@@ -505,6 +505,58 @@ class ConfigLoaderTest : FunSpec() {
bwrap:
enabled: true
rootfs: /host/rootfs.tar.zst
werkdock: /opt/bin/werkdock
env:
FOO: bar
""".trimIndent(),
)
val settings = loader.load(dir).buildSettings("any-branch", "default")
settings.werkdock.enabled shouldBe true
settings.werkdock.rootfs shouldBe "/host/rootfs.tar.zst"
settings.werkdock.binary shouldBe "/opt/bin/werkdock"
settings.werkdock.env shouldBe mapOf("FOO" to "bar")
}
test("a legacy bwrap section on a branch is pinned exactly like the new name") {
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
werkdock:
enabled: true
rootfs: /host/rootfs.tar.zst
""".trimIndent(),
)
val worktree = Files.createTempDirectory("werkator-test-worktree")
// the old name must not become a way around the pinning
worktree.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
bwrap:
enabled: false
rootfs: /attacker/rootfs.tar.zst
""".trimIndent(),
)
val settings = loader.loadForWorktree(dir, worktree).buildSettings("any-branch", "default")
settings.werkdock.enabled shouldBe true
settings.werkdock.rootfs shouldBe "/host/rootfs.tar.zst"
}
test("a branch cannot disable its werkdock sandbox or substitute a foreign rootfs through a build definition") {
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
werkdock:
enabled: true
rootfs: /host/rootfs.tar.zst
""".trimIndent(),
)
val worktree = Files.createTempDirectory("werkator-test-worktree")
@@ -512,7 +564,7 @@ class ConfigLoaderTest : FunSpec() {
"""
builds:
default:
bwrap:
werkdock:
enabled: false
rootfs: /attacker/rootfs.tar.zst
env:
@@ -523,10 +575,10 @@ class ConfigLoaderTest : FunSpec() {
val settings = loader.loadForWorktree(dir, worktree).buildSettings("any-branch", "default")
// pinned: the sandbox can neither be switched off nor pointed at a foreign rootfs
settings.bwrap.enabled shouldBe true
settings.bwrap.rootfs shouldBe "/host/rootfs.tar.zst"
settings.werkdock.enabled shouldBe true
settings.werkdock.rootfs shouldBe "/host/rootfs.tar.zst"
// everything that describes the build itself stays the branch's own business
settings.bwrap.env shouldBe mapOf("FOO" to "from-branch")
settings.werkdock.env shouldBe mapOf("FOO" to "from-branch")
}
test("a build the branch invents inherits the host's sandbox policy") {
@@ -565,7 +617,7 @@ class ConfigLoaderTest : FunSpec() {
settings.requirePullRequest shouldBe true
}
test("enabling both docker and bwrap on a build is rejected, not picked silently") {
test("enabling both docker and werkdock on a build is rejected, not picked silently") {
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
@@ -574,7 +626,7 @@ class ConfigLoaderTest : FunSpec() {
docker:
enabled: true
image: build-env
bwrap:
werkdock:
enabled: true
rootfs: /srv/rootfs.tar.zst
""".trimIndent(),
@@ -586,7 +638,7 @@ class ConfigLoaderTest : FunSpec() {
config.buildSettings("any-branch", "default")
}
exception.message shouldContain "both docker and bwrap"
exception.message shouldContain "both docker and werkdock"
exception.message shouldContain "builds.default"
}