diff --git a/build.gradle.kts b/build.gradle.kts index 7be152a..bf73426 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ val quarkusPlatformArtifactId: String by project val quarkusPlatformVersion: String by project dependencies { + implementation("io.quarkus:quarkus-smallrye-fault-tolerance") implementation(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion")) implementation("io.quarkus:quarkus-config-yaml") implementation("io.quarkus:quarkus-scheduler") @@ -31,7 +32,6 @@ dependencies { implementation("io.quarkus:quarkus-smallrye-health") implementation("io.quarkus:quarkus-rest-client-reactive-jackson") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("io.quarkus:quarkus-container-image-jib") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("io.quarkus:quarkus-arc") implementation("io.quarkus:quarkus-hibernate-orm") diff --git a/gradle.properties b/gradle.properties index 5f4a1ba..a1fd1f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ #Gradle properties +#Sun Nov 19 14:11:53 CET 2023 +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformVersion=3.5.0 quarkusPluginId=io.quarkus quarkusPluginVersion=3.5.0 -quarkusPlatformGroupId=io.quarkus.platform -quarkusPlatformArtifactId=quarkus-bom -quarkusPlatformVersion=3.5.0 \ No newline at end of file diff --git a/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysApiAdapter.kt b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysApiAdapter.kt new file mode 100644 index 0000000..cb5729b --- /dev/null +++ b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysApiAdapter.kt @@ -0,0 +1,87 @@ +package com.faforever.icebreaker.service.xirsys + +import com.faforever.icebreaker.config.FafProperties +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import jakarta.inject.Singleton +import org.eclipse.microprofile.faulttolerance.Retry +import org.eclipse.microprofile.rest.client.RestClientBuilder +import java.io.IOException +import java.net.URI + +/** + * The Xirsys API does not comply to proper REST standards, thus we need an adapter. + */ +@Singleton +class XirsysApiAdapter( + private val fafProperties: FafProperties, + private val xirsysProperties: XirsysProperties, + private val objectMapper: ObjectMapper, +) { + private val xirsysApiClient: XirsysApiClient = RestClientBuilder.newBuilder() + .baseUri(URI.create(xirsysProperties.baseUrl())) + .register( + BasicAuthenticationRequestFilter( + username = xirsysProperties.ident(), + password = xirsysProperties.secret(), + ), + ) + .build(XirsysApiClient::class.java) + + @Retry + fun listChannel(): List = + parseAndUnwrap { + xirsysApiClient.listChannel( + namespace = xirsysProperties.channelNamespace(), + environment = fafProperties.environment(), + ) + } + + @Retry + fun createChannel(channelName: String): Map = + parseAndUnwrap { + xirsysApiClient.createChannel( + namespace = xirsysProperties.channelNamespace(), + environment = fafProperties.environment(), + channelName = channelName, + ) + } + + @Retry + fun deleteChannel(channelName: String): Int = + parseAndUnwrap { + xirsysApiClient.deleteChannel( + namespace = xirsysProperties.channelNamespace(), + environment = fafProperties.environment(), + channelName = channelName, + ) + } + + @Retry + fun requestIceServers(channelName: String, turnRequest: TurnRequest = TurnRequest()): TurnResponse = + parseAndUnwrap { + xirsysApiClient.requestIceServers( + namespace = xirsysProperties.channelNamespace(), + environment = fafProperties.environment(), + channelName = channelName, + turnRequest = turnRequest, + ) + } + + @Throws(IOException::class) + private inline fun parseAndUnwrap(getResponse: () -> String): T { + val response = getResponse() + return try { + when (val result = objectMapper.readValue>(response)) { + is XirsysResponse.Error -> throw XirsysSpecifiedApiException( + errorCode = result.code, + message = "Listing sessions failed: ${result.code}", + ) + + is XirsysResponse.Success -> result.data + } + } catch (e: IOException) { + throw XirsysUnspecifiedApiException(errorResponse = response, cause = e) + } + } +} diff --git a/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysClient.kt b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysApiClient.kt similarity index 90% rename from src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysClient.kt rename to src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysApiClient.kt index 16809cd..400bd6f 100644 --- a/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysClient.kt +++ b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysApiClient.kt @@ -14,14 +14,14 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient @ApplicationScoped @RegisterRestClient @Consumes(MediaType.APPLICATION_JSON) -interface XirsysClient { +interface XirsysApiClient { @GET @Path("/_ns/{namespace}/{environment}") @ClientQueryParam(name = "depth", value = ["10"]) fun listChannel( @PathParam("namespace") namespace: String, @PathParam("environment") environment: String, - ): XirsysResponse> + ): String @PUT @Path("/_ns/{namespace}/{environment}/{channelName}") @@ -29,7 +29,7 @@ interface XirsysClient { @PathParam("namespace") namespace: String, @PathParam("environment") environment: String, @PathParam("channelName") channelName: String, - ): XirsysResponse> + ): String @DELETE @Path("/_ns/{namespace}/{environment}/{channelName}") @@ -37,7 +37,7 @@ interface XirsysClient { @PathParam("namespace") namespace: String, @PathParam("environment") environment: String, @PathParam("channelName") channelName: String, - ): XirsysResponse + ): String @PUT @Path("/_turn/{namespace}/{environment}/{channelName}") @@ -46,5 +46,5 @@ interface XirsysClient { @PathParam("environment") environment: String, @PathParam("channelName") channelName: String, turnRequest: TurnRequest, - ): XirsysResponse + ): String } diff --git a/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysSessionHandler.kt b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysSessionHandler.kt index f772546..ab69adc 100644 --- a/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysSessionHandler.kt +++ b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysSessionHandler.kt @@ -1,36 +1,23 @@ package com.faforever.icebreaker.service.xirsys -import com.faforever.icebreaker.config.FafProperties import com.faforever.icebreaker.service.Server import com.faforever.icebreaker.service.Session import com.faforever.icebreaker.service.SessionHandler import jakarta.inject.Singleton -import org.eclipse.microprofile.rest.client.RestClientBuilder import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.net.URI private val LOG: Logger = LoggerFactory.getLogger(XirsysSessionHandler::class.java) @Singleton class XirsysSessionHandler( - private val fafProperties: FafProperties, - private val xirsysProperties: XirsysProperties, + xirsysProperties: XirsysProperties, + private val xirsysApiAdapter: XirsysApiAdapter, ) : SessionHandler { companion object { const val SERVER_NAME = "xirsys.com" } - private val xirsysClient: XirsysClient = RestClientBuilder.newBuilder() - .baseUri(URI.create(xirsysProperties.baseUrl())) - .register( - BasicAuthenticationRequestFilter( - username = xirsysProperties.ident(), - password = xirsysProperties.secret(), - ), - ) - .build(XirsysClient::class.java) - override val active = xirsysProperties.enabled() override fun createSession(id: String) { @@ -41,72 +28,34 @@ class XirsysSessionHandler( LOG.debug("Creating session id $id") - val result = xirsysClient.createChannel( - namespace = xirsysProperties.channelNamespace(), - environment = fafProperties.environment(), - channelName = id, - ) - - if (result is XirsysResponse.Error) { - LOG.error("Creating session failed: ${result.code}") - } + xirsysApiAdapter.createChannel(id) } override fun deleteSession(id: String) { - val result = xirsysClient.deleteChannel( - namespace = xirsysProperties.channelNamespace(), - environment = fafProperties.environment(), - channelName = id, - ) - - if (result is XirsysResponse.Error) { - LOG.error("Deleting session failed: ${result.code}") - } + xirsysApiAdapter.deleteChannel(channelName = id) } - private fun listSessions(): List = - when ( - val result = xirsysClient.listChannel( - namespace = xirsysProperties.channelNamespace(), - environment = fafProperties.environment(), - ) - ) { - is XirsysResponse.Error -> emptyList().also { - LOG.error("Listing sessions failed: ${result.code}") - } - - is XirsysResponse.Success -> result.data - } + private fun listSessions(): List = xirsysApiAdapter.listChannel() override fun getIceServers() = listOf(Server(id = SERVER_NAME, region = "Global")) override fun getIceServersSession(sessionId: String): List = - when ( - val result = xirsysClient.requestIceServers( - namespace = xirsysProperties.channelNamespace(), - environment = fafProperties.environment(), - channelName = sessionId, - turnRequest = TurnRequest(), + xirsysApiAdapter.requestIceServers( + channelName = sessionId, + turnRequest = TurnRequest(), + ).iceServers.let { + listOf( + Session.Server( + id = SERVER_NAME, + username = it.username, + credential = it.credential, + urls = it.urls.map { url -> + // A sample response looks like "stun:fr-turn1.xirsys.com" + // The java URI class fails to read host and port due to the missing // after the : + // Thus we "normalize" the uri, even though it is technically valid + url.replaceFirst(":", "://") + }, + ), ) - ) { - is XirsysResponse.Error -> emptyList().also { - LOG.error("Requesting ICE servers failed: ${result.code}") - } - - is XirsysResponse.Success -> result.data.iceServers.let { - listOf( - Session.Server( - id = SERVER_NAME, - username = it.username, - credential = it.credential, - urls = it.urls.map { url -> - // A sample response looks like "stun:fr-turn1.xirsys.com" - // The java URI class fails to read host and port due to the missing // after the : - // Thus we "normalize" the uri, even though it is technically valid - url.replaceFirst(":", "://") - }, - ), - ) - } } } diff --git a/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysSpecifiedApiException.kt b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysSpecifiedApiException.kt new file mode 100644 index 0000000..61309ee --- /dev/null +++ b/src/main/kotlin/com/faforever/icebreaker/service/xirsys/XirsysSpecifiedApiException.kt @@ -0,0 +1,10 @@ +package com.faforever.icebreaker.service.xirsys + +import java.io.IOException + +interface XirsysApiException + +class XirsysSpecifiedApiException(val errorCode: String, override val message: String) : + IOException("Xirsys API responded with error code: $errorCode"), XirsysApiException +class XirsysUnspecifiedApiException(val errorResponse: String, cause: Exception? = null) : + IOException("Xirsys API failed with unparseable message: $errorResponse", cause), XirsysApiException