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