parent
b271bc3b88
commit
2f65800ba6
@ -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()
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
@ -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<Handle>()
|
||||
|
||||
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
|
||||
)
|
@ -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
|
||||
)
|
||||
|
@ -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")
|
||||
}
|
Loading…
Reference in new issue