diff --git a/pom.xml b/pom.xml
index 31f1d47..a4c89ba 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,7 +39,7 @@
io.quarkus
- quarkus-resteasy-jsonb
+ quarkus-resteasy-jackson
org.jetbrains.kotlin
diff --git a/src/main/kotlin/de/joshavg/Builds.kt b/src/main/kotlin/alfred/Builds.kt
similarity index 85%
rename from src/main/kotlin/de/joshavg/Builds.kt
rename to src/main/kotlin/alfred/Builds.kt
index e0d17a0..10aff1b 100644
--- a/src/main/kotlin/de/joshavg/Builds.kt
+++ b/src/main/kotlin/alfred/Builds.kt
@@ -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
-) {
- fun asPublic() = copy(apikey = "", env = emptyMap())
-}
+)
typealias BuildId = String
diff --git a/src/main/kotlin/de/joshavg/Endpoints.kt b/src/main/kotlin/alfred/http/Endpoints.kt
similarity index 74%
rename from src/main/kotlin/de/joshavg/Endpoints.kt
rename to src/main/kotlin/alfred/http/Endpoints.kt
index 0a36c83..dd6d53d 100644
--- a/src/main/kotlin/de/joshavg/Endpoints.kt
+++ b/src/main/kotlin/alfred/http/Endpoints.kt
@@ -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
diff --git a/src/main/kotlin/de/joshavg/Security.kt b/src/main/kotlin/alfred/http/Security.kt
similarity index 79%
rename from src/main/kotlin/de/joshavg/Security.kt
rename to src/main/kotlin/alfred/http/Security.kt
index f98804d..774bf52 100644
--- a/src/main/kotlin/de/joshavg/Security.kt
+++ b/src/main/kotlin/alfred/http/Security.kt
@@ -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)
diff --git a/src/main/kotlin/alfred/running/GitRunner.kt b/src/main/kotlin/alfred/running/GitRunner.kt
new file mode 100644
index 0000000..e4a26a0
--- /dev/null
+++ b/src/main/kotlin/alfred/running/GitRunner.kt
@@ -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"
+}
diff --git a/src/main/kotlin/de/joshavg/Handles.kt b/src/main/kotlin/alfred/running/Handles.kt
similarity index 78%
rename from src/main/kotlin/de/joshavg/Handles.kt
rename to src/main/kotlin/alfred/running/Handles.kt
index e63f145..e6cbd47 100644
--- a/src/main/kotlin/de/joshavg/Handles.kt
+++ b/src/main/kotlin/alfred/running/Handles.kt
@@ -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?
)
diff --git a/src/main/kotlin/de/joshavg/ScriptRunner.kt b/src/main/kotlin/alfred/running/ScriptRunner.kt
similarity index 60%
rename from src/main/kotlin/de/joshavg/ScriptRunner.kt
rename to src/main/kotlin/alfred/running/ScriptRunner.kt
index bf86929..1c8799d 100644
--- a/src/main/kotlin/de/joshavg/ScriptRunner.kt
+++ b/src/main/kotlin/alfred/running/ScriptRunner.kt
@@ -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()
diff --git a/src/main/kotlin/alfred/running/process.kt b/src/main/kotlin/alfred/running/process.kt
new file mode 100644
index 0000000..499c967
--- /dev/null
+++ b/src/main/kotlin/alfred/running/process.kt
@@ -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"
+ )
diff --git a/src/main/kotlin/de/joshavg/GitRunner.kt b/src/main/kotlin/de/joshavg/GitRunner.kt
deleted file mode 100644
index b2178b0..0000000
--- a/src/main/kotlin/de/joshavg/GitRunner.kt
+++ /dev/null
@@ -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"
-}
diff --git a/src/main/kotlin/de/joshavg/process.kt b/src/main/kotlin/de/joshavg/process.kt
deleted file mode 100644
index 324eddb..0000000
--- a/src/main/kotlin/de/joshavg/process.kt
+++ /dev/null
@@ -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")
-}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f654e75..96c4169 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -2,3 +2,5 @@
# key = value
ALFRED_HOME=/tmp/alfred
+ALFRED_PORT=8080
+quarkus.http.port=${ALFRED_PORT}
diff --git a/src/test/kotlin/de/joshavg/ExampleResourceTest.kt b/src/test/kotlin/de/joshavg/ExampleResourceTest.kt
deleted file mode 100644
index 0d7fa34..0000000
--- a/src/test/kotlin/de/joshavg/ExampleResourceTest.kt
+++ /dev/null
@@ -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"))
- }
-
-}
\ No newline at end of file
diff --git a/src/test/kotlin/de/joshavg/NativeExampleResourceIT.kt b/src/test/kotlin/de/joshavg/NativeExampleResourceIT.kt
deleted file mode 100644
index 9ef3043..0000000
--- a/src/test/kotlin/de/joshavg/NativeExampleResourceIT.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package de.joshavg
-
-import io.quarkus.test.junit.NativeImageTest
-
-@NativeImageTest
-open class NativeExampleResourceIT : ExampleResourceTest()
\ No newline at end of file