Skip to content

Commit

Permalink
refactor(server): Restructure JitBindingServer
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoColman committed Feb 11, 2025
1 parent 444b5ff commit 907804d
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 176 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.github.typesafegithub.workflows.jitbindingserver

import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords
import io.ktor.http.Parameters

fun ActionCoords(
parameters: Parameters,
extractVersion: Boolean,
): ActionCoords {
val owner = parameters["owner"]!!
val nameAndPath = parameters["name"]!!.split("__")
val name = nameAndPath.first()
val path = nameAndPath.drop(1).joinToString("/").takeUnless { it.isBlank() }
val version = if (extractVersion) parameters["version"]!! else "irrelevant"

return ActionCoords(owner, name, version, path)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package io.github.typesafegithub.workflows.jitbindingserver

import io.github.reactivecircus.cache4k.Cache
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.prettyPrint
import io.github.typesafegithub.workflows.mavenbinding.Artifact
import io.github.typesafegithub.workflows.mavenbinding.JarArtifact
import io.github.typesafegithub.workflows.mavenbinding.TextArtifact
import io.github.typesafegithub.workflows.mavenbinding.buildVersionArtifacts
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respondBytes
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.routing.head
import io.ktor.server.routing.route
import kotlin.time.Duration.Companion.hours

typealias ArtifactResult = Result<Map<String, Artifact>>

val bindingsCache = Cache.Builder<ActionCoords, ArtifactResult>().expireAfterWrite(1.hours).build()

fun Routing.artifactRoute() {
route("{owner}/{name}/{version}/{file}") {
artifact(refresh = false)
}

route("/refresh/{owner}/{name}/{version}/{file}") {
artifact(refresh = true)
}
}

fun Route.artifact(refresh: Boolean = false) {
headArtifact(refresh)
getArtifact(refresh)
}

private fun Route.headArtifact(refresh: Boolean) {
head {
val bindingArtifacts = call.toBindingArtifacts(refresh) ?: return@head call.respondNotFound()

val file = call.parameters["file"] ?: return@head call.respondNotFound()

if (file in bindingArtifacts) {
call.respondText("Exists", status = HttpStatusCode.OK)
} else {
call.respondNotFound()
}
}
}

private fun Route.getArtifact(refresh: Boolean) {
get {
val bindingArtifacts = call.toBindingArtifacts(refresh) ?: return@get call.respondNotFound()

if (refresh && !deliverOnRefreshRoute) return@get call.respondText("OK")

val file = call.parameters["file"] ?: return@get call.respondNotFound()

val artifact = bindingArtifacts[file] ?: return@get call.respondNotFound()

when (artifact) {
is TextArtifact -> call.respondText(artifact.data())
is JarArtifact -> call.respondBytes(artifact.data(), ContentType.parse("application/java-archive"))
else -> call.respondNotFound()
}
}
}

private suspend fun ApplicationCall.toBindingArtifacts(refresh: Boolean): Map<String, Artifact>? {
val actionCoords = ActionCoords(parameters, true)

logger.info { "➡️ Requesting ${actionCoords.prettyPrint}" }
return if (refresh) {
actionCoords.buildVersionArtifacts().also {
bindingsCache.put(actionCoords, runCatching { it!! })
}
} else {
bindingsCache.get(actionCoords) { runCatching { actionCoords.buildVersionArtifacts()!! } }.getOrNull()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.typesafegithub.workflows.jitbindingserver

import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get

fun Routing.internalRoutes() {
get("/status") {
call.respondText("OK")
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,14 @@
package io.github.typesafegithub.workflows.jitbindingserver

import io.github.oshai.kotlinlogging.KotlinLogging.logger
import io.github.reactivecircus.cache4k.Cache
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.prettyPrint
import io.github.typesafegithub.workflows.mavenbinding.Artifact
import io.github.typesafegithub.workflows.mavenbinding.JarArtifact
import io.github.typesafegithub.workflows.mavenbinding.TextArtifact
import io.github.typesafegithub.workflows.mavenbinding.buildPackageArtifacts
import io.github.typesafegithub.workflows.mavenbinding.buildVersionArtifacts
import io.github.typesafegithub.workflows.shared.internal.getGithubToken
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders.XRequestId
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.callid.CallId
import io.ktor.server.plugins.callid.callIdMdc
import io.ktor.server.plugins.callid.generate
import io.ktor.server.plugins.calllogging.CallLogging
import io.ktor.server.response.respondBytes
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.head
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.opentelemetry.instrumentation.ktor.v3_0.server.KtorServerTracing
import kotlin.time.Duration.Companion.hours

private val logger =
val logger =
System
/*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Expand All @@ -47,161 +24,18 @@ fun main() {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
logger.error(throwable) { "Uncaught exception in thread $thread" }
}

val bindingsCache =
Cache
.Builder<ActionCoords, Result<Map<String, Artifact>>>()
.expireAfterWrite(1.hours)
.build()
val openTelemetry = buildOpenTelemetryConfig(serviceName = "github-actions-bindings")

embeddedServer(Netty, port = 8080) {
install(CallId) {
generate(
length = 15,
dictionary = "abcdefghijklmnopqrstuvwxyz0123456789",
)
replyToHeader(XRequestId)
}
install(CallLogging) {
callIdMdc("request-id")
}
install(KtorServerTracing) {
setOpenTelemetry(openTelemetry)
}
routing {
route("{owner}/{name}/{version}/{file}") {
artifact(bindingsCache)
}

route("{owner}/{name}/{file}") {
metadata()
}

route("/refresh") {
route("{owner}/{name}/{version}/{file}") {
artifact(bindingsCache, refresh = true)
}
installPlugins()

route("{owner}/{name}/{file}") {
metadata(refresh = true)
}
}
routing {
internalRoutes()

get("/status") {
call.respondText("OK")
}
artifactRoute()
metadataRoute()
}
}.start(wait = true)
}

private fun Route.metadata(refresh: Boolean = false) {
get {
if (refresh && !deliverOnRefreshRoute) {
call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
return@get
}

val owner = call.parameters["owner"]!!
val nameAndPath = call.parameters["name"]!!.split("__")
val name = nameAndPath.first()
val file = call.parameters["file"]!!
val actionCoords =
ActionCoords(
owner = owner,
name = name,
version = "irrelevant",
path = nameAndPath.drop(1).joinToString("/").takeUnless { it.isBlank() },
)
val bindingArtifacts = actionCoords.buildPackageArtifacts(githubToken = getGithubToken())
if (file in bindingArtifacts) {
when (val artifact = bindingArtifacts[file]) {
is String -> call.respondText(artifact)
else -> call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
}
} else {
call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
}
}
}

private fun Route.artifact(
bindingsCache: Cache<ActionCoords, Result<Map<String, Artifact>>>,
refresh: Boolean = false,
) {
get {
val bindingArtifacts = call.toBindingArtifacts(bindingsCache, refresh)
if (bindingArtifacts == null) {
call.respondText("Not found", status = HttpStatusCode.NotFound)
return@get
} else if (refresh && !deliverOnRefreshRoute) {
call.respondText(text = "OK")
return@get
}

val file = call.parameters["file"]!!
if (file in bindingArtifacts) {
when (val artifact = bindingArtifacts[file]) {
is TextArtifact -> call.respondText(text = artifact.data())
is JarArtifact ->
call.respondBytes(
bytes = artifact.data(),
contentType = ContentType.parse("application/java-archive"),
)

else -> call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
}
} else {
call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
}
}

head {
val bindingArtifacts = call.toBindingArtifacts(bindingsCache, refresh)
val file = call.parameters["file"]!!
if (bindingArtifacts == null) {
call.respondText("Not found", status = HttpStatusCode.NotFound)
return@head
}
if (file in bindingArtifacts) {
call.respondText("Exists", status = HttpStatusCode.OK)
} else {
call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
}
}
}

private suspend fun ApplicationCall.toBindingArtifacts(
bindingsCache: Cache<ActionCoords, Result<Map<String, Artifact>>>,
refresh: Boolean,
): Map<String, Artifact>? {
val owner = parameters["owner"]!!
val nameAndPath = parameters["name"]!!.split("__")
val name = nameAndPath.first()
val version = parameters["version"]!!
val actionCoords =
ActionCoords(
owner = owner,
name = name,
version = version,
path = nameAndPath.drop(1).joinToString("/").takeUnless { it.isBlank() },
)
logger.info { "➡️ Requesting ${actionCoords.prettyPrint}" }
val bindingArtifacts =
if (refresh) {
actionCoords.buildVersionArtifacts().also {
bindingsCache.put(actionCoords, Result.of(it))
}
} else {
bindingsCache
.get(actionCoords) { Result.of(actionCoords.buildVersionArtifacts()) }
.getOrNull()
}
return bindingArtifacts
}

private fun Result.Companion.failure(): Result<Nothing> = failure(object : Throwable() {})

private fun <T> Result.Companion.of(value: T?): Result<T> = value?.let { success(it) } ?: failure()
val deliverOnRefreshRoute = System.getenv("GWKT_DELIVER_ON_REFRESH").toBoolean()

private val deliverOnRefreshRoute = System.getenv("GWKT_DELIVER_ON_REFRESH").toBoolean()
suspend fun ApplicationCall.respondNotFound() = respondText("Not found", status = HttpStatusCode.NotFound)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.typesafegithub.workflows.jitbindingserver

import io.github.typesafegithub.workflows.mavenbinding.buildPackageArtifacts
import io.github.typesafegithub.workflows.shared.internal.getGithubToken
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.routing.route

fun Routing.metadataRoute() {
route("{owner}/{name}/{file}") {
metadata()
}

route("/refresh/{owner}/{name}/{file}") {
metadata(true)
}
}

fun Route.metadata(refresh: Boolean = false) {
get {
if (refresh && !deliverOnRefreshRoute) return@get call.respondNotFound()

val file = call.parameters["file"] ?: return@get call.respondNotFound()
val actionCoords = ActionCoords(call.parameters, false)

val bindingArtifacts = actionCoords.buildPackageArtifacts(githubToken = getGithubToken())
if (file in bindingArtifacts) {
when (val artifact = bindingArtifacts[file]) {
is String -> call.respondText(artifact)
else -> call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
}
} else {
call.respondText(text = "Not found", status = HttpStatusCode.NotFound)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor
import java.util.concurrent.TimeUnit

private val logger = logger { }

/*
How to test locally:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.typesafegithub.workflows.jitbindingserver

import io.ktor.http.HttpHeaders
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.callid.CallId
import io.ktor.server.plugins.callid.callIdMdc
import io.ktor.server.plugins.callid.generate
import io.ktor.server.plugins.calllogging.CallLogging
import io.opentelemetry.instrumentation.ktor.v3_0.server.KtorServerTracing

fun Application.installPlugins() {
install(CallId) {
generate(15, "abcdefghijklmnopqrstuvwxyz0123456789")
replyToHeader(HttpHeaders.XRequestId)
}

install(CallLogging) {
callIdMdc("request-id")
}

install(KtorServerTracing) {
setOpenTelemetry(buildOpenTelemetryConfig("github-actions-bindings"))
}
}

0 comments on commit 907804d

Please sign in to comment.