major reordering, making stuff fit for native compilation

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

@ -39,7 +39,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId> <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlin</groupId> <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.eclipse.microprofile.config.inject.ConfigProperty
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -37,8 +40,7 @@ class Builds(
val env = props val env = props
.entries .entries
.filter { it.key.toString() == it.key.toString().toUpperCase() } .filter { it.key.toString() == it.key.toString().toUpperCase() }
.map { Pair(it.key.toString(), it.value.toString()) } .associate { Pair(it.key.toString(), it.value.toString()) }
.toMap()
return BuildConfig( return BuildConfig(
user = props.getProperty("user"), 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 nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val fileName = "$build.$nowStr.log" val fileName = "$build.$nowStr.log"
@ -67,15 +69,16 @@ class UnknownBuild(val build: BuildId) : RuntimeException() {
"unknown build: $build" "unknown build: $build"
} }
@RegisterForReflection
data class BuildConfig( data class BuildConfig(
val user: String, val user: String,
val workspace: String, val workspace: String,
@field:JsonIgnore
val apikey: String, val apikey: String,
val script: String?, val script: String?,
val gitRepo: String?, val gitRepo: String?,
@field:JsonIgnore
val env: Map<String, String> val env: Map<String, String>
) { )
fun asPublic() = copy(apikey = "", env = emptyMap())
}
typealias BuildId = String 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.inject.Inject
import javax.ws.rs.* import javax.ws.rs.*
import javax.ws.rs.core.MediaType import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response import javax.ws.rs.core.Response
import javax.ws.rs.core.Response.Status.PRECONDITION_REQUIRED import javax.ws.rs.core.Response.Status.PRECONDITION_REQUIRED
@Path("builds") @Path("build")
class Endpoints( class Endpoints {
@Inject
val builds: Builds, @field:Inject
@Inject lateinit var builds: Builds
val security: Security,
@Inject @field:Inject
val scriptRunner: ScriptRunner, lateinit var security: Security
@Inject
val gitRunner: GitRunner, @field:Inject
@Inject lateinit var scriptRunner: ScriptRunner
val handles: Handles
) { @field:Inject
lateinit var gitRunner: GitRunner
@field:Inject
lateinit var handles: Handles
@GET @GET
@Path("{build}/info") @Path("{build}/info")
@ -29,7 +38,7 @@ class Endpoints(
@QueryParam("key") @QueryParam("key")
key: String? key: String?
) = security.requireKey(build, key) { ) = security.requireKey(build, key) {
it.asPublic() it
} }
@POST @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.enterprise.context.ApplicationScoped
import javax.inject.Inject import javax.inject.Inject
import javax.ws.rs.core.Response import javax.ws.rs.core.Response
@ApplicationScoped @ApplicationScoped
class Security( class Security {
@Inject
val builds: Builds @field:Inject
) { lateinit var builds: Builds
fun requireKey(build: BuildId, apikey: String?, block: (BuildConfig) -> Any): Response { fun requireKey(build: BuildId, apikey: String?, block: (BuildConfig) -> Any): Response {
val buildConfig = builds.buildConfig(build) 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 io.quarkus.scheduler.Scheduled
import java.time.Instant import java.time.Instant
import javax.enterprise.context.ApplicationScoped import javax.enterprise.context.ApplicationScoped
@ -19,6 +21,7 @@ class Handles {
HandleInfo( HandleInfo(
command = it.command, command = it.command,
startedAt = it.startedAt, startedAt = it.startedAt,
user = it.user,
alive = it.handle.isAlive alive = it.handle.isAlive
) )
} }
@ -36,10 +39,13 @@ data class Handle(
) { ) {
val command: String = handle.info().commandLine().orElse("") val command: String = handle.info().commandLine().orElse("")
val startedAt: Instant? = handle.info().startInstant().orElse(null) val startedAt: Instant? = handle.info().startInstant().orElse(null)
val user: String? = handle.info().user().orElse(null)
} }
@RegisterForReflection
data class HandleInfo( data class HandleInfo(
val command: String, val command: String,
val startedAt: Instant?, 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.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.time.ZonedDateTime import java.nio.file.Paths
import javax.enterprise.context.ApplicationScoped import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject import javax.inject.Inject
@ApplicationScoped @ApplicationScoped
class ScriptRunner( class ScriptRunner {
@Inject
val builds: Builds, @field:Inject
@Inject lateinit var builds: Builds
val handles: Handles
) { @field:Inject
lateinit var handles: Handles
fun run(buildId: BuildId, rev: String): ProcessInfo { fun run(buildId: BuildId, rev: String): ProcessInfo {
val config = builds.buildConfig(buildId) val config = builds.buildConfig(buildId)
val logFile = builds.createLogFile(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") 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)) .directory(File(config.workspace))
.redirectOutput(logFile)
.redirectError(logFile)
.apply {
environment().putAll(config.env)
environment()["ALFRED_LOG_FILE"] = logFile.absolutePath
environment()["ALFRED_REV"] = rev
}
.start() .start()
handles.add(Handle(process.toHandle(), buildId)) handles.add(Handle(process.toHandle(), buildId))
process.onExit().whenComplete { _, _ -> logFile.footer() }
val pid = process.pid() 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 # key = value
ALFRED_HOME=/tmp/alfred 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