fix(official-bots): NCS-70-auto-register official bots with account service #59
@@ -49,3 +49,5 @@ case class RotatedTokenDto(token: String)
|
|||||||
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
|
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
|
||||||
|
|
||||||
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
|
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
|
||||||
|
|
||||||
|
case class SyncOfficialBotsRequest(bots: List[String])
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ class OfficialBotAccountRepository:
|
|||||||
def findAll(): List[OfficialBotAccount] =
|
def findAll(): List[OfficialBotAccount] =
|
||||||
em.createQuery("FROM OfficialBotAccount", classOf[OfficialBotAccount]).getResultList.asScala.toList
|
em.createQuery("FROM OfficialBotAccount", classOf[OfficialBotAccount]).getResultList.asScala.toList
|
||||||
|
|
||||||
|
def findByName(name: String): Option[OfficialBotAccount] =
|
||||||
|
em.createQuery("FROM OfficialBotAccount WHERE name = :name", classOf[OfficialBotAccount])
|
||||||
|
.setParameter("name", name)
|
||||||
|
.getResultList
|
||||||
|
.asScala
|
||||||
|
.headOption
|
||||||
|
|
||||||
def persist(bot: OfficialBotAccount): OfficialBotAccount =
|
def persist(bot: OfficialBotAccount): OfficialBotAccount =
|
||||||
em.persist(bot)
|
em.persist(bot)
|
||||||
bot
|
bot
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
|
|||||||
import de.nowchess.account.dto.*
|
import de.nowchess.account.dto.*
|
||||||
import de.nowchess.account.error.AccountError
|
import de.nowchess.account.error.AccountError
|
||||||
import de.nowchess.account.service.AccountService
|
import de.nowchess.account.service.AccountService
|
||||||
|
import de.nowchess.security.InternalOnly
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
@@ -179,6 +180,13 @@ class AccountResource:
|
|||||||
createdAt = bot.createdAt.toString,
|
createdAt = bot.createdAt.toString,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/official-bots/sync")
|
||||||
|
@InternalOnly
|
||||||
|
def syncOfficialBots(req: SyncOfficialBotsRequest): Response =
|
||||||
|
accountService.syncOfficialBots(req.bots)
|
||||||
|
Response.noContent().build()
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/official-bots")
|
@Path("/official-bots")
|
||||||
def getOfficialBots: Response =
|
def getOfficialBots: Response =
|
||||||
|
|||||||
@@ -206,6 +206,17 @@ class AccountService:
|
|||||||
officialBotAccountRepository.persist(bot)
|
officialBotAccountRepository.persist(bot)
|
||||||
Right(bot)
|
Right(bot)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def syncOfficialBots(botNames: List[String]): Unit =
|
||||||
|
botNames.foreach { name =>
|
||||||
|
if officialBotAccountRepository.findByName(name).isEmpty then
|
||||||
|
val bot = new OfficialBotAccount()
|
||||||
|
bot.name = name
|
||||||
|
bot.createdAt = Instant.now()
|
||||||
|
officialBotAccountRepository.persist(bot)
|
||||||
|
log.infof("Auto-registered official bot: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
def getOfficialBotAccounts(): List[OfficialBotAccount] =
|
def getOfficialBotAccounts(): List[OfficialBotAccount] =
|
||||||
officialBotAccountRepository.findAll()
|
officialBotAccountRepository.findAll()
|
||||||
|
|
||||||
|
|||||||
@@ -154,3 +154,35 @@ class AccountResourceTest:
|
|||||||
.post("/api/account/refresh")
|
.post("/api/account/refresh")
|
||||||
.`then`()
|
.`then`()
|
||||||
.statusCode(401)
|
.statusCode(401)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def syncOfficialBotsCreatesNewBots(): Unit =
|
||||||
|
givenRequest()
|
||||||
|
.body("""{"bots":["sync-easy","sync-hard"]}""")
|
||||||
|
.when()
|
||||||
|
.post("/api/account/official-bots/sync")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(204)
|
||||||
|
RestAssured
|
||||||
|
.`given`()
|
||||||
|
.when()
|
||||||
|
.get("/api/account/official-bots")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("name", hasItems("sync-easy", "sync-hard"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def syncOfficialBotsIsIdempotent(): Unit =
|
||||||
|
val body = """{"bots":["idempotent-bot"]}"""
|
||||||
|
givenRequest()
|
||||||
|
.body(body)
|
||||||
|
.when()
|
||||||
|
.post("/api/account/official-bots/sync")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(204)
|
||||||
|
givenRequest()
|
||||||
|
.body(body)
|
||||||
|
.when()
|
||||||
|
.post("/api/account/official-bots/sync")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(204)
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ dependencies {
|
|||||||
implementation(project(":modules:api"))
|
implementation(project(":modules:api"))
|
||||||
implementation(project(":modules:io"))
|
implementation(project(":modules:io"))
|
||||||
implementation(project(":modules:rule"))
|
implementation(project(":modules:rule"))
|
||||||
|
implementation(project(":modules:security"))
|
||||||
|
implementation("io.quarkus:quarkus-rest-client-jackson")
|
||||||
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
|
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
|
||||||
implementation("io.quarkus:quarkus-redis-client")
|
implementation("io.quarkus:quarkus-redis-client")
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ quarkus:
|
|||||||
name: nowchess-official-bots
|
name: nowchess-official-bots
|
||||||
redis:
|
redis:
|
||||||
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
|
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
|
||||||
|
rest-client:
|
||||||
|
account-service:
|
||||||
|
url: http://localhost:8083
|
||||||
smallrye-jwt:
|
smallrye-jwt:
|
||||||
enabled: true
|
enabled: true
|
||||||
log:
|
log:
|
||||||
@@ -15,6 +18,8 @@ nowchess:
|
|||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
prefix: nowchess
|
prefix: nowchess
|
||||||
|
internal:
|
||||||
|
secret: 123abc
|
||||||
|
|
||||||
"%deployed":
|
"%deployed":
|
||||||
quarkus:
|
quarkus:
|
||||||
@@ -28,8 +33,13 @@ nowchess:
|
|||||||
exporter:
|
exporter:
|
||||||
otlp:
|
otlp:
|
||||||
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
||||||
|
rest-client:
|
||||||
|
account-service:
|
||||||
|
url: ${ACCOUNT_SERVICE_URL}
|
||||||
nowchess:
|
nowchess:
|
||||||
redis:
|
redis:
|
||||||
host: ${REDIS_HOST:localhost}
|
host: ${REDIS_HOST:localhost}
|
||||||
port: ${REDIS_PORT:6379}
|
port: ${REDIS_PORT:6379}
|
||||||
prefix: ${REDIS_PREFIX:nowchess}
|
prefix: ${REDIS_PREFIX:nowchess}
|
||||||
|
internal:
|
||||||
|
secret: ${INTERNAL_SECRET}
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package de.nowchess.bot.client
|
||||||
|
|
||||||
|
import de.nowchess.security.{InternalClientHeadersFactory, InternalSecretClientFilter}
|
||||||
|
import jakarta.ws.rs.*
|
||||||
|
import jakarta.ws.rs.core.MediaType
|
||||||
|
import org.eclipse.microprofile.rest.client.annotation.{RegisterClientHeaders, RegisterProvider}
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||||
|
|
||||||
|
case class SyncOfficialBotsRequest(bots: List[String])
|
||||||
|
|
||||||
|
@Path("/api/account/official-bots")
|
||||||
|
@RegisterRestClient(configKey = "account-service")
|
||||||
|
@RegisterProvider(classOf[InternalSecretClientFilter])
|
||||||
|
@RegisterClientHeaders(classOf[InternalClientHeadersFactory])
|
||||||
|
trait AccountServiceClient:
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/sync")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def syncBots(req: SyncOfficialBotsRequest): Unit
|
||||||
+13
-1
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.bot.BotController
|
import de.nowchess.bot.BotController
|
||||||
import de.nowchess.bot.BotDifficulty
|
import de.nowchess.bot.BotDifficulty
|
||||||
|
import de.nowchess.bot.client.{AccountServiceClient, SyncOfficialBotsRequest}
|
||||||
import de.nowchess.bot.config.RedisConfig
|
import de.nowchess.bot.config.RedisConfig
|
||||||
import de.nowchess.io.fen.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
import io.micrometer.core.instrument.MeterRegistry
|
import io.micrometer.core.instrument.MeterRegistry
|
||||||
@@ -13,6 +14,8 @@ import jakarta.annotation.PostConstruct
|
|||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.enterprise.event.Observes
|
import jakarta.enterprise.event.Observes
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
|
import org.jboss.logging.Logger
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -20,12 +23,18 @@ import java.util.concurrent.TimeUnit
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
class OfficialBotService:
|
class OfficialBotService:
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[OfficialBotService])
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject var redis: RedisDataSource = uninitialized
|
@Inject var redis: RedisDataSource = uninitialized
|
||||||
@Inject var redisConfig: RedisConfig = uninitialized
|
@Inject var redisConfig: RedisConfig = uninitialized
|
||||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||||
@Inject var botController: BotController = uninitialized
|
@Inject var botController: BotController = uninitialized
|
||||||
@Inject var meterRegistry: MeterRegistry = uninitialized
|
@Inject var meterRegistry: MeterRegistry = uninitialized
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@RestClient
|
||||||
|
var accountServiceClient: AccountServiceClient = uninitialized
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
private val terminalStatuses =
|
private val terminalStatuses =
|
||||||
@@ -39,7 +48,10 @@ class OfficialBotService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def onStart(@Observes event: StartupEvent): Unit =
|
def onStart(@Observes event: StartupEvent): Unit =
|
||||||
BotController.listBots.foreach(subscribeToEventChannel)
|
val bots = BotController.listBots
|
||||||
|
try accountServiceClient.syncBots(SyncOfficialBotsRequest(bots))
|
||||||
|
catch case ex: Exception => log.errorf(ex, "Failed to auto-register official bots with account service")
|
||||||
|
bots.foreach(subscribeToEventChannel)
|
||||||
|
|
||||||
private def subscribeToEventChannel(botName: String): Unit =
|
private def subscribeToEventChannel(botName: String): Unit =
|
||||||
val handler: Consumer[String] = msg => handleBotEvent(botName, msg)
|
val handler: Consumer[String] = msg => handleBotEvent(botName, msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user