Merge main into the deflake-maxconcurrent-test branch
This commit is contained in:
@@ -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)
|
||||
|
||||
+17
-17
@@ -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
|
||||
|
||||
|
||||
+21
-21
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user