some changes, git runner

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

@ -9,16 +9,16 @@
<version>1</version> <version>1</version>
<properties> <properties>
<compiler-plugin.version>3.8.1</compiler-plugin.version> <compiler-plugin.version>3.8.1</compiler-plugin.version>
<kotlin.version>1.3.41</kotlin.version> <kotlin.version>1.4.30</kotlin.version>
<maven.compiler.parameters>true</maven.compiler.parameters> <maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus-plugin.version>1.1.0.Final</quarkus-plugin.version> <quarkus-plugin.version>1.13.2.Final</quarkus-plugin.version>
<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id> <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id> <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>1.1.0.Final</quarkus.platform.version> <quarkus.platform.version>1.13.2.Final</quarkus.platform.version>
<surefire-plugin.version>2.22.1</surefire-plugin.version> <surefire-plugin.version>2.22.1</surefire-plugin.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@ -61,15 +61,11 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId> <artifactId>quarkus-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-undertow</artifactId> <artifactId>quarkus-scheduler</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
@ -97,8 +93,7 @@
<version>${surefire-plugin.version}</version> <version>${surefire-plugin.version}</version>
<configuration> <configuration>
<systemProperties> <systemProperties>
<java.util.logging.manager>org.jboss.logmanager.LogManager <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</java.util.logging.manager>
</systemProperties> </systemProperties>
</configuration> </configuration>
</plugin> </plugin>
@ -133,8 +128,7 @@
</compilerPlugins> </compilerPlugins>
<pluginOptions> <pluginOptions>
<option>all-open:annotation=javax.ws.rs.Path</option> <option>all-open:annotation=javax.ws.rs.Path</option>
<option>all-open:annotation=javax.enterprise.context.ApplicationScoped <option>all-open:annotation=javax.enterprise.context.ApplicationScoped</option>
</option>
</pluginOptions> </pluginOptions>
</configuration> </configuration>
</plugin> </plugin>

@ -4,6 +4,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty
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.lang.RuntimeException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.time.LocalDateTime import java.time.LocalDateTime
@ -11,17 +12,11 @@ import java.time.format.DateTimeFormatter
import java.util.* import java.util.*
import javax.enterprise.context.ApplicationScoped import javax.enterprise.context.ApplicationScoped
data class BuildConfig(val exec: String,
val user: String,
val dir: String,
val key: String,
val env: Map<String, String>) {
fun asPublic() = copy(key = "")
}
@ApplicationScoped @ApplicationScoped
class Builds(@ConfigProperty(name = "ALFRED_HOME") class Builds(
private val home: String) { @ConfigProperty(name = "ALFRED_HOME")
private val home: String
) {
private val logger: Logger = LoggerFactory.getLogger(this::class.java) 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") logger.info("ALFRED_HOME is $home")
} }
fun buildConfig(build: String): BuildConfig { fun buildConfig(build: BuildId): BuildConfig {
val path = Paths.get(home, "builds", "$build.properties") val path = Paths.get(home, "builds", "$build.properties")
if (!path.toFile().exists()) {
throw UnknownBuild(build)
}
val props = Properties() val props = Properties()
props.load(path.toFile().inputStream()) props.load(path.toFile().inputStream())
@ -41,14 +41,16 @@ class Builds(@ConfigProperty(name = "ALFRED_HOME")
.toMap() .toMap()
return BuildConfig( return BuildConfig(
exec = props.getProperty("exec"),
user = props.getProperty("user"), user = props.getProperty("user"),
dir = props.getProperty("dir"), workspace = props.getProperty("workspace"),
key = props.getProperty("key") ?: "", apikey = props.getProperty("apikey") ?: "",
env = env) 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 nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val fileName = "$build.$nowStr.log" 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<String, String>
) {
fun asPublic() = copy(apikey = "", env = emptyMap())
}
typealias BuildId = String

@ -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
)

@ -5,17 +5,19 @@ import javax.inject.Inject
import javax.ws.rs.core.Response import javax.ws.rs.core.Response
@ApplicationScoped @ApplicationScoped
class Security(@Inject class Security(
val builds: Builds) { @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) val buildConfig = builds.buildConfig(build)
if (buildConfig.key != "" && buildConfig.key != key) { if (buildConfig.apikey != "" && buildConfig.apikey != apikey) {
return Response.status(Response.Status.UNAUTHORIZED).build() return Response.status(Response.Status.UNAUTHORIZED).build()
} }
val entity = block(buildConfig) val entity = block(buildConfig)
if(entity is Response) { if (entity is Response) {
return entity return entity
} }

@ -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…
Cancel
Save