major reordering, making stuff fit for native compilation

master
Josha von Gizycki 3 years ago committed by Josha von Gizycki
parent 2f65800ba6
commit 4c070a3745

@ -39,7 +39,7 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>

@ -1,5 +1,8 @@
package de.joshavg
package alfred
import alfred.running.LogFile
import com.fasterxml.jackson.annotation.JsonIgnore
import io.quarkus.runtime.annotations.RegisterForReflection
import org.eclipse.microprofile.config.inject.ConfigProperty
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -37,8 +40,7 @@ class Builds(
val env = props
.entries
.filter { it.key.toString() == it.key.toString().toUpperCase() }
.map { Pair(it.key.toString(), it.value.toString()) }
.toMap()
.associate { Pair(it.key.toString(), it.value.toString()) }
return BuildConfig(
user = props.getProperty("user"),
@ -50,7 +52,7 @@ class Builds(
)
}
fun createLogFile(build: BuildId): File {
fun createLogFile(build: BuildId): LogFile {
val nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val fileName = "$build.$nowStr.log"
@ -67,15 +69,16 @@ class UnknownBuild(val build: BuildId) : RuntimeException() {
"unknown build: $build"
}
@RegisterForReflection
data class BuildConfig(
val user: String,
val workspace: String,
@field:JsonIgnore
val apikey: String,
val script: String?,
val gitRepo: String?,
@field:JsonIgnore
val env: Map<String, String>
) {
fun asPublic() = copy(apikey = "", env = emptyMap())
}
)
typealias BuildId = String

@ -1,24 +1,33 @@
package de.joshavg
package alfred.http
import alfred.*
import alfred.running.GitRunner
import alfred.running.Handles
import alfred.running.ScriptRunner
import javax.enterprise.inject.Default
import javax.inject.Inject
import javax.ws.rs.*
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
import javax.ws.rs.core.Response.Status.PRECONDITION_REQUIRED
@Path("builds")
class Endpoints(
@Inject
val builds: Builds,
@Inject
val security: Security,
@Inject
val scriptRunner: ScriptRunner,
@Inject
val gitRunner: GitRunner,
@Inject
val handles: Handles
) {
@Path("build")
class Endpoints {
@field:Inject
lateinit var builds: Builds
@field:Inject
lateinit var security: Security
@field:Inject
lateinit var scriptRunner: ScriptRunner
@field:Inject
lateinit var gitRunner: GitRunner
@field:Inject
lateinit var handles: Handles
@GET
@Path("{build}/info")
@ -29,7 +38,7 @@ class Endpoints(
@QueryParam("key")
key: String?
) = security.requireKey(build, key) {
it.asPublic()
it
}
@POST

@ -1,14 +1,17 @@
package de.joshavg
package alfred.http
import alfred.BuildConfig
import alfred.BuildId
import alfred.Builds
import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
import javax.ws.rs.core.Response
@ApplicationScoped
class Security(
@Inject
val builds: Builds
) {
class Security {
@field:Inject
lateinit var builds: Builds
fun requireKey(build: BuildId, apikey: String?, block: (BuildConfig) -> Any): Response {
val buildConfig = builds.buildConfig(build)

@ -0,0 +1,154 @@
package alfred.running
import alfred.BuildConfig
import alfred.BuildId
import alfred.Builds
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.TimeUnit
import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
@ApplicationScoped
class GitRunner {
@field:Inject
lateinit var builds: Builds
@field:Inject
lateinit var handles: Handles
val scriptsDir = ".alfred"
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")
val ctx = BuildContext(
buildId = buildId,
config = config,
wsId = UUID.randomUUID().toString(),
logFile = logFile,
rev = rev
)
logFile.header(buildId, rev, ctx.workspace)
assertEmptyWorkspace(ctx)
Thread {
try {
clone(ctx)
execScripts(ctx)
} finally {
deleteWorkspace(ctx)
logFile.footer()
}
}.start()
return ProcessInfo(-1, logFile)
}
private fun clone(ctx: BuildContext) {
logger.info("build ${ctx.buildId}: cloning ${ctx.config.gitRepo} into ${ctx.workspace}")
val process = processBuilder(ctx.config, ctx.logFile, "")
.command("git", "clone", ctx.config.gitRepo, ".")
.directory(ctx.workspace.toFile())
.start()
handles.add(Handle(process.toHandle(), ctx.buildId))
val cloneSuccess = process.waitFor(30, TimeUnit.SECONDS)
logger.info("build ${ctx.buildId}: checkout rev ${ctx.rev}")
processBuilder(ctx.config, ctx.logFile, "")
.command("git", "checkout", ctx.rev)
.directory(ctx.workspace.toFile())
.start()
.waitFor()
if (!cloneSuccess) {
throw FailedToClone(ctx.buildId, ctx.config.gitRepo ?: "[no repo configured]")
}
}
private fun execScripts(ctx: BuildContext) {
val scriptFiles = listOf("pre.sh", "job.sh", "post.sh")
logger.info("build ${ctx.buildId}: looking for scripts $scriptFiles in $scriptsDir/")
scriptFiles.forEach { script ->
if (shFile(ctx, script).exists()) {
logger.info("build ${ctx.buildId}: found script $script, running it")
ctx.logFile.append("\nRunning build file: $script\n")
val scriptProcess = processBuilder(ctx.config, ctx.logFile, ctx.rev)
.command("$scriptsDir/$script")
.directory(ctx.workspace.toFile())
.start()
handles.add(Handle(scriptProcess.toHandle(), ctx.buildId))
val ret = scriptProcess.waitFor()
logger.info("build ${ctx.buildId}: script $script returned $ret")
ctx.logFile.append("\n$script returned $ret\n")
} else {
ctx.logFile.append("\nBuild file $scriptsDir/$script not found\n")
}
}
}
private fun shFile(ctx: BuildContext, name: String) =
ctx.workspace.resolve(scriptsDir).resolve(name).toFile()
private fun deleteWorkspace(ctx: BuildContext) {
ctx.workspace.toFile().deleteRecursively()
}
private fun assertEmptyWorkspace(config: BuildContext) {
val workspace = config.workspace.toFile()
if (!workspace.exists()) {
workspace.mkdirs()
}
val wsList = workspace.list()
if (wsList != null && wsList.isNotEmpty()) {
throw WorkspaceIsNotEmpty(config.workspace.toString())
}
}
val logger: Logger = LoggerFactory.getLogger(this::class.java)
}
data class BuildContext(
val config: BuildConfig,
val wsId: String,
val logFile: LogFile,
val rev: String,
val buildId: BuildId
) {
val workspace: Path = Paths.get(config.workspace, wsId)
}
class WorkspaceIsNotEmpty(
private val ws: String
) : RuntimeException() {
override fun toString() =
"${this::class}: workspace $ws is not empty"
}
class FailedToClone(
private val buildId: BuildId,
private val gitRepo: String
) : RuntimeException() {
override fun toString() =
"${this::class}: failed to clone $gitRepo for build id $buildId"
}

@ -1,5 +1,7 @@
package de.joshavg
package alfred.running
import alfred.BuildId
import io.quarkus.runtime.annotations.RegisterForReflection
import io.quarkus.scheduler.Scheduled
import java.time.Instant
import javax.enterprise.context.ApplicationScoped
@ -19,6 +21,7 @@ class Handles {
HandleInfo(
command = it.command,
startedAt = it.startedAt,
user = it.user,
alive = it.handle.isAlive
)
}
@ -36,10 +39,13 @@ data class Handle(
) {
val command: String = handle.info().commandLine().orElse("")
val startedAt: Instant? = handle.info().startInstant().orElse(null)
val user: String? = handle.info().user().orElse(null)
}
@RegisterForReflection
data class HandleInfo(
val command: String,
val startedAt: Instant?,
val alive: Boolean
val alive: Boolean,
val user: String?
)

@ -1,40 +1,39 @@
package de.joshavg
package alfred.running
import alfred.BuildId
import alfred.Builds
import alfred.http.Security
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.time.ZonedDateTime
import java.nio.file.Paths
import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
@ApplicationScoped
class ScriptRunner(
@Inject
val builds: Builds,
@Inject
val handles: Handles
) {
class ScriptRunner {
@field:Inject
lateinit var builds: Builds
@field:Inject
lateinit var handles: Handles
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.asPublic()}")
logger.info("preparing process for build $buildId with config $config")
logger.info("log file: $logFile")
logFileHeader(logFile, buildId, rev)
logFile.header(buildId, rev, Paths.get(config.workspace))
val process = ProcessBuilder(config.script)
val process = processBuilder(config, logFile, rev)
.command(config.script)
.directory(File(config.workspace))
.redirectOutput(logFile)
.redirectError(logFile)
.apply {
environment().putAll(config.env)
environment()["ALFRED_LOG_FILE"] = logFile.absolutePath
environment()["ALFRED_REV"] = rev
}
.start()
handles.add(Handle(process.toHandle(), buildId))
process.onExit().whenComplete { _, _ -> logFile.footer() }
val pid = process.pid()

@ -0,0 +1,40 @@
package alfred.running
import alfred.BuildConfig
import alfred.BuildId
import java.io.File
import java.nio.file.Path
import java.time.ZonedDateTime
fun processBuilder(config: BuildConfig, logFile: File, rev: String): ProcessBuilder =
ProcessBuilder()
.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile))
.redirectError(ProcessBuilder.Redirect.appendTo(logFile))
.apply {
environment().putAll(config.env)
environment()["ALFRED_LOG_FILE"] = logFile.absolutePath
environment()["ALFRED_REV"] = rev
}
typealias LogFile = File
fun LogFile.header(buildId: BuildId, rev: String, workspace: Path) {
this.append("At your service.")
this.append("Build $buildId, rev [$rev] started at ${ZonedDateTime.now()}")
this.append("Workspace directory is: $workspace\n")
}
fun LogFile.footer() {
this.append("Build finished at ${ZonedDateTime.now()}")
}
fun LogFile.append(content: String) =
this.appendText(
content.lines().joinToString(separator = "\n") {
if (it.trim().isEmpty()) {
""
} else {
"Alfred: $it"
}
} + "\n"
)

@ -1,132 +0,0 @@
package de.joshavg
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
@ApplicationScoped
class GitRunner(
@Inject
val builds: Builds,
@Inject
val handles: Handles
) {
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.asPublic()}")
logger.info("log file: $logFile")
logFileHeader(logFile, buildId, rev)
assertEmptyWorkspace(config)
Thread {
try {
clone(config, logFile, config.gitRepo!!, rev, buildId)
execScripts(config, logFile, rev, buildId)
} finally {
cleanupWorkspace(config, buildId)
}
}.start()
return ProcessInfo(-1, logFile)
}
private fun clone(
config: BuildConfig,
logFile: File,
gitRepo: String,
rev: String,
buildId: BuildId
) {
logger.info("build $buildId: cloning ${config.gitRepo} into ${config.workspace}")
val process = processBuilder(config, logFile, "")
.command("git", "clone", config.gitRepo, ".")
.start()
handles.add(Handle(process.toHandle(), buildId))
val cloneSuccess = process.waitFor(30, TimeUnit.SECONDS)
logger.info("build $buildId: checkout rev $rev")
processBuilder(config, logFile, "")
.command("git", "checkout", rev)
.start()
.waitFor()
if (!cloneSuccess) {
throw FailedToClone(buildId, gitRepo)
}
}
private fun execScripts(config: BuildConfig, logFile: File, rev: String, buildId: BuildId) {
val scriptFiles = listOf("pre.sh", "job.sh", "post.sh")
logger.info("build $buildId: looking for scripts $scriptFiles in .alfred/")
scriptFiles.forEach { script ->
if (Paths.get(config.workspace, ".alfred", script).toFile().exists()) {
logger.info("build $buildId: found script $script, running it")
val scriptProcess = processBuilder(config, logFile, rev)
.command(".alfred/$script")
.start()
handles.add(Handle(scriptProcess.toHandle(), buildId))
val ret = scriptProcess.waitFor()
logger.info("build $buildId: script $script returned $ret")
logFile.appendText("\n$script returned $ret\n")
}
}
}
private fun cleanupWorkspace(config: BuildConfig, buildId: BuildId) {
val wsList = File(config.workspace).list()
if (wsList != null && wsList.isNotEmpty()) {
logger.info("build $buildId: cleaning up workspace ${config.workspace}")
val ws = File(config.workspace)
ws.deleteRecursively()
ws.mkdirs()
}
}
private fun assertEmptyWorkspace(config: BuildConfig) {
val workspace = File(config.workspace)
if (!workspace.exists()) {
workspace.mkdirs()
}
val wsList = workspace.list()
if (wsList != null && wsList.isNotEmpty()) {
throw WorkspaceIsNotEmpty(config.workspace)
}
}
val logger: Logger = LoggerFactory.getLogger(this::class.java)
}
class WorkspaceIsNotEmpty(
private val ws: String
) : RuntimeException() {
override fun toString() =
"${this::class}: workspace $ws is not empty"
}
class FailedToClone(
private val buildId: BuildId,
private val gitRepo: String
) : RuntimeException() {
override fun toString() =
"${this::class}: failed to clone $gitRepo for build id $buildId"
}

@ -1,20 +0,0 @@
package de.joshavg
import java.io.File
import java.time.ZonedDateTime
fun processBuilder(config: BuildConfig, logFile: File, rev: String): ProcessBuilder =
ProcessBuilder()
.directory(File(config.workspace))
.redirectOutput(logFile)
.redirectError(logFile)
.apply {
environment().putAll(config.env)
environment()["ALFRED_LOG_FILE"] = logFile.absolutePath
environment()["ALFRED_REV"] = rev
}
fun logFileHeader(logFile: File, buildId : BuildId, rev: String) {
logFile.appendText("THE ALFRED!\n")
logFile.appendText("Build $buildId, rev $rev started at ${ZonedDateTime.now()}\n\n")
}

@ -2,3 +2,5 @@
# key = value
ALFRED_HOME=/tmp/alfred
ALFRED_PORT=8080
quarkus.http.port=${ALFRED_PORT}

@ -1,20 +0,0 @@
package de.joshavg
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured.given
import org.hamcrest.CoreMatchers.`is`
import org.junit.jupiter.api.Test
@QuarkusTest
open class ExampleResourceTest {
@Test
fun testHelloEndpoint() {
given()
.`when`().get("/hello")
.then()
.statusCode(200)
.body(`is`("hello"))
}
}

@ -1,6 +0,0 @@
package de.joshavg
import io.quarkus.test.junit.NativeImageTest
@NativeImageTest
open class NativeExampleResourceIT : ExampleResourceTest()
Loading…
Cancel
Save