Skip to content

Commit

Permalink
Public version endpoint (#158)
Browse files Browse the repository at this point in the history
Refactoring routes, add public version endpoint

- Refactors code around routes with the goal to open it for extension
  and make it better testable
- Add tests for all provided routes
- Add a public `/version` endpoint returning version information for
  this service
- Use `/api/search` as a url path prefix for the public routes, so
  querying will be `/api/search/query` and version is
  `/api/search/version`
- This goes together with a change in gateway to pass the full request
  path
- The old way, `/search`, is kept alive to have an easier transition
- Additionally provide the same under `/search/query` so clients (ui)
  can already adopt to the new path even when gateway hasn't been
  updated
- This "old" behaviour is in separate "legacy" classes and can be
  removed later
- Log request and responses to the public api, but not to internal
  endpoints; allowing to be extended with more middlewares if
  necessary
  • Loading branch information
eikek authored Jun 20, 2024
1 parent 5531675 commit 87ba0f9
Show file tree
Hide file tree
Showing 16 changed files with 706 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,25 @@ import fs2.io.net.Network
import org.http4s.HttpRoutes
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Server
import org.http4s.HttpApp

object HttpServer:

def build[F[_]: Async: Network](
routes: HttpRoutes[F],
config: HttpServerConfig
): Resource[F, Server] =
def apply[F[_]: Async: Network](config: HttpServerConfig): EmberServerBuilder[F] =
EmberServerBuilder
.default[F]
.withHost(config.bindAddress)
.withPort(config.port)
.withShutdownTimeout(config.shutdownTimeout)
.withHttpApp(routes.orNotFound)
.build

def build[F[_]: Async: Network](
routes: HttpRoutes[F],
config: HttpServerConfig
): Resource[F, Server] =
apply[F](config).withHttpApp(routes.orNotFound).build

def buildApp[F[_]: Async: Network](
app: HttpApp[F],
config: HttpServerConfig
): Resource[F, Server] =
apply[F](config).withHttpApp(app).build
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ import io.renku.search.common.CurrentVersion
import io.renku.search.http.borer.TapirBorerJson

object OperationRoutes extends TapirBorerJson {
private def pingEndpoint[F[_]: Async] =
def pingEndpoint[F[_]: Async] =
endpoint.get
.in("ping")
.out(stringBody)
.description("Ping")
.serverLogic[F](_ => "pong".asRight[Unit].pure[F])
.serverLogicSuccess[F](_ => "pong".pure[F])

private given Schema[CurrentVersion] = Schema.derived

private def versionEndpoint[F[_]: Async] =
def versionEndpoint[F[_]: Async] =
endpoint.get
.in("version")
.out(borerJsonBody[CurrentVersion])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,28 @@

package io.renku.search.api

import cats.effect.{ExitCode, IO, IOApp}
import cats.effect.*

import io.renku.logging.LoggingSetup
import io.renku.search.http.HttpServer

object Microservice extends IOApp:
private val logger = scribe.cats.io
private val loadConfig = SearchApiConfig.config.load[IO]
val pathPrefix = List("api", "search")

override def run(args: List[String]): IO[ExitCode] =
createServer.useForever

def createServer =
for {
config <- loadConfig
_ <- IO(LoggingSetup.doConfigure(config.verbosity))
_ <- Routes[IO](config.solrConfig, config.jwtVerifyConfig).makeRoutes
.flatMap(HttpServer.build(_, config.httpServerConfig))
.use { _ =>
logger.info(
s"Search microservice running: ${config.httpServerConfig}"
) >> IO.never
}
} yield ExitCode.Success
config <- Resource.eval(loadConfig)
_ <- Resource.eval(IO(LoggingSetup.doConfigure(config.verbosity)))
logger <- Resource.pure(scribe.cats.io)
app <- ServiceRoutes[IO](config, pathPrefix)
server <- SearchServer.create[IO](config, app)
_ <- Resource.eval(
logger.info(
s"Search microservice running: ${config.httpServerConfig}"
)
)
} yield server
87 changes: 0 additions & 87 deletions modules/search-api/src/main/scala/io/renku/search/api/Routes.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2024 Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.renku.search.api

import cats.effect.*
import cats.syntax.all.*
import fs2.io.net.Network

import io.renku.search.api.routes.OpenApiLegacyRoute
import io.renku.search.http.HttpServer
import io.renku.search.http.metrics.MetricsRoutes
import io.renku.search.http.routes.OperationRoutes
import io.renku.search.metrics.CollectorRegistryBuilder
import org.http4s.HttpRoutes
import org.http4s.server.middleware.ResponseLogger
import org.http4s.server.middleware.{RequestId, RequestLogger}
import scribe.Scribe

object SearchServer:
def create[F[_]: Async: Network](config: SearchApiConfig, app: ServiceRoutes[F]) =
for
routes <- makeHttpRoutes(app)
server <- HttpServer[F](config.httpServerConfig)
.withHttpApp(routes.orNotFound)
.build
yield server

def makeHttpRoutes[F[_]: Async](
app: ServiceRoutes[F]
): Resource[F, HttpRoutes[F]] =
val openApiLegacy = OpenApiLegacyRoute(app.docEndpoints).routes
val opRoutes = OperationRoutes[F]
val metrics = MetricsRoutes[F](CollectorRegistryBuilder[F].withJVMMetrics)
for
logger <- Resource.pure(scribe.cats.effect[F])
businessRoutes <- metrics.makeRoutes(app.routes)
routes = List(
app.openapiDocRoutes,
openApiLegacy,
withMiddleware(logger, businessRoutes),
opRoutes
).reduce(_ <+> _)
yield routes

def withMiddleware[F[_]: Async](
logger: Scribe[F],
httpRoutes: HttpRoutes[F]
): HttpRoutes[F] =
middleWares[F](logger).foldLeft(httpRoutes)((r, mf) => mf(r))

private def middleWares[F[_]: Async](
logger: Scribe[F]
): List[HttpRoutes[F] => HttpRoutes[F]] =
val log = (str: String) => logger.info(str)
List(
RequestLogger
.httpRoutes(logHeaders = true, logBody = false, logAction = log.some),
RequestId.httpRoutes[F],
ResponseLogger.httpRoutes(
logHeaders = true,
logBody = false,
logAction = log.some
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2024 Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.renku.search.api

import cats.effect.{Async, Resource}
import cats.syntax.all.*
import fs2.io.net.Network

import io.renku.openid.keycloak.JwtVerify
import io.renku.search.api.auth.Authenticate
import io.renku.search.api.routes.*
import io.renku.search.http.ClientBuilder
import io.renku.search.http.RetryConfig
import org.http4s.HttpRoutes
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.server.Router

/** Defines the routes for the whole search service */
trait ServiceRoutes[F[_]] extends RoutesDefinition[F]:
def config: SearchApiConfig
def pathPrefix: List[String]
def openapiDocRoutes: HttpRoutes[F]

object ServiceRoutes:
def apply[F[_]: Async: Network](
cfg: SearchApiConfig,
prefix: List[String]
): Resource[F, ServiceRoutes[F]] =
for
logger <- Resource.pure(scribe.cats.effect[F])
httpClient <- ClientBuilder(EmberClientBuilder.default[F])
.withDefaultRetry(RetryConfig.default)
.withLogging(logBody = false, logger)
.build

searchApi <- SearchApi[F](cfg.solrConfig)
jwtVerify <- Resource.eval(JwtVerify(httpClient, cfg.jwtVerifyConfig))
authenticate = Authenticate[F](jwtVerify, logger)

routeDefs = List(
SearchRoutes(searchApi, authenticate, Nil),
VersionRoute[F](Nil)
)
legacyDefs = List(
SearchLegacyRoutes(searchApi, authenticate, Nil)
)
yield new ServiceRoutes[F] {
override val config = cfg
override val pathPrefix = prefix
private val pathPrefixStr = prefix.mkString("/", "/", "")

override val docEndpoints =
(routeDefs ++ legacyDefs).map(_.docEndpoints).reduce(_ ++ _)
override val openapiDocRoutes: HttpRoutes[F] =
OpenApiRoute(docEndpoints, prefix).routes
override val routes = Router(
pathPrefixStr -> routeDefs.map(_.routes).reduce(_ <+> _),
"/search/query" -> legacyDefs.map(_.routes).reduce(_ <+> _),
"/search" -> legacyDefs.map(_.routes).reduce(_ <+> _)
)
}
Loading

0 comments on commit 87ba0f9

Please sign in to comment.