From 907804dcd21a63e09e24afb6f951c082bee462cc Mon Sep 17 00:00:00 2001 From: Leonardo Colman Lopes Date: Tue, 11 Feb 2025 13:06:23 -0300 Subject: [PATCH] refactor(server): Restructure JitBindingServer --- .../jitbindingserver/ActionCoords.kt | 17 ++ .../jitbindingserver/ArtifactRoute.kt | 84 ++++++++ .../jitbindingserver/InternalRoutes.kt | 11 ++ .../workflows/jitbindingserver/Main.kt | 182 +----------------- .../jitbindingserver/MetadataRoute.kt | 39 ++++ .../jitbindingserver/OpenTelemetryConfig.kt | 2 - .../workflows/jitbindingserver/Plugins.kt | 25 +++ 7 files changed, 184 insertions(+), 176 deletions(-) create mode 100644 jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt create mode 100644 jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoute.kt create mode 100644 jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt create mode 100644 jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoute.kt create mode 100644 jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Plugins.kt diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt new file mode 100644 index 000000000..17451c59f --- /dev/null +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt @@ -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) +} diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoute.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoute.kt new file mode 100644 index 000000000..0d3b7c685 --- /dev/null +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoute.kt @@ -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> + +val bindingsCache = Cache.Builder().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? { + 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() + } +} diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt new file mode 100644 index 000000000..75b2f0c8a --- /dev/null +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt @@ -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") + } +} diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt index e07fdbd86..be8e49f2a 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt @@ -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 /* * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -47,161 +24,18 @@ fun main() { Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> logger.error(throwable) { "Uncaught exception in thread $thread" } } - - val bindingsCache = - Cache - .Builder>>() - .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>>, - 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>>, - refresh: Boolean, -): Map? { - 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 = failure(object : Throwable() {}) - -private fun Result.Companion.of(value: T?): Result = 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) diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoute.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoute.kt new file mode 100644 index 000000000..019888a41 --- /dev/null +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoute.kt @@ -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) + } + } +} diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/OpenTelemetryConfig.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/OpenTelemetryConfig.kt index 3002b7f2f..6196bd4eb 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/OpenTelemetryConfig.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/OpenTelemetryConfig.kt @@ -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: diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Plugins.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Plugins.kt new file mode 100644 index 000000000..97cc7daa0 --- /dev/null +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Plugins.kt @@ -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")) + } +}