From 992a5a922df0824c06e1595d702f0ab4019dc436 Mon Sep 17 00:00:00 2001 From: Josha von Gizycki Date: Thu, 14 Aug 2025 00:21:22 +0200 Subject: [PATCH] major moving around, rework of script triggering and running --- src/main/kotlin/alfred/web/WebApplication.kt | 2 +- src/main/kotlin/alfred/web/core/Handles.kt | 1 + src/main/kotlin/alfred/web/core/Processes.kt | 4 +- .../kotlin/alfred/web/core/ScriptRunner.kt | 46 ------------- .../alfred/web/core/build/BuildContext.kt | 8 +++ .../alfred/web/core/{ => build}/Builds.kt | 35 ++++++---- .../alfred/web/core/{ => build}/LogFile.kt | 14 ++-- .../alfred/web/core/build/ProcessInfo.kt | 6 ++ .../alfred/web/core/{ => build}/Workspaces.kt | 28 +++++--- .../alfred/web/core/{ => runner}/GitRunner.kt | 24 ++++--- .../alfred/web/core/runner/ScriptRunner.kt | 61 +++++++++++++++++ .../kotlin/alfred/web/cron/CronTrigger.kt | 18 +++++ src/main/kotlin/alfred/web/http/BuildsInfo.kt | 45 ++++++++++++ .../web/http/{Endpoints.kt => HttpTrigger.kt} | 68 ++++++++++--------- src/main/kotlin/alfred/web/http/Security.kt | 6 +- 15 files changed, 247 insertions(+), 119 deletions(-) delete mode 100644 src/main/kotlin/alfred/web/core/ScriptRunner.kt create mode 100644 src/main/kotlin/alfred/web/core/build/BuildContext.kt rename src/main/kotlin/alfred/web/core/{ => build}/Builds.kt (72%) rename src/main/kotlin/alfred/web/core/{ => build}/LogFile.kt (60%) create mode 100644 src/main/kotlin/alfred/web/core/build/ProcessInfo.kt rename src/main/kotlin/alfred/web/core/{ => build}/Workspaces.kt (71%) rename src/main/kotlin/alfred/web/core/{ => runner}/GitRunner.kt (88%) create mode 100644 src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt create mode 100644 src/main/kotlin/alfred/web/cron/CronTrigger.kt create mode 100644 src/main/kotlin/alfred/web/http/BuildsInfo.kt rename src/main/kotlin/alfred/web/http/{Endpoints.kt => HttpTrigger.kt} (52%) diff --git a/src/main/kotlin/alfred/web/WebApplication.kt b/src/main/kotlin/alfred/web/WebApplication.kt index 7c46dba..d5c276e 100644 --- a/src/main/kotlin/alfred/web/WebApplication.kt +++ b/src/main/kotlin/alfred/web/WebApplication.kt @@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class WebApplication +open class WebApplication fun main(args: Array) { runApplication(*args) diff --git a/src/main/kotlin/alfred/web/core/Handles.kt b/src/main/kotlin/alfred/web/core/Handles.kt index 44b6da7..96c3a7a 100644 --- a/src/main/kotlin/alfred/web/core/Handles.kt +++ b/src/main/kotlin/alfred/web/core/Handles.kt @@ -1,5 +1,6 @@ package alfred.web.core +import alfred.web.core.build.BuildId import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import java.time.Instant diff --git a/src/main/kotlin/alfred/web/core/Processes.kt b/src/main/kotlin/alfred/web/core/Processes.kt index 29b5741..6923792 100644 --- a/src/main/kotlin/alfred/web/core/Processes.kt +++ b/src/main/kotlin/alfred/web/core/Processes.kt @@ -1,11 +1,13 @@ package alfred.web.core +import alfred.web.core.build.BuildConfig +import alfred.web.core.build.LogFile import org.springframework.stereotype.Service @Service class Processes { - fun builder(config: BuildConfig, logFile: LogFile, rev: String): ProcessBuilder = + fun builder(config: BuildConfig, logFile: LogFile, rev: String?): ProcessBuilder = ProcessBuilder() .redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.backingFile)) .redirectError(ProcessBuilder.Redirect.appendTo(logFile.backingFile)) diff --git a/src/main/kotlin/alfred/web/core/ScriptRunner.kt b/src/main/kotlin/alfred/web/core/ScriptRunner.kt deleted file mode 100644 index 727876b..0000000 --- a/src/main/kotlin/alfred/web/core/ScriptRunner.kt +++ /dev/null @@ -1,46 +0,0 @@ -package alfred.web.core - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Service -import java.io.File -import java.nio.file.Paths - -@Service -class ScriptRunner( - val builds: Builds, - val handles: Handles, - val processes: Processes -) { - - fun run(buildId: BuildId, rev: String): ProcessInfo { - val config = builds.buildConfig(buildId) - val logFile = builds.createLogFile(buildId) - - logger.info("preparing process for build $buildId with config $config") - logger.info("log file: $logFile") - - logFile.header(buildId, rev, Paths.get(config.gitWorkspace)) - - val process = processes.builder(config, logFile, rev) - .command(config.script) - .start() - handles.add(Handle(process.toHandle(), buildId)) - process.onExit().whenComplete { _, _ -> logFile.footer() } - - val pid = process.pid() - - logger.info("pid for build $buildId is: $pid") - - return ProcessInfo(pid = pid, logFile = logFile) - } - - val logger: Logger = LoggerFactory.getLogger(this::class.java) - -} - -data class ProcessInfo( - val pid: Long, - val logFile: LogFile -) - diff --git a/src/main/kotlin/alfred/web/core/build/BuildContext.kt b/src/main/kotlin/alfred/web/core/build/BuildContext.kt new file mode 100644 index 0000000..bc4cdb9 --- /dev/null +++ b/src/main/kotlin/alfred/web/core/build/BuildContext.kt @@ -0,0 +1,8 @@ +package alfred.web.core.build + +data class BuildContext( + val config: BuildConfig, + val logFile: LogFile, + val rev: String, + val buildId: BuildId +) diff --git a/src/main/kotlin/alfred/web/core/Builds.kt b/src/main/kotlin/alfred/web/core/build/Builds.kt similarity index 72% rename from src/main/kotlin/alfred/web/core/Builds.kt rename to src/main/kotlin/alfred/web/core/build/Builds.kt index f909412..c36b155 100644 --- a/src/main/kotlin/alfred/web/core/Builds.kt +++ b/src/main/kotlin/alfred/web/core/build/Builds.kt @@ -1,4 +1,4 @@ -package alfred.web.core +package alfred.web.core.build import com.fasterxml.jackson.annotation.JsonIgnore import org.slf4j.Logger @@ -8,11 +8,17 @@ import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.web.bind.annotation.ResponseStatus import java.lang.RuntimeException -import java.nio.file.Files +import java.nio.file.Files.createFile import java.nio.file.Paths import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* +import kotlin.collections.associate +import kotlin.collections.filter +import kotlin.io.inputStream +import kotlin.jvm.java +import kotlin.text.toLong +import kotlin.text.uppercase @Service class Builds( @@ -42,15 +48,7 @@ class Builds( .associate { Pair(it.key.toString(), it.value.toString()) } // diverge into ScriptedBasedConfig and GitBasedConfig - return BuildConfig( - user = props.getProperty("user"), - gitWorkspace = props.getProperty("workspace") ?: "", - apikey = props.getProperty("apikey") ?: "", - script = props.getProperty("script"), - gitRepo = props.getProperty("git.repo.url"), - gitCloneTimeout = props.getProperty("git.close.timeout")?.toLong() ?: 30L, - env = env - ) + return props.toBuildConfig(env) } fun createLogFile(build: BuildId): LogFile { @@ -60,11 +58,22 @@ class Builds( val path = Paths.get(home, "logs", fileName) path.toFile().parentFile.mkdirs() - return LogFile(Files.createFile(path).toFile()) + return LogFile(createFile(path).toFile()) } } +private fun Properties.toBuildConfig(env: Map) = + BuildConfig( + user = getProperty("user"), + workspace = getProperty("workspace"), + apikey = getProperty("apikey") ?: "", + script = getProperty("script"), + gitRepo = getProperty("git.repo.url"), + gitCloneTimeout = getProperty("git.close.timeout")?.toLong() ?: 30L, + env = env + ) + @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Unknown Build Id") class UnknownBuild(val build: BuildId) : RuntimeException() { override fun toString() = @@ -78,7 +87,7 @@ data class BuildConfig( val script: String?, val gitRepo: String?, val gitCloneTimeout: Long, - val gitWorkspace: String, + val workspace: String, @field:JsonIgnore val env: Map ) diff --git a/src/main/kotlin/alfred/web/core/LogFile.kt b/src/main/kotlin/alfred/web/core/build/LogFile.kt similarity index 60% rename from src/main/kotlin/alfred/web/core/LogFile.kt rename to src/main/kotlin/alfred/web/core/build/LogFile.kt index 6a549b0..052b845 100644 --- a/src/main/kotlin/alfred/web/core/LogFile.kt +++ b/src/main/kotlin/alfred/web/core/build/LogFile.kt @@ -1,18 +1,22 @@ -package alfred.web.core +package alfred.web.core.build import java.io.File import java.nio.file.Path import java.time.ZonedDateTime +import kotlin.collections.joinToString import kotlin.io.appendText +import kotlin.text.isEmpty +import kotlin.text.lines +import kotlin.text.trim class LogFile( val backingFile: File ) { - fun header(buildId: BuildId, rev: String, workspace: Path) { + fun header(buildId: BuildId, rev: String?, workspace: Path) { append("At your service.") - append("Build $buildId, rev [$rev] started at ${ZonedDateTime.now()}") - append("Workspace directory is: $workspace\n") + append("Build $buildId, rev [${rev ?: "-none-"}] started at ${ZonedDateTime.now()}") + append("Workspace directory is: ${workspace.toFile().absolutePath}") } fun footer() { @@ -30,4 +34,4 @@ class LogFile( } + "\n" ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/alfred/web/core/build/ProcessInfo.kt b/src/main/kotlin/alfred/web/core/build/ProcessInfo.kt new file mode 100644 index 0000000..3a2e2be --- /dev/null +++ b/src/main/kotlin/alfred/web/core/build/ProcessInfo.kt @@ -0,0 +1,6 @@ +package alfred.web.core.build + +data class ProcessInfo( + val pid: Long, + val logFile: LogFile +) diff --git a/src/main/kotlin/alfred/web/core/Workspaces.kt b/src/main/kotlin/alfred/web/core/build/Workspaces.kt similarity index 71% rename from src/main/kotlin/alfred/web/core/Workspaces.kt rename to src/main/kotlin/alfred/web/core/build/Workspaces.kt index b02d36e..3892a82 100644 --- a/src/main/kotlin/alfred/web/core/Workspaces.kt +++ b/src/main/kotlin/alfred/web/core/build/Workspaces.kt @@ -1,22 +1,31 @@ -package alfred.web.core +package alfred.web.core.build import org.springframework.stereotype.Service import java.nio.file.Path import java.nio.file.Paths import java.util.UUID import kotlin.collections.isNotEmpty +import kotlin.io.deleteRecursively @Service class Workspaces { fun withWorkspace(ctx: BuildContext, block: (Path) -> Unit) { + val wsDir = prepare(ctx) + + try { + block(wsDir) + } finally { + wsDir.toFile().deleteRecursively() + } + } + + fun prepare(ctx: BuildContext): Path { val workspacePath = Paths.get( - ctx.config.gitWorkspace, UUID.randomUUID().toString() + ctx.config.workspace, UUID.randomUUID().toString() ) val workspaceDir = workspacePath.toFile() - ctx.logFile.header(ctx.buildId, ctx.rev, workspacePath) - if (!workspaceDir.exists()) { ctx.logFile.append("creating workspace ${workspaceDir.absolutePath}") workspaceDir.mkdirs() @@ -28,12 +37,11 @@ class Workspaces { throw WorkspaceIsNotEmpty(workspaceDir.toString()) } - try { - block(workspaceDir.toPath()) - } finally { - ctx.logFile.footer() - workspaceDir.deleteRecursively() - } + return workspacePath + } + + fun cleanUp(wsDir: Path) { + wsDir.toFile().deleteRecursively() } } diff --git a/src/main/kotlin/alfred/web/core/GitRunner.kt b/src/main/kotlin/alfred/web/core/runner/GitRunner.kt similarity index 88% rename from src/main/kotlin/alfred/web/core/GitRunner.kt rename to src/main/kotlin/alfred/web/core/runner/GitRunner.kt index c23f0b5..7c01133 100644 --- a/src/main/kotlin/alfred/web/core/GitRunner.kt +++ b/src/main/kotlin/alfred/web/core/runner/GitRunner.kt @@ -1,10 +1,20 @@ -package alfred.web.core - +package alfred.web.core.runner + +import alfred.web.core.Handle +import alfred.web.core.Handles +import alfred.web.core.Processes +import alfred.web.core.build.BuildContext +import alfred.web.core.build.BuildId +import alfred.web.core.build.Builds +import alfred.web.core.build.ProcessInfo +import alfred.web.core.build.Workspaces import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.nio.file.Path import java.util.concurrent.TimeUnit +import kotlin.collections.forEach +import kotlin.jvm.java @Service class GitRunner( @@ -32,8 +42,12 @@ class GitRunner( Thread { workspaces.withWorkspace(ctx) { wsPath -> + logFile.header(buildId, rev, wsPath) + clone(ctx, wsPath) execScripts(ctx, wsPath) + + logFile.footer() } }.start() @@ -97,12 +111,6 @@ class GitRunner( } -data class BuildContext( - val config: BuildConfig, - val logFile: LogFile, - val rev: String, - val buildId: BuildId -) class FailedToClone( private val buildId: BuildId, diff --git a/src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt b/src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt new file mode 100644 index 0000000..af5f7f0 --- /dev/null +++ b/src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt @@ -0,0 +1,61 @@ +package alfred.web.core.runner + +import alfred.web.core.Handle +import alfred.web.core.Handles +import alfred.web.core.Processes +import alfred.web.core.build.BuildContext +import alfred.web.core.build.BuildId +import alfred.web.core.build.Builds +import alfred.web.core.build.ProcessInfo +import alfred.web.core.build.Workspaces +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import kotlin.jvm.java + +@Service +class ScriptRunner( + val builds: Builds, + val handles: Handles, + val processes: Processes, + val workspaces: Workspaces +) { + + fun run(buildId: BuildId, rev: String?): ProcessInfo { + val config = builds.buildConfig(buildId) + val logFile = builds.createLogFile(buildId) + + logger.info("preparing process for build $buildId with config $config") + logger.info("log file: ${logFile.backingFile.absolutePath}") + + val ctx = BuildContext( + config = config, + logFile = logFile, + rev = rev ?: "", + buildId = buildId + ) + + val wsDir = workspaces.prepare(ctx) + logFile.header(buildId, rev, wsDir) + + val process = processes.builder(config, logFile, rev) + .command(config.script) + .directory(wsDir.toFile()) + .start() + handles.add(Handle(process.toHandle(), buildId)) + + process.onExit().whenComplete { _, _ -> + logFile.footer() + workspaces.cleanUp(wsDir) + } + + val pid = process.pid() + + logger.info("pid for build $buildId is: $pid") + + return ProcessInfo(pid = pid, logFile = logFile) + } + + val logger: Logger = LoggerFactory.getLogger(this::class.java) + +} diff --git a/src/main/kotlin/alfred/web/cron/CronTrigger.kt b/src/main/kotlin/alfred/web/cron/CronTrigger.kt new file mode 100644 index 0000000..a8a6dde --- /dev/null +++ b/src/main/kotlin/alfred/web/cron/CronTrigger.kt @@ -0,0 +1,18 @@ +package alfred.web.cron + +import alfred.web.core.build.Builds +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +@Service +class CronTrigger( + private val builds: Builds +) { + + @Scheduled(fixedDelay = 1L, timeUnit= TimeUnit.MINUTES) + fun checkTrigger() { + + } + +} diff --git a/src/main/kotlin/alfred/web/http/BuildsInfo.kt b/src/main/kotlin/alfred/web/http/BuildsInfo.kt new file mode 100644 index 0000000..d43232f --- /dev/null +++ b/src/main/kotlin/alfred/web/http/BuildsInfo.kt @@ -0,0 +1,45 @@ +package alfred.web.http + +import alfred.web.core.Handles +import alfred.web.core.build.BuildId +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("build") +class BuildsInfo( + val security: Security, + val handles: Handles +) { + + @GetMapping( + "{build}/info", + produces = [MediaType.APPLICATION_JSON_VALUE] + ) + fun info( + @PathVariable("build") + build: BuildId, + @RequestParam("key") + key: String? + ) = security.requireKey(build, key) { + it + } + + @GetMapping( + "{build}/handles", + produces = [MediaType.APPLICATION_JSON_VALUE] + ) + fun handles( + @PathVariable("build") + build: BuildId, + @RequestParam("key") + key: String? + ) = security.requireKey(build, key) { + handles.active(build) + } + +} diff --git a/src/main/kotlin/alfred/web/http/Endpoints.kt b/src/main/kotlin/alfred/web/http/HttpTrigger.kt similarity index 52% rename from src/main/kotlin/alfred/web/http/Endpoints.kt rename to src/main/kotlin/alfred/web/http/HttpTrigger.kt index 89d35e2..ae10665 100644 --- a/src/main/kotlin/alfred/web/http/Endpoints.kt +++ b/src/main/kotlin/alfred/web/http/HttpTrigger.kt @@ -1,47 +1,33 @@ package alfred.web.http -import alfred.web.core.BuildId -import alfred.web.core.Builds -import alfred.web.core.GitRunner -import alfred.web.core.Handles -import alfred.web.core.ScriptRunner +import alfred.web.core.build.BuildId +import alfred.web.core.build.Builds +import alfred.web.core.runner.GitRunner +import alfred.web.core.runner.ScriptRunner +import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("build") -class Endpoints( +class HttpTrigger( val builds: Builds, val security: Security, val scriptRunner: ScriptRunner, val gitRunner: GitRunner, - val handles: Handles ) { - @GetMapping( - "{build}/info", - produces = [MediaType.APPLICATION_JSON_VALUE] - ) - fun info( - @PathVariable("build") - build: BuildId, - @RequestParam("key") - key: String? - ) = security.requireKey(build, key) { - it - } - @PostMapping( - "{build}/trigger", + "{build}/trigger/git", produces = [MediaType.APPLICATION_JSON_VALUE] ) - fun trigger( + fun triggerGit( @PathVariable("build") build: BuildId, @RequestParam("key") @@ -49,12 +35,13 @@ class Endpoints( @RequestParam("rev") rev: String ) = security.requireKey(build, key) { - val info = if (builds.buildConfig(build).script != null) { - scriptRunner.run(build, rev) - } else { - gitRunner.run(build, rev) + val config = builds.buildConfig(build) + if (config.gitRepo == null) { + throw UnsupportedMode() } + val info = gitRunner.run(build, rev) + ResponseEntity.ok( mapOf( "log" to info.logFile.backingFile.absolutePath, @@ -63,18 +50,35 @@ class Endpoints( ) } - @GetMapping( - "{build}/handles", + @PostMapping( + "{build}/trigger/script", produces = [MediaType.APPLICATION_JSON_VALUE] ) - fun handles( + fun triggerScript( @PathVariable("build") build: BuildId, @RequestParam("key") - key: String? + key: String?, + @RequestParam("rev") + rev: String? ) = security.requireKey(build, key) { - handles.active(build) + val config = builds.buildConfig(build) + if (config.script == null) { + throw UnsupportedMode() + } + + val info = scriptRunner.run(build, rev) + + ResponseEntity.ok( + mapOf( + "log" to info.logFile.backingFile.absolutePath, + "pid" to info.pid + ) + ) } + @ResponseStatus(value = HttpStatus.PRECONDITION_FAILED, reason = "build mode is not configured") + class UnsupportedMode : Exception() + } diff --git a/src/main/kotlin/alfred/web/http/Security.kt b/src/main/kotlin/alfred/web/http/Security.kt index 875bb6c..114026b 100644 --- a/src/main/kotlin/alfred/web/http/Security.kt +++ b/src/main/kotlin/alfred/web/http/Security.kt @@ -1,8 +1,8 @@ package alfred.web.http -import alfred.web.core.BuildConfig -import alfred.web.core.BuildId -import alfred.web.core.Builds +import alfred.web.core.build.BuildConfig +import alfred.web.core.build.BuildId +import alfred.web.core.build.Builds import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service