Step 23 session A: init --apply, the applied instance layer, control-token
'werkator init --apply FILE' installs a config-schema YAML fragment as its own layer: validated strictly (an unknown key is refused loudly, never ignored — a typo must not install a silent no-op), then copied verbatim to .git/werkator/.werkator.applied.yml, above the project config and below the hand-edited machine config, which always wins. Deviation from the plan sketch, recorded there: a separate layer instead of an in-place merge, because merging would re-serialize the machine config — destroying its comments and rewriting the file that holds the secrets; re-apply is a plain file replacement. init --systemd now also generates werkator.htaccess beside the units whenever a publicBaseUrl is configured — generated host integration for the managed-webspace Apache, copied into the docroot by the wrapper. New subcommand 'werkator control-token' prints (and lazily creates) the token via ControlTokenService, so no wrapper needs its own generator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7a24ad1d7f
commit
5f1a669771
@@ -0,0 +1,37 @@
|
||||
package de.hoennig.werkator.commands
|
||||
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldMatch
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import java.nio.file.Files
|
||||
|
||||
class ControlTokenCommandTest : FunSpec() {
|
||||
private val gitService = mockk<GitService>()
|
||||
private val command = ControlTokenCommand(gitService)
|
||||
|
||||
init {
|
||||
test("creates the token like the server would and prints the same one on a re-run") {
|
||||
val tempDir = Files.createTempDirectory("werkator-token-test")
|
||||
command.workingDir = tempDir
|
||||
every { gitService.getTopLevel(any()) } returns tempDir
|
||||
|
||||
command.call() shouldBe 0
|
||||
|
||||
val tokenFile = tempDir.resolve(".git/werkator/control-token")
|
||||
val token = tokenFile.toFile().readText().trim()
|
||||
token shouldMatch Regex("[0-9a-f]{48}")
|
||||
|
||||
command.call() shouldBe 0
|
||||
tokenFile.toFile().readText().trim() shouldBe token
|
||||
}
|
||||
|
||||
test("fails with exit code 2 outside a repository") {
|
||||
every { gitService.getTopLevel(any()) } throws IllegalStateException("not a git repository")
|
||||
|
||||
command.call() shouldBe 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,10 @@ class InitCommandTest : FunSpec() {
|
||||
private val initCommand =
|
||||
InitCommand(
|
||||
gitService,
|
||||
// default (null) BuildProperties provider: a relaxed ObjectProvider mock
|
||||
// returns a raw Object under type erasure and breaks the version check
|
||||
de.hoennig.werkator.config
|
||||
.ConfigLoader(mockk(relaxed = true)),
|
||||
.ConfigLoader(),
|
||||
)
|
||||
|
||||
init {
|
||||
@@ -122,6 +124,47 @@ class InitCommandTest : FunSpec() {
|
||||
projectConfig.toFile().readText() shouldBe "existing: content"
|
||||
}
|
||||
|
||||
test("--apply installs the fragment as the applied layer and the effective config sees it") {
|
||||
val tempDir = Files.createTempDirectory("werkator-init-test")
|
||||
initCommand.workingDir = tempDir
|
||||
val fragment = tempDir.resolve("mih34.yml")
|
||||
fragment.toFile().writeText("server:\n port: 18088\n")
|
||||
initCommand.apply = fragment
|
||||
|
||||
every { gitService.getTopLevel(tempDir) } returns tempDir
|
||||
every { gitService.getOriginUrl(tempDir) } returns "https://git.example.org/my-org/my-repo.git"
|
||||
|
||||
initCommand.run()
|
||||
|
||||
tempDir
|
||||
.resolve(de.hoennig.werkator.config.ConfigFiles.APPLIED)
|
||||
.toFile()
|
||||
.shouldExist()
|
||||
de.hoennig.werkator.config
|
||||
.ConfigLoader()
|
||||
.load(tempDir)
|
||||
.server.port shouldBe 18088
|
||||
initCommand.apply = null
|
||||
}
|
||||
|
||||
test("--apply with an invalid fragment installs nothing") {
|
||||
val tempDir = Files.createTempDirectory("werkator-init-test")
|
||||
initCommand.workingDir = tempDir
|
||||
val fragment = tempDir.resolve("typo.yml")
|
||||
fragment.toFile().writeText("server:\n prot: 18088\n")
|
||||
initCommand.apply = fragment
|
||||
|
||||
every { gitService.getTopLevel(tempDir) } returns tempDir
|
||||
every { gitService.getOriginUrl(tempDir) } returns "https://git.example.org/my-org/my-repo.git"
|
||||
|
||||
initCommand.run()
|
||||
|
||||
Files
|
||||
.exists(tempDir.resolve(de.hoennig.werkator.config.ConfigFiles.APPLIED))
|
||||
.shouldBeFalse()
|
||||
initCommand.apply = null
|
||||
}
|
||||
|
||||
test("--systemd generates unit and environment file with install instructions") {
|
||||
val tempDir = Files.createTempDirectory("werkator-init-test")
|
||||
initCommand.workingDir = tempDir
|
||||
|
||||
@@ -91,5 +91,11 @@ class SystemdServiceFilesTest : FunSpec() {
|
||||
content shouldContain "#JAVA_OPTS="
|
||||
content shouldContain ".werkator.yml"
|
||||
}
|
||||
test("the htaccess proxies everything to the configured localhost port") {
|
||||
val content = SystemdServiceFiles.htaccessContent(18088)
|
||||
|
||||
content shouldContain "DirectoryIndex disabled"
|
||||
content shouldContain "RewriteRule .* http://127.0.0.1:18088%{REQUEST_URI} [proxy]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,63 @@ class ConfigLoaderTest : FunSpec() {
|
||||
"./gradlew fromBranch"
|
||||
}
|
||||
|
||||
test("the applied instance fragment layers above the project config and below the machine config") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
dir.resolve(".werkator.yml").toFile().writeText("server:\n port: 1000\n publicBaseUrl: \"https://project/\"\n")
|
||||
Files.createDirectories(dir.resolve(".git/werkator"))
|
||||
dir.resolve(ConfigFiles.APPLIED).toFile().writeText("server:\n port: 2000\n bindAddress: 0.0.0.0\n")
|
||||
dir
|
||||
.resolve(".git/werkator/.werkator.yml")
|
||||
.toFile()
|
||||
.writeText("server:\n port: 3000\n")
|
||||
|
||||
val server = loader.load(dir).server
|
||||
|
||||
// machine wins over applied wins over project; untouched keys fall through
|
||||
server.port shouldBe 3000
|
||||
server.bindAddress shouldBe "0.0.0.0"
|
||||
server.publicBaseUrl shouldBe "https://project/"
|
||||
}
|
||||
|
||||
test("applyInstanceFragment installs a valid fragment verbatim, and re-applying replaces it") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
val fragment = dir.resolve("mih34.yml")
|
||||
fragment.toFile().writeText("# instance mih34\nserver:\n port: 18088\n")
|
||||
|
||||
val target = loader.applyInstanceFragment(dir, fragment)
|
||||
|
||||
target shouldBe dir.resolve(ConfigFiles.APPLIED)
|
||||
// verbatim copy: the comment survives
|
||||
target.toFile().readText() shouldContain "# instance mih34"
|
||||
loader.load(dir).server.port shouldBe 18088
|
||||
|
||||
fragment.toFile().writeText("server:\n port: 19099\n")
|
||||
loader.applyInstanceFragment(dir, fragment)
|
||||
loader.load(dir).server.port shouldBe 19099
|
||||
}
|
||||
|
||||
test("applyInstanceFragment refuses an unknown key loudly instead of installing a silent no-op") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
val fragment = dir.resolve("typo.yml")
|
||||
fragment.toFile().writeText("server:\n prot: 18088\n")
|
||||
|
||||
val exception =
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
loader.applyInstanceFragment(dir, fragment)
|
||||
}
|
||||
|
||||
exception.message shouldContain "typo.yml"
|
||||
Files.exists(dir.resolve(ConfigFiles.APPLIED)).shouldBeFalse()
|
||||
}
|
||||
|
||||
test("applyInstanceFragment refuses a missing or empty fragment") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
loader.applyInstanceFragment(dir, dir.resolve("absent.yml"))
|
||||
}
|
||||
}
|
||||
|
||||
test("reads executor.maxConcurrent and defaults it to 1") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
loader.load(dir).executor.maxConcurrent shouldBe 1
|
||||
|
||||
Reference in New Issue
Block a user