diff --git a/pom.xml b/pom.xml
index 4e0ad79..31f1d47 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,16 +9,16 @@
1
3.8.1
- 1.3.41
+ 1.4.30
true
1.8
1.8
UTF-8
UTF-8
- 1.1.0.Final
+ 1.13.2.Final
quarkus-universe-bom
io.quarkus
- 1.1.0.Final
+ 1.13.2.Final
2.22.1
@@ -61,15 +61,11 @@
io.quarkus
- quarkus-scheduler
-
-
- io.quarkus
- quarkus-mailer
+ quarkus-undertow
io.quarkus
- quarkus-undertow
+ quarkus-scheduler
@@ -97,8 +93,7 @@
${surefire-plugin.version}
- org.jboss.logmanager.LogManager
-
+ org.jboss.logmanager.LogManager
@@ -133,8 +128,7 @@
-
+
diff --git a/src/main/kotlin/de/joshavg/Builds.kt b/src/main/kotlin/de/joshavg/Builds.kt
index ad9af1e..e0d17a0 100644
--- a/src/main/kotlin/de/joshavg/Builds.kt
+++ b/src/main/kotlin/de/joshavg/Builds.kt
@@ -4,6 +4,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
+import java.lang.RuntimeException
import java.nio.file.Files
import java.nio.file.Paths
import java.time.LocalDateTime
@@ -11,17 +12,11 @@ import java.time.format.DateTimeFormatter
import java.util.*
import javax.enterprise.context.ApplicationScoped
-data class BuildConfig(val exec: String,
- val user: String,
- val dir: String,
- val key: String,
- val env: Map) {
- fun asPublic() = copy(key = "")
-}
-
@ApplicationScoped
-class Builds(@ConfigProperty(name = "ALFRED_HOME")
- private val home: String) {
+class Builds(
+ @ConfigProperty(name = "ALFRED_HOME")
+ private val home: String
+) {
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
@@ -29,8 +24,13 @@ class Builds(@ConfigProperty(name = "ALFRED_HOME")
logger.info("ALFRED_HOME is $home")
}
- fun buildConfig(build: String): BuildConfig {
+ fun buildConfig(build: BuildId): BuildConfig {
val path = Paths.get(home, "builds", "$build.properties")
+
+ if (!path.toFile().exists()) {
+ throw UnknownBuild(build)
+ }
+
val props = Properties()
props.load(path.toFile().inputStream())
@@ -41,14 +41,16 @@ class Builds(@ConfigProperty(name = "ALFRED_HOME")
.toMap()
return BuildConfig(
- exec = props.getProperty("exec"),
user = props.getProperty("user"),
- dir = props.getProperty("dir"),
- key = props.getProperty("key") ?: "",
- env = env)
+ workspace = props.getProperty("workspace"),
+ apikey = props.getProperty("apikey") ?: "",
+ script = props.getProperty("script"),
+ gitRepo = props.getProperty("gitrepo"),
+ env = env
+ )
}
- fun createLogFile(build: String): File {
+ fun createLogFile(build: BuildId): File {
val nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val fileName = "$build.$nowStr.log"
@@ -59,3 +61,21 @@ class Builds(@ConfigProperty(name = "ALFRED_HOME")
}
}
+
+class UnknownBuild(val build: BuildId) : RuntimeException() {
+ override fun toString() =
+ "unknown build: $build"
+}
+
+data class BuildConfig(
+ val user: String,
+ val workspace: String,
+ val apikey: String,
+ val script: String?,
+ val gitRepo: String?,
+ val env: Map
+) {
+ fun asPublic() = copy(apikey = "", env = emptyMap())
+}
+
+typealias BuildId = String
diff --git a/src/main/kotlin/de/joshavg/BuildsEndpoint.kt b/src/main/kotlin/de/joshavg/BuildsEndpoint.kt
deleted file mode 100644
index a35e310..0000000
--- a/src/main/kotlin/de/joshavg/BuildsEndpoint.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package de.joshavg
-
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import java.io.File
-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 BuildsEndpoint(
- @Inject
- val builds: Builds,
- @Inject
- val security: Security
-) {
-
- val logger: Logger = LoggerFactory.getLogger(this::class.java)
-
- @GET
- @Path("/{build}/info")
- @Produces(MediaType.APPLICATION_JSON)
- fun info(@PathParam("build")
- build: String,
- @QueryParam("key")
- key: String?) =
- security.requireKey(build, key) {
- it.asPublic()
- }
-
- @GET
- @Path("/{build}/trigger")
- @Produces(MediaType.APPLICATION_JSON)
- fun trigger(@PathParam("build")
- build: String,
- @QueryParam("key")
- key: String?,
- @QueryParam("rev")
- rev: String?) =
- security.requireKey(build, key) {
- val logFile = builds.createLogFile(build)
-
- if (rev == null) {
- return@requireKey Response.status(PRECONDITION_REQUIRED).build()
- }
-
- startBuildThread(build, rev, logFile, it)
-
- Response.ok(mapOf(
- "log" to logFile.absolutePath
- )).build()
- }
-
- fun startBuildThread(build: String, rev: String, logFile: File, config: BuildConfig) =
- Thread {
- logger.info("preparing process for build $build with config ${config.asPublic()}")
- logger.info("log file: $logFile")
-
- val pid = ProcessBuilder(config.exec)
- .directory(File(config.dir))
- .redirectOutput(logFile)
- .redirectError(logFile)
- .apply {
- environment().putAll(config.env)
- environment()["ALFRED_LOG_FILE"] = logFile.absolutePath
- environment()["ALFRED_GIT_REV"] = rev
- }
- .start()
- .toHandle()
- .pid()
-
- logger.info("pid for build $build is: $pid")
- }.start()
-
-}
-
diff --git a/src/main/kotlin/de/joshavg/Endpoints.kt b/src/main/kotlin/de/joshavg/Endpoints.kt
new file mode 100644
index 0000000..0a36c83
--- /dev/null
+++ b/src/main/kotlin/de/joshavg/Endpoints.kt
@@ -0,0 +1,77 @@
+package de.joshavg
+
+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
+) {
+
+ @GET
+ @Path("{build}/info")
+ @Produces(MediaType.APPLICATION_JSON)
+ fun info(
+ @PathParam("build")
+ build: BuildId,
+ @QueryParam("key")
+ key: String?
+ ) = security.requireKey(build, key) {
+ it.asPublic()
+ }
+
+ @POST
+ @Path("{build}/trigger")
+ @Produces(MediaType.APPLICATION_JSON)
+ fun trigger(
+ @PathParam("build")
+ build: BuildId,
+ @QueryParam("key")
+ key: String?,
+ @QueryParam("rev")
+ rev: String?
+ ) = security.requireKey(build, key) {
+ if (rev == null) {
+ return@requireKey Response.status(PRECONDITION_REQUIRED).build()
+ }
+
+ val info = if (builds.buildConfig(build).script != null) {
+ scriptRunner.run(build, rev)
+ } else {
+ gitRunner.run(build, rev)
+ }
+
+ Response.ok(
+ mapOf(
+ "log" to info.logFile.absolutePath,
+ "pid" to info.pid
+ )
+ ).build()
+ }
+
+ @GET
+ @Path("{build}/handles")
+ @Produces(MediaType.APPLICATION_JSON)
+ fun handles(
+ @PathParam("build")
+ build: BuildId,
+ @QueryParam("key")
+ key: String?
+ ) = security.requireKey(build, key) {
+ handles.active(build)
+ }
+
+}
+
diff --git a/src/main/kotlin/de/joshavg/GitRunner.kt b/src/main/kotlin/de/joshavg/GitRunner.kt
new file mode 100644
index 0000000..b2178b0
--- /dev/null
+++ b/src/main/kotlin/de/joshavg/GitRunner.kt
@@ -0,0 +1,132 @@
+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/Handles.kt b/src/main/kotlin/de/joshavg/Handles.kt
new file mode 100644
index 0000000..e63f145
--- /dev/null
+++ b/src/main/kotlin/de/joshavg/Handles.kt
@@ -0,0 +1,45 @@
+package de.joshavg
+
+import io.quarkus.scheduler.Scheduled
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+
+@ApplicationScoped
+class Handles {
+
+ private val handles = mutableListOf()
+
+ fun add(handle: Handle) =
+ handles.add(handle)
+
+ fun active(buildId: BuildId) =
+ handles
+ .filter { it.buildId == buildId }
+ .map {
+ HandleInfo(
+ command = it.command,
+ startedAt = it.startedAt,
+ alive = it.handle.isAlive
+ )
+ }
+
+ @Scheduled(every = "60s")
+ fun cleanup() {
+ handles.removeIf { !it.handle.isAlive }
+ }
+
+}
+
+data class Handle(
+ val handle: ProcessHandle,
+ val buildId: BuildId
+) {
+ val command: String = handle.info().commandLine().orElse("")
+ val startedAt: Instant? = handle.info().startInstant().orElse(null)
+}
+
+data class HandleInfo(
+ val command: String,
+ val startedAt: Instant?,
+ val alive: Boolean
+)
diff --git a/src/main/kotlin/de/joshavg/ScriptRunner.kt b/src/main/kotlin/de/joshavg/ScriptRunner.kt
new file mode 100644
index 0000000..bf86929
--- /dev/null
+++ b/src/main/kotlin/de/joshavg/ScriptRunner.kt
@@ -0,0 +1,54 @@
+package de.joshavg
+
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.File
+import java.time.ZonedDateTime
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+
+@ApplicationScoped
+class ScriptRunner(
+ @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)
+
+ val process = ProcessBuilder(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))
+
+ 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: File
+)
+
diff --git a/src/main/kotlin/de/joshavg/Security.kt b/src/main/kotlin/de/joshavg/Security.kt
index 9faa97f..f98804d 100644
--- a/src/main/kotlin/de/joshavg/Security.kt
+++ b/src/main/kotlin/de/joshavg/Security.kt
@@ -5,17 +5,19 @@ import javax.inject.Inject
import javax.ws.rs.core.Response
@ApplicationScoped
-class Security(@Inject
- val builds: Builds) {
+class Security(
+ @Inject
+ val builds: Builds
+) {
- fun requireKey(build: String, key: String?, block: (BuildConfig) -> Any): Response {
+ fun requireKey(build: BuildId, apikey: String?, block: (BuildConfig) -> Any): Response {
val buildConfig = builds.buildConfig(build)
- if (buildConfig.key != "" && buildConfig.key != key) {
+ if (buildConfig.apikey != "" && buildConfig.apikey != apikey) {
return Response.status(Response.Status.UNAUTHORIZED).build()
}
val entity = block(buildConfig)
- if(entity is Response) {
+ if (entity is Response) {
return entity
}
diff --git a/src/main/kotlin/de/joshavg/process.kt b/src/main/kotlin/de/joshavg/process.kt
new file mode 100644
index 0000000..324eddb
--- /dev/null
+++ b/src/main/kotlin/de/joshavg/process.kt
@@ -0,0 +1,20 @@
+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