major moving around, rework of script triggering and running

master
Josha von Gizycki 1 day ago
parent 5345da6232
commit 992a5a922d

@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
@SpringBootApplication @SpringBootApplication
class WebApplication open class WebApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {
runApplication<WebApplication>(*args) runApplication<WebApplication>(*args)

@ -1,5 +1,6 @@
package alfred.web.core package alfred.web.core
import alfred.web.core.build.BuildId
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant import java.time.Instant

@ -1,11 +1,13 @@
package alfred.web.core package alfred.web.core
import alfred.web.core.build.BuildConfig
import alfred.web.core.build.LogFile
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class Processes { class Processes {
fun builder(config: BuildConfig, logFile: LogFile, rev: String): ProcessBuilder = fun builder(config: BuildConfig, logFile: LogFile, rev: String?): ProcessBuilder =
ProcessBuilder() ProcessBuilder()
.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.backingFile)) .redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.backingFile))
.redirectError(ProcessBuilder.Redirect.appendTo(logFile.backingFile)) .redirectError(ProcessBuilder.Redirect.appendTo(logFile.backingFile))

@ -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
)

@ -0,0 +1,8 @@
package alfred.web.core.build
data class BuildContext(
val config: BuildConfig,
val logFile: LogFile,
val rev: String,
val buildId: BuildId
)

@ -1,4 +1,4 @@
package alfred.web.core package alfred.web.core.build
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import org.slf4j.Logger import org.slf4j.Logger
@ -8,11 +8,17 @@ import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus
import java.lang.RuntimeException import java.lang.RuntimeException
import java.nio.file.Files import java.nio.file.Files.createFile
import java.nio.file.Paths import java.nio.file.Paths
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.* 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 @Service
class Builds( class Builds(
@ -42,15 +48,7 @@ class Builds(
.associate { Pair(it.key.toString(), it.value.toString()) } .associate { Pair(it.key.toString(), it.value.toString()) }
// diverge into ScriptedBasedConfig and GitBasedConfig // diverge into ScriptedBasedConfig and GitBasedConfig
return BuildConfig( return props.toBuildConfig(env)
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
)
} }
fun createLogFile(build: BuildId): LogFile { fun createLogFile(build: BuildId): LogFile {
@ -60,11 +58,22 @@ class Builds(
val path = Paths.get(home, "logs", fileName) val path = Paths.get(home, "logs", fileName)
path.toFile().parentFile.mkdirs() path.toFile().parentFile.mkdirs()
return LogFile(Files.createFile(path).toFile()) return LogFile(createFile(path).toFile())
} }
} }
private fun Properties.toBuildConfig(env: Map<String, String>) =
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") @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Unknown Build Id")
class UnknownBuild(val build: BuildId) : RuntimeException() { class UnknownBuild(val build: BuildId) : RuntimeException() {
override fun toString() = override fun toString() =
@ -78,7 +87,7 @@ data class BuildConfig(
val script: String?, val script: String?,
val gitRepo: String?, val gitRepo: String?,
val gitCloneTimeout: Long, val gitCloneTimeout: Long,
val gitWorkspace: String, val workspace: String,
@field:JsonIgnore @field:JsonIgnore
val env: Map<String, String> val env: Map<String, String>
) )

@ -1,18 +1,22 @@
package alfred.web.core package alfred.web.core.build
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.Path
import java.time.ZonedDateTime import java.time.ZonedDateTime
import kotlin.collections.joinToString
import kotlin.io.appendText import kotlin.io.appendText
import kotlin.text.isEmpty
import kotlin.text.lines
import kotlin.text.trim
class LogFile( class LogFile(
val backingFile: File val backingFile: File
) { ) {
fun header(buildId: BuildId, rev: String, workspace: Path) { fun header(buildId: BuildId, rev: String?, workspace: Path) {
append("At your service.") append("At your service.")
append("Build $buildId, rev [$rev] started at ${ZonedDateTime.now()}") append("Build $buildId, rev [${rev ?: "-none-"}] started at ${ZonedDateTime.now()}")
append("Workspace directory is: $workspace\n") append("Workspace directory is: ${workspace.toFile().absolutePath}")
} }
fun footer() { fun footer() {

@ -0,0 +1,6 @@
package alfred.web.core.build
data class ProcessInfo(
val pid: Long,
val logFile: LogFile
)

@ -1,22 +1,31 @@
package alfred.web.core package alfred.web.core.build
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.UUID import java.util.UUID
import kotlin.collections.isNotEmpty import kotlin.collections.isNotEmpty
import kotlin.io.deleteRecursively
@Service @Service
class Workspaces { class Workspaces {
fun withWorkspace(ctx: BuildContext, block: (Path) -> Unit) { 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( val workspacePath = Paths.get(
ctx.config.gitWorkspace, UUID.randomUUID().toString() ctx.config.workspace, UUID.randomUUID().toString()
) )
val workspaceDir = workspacePath.toFile() val workspaceDir = workspacePath.toFile()
ctx.logFile.header(ctx.buildId, ctx.rev, workspacePath)
if (!workspaceDir.exists()) { if (!workspaceDir.exists()) {
ctx.logFile.append("creating workspace ${workspaceDir.absolutePath}") ctx.logFile.append("creating workspace ${workspaceDir.absolutePath}")
workspaceDir.mkdirs() workspaceDir.mkdirs()
@ -28,12 +37,11 @@ class Workspaces {
throw WorkspaceIsNotEmpty(workspaceDir.toString()) throw WorkspaceIsNotEmpty(workspaceDir.toString())
} }
try { return workspacePath
block(workspaceDir.toPath()) }
} finally {
ctx.logFile.footer() fun cleanUp(wsDir: Path) {
workspaceDir.deleteRecursively() wsDir.toFile().deleteRecursively()
}
} }
} }

@ -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.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.nio.file.Path import java.nio.file.Path
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.collections.forEach
import kotlin.jvm.java
@Service @Service
class GitRunner( class GitRunner(
@ -32,8 +42,12 @@ class GitRunner(
Thread { Thread {
workspaces.withWorkspace(ctx) { wsPath -> workspaces.withWorkspace(ctx) { wsPath ->
logFile.header(buildId, rev, wsPath)
clone(ctx, wsPath) clone(ctx, wsPath)
execScripts(ctx, wsPath) execScripts(ctx, wsPath)
logFile.footer()
} }
}.start() }.start()
@ -97,12 +111,6 @@ class GitRunner(
} }
data class BuildContext(
val config: BuildConfig,
val logFile: LogFile,
val rev: String,
val buildId: BuildId
)
class FailedToClone( class FailedToClone(
private val buildId: BuildId, private val buildId: BuildId,

@ -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)
}

@ -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() {
}
}

@ -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)
}
}

@ -1,47 +1,33 @@
package alfred.web.http package alfred.web.http
import alfred.web.core.BuildId import alfred.web.core.build.BuildId
import alfred.web.core.Builds import alfred.web.core.build.Builds
import alfred.web.core.GitRunner import alfred.web.core.runner.GitRunner
import alfred.web.core.Handles import alfred.web.core.runner.ScriptRunner
import alfred.web.core.ScriptRunner import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity 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.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("build") @RequestMapping("build")
class Endpoints( class HttpTrigger(
val builds: Builds, val builds: Builds,
val security: Security, val security: Security,
val scriptRunner: ScriptRunner, val scriptRunner: ScriptRunner,
val gitRunner: GitRunner, 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( @PostMapping(
"{build}/trigger", "{build}/trigger/git",
produces = [MediaType.APPLICATION_JSON_VALUE] produces = [MediaType.APPLICATION_JSON_VALUE]
) )
fun trigger( fun triggerGit(
@PathVariable("build") @PathVariable("build")
build: BuildId, build: BuildId,
@RequestParam("key") @RequestParam("key")
@ -49,12 +35,13 @@ class Endpoints(
@RequestParam("rev") @RequestParam("rev")
rev: String rev: String
) = security.requireKey(build, key) { ) = security.requireKey(build, key) {
val info = if (builds.buildConfig(build).script != null) { val config = builds.buildConfig(build)
scriptRunner.run(build, rev) if (config.gitRepo == null) {
} else { throw UnsupportedMode()
gitRunner.run(build, rev)
} }
val info = gitRunner.run(build, rev)
ResponseEntity.ok( ResponseEntity.ok(
mapOf( mapOf(
"log" to info.logFile.backingFile.absolutePath, "log" to info.logFile.backingFile.absolutePath,
@ -63,18 +50,35 @@ class Endpoints(
) )
} }
@GetMapping( @PostMapping(
"{build}/handles", "{build}/trigger/script",
produces = [MediaType.APPLICATION_JSON_VALUE] produces = [MediaType.APPLICATION_JSON_VALUE]
) )
fun handles( fun triggerScript(
@PathVariable("build") @PathVariable("build")
build: BuildId, build: BuildId,
@RequestParam("key") @RequestParam("key")
key: String? key: String?,
@RequestParam("rev")
rev: String?
) = security.requireKey(build, key) { ) = 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()
} }

@ -1,8 +1,8 @@
package alfred.web.http package alfred.web.http
import alfred.web.core.BuildConfig import alfred.web.core.build.BuildConfig
import alfred.web.core.BuildId import alfred.web.core.build.BuildId
import alfred.web.core.Builds import alfred.web.core.build.Builds
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service import org.springframework.stereotype.Service

Loading…
Cancel
Save