diff --git a/.scalafmt.conf b/.scalafmt.conf index 5e597a2..29851f7 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,20 +1,28 @@ -version = "3.0.7" +version = "3.8.2" runner.dialect = scala3 + align.preset = most docstrings.style = Asterisk includeNoParensInSelectChains = true -maxColumn = 100 +indent.main = 4 +indent.callSite = 4 +# Recommended, to not penalize `match` statements +# indent.matchSite = 0 +maxColumn = 120 newlines.alwaysBeforeElseAfterCurlyIf = true rewrite.rules = [ PreferCurlyFors, RedundantParens, SortModifiers, - SortImports + Imports ] -rewrite.scala3.convertToNewSyntax = false +rewrite.imports.expand = false +rewrite.imports.sort = original +rewrite.scala3.convertToNewSyntax = true +# rewrite.scala3.insertEndMarkerMinLines = 5 rewrite.scala3.removeOptionalBraces = yes diff --git a/build.sbt b/build.sbt index 5804963..6ca6efe 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,7 @@ Test / fork := true ThisBuild / licenses := Seq(("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0.html"))) ThisBuild / organization := "org.mbari" ThisBuild / organizationName := "MBARI" -ThisBuild / scalaVersion := "3.3.2" +ThisBuild / scalaVersion := "3.3.3" ThisBuild / startYear := Some(2021) // ThisBuild / version := "0.0.1" ThisBuild / versionScheme := Some("semver-spec") diff --git a/build.sh b/build.sh index 6cc0f2c..444b725 100755 --- a/build.sh +++ b/build.sh @@ -19,4 +19,4 @@ else sbt 'Docker / publish' fi -# -t mbari/raziel:latest \ \ No newline at end of file +# docker buildx build -t mbari/raziel:latest \ \ No newline at end of file diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 86e9d49..c7daae1 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,30 +4,30 @@ object Dependencies { lazy val auth0 = "com.auth0" % "java-jwt" % "4.4.0" - private val circeVersion = "0.14.6" + private val circeVersion = "0.14.9" lazy val circeCore = "io.circe" %% "circe-core" % circeVersion lazy val circeGeneric = "io.circe" %% "circe-generic" % circeVersion lazy val circeParser = "io.circe" %% "circe-parser" % circeVersion lazy val jansi = "org.fusesource.jansi" % "jansi" % "2.4.1" lazy val jasypt = "org.jasypt" % "jasypt" % "1.9.3" - lazy val logback = "ch.qos.logback" % "logback-classic" % "1.5.0" + lazy val logback = "ch.qos.logback" % "logback-classic" % "1.5.6" lazy val methanol = "com.github.mizosoft.methanol" % "methanol" % "1.7.0" - lazy val munit = "org.scalameta" %% "munit" % "1.0.0-M11" - lazy val picocli = "info.picocli" % "picocli" % "4.7.5" + lazy val munit = "org.scalameta" %% "munit" % "1.0.0" + lazy val picocli = "info.picocli" % "picocli" % "4.7.6" - lazy val slf4jVersion = "2.0.12" + lazy val slf4jVersion = "2.0.13" lazy val slf4jApi = "org.slf4j" % "slf4j-api" % slf4jVersion lazy val slf4jJul = "org.slf4j" % "jul-to-slf4j" % slf4jVersion - private val tapirVersion = "1.9.10" + private val tapirVersion = "1.10.15" lazy val tapirStubServer = "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % tapirVersion lazy val tapirSwagger = "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion lazy val tapirCirce = "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion - lazy val tapirCirceClient = "com.softwaremill.sttp.client3" %% "circe" % "3.9.3" + lazy val tapirCirceClient = "com.softwaremill.sttp.client3" %% "circe" % "3.9.7" lazy val tapirVertx = "com.softwaremill.sttp.tapir" %% "tapir-vertx-server" % tapirVersion lazy val typesafeConfig = "com.typesafe" % "config" % "1.4.3" - lazy val zio = "dev.zio" %% "zio" % "2.0.21" + lazy val zio = "dev.zio" %% "zio" % "2.1.6" } \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index 8cf07b7..cb409aa 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.8 \ No newline at end of file +sbt.version=1.10.1 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 91aa10d..a8d9b04 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.3") -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.1") -addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.2") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.7.5") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.5.0") -addSbtPlugin("org.planet42" % "laika-sbt" % "0.18.0") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") +addSbtPlugin("org.planet42" % "laika-sbt" % "0.19.5") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") diff --git a/src/docs/_docs/deployment/configuration.md b/src/docs/_docs/deployment/configuration.md new file mode 100644 index 0000000..ebda51f --- /dev/null +++ b/src/docs/_docs/deployment/configuration.md @@ -0,0 +1,78 @@ +# Configuration + +Raziel is a configuration server, the keeper of secrets. It can be used to lookup the configuration of a M3/VARS installation. This requires that Raziel is configured with enough information to access the M3/VARS installation. The following is a list of configuration options: + +## Environment Variables + +The following environment variables are used to configure Raziel: + +### [Raziel](https://github.com/mbari-org/raziel) + +- `RAZIEL_HTTP_CONTEXT` - The context path for the HTTP server. Default is `/`. +- `RAZIEL_HTTP_PORT` - The port for the HTTP server. Default is `8080`. +- `RAZIEL_JWT_EXPIRATION` - The expiration time for the JWT token. Default is `180 days`. +- `RAZIEL_JWT_ISSUER` - The issuer for the JWT token. Default is `http://www.mbari.org`. +- `RAZIEL_JWT_SIGNING_SECRET` - The secret used to sign the JWT token. Default is `DEFAULT`. +- `RAZIEL_MASTER_KEY` - The master key used for access. Default is `DEFAULT`. + + +### [Annosaurus](https://github.com/mbari-org/annosaurus) + +- `ANNOSAURUS_URL` - The URL for the Annosaurus service. Default is `http://localhost:8082/anno/v1`. +- `ANNOSAURUS_INTERNAL_URL` - The internal URL for the Annosaurus service. Default is `http://localhost:8082/anno/v1`. This is used for internal communication between services and may be different that the ANNOSAURUS_URL in a docker context. +- `ANNOSAURUS_TIMEOUT` - The timeout for the Annosaurus service. Default is `10 seconds`. +- `ANNOSAURUS_SECRET` - The secret for the Annosaurus service. Default is `secret`. + +### [Beholder](https://github.com/mbari-org/beholder) + +- `BEHOLDER_URL` - The URL for the Beholder service. Default is `http://localhost:8088`. +- `BEHOLDER_INTERNAL_URL` - The internal URL for the Beholder service. Default is `http://localhost:8088`. This is used for internal communication between services and may be different that the BEHOLDER_URL in a docker context. +- `BEHOLDER_TIMEOUT` - The timeout for the Beholder service. Default is `10 seconds`. +- `BEHOLDER_SECRET` - The secret for the Beholder service. Default is `secret`. + +### [Charybdis](https://github.com/mbari-org/charybdis) + +- `CHARYBDIS_URL` - The URL for the Charybdis service. Default is `http://localhost:8086`. +- `CHARYBDIS_INTERNAL_URL` - The internal URL for the Charybdis service. Default is `http://localhost:8086`. This is used for internal communication between services and may be different that the CHARYBDIS_URL in a docker context. +- `CHARYBDIS_TIMEOUT` - The timeout for the Charybdis service. Default is `10 seconds`. + +### [Oni](https://github.com/mbari-org/oni) + +Note that Oni is a modern replacement for the legacy VARS KB and User services. The VARS KB and User services are still supported for backwards compatibility. If Oni is used, the VARS KB and User services should not be configured. + +- `ONI_URL` - The URL for the Oni service. No default is configured. Example:`http://localhost:8083`. +- `ONI_INTERNAL_URL` - The internal URL for the Oni service. Default is `http://localhost:8083`. This is used for internal communication between services and may be different that the ONI_URL in a docker context. +- `ONI_TIMEOUT` - The timeout for the Oni service. Default is `10 seconds`. +- `ONI_SECRET` - The secret for the Oni service. Default is `secret`. + +### [Panoptes](https://github.com/mbari-org/panoptes) + +- `PANOPTES_URL` - The URL for the Panoptes service. Default is `http://localhost:8085/panoptes/v1`. +- `PANOPTES_INTERNAL_URL` - The internal URL for the Panoptes service. Default is `http://localhost:8085/panoptes/v1`. This is used for internal communication between services and may be different that the PANOPTES_URL in a docker context. +- `PANOPTES_TIMEOUT` - The timeout for the Panoptes service. Default is `10 seconds`. +- `PANOPTES_SECRET` - The secret for the Panoptes service. Default is `secret`. + +### [Vampire Squid](https://github.com/mbari-org/vampire-squid) + +- `VAMPIRE_SQUID_URL` - The URL for the Vampire Squid service. Default is `http://localhost:8084/vam/v1`. +- `VAMPIRE_SQUID_INTERNAL_URL` - The internal URL for the Vampire Squid service. Default is `http://localhost:8084/vam/v1`. This is used for internal communication between services and may be different that the VAMPIRE_SQUID_URL in a docker context. +- `VAMPIRE_SQUID_TIMEOUT` - The timeout for the Vampire Squid service. Default is `10 seconds`. +- `VAMPIRE_SQUID_SECRET` - The secret for the Vampire Squid service. Default is `secret`. + +### [VARS KB Server](https://github.com/mbari-org/vars-kb-server) + +The VARS KB Server is a legacy service. It is still supported for backwards compatibility. If Oni is used, the VARS KB and User services should not be configured. + +- `VARS_KB_SERVER_URL` - The URL for the VARS KB server. No default is configured. Example: `http://localhost:8083/kb/v1`. +- `VARS_KB_SERVER_INTERNAL_URL` - The internal URL for the VARS KB server. Default is `http://localhost:8083/kb/v1`. This is used for internal communication between services and may be different that the VARS_KB_SERVER_URL in a docker context. +- `VARS_KB_SERVER_TIMEOUT` - The timeout for the VARS KB server. Default is `10 seconds`. +- `VARS_KB_SERVER_SECRET` - The secret for the VARS KB server. Default is `secret`. + +### [VARS User Server](https://github.com/mbari-org/vars-user-server) + +The VARS User Server is a legacy service. It is still supported for backwards compatibility. If Oni is used, the VARS KB and User services should not be configured. + +- `VARS_USER_SERVER_URL` - The URL for the VARS User server. No default is configured. Example `http://localhost:8087/users/v1`. +- `VARS_USER_SERVER_INTERNAL_URL` - The internal URL for the VARS User server. Default is `http://localhost:8087/users/v1`. This is used for internal communication between services and may be different that the VARS_USER_SERVER_URL in a docker context. +- `VARS_USER_SERVER_TIMEOUT` - The timeout for the VARS User server. Default is `10 seconds`. +- `VARS_USER_SERVER_SECRET` - The secret for the VARS User server \ No newline at end of file diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index db00042..6a67fb2 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -55,6 +55,18 @@ charybdis { timeout = ${?CHARYBDIS_TIMEOUT} } +oni { + # url = "http://tbd.shore.mbari.org:8080/oni/v1" + url = "FAIL" + url = ${?ONI_URL} + internal.url = ${oni.url} + internal.url = ${?ONI_INTERNAL_URL} + timeout = "10 seconds" + timeout = ${?ONI_TIMEOUT} + secret = "secret" + secret = ${?ONI_SECRET} +} + panoptes { # url = "http://singularity.shore.mbari.org:8080/panoptes/v1" url = "http://localhost:8085/panoptes/v1" @@ -81,7 +93,7 @@ vampire.squid { vars.kb.server { # url = "http://m3.shore.mbari.org/kb/v1" - url = "http://localhost:8083/kb/v1" + url = "FAIL" url = ${?VARS_KB_SERVER_URL} internal.url = ${vars.kb.server.url} internal.url = ${?VARS_KB_SERVER_INTERNAL_URL} @@ -91,7 +103,7 @@ vars.kb.server { vars.user.server { # url = "http://m3.shore.mbari.org/accounts/v1" - url = "http://localhost:8087/users/v1" + url = "FAIL" url = ${?VARS_USER_SERVER_URL} internal.url = ${vars.user.server.url} internal.url = ${?VARS_USER_SERVER_INTERNAL_URL} diff --git a/src/main/scala/org/mbari/raziel/AppConfig.scala b/src/main/scala/org/mbari/raziel/AppConfig.scala index 13fdddd..bb950fe 100644 --- a/src/main/scala/org/mbari/raziel/AppConfig.scala +++ b/src/main/scala/org/mbari/raziel/AppConfig.scala @@ -17,10 +17,15 @@ package org.mbari.raziel import com.typesafe.config.ConfigFactory + import java.net.URL import org.mbari.raziel.domain.EndpointConfig + import scala.util.Try -import org.mbari.raziel.etc.jdk.Logging.{given, *} +import org.mbari.raziel.etc.jdk.Logging.{*, given} + +import java.net.URI +import scala.util.control.NonFatal /** * A typesafe wrapper around the application.conf file. @@ -29,102 +34,186 @@ import org.mbari.raziel.etc.jdk.Logging.{given, *} */ object AppConfig: - private val log = System.getLogger(getClass.getName) - - private val Default = "DEFAULT" - - private val config = ConfigFactory.load() - - private def asUrl(path: String): URL = - if (!path.endsWith("/")) new URL(path) - else new URL(path.substring(0, path.length - 1)) - - val Name = "raziel" - - val Version = Try(getClass.getPackage.getImplementationVersion).getOrElse("0.0.0") - - val Description = "Configuration/Key Store" - - lazy val MasterKey = - val key = config.getString("raziel.master.key") - if (key.trim.isEmpty || key.toUpperCase == Default) - log - .atWarn - .log( - "Using default master key. This is not recommended for production. Set the RAZIEL_MASTER_KEY environment variable to set a master key." - ) - key - - lazy val Annosaurus: EndpointConfig = - val url = asUrl(config.getString("annosaurus.url")) - val timeout = config.getDuration("annosaurus.timeout") - val secret = config.getString("annosaurus.secret") - val internalUrl = asUrl(config.getString("annosaurus.internal.url")) - log.atDebug.log(s"Annosaurus URL: $url") - EndpointConfig("annosaurus", url, timeout, Some(secret), "/anno", internalUrl) - - lazy val Beholder: EndpointConfig = - val url = asUrl(config.getString("beholder.url")) - val timeout = config.getDuration("beholder.timeout") - val secret = config.getString("beholder.secret") - val internalUrl = asUrl(config.getString("beholder.internal.url")) - log.atDebug.log(s"Beholder URL: $url") - EndpointConfig("beholder", url, timeout, Some(secret), "/beholder", internalUrl) - - lazy val Charybdis: EndpointConfig = - val url = asUrl(config.getString("charybdis.url")) - val timeout = config.getDuration("charybdis.timeout") - val internalUrl = asUrl(config.getString("charybdis.internal.url")) - log.atDebug.log(s"Charybdis URL: $url") - EndpointConfig("charybdis", url, timeout, None, "/references", internalUrl) - - object Http: - val Context = config.getString("raziel.http.context") - val Port = config.getInt("raziel.http.port") - val StopTimeout = config.getDuration("raziel.http.stop.timeout") - val Webapp = config.getString("raziel.http.webapp") - - object Jwt: - val Expiration = config.getDuration("raziel.jwt.expiration") - val Issuer = config.getString("raziel.jwt.issuer") - lazy val SigningSecret = - val secret = config.getString("raziel.jwt.signing.secret") - if (secret.trim.isEmpty || secret.toUpperCase == Default) - System - .getLogger(getClass.getName) - .log( - System.Logger.Level.WARNING, - "Using default signing secret. This is not recommended for production. Set the RAZIEL_JWT_SIGNING_SECRET environment variable to set a signing secret." - ) - secret - - lazy val Panoptes: EndpointConfig = - val url = asUrl(config.getString("panoptes.url")) - val timeout = config.getDuration("panoptes.timeout") - val secret = config.getString("panoptes.secret") - val internalUrl = asUrl(config.getString("panoptes.internal.url")) - log.atDebug.log(s"Panoptes URL: $url") - EndpointConfig("panoptes", url, timeout, Some(secret), "/panoptes", internalUrl) - - lazy val VampireSquid: EndpointConfig = - val url = asUrl(config.getString("vampire.squid.url")) - val timeout = config.getDuration("vampire.squid.timeout") - val secret = config.getString("vampire.squid.secret") - val internalUrl = asUrl(config.getString("vampire.squid.internal.url")) - log.atDebug.log(s"Vampire-squid URL: $url") - EndpointConfig("vampire-squid", url, timeout, Some(secret), "/vam", internalUrl) - - lazy val VarsKbServer: EndpointConfig = - val url = asUrl(config.getString("vars.kb.server.url")) - val timeout = config.getDuration("vars.kb.server.timeout") - val internalUrl = asUrl(config.getString("vars.kb.server.internal.url")) - log.atDebug.log(s"VARS KB Server URL: $url") - EndpointConfig("vars-kb-server", url, timeout, None, "/kb", internalUrl) - - lazy val VarsUserServer: EndpointConfig = - val url = asUrl(config.getString("vars.user.server.url")) - val timeout = config.getDuration("vars.user.server.timeout") - val secret = config.getString("vars.user.server.secret") - val internalUrl = asUrl(config.getString("vars.user.server.internal.url")) - log.atDebug.log(s"VARS User Server URL: $url") - EndpointConfig("vars-user-server", url, timeout, Some(secret), "/accounts", internalUrl) + private val log = System.getLogger(getClass.getName) + + private val Default = "DEFAULT" + + private val config = ConfigFactory.load() + + private def asUrl(path: String): URL = + if !path.endsWith("/") then URI.create(path).toURL() + else URI.create(path.substring(0, path.length - 1)).toURL() + + val Name: String = "raziel" + + val Version: String = Try(getClass.getPackage.getImplementationVersion).getOrElse("0.0.0") + + val Description = "Configuration/Key Store" + + lazy val MasterKey: String = + val key = config.getString("raziel.master.key") + if key.trim.isEmpty || key.toUpperCase == Default then + log + .atWarn + .log( + "Using default master key. This is not recommended for production. Set the RAZIEL_MASTER_KEY environment variable to set a master key." + ) + key + + val AnnosaurusName: String = "annosaurus" + lazy val Annosaurus: Option[EndpointConfig] = + try + val url = asUrl(config.getString("annosaurus.url")) + val timeout = config.getDuration("annosaurus.timeout") + val secret = config.getString("annosaurus.secret") + val internalUrl = asUrl(config.getString("annosaurus.internal.url")) + log.atDebug.log(s"Annosaurus URL: $url") + Some(EndpointConfig(AnnosaurusName, url, timeout, Some(secret), "/anno", internalUrl)) + catch + case NonFatal(e) => + log.atWarn + .withCause(e) + .log("Annosaurus is not configured. Raziel will not be able to access the annotation service.") + None + + val BeholderName: String = "beholder" + lazy val Beholder: Option[EndpointConfig] = + try + val url = asUrl(config.getString("beholder.url")) + val timeout = config.getDuration("beholder.timeout") + val secret = config.getString("beholder.secret") + val internalUrl = asUrl(config.getString("beholder.internal.url")) + log.atDebug.log(s"Beholder URL: $url") + Some(EndpointConfig(BeholderName, url, timeout, Some(secret), "/beholder", internalUrl)) + catch + case NonFatal(e) => + log.atWarn + .withCause(e) + .log("Beholder is not configured. Raziel will not be able to access the image capture service.") + None + + lazy val CharybdisName: String = "charybdis" + lazy val Charybdis: Option[EndpointConfig] = + try + val url = asUrl(config.getString("charybdis.url")) + val timeout = config.getDuration("charybdis.timeout") + val internalUrl = asUrl(config.getString("charybdis.internal.url")) + log.atDebug.log(s"Charybdis URL: $url") + Some(EndpointConfig(CharybdisName, url, timeout, None, "/references", internalUrl)) + catch + case NonFatal(e) => + log.atWarn + .withCause(e) + .log("Charybdis is not configured. Raziel will not be able to access the reference service.") + None + + object Http: + val Context = config.getString("raziel.http.context") + val Port = config.getInt("raziel.http.port") + val StopTimeout = config.getDuration("raziel.http.stop.timeout") + val Webapp = config.getString("raziel.http.webapp") + + object Jwt: + val Expiration = config.getDuration("raziel.jwt.expiration") + val Issuer = config.getString("raziel.jwt.issuer") + lazy val SigningSecret = + val secret = config.getString("raziel.jwt.signing.secret") + if secret.trim.isEmpty || secret.toUpperCase == Default then + System + .getLogger(getClass.getName) + .log( + System.Logger.Level.WARNING, + "Using default signing secret. This is not recommended for production. Set the RAZIEL_JWT_SIGNING_SECRET environment variable to set a signing secret." + ) + secret + + lazy val OniName: String = "oni" + lazy val Oni: Option[EndpointConfig] = + try + val url = asUrl(config.getString("oni.url")) + val timeout = config.getDuration("oni.timeout") + val secret = config.getString("oni.secret") + val internalUrl = asUrl(config.getString("oni.internal.url")) + log.atDebug.log(s"Oni URL: $url") + Some(EndpointConfig(OniName, url, timeout, Some(secret), "/oni", internalUrl)) + catch + case NonFatal(e) => + log.atInfo.withCause(e).log("Oni is not configured. Writing to the VARS KB will not be possible.") + None + + lazy val PanoptesName: String = "panoptes" + lazy val Panoptes: Option[EndpointConfig] = + try + val url = asUrl(config.getString("panoptes.url")) + val timeout = config.getDuration("panoptes.timeout") + val secret = config.getString("panoptes.secret") + val internalUrl = asUrl(config.getString("panoptes.internal.url")) + log.atDebug.log(s"Panoptes URL: $url") + Some(EndpointConfig(PanoptesName, url, timeout, Some(secret), "/panoptes", internalUrl)) + catch + case NonFatal(e) => + log.atWarn + .withCause(e) + .log("Panoptes is not configured. Raziel will not be able to access the image archiving service.") + None + + lazy val VampireSquidName: String = "vampire-squid" + lazy val VampireSquid: Option[EndpointConfig] = + try + val url = asUrl(config.getString("vampire.squid.url")) + val timeout = config.getDuration("vampire.squid.timeout") + val secret = config.getString("vampire.squid.secret") + val internalUrl = asUrl(config.getString("vampire.squid.internal.url")) + log.atDebug.log(s"Vampire-squid URL: $url") + Some(EndpointConfig(VampireSquidName, url, timeout, Some(secret), "/vam", internalUrl)) + catch + case NonFatal(e) => + log.atWarn + .withCause(e) + .log( + "Vampire-squid is not configured. Raziel will not be able to access the video asset manager service." + ) + None + + lazy val VarsKbServerName: String = "vars-kb-server" + lazy val VarsKbServer: Option[EndpointConfig] = + Oni match + case Some(oni) => + log.atInfo.log(s"Using $OniName instead of $VarsKbServerName for the VARS knowledgebase") + Some(oni.copy(name = VarsKbServerName)) + case None => + try + val url = asUrl(config.getString("vars.kb.server.url")) + val timeout = config.getDuration("vars.kb.server.timeout") + val internalUrl = asUrl(config.getString("vars.kb.server.internal.url")) + log.atDebug.log(s"VARS KB Server URL: $url") + Some(EndpointConfig(VarsKbServerName, url, timeout, None, "/kb", internalUrl)) + catch + case NonFatal(e) => + log.atInfo + .withCause(e) + .log(s"$VarsKbServerName is not configured. Access to the VARS KB will not be possible.") + None + + lazy val VarsUserServerName: String = "vars-user-server" + lazy val VarsUserServer: Option[EndpointConfig] = + Oni match + case Some(oni) => + log.atInfo.log(s"Using $OniName instead of $VarsUserServerName for VARS user services.") + Some(oni.copy(name = VarsUserServerName)) + case None => + try + val url = asUrl(config.getString("vars.user.server.url")) + val timeout = config.getDuration("vars.user.server.timeout") + val secret = config.getString("vars.user.server.secret") + val internalUrl = asUrl(config.getString("vars.user.server.internal.url")) + log.atDebug.log(s"VARS User Server URL: $url") + Some(EndpointConfig(VarsUserServerName, url, timeout, Some(secret), "/accounts", internalUrl)) + catch + case NonFatal(e) => + log.atInfo + .withCause(e) + .log( + s"$VarsUserServerName is not configured. Raziel will not function correctly with a user service configured." + ) + None diff --git a/src/main/scala/org/mbari/raziel/Main.scala b/src/main/scala/org/mbari/raziel/Main.scala index 4754f2b..5f131af 100644 --- a/src/main/scala/org/mbari/raziel/Main.scala +++ b/src/main/scala/org/mbari/raziel/Main.scala @@ -26,90 +26,88 @@ import org.mbari.raziel.domain.EndpointConfig import org.mbari.raziel.etc.jdk.Logging.given import org.mbari.raziel.services.* import picocli.CommandLine -import picocli.CommandLine.{Command, Option => Opt, Parameters} +import picocli.CommandLine.{Command, Option as Opt, Parameters} import scala.concurrent.duration.Duration import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutor} import scala.util.Try import sttp.tapir.server.vertx.VertxFutureServerInterpreter import sttp.tapir.server.vertx.VertxFutureServerInterpreter.* import io.vertx.ext.web.handler.CorsHandler +import org.mbari.raziel.etc.circe.CirceCodecs.{given, *} @Command( - description = Array("Start the server"), - name = "main", - mixinStandardHelpOptions = true, - version = Array("0.0.1") + description = Array("Start the server"), + name = "main", + mixinStandardHelpOptions = true, + version = Array("0.0.1") ) class MainRunner extends Callable[Int]: - @Opt( - names = Array("-p", "--port"), - description = Array("The port of the server. default: ${DEFAULT-VALUE}") - ) - private var port: Int = AppConfig.Http.Port + @Opt( + names = Array("-p", "--port"), + description = Array("The port of the server. default: ${DEFAULT-VALUE}") + ) + private var port: Int = AppConfig.Http.Port - override def call(): Int = - Main.run(port) - 0 + override def call(): Int = + Main.run(port) + 0 object Main: - private val log = System.getLogger(getClass.getName) + private val log = System.getLogger(getClass.getName) - def main(args: Array[String]): Unit = - val s = """ + def main(args: Array[String]): Unit = + val s = """ | ______ _______ ______ _____ _______ | |_____/ |_____| ____/ | |______ | | | \_ | | /_____ __|__ |______ |_____""".stripMargin - println(s) - new CommandLine(new MainRunner()).execute(args: _*) - - def run(port: Int): Unit = - log.atInfo.log(s"Starting up ${AppConfig.Name} v${AppConfig.Version} on port $port") - - given executionContext: ExecutionContextExecutor = ExecutionContext.global - - // -- Service providers - val annosaurus = Annosaurus.default - val beholder = Beholder.default - val charybdis = Charybdis.default - val panoptes = Panoptes.default - val vampireSquid = VampireSquid.default - val varsKbServer = VarsKbServer.default - val varsUserServer = VarsUserServer.default - val healthServices = - Seq(annosaurus, beholder, charybdis, panoptes, vampireSquid, varsKbServer, varsUserServer) - - // -- Tapir endpoints - val context = AppConfig.Http.Context - val authEndpoints = AuthEndpoints(AuthController(varsUserServer), context) - val healthEndpoints = HealthEndpoints(HealthController(healthServices), context) - val endpointEndpoints = EndpointsEndpoints(context) - val swaggerEndpoints = SwaggerEndpoints(authEndpoints, endpointEndpoints, healthEndpoints) - val allEndpointImpls = authEndpoints.allImpl ++ - healthEndpoints.allImpl ++ - endpointEndpoints.allImpl ++ - swaggerEndpoints.allImpl - - // -- Vert.x server - val vertx = Vertx.vertx() - val server = vertx.createHttpServer() - val router = Router.router(vertx) - - // Add CORS - val corsHandler = CorsHandler.create("*") - router.route().handler(corsHandler) - - // Add Tapir endpoints - val interpreter = VertxFutureServerInterpreter() - authEndpoints.allImpl.foreach(endpoint => interpreter.blockingRoute(endpoint).apply(router)) - healthEndpoints.allImpl.foreach(endpoint => interpreter.blockingRoute(endpoint).apply(router)) - endpointEndpoints.allImpl .foreach(endpoint => interpreter.route(endpoint).apply(router)) - swaggerEndpoints.allImpl.foreach(endpoint => interpreter.route(endpoint).apply(router)) - - -// for endpoint <- allEndpointImpls do -// val attach = VertxFutureServerInterpreter().route(endpoint) -// attach(router) - - Await.result(server.requestHandler(router).listen(port).asScala, Duration.Inf) + println(s) + new CommandLine(new MainRunner()).execute(args*) + + def run(port: Int): Unit = + log.atInfo.log(s"Starting up ${AppConfig.Name} v${AppConfig.Version} on port $port") + + given executionContext: ExecutionContextExecutor = ExecutionContext.global + + // -- Service providers + val healthServices = HealthServices.init + + healthServices.foreach(service => log.atInfo.log(s"Found service: ${service.name} with health check at ${service.healthUri}")) + val varsUserServer: VarsUserServer = healthServices + .find(_.name == AppConfig.VarsUserServerName) + .getOrElse( + throw RuntimeException( + s"Could not find service ${AppConfig.VarsUserServerName} or ${AppConfig.OniName}. This is required for Raziel to run. Exiting ..." + ) + ) + .asInstanceOf[VarsUserServer] + + // -- Tapir endpoints + val context = AppConfig.Http.Context + val authEndpoints = AuthEndpoints(AuthController(varsUserServer), context) + val healthEndpoints = HealthEndpoints(HealthController(healthServices), context) + val endpointEndpoints = EndpointsEndpoints(context) + val swaggerEndpoints = SwaggerEndpoints(authEndpoints, endpointEndpoints, healthEndpoints) + val allEndpointImpls = authEndpoints.allImpl ++ + healthEndpoints.allImpl ++ + endpointEndpoints.allImpl ++ + swaggerEndpoints.allImpl + + // -- Vert.x server + val vertx = Vertx.vertx() + val server = vertx.createHttpServer() + val router = Router.router(vertx) + + // Add CORS + val corsHandler = CorsHandler.create("*") + router.route().handler(corsHandler) + + // Add Tapir endpoints + val interpreter = VertxFutureServerInterpreter() + authEndpoints.allImpl.foreach(endpoint => interpreter.blockingRoute(endpoint).apply(router)) + healthEndpoints.allImpl.foreach(endpoint => interpreter.blockingRoute(endpoint).apply(router)) + endpointEndpoints.allImpl.foreach(endpoint => interpreter.route(endpoint).apply(router)) + swaggerEndpoints.allImpl.foreach(endpoint => interpreter.route(endpoint).apply(router)) + + Await.result(server.requestHandler(router).listen(port).asScala, Duration.Inf) diff --git a/src/main/scala/org/mbari/raziel/api/AuthController.scala b/src/main/scala/org/mbari/raziel/api/AuthController.scala index 01a3648..ac02e96 100644 --- a/src/main/scala/org/mbari/raziel/api/AuthController.scala +++ b/src/main/scala/org/mbari/raziel/api/AuthController.scala @@ -36,71 +36,69 @@ import org.mbari.raziel.etc.zio.ZioUtil class AuthController(varsUserServer: VarsUserServer): - private val jwtHelper = JwtHelper.default + private val jwtHelper = JwtHelper.default - def authenticate(xApiKey: Option[String], auth: Option[BasicAuth]): Either[ErrorMsg, BearerAuth] = - xApiKey match - case Some(key) => - if (key == AppConfig.MasterKey) - val token = jwtHelper.createJwt(Map("username" -> "master")) - Right(BearerAuth(token)) - else Left(Unauthorized("Invalid credentials")) - case None => - val app = for - a <- ZIO.fromEither(auth.toRight(Unauthorized("Missing or invalid basic credentials"))) - // _ <- Task.succeed(log.info(s"auth: $a")) - u <- varsUserServer.Users.findByName(a.username) - // _ <- Task.succeed(log.info(s"user: $u")) - ok <- ZIO.succeed(u.map(v => v.authenticate(a.password)).getOrElse(false)) - payload <- ZIO.succeed( - if (ok) - Some(JwtAuthPayload.fromUser(u.get)) - else - None - ) - yield payload + def authenticate(xApiKey: Option[String], auth: Option[BasicAuth]): Either[ErrorMsg, BearerAuth] = + xApiKey match + case Some(key) => + if key == AppConfig.MasterKey then + val token = jwtHelper.createJwt(Map("username" -> "master")) + Right(BearerAuth(token)) + else Left(Unauthorized("Invalid credentials")) + case None => + val app = for + a <- ZIO.fromEither(auth.toRight(Unauthorized("Missing or invalid basic credentials"))) + // _ <- Task.succeed(log.info(s"auth: $a")) + u <- varsUserServer.Users.findByName(a.username) + // _ <- Task.succeed(log.info(s"user: $u")) + ok <- ZIO.succeed(u.map(v => v.authenticate(a.password)).getOrElse(false)) + payload <- ZIO.succeed( + if ok then Some(JwtAuthPayload.fromUser(u.get)) + else None + ) + yield payload - Try(ZioUtil.unsafeRun(app)) match - case Success(payload) => - payload match - case Some(p) => - val token = jwtHelper.createJwt(p.asMap()) - Right(BearerAuth(token)) - case None => - Left(Unauthorized("Invalid credentials")) - case Failure(e) => - Left(ServerError(e.getMessage)) + Try(ZioUtil.unsafeRun(app)) match + case Success(payload) => + payload match + case Some(p) => + val token = jwtHelper.createJwt(p.asMap()) + Right(BearerAuth(token)) + case None => + Left(Unauthorized("Invalid credentials")) + case Failure(e) => + Left(ServerError(e.getMessage)) - def authenticateRaw( - xApiKey: Option[String] = None, - authorization: Option[String] = None - ): Either[ErrorMsg, BearerAuth] = - authenticate(xApiKey, authorization.flatMap(BasicAuth.parse)) + def authenticateRaw( + xApiKey: Option[String] = None, + authorization: Option[String] = None + ): Either[ErrorMsg, BearerAuth] = + authenticate(xApiKey, authorization.flatMap(BasicAuth.parse)) - def verify(tokenOpt: Option[String]): Either[ErrorMsg, Map[String, String]] = - val either = for - token <- tokenOpt.toRight(Unauthorized("Missing or invalid token")) - decodedJwt <- jwtHelper.verifyJwt(token) - yield decodedJwt + def verify(tokenOpt: Option[String]): Either[ErrorMsg, Map[String, String]] = + val either = for + token <- tokenOpt.toRight(Unauthorized("Missing or invalid token")) + decodedJwt <- jwtHelper.verifyJwt(token) + yield decodedJwt - either match - case Right(jwt) => - val claims = jwt - .getClaims - .asScala - .toMap - .filter((key, claim) => claim.asString != null && claim.asString.nonEmpty) - .map((key, claim) => (key, claim.asString())) + either match + case Right(jwt) => + val claims = jwt + .getClaims + .asScala + .toMap + .filter((key, claim) => claim.asString != null && claim.asString.nonEmpty) + .map((key, claim) => (key, claim.asString())) - Right(claims) + Right(claims) - case Left(e) => - Left(Unauthorized(s"Invalid credentials: ${e.getClass}")) + case Left(e) => + Left(Unauthorized(s"Invalid credentials: ${e.getClass}")) - def verifyRaw(authorization: Option[String]): Either[ErrorMsg, Map[String, String]] = + def verifyRaw(authorization: Option[String]): Either[ErrorMsg, Map[String, String]] = - val tokenOpt = authorization - .flatMap(BearerAuth.parse) - .map(_.accessToken) + val tokenOpt = authorization + .flatMap(BearerAuth.parse) + .map(_.accessToken) - verify(tokenOpt) + verify(tokenOpt) diff --git a/src/main/scala/org/mbari/raziel/api/AuthEndpoints.scala b/src/main/scala/org/mbari/raziel/api/AuthEndpoints.scala index d3c3d48..b9b655e 100644 --- a/src/main/scala/org/mbari/raziel/api/AuthEndpoints.scala +++ b/src/main/scala/org/mbari/raziel/api/AuthEndpoints.scala @@ -16,13 +16,13 @@ package org.mbari.raziel.api -import io.circe.generic.auto._ +import io.circe.generic.auto.* import org.mbari.raziel.domain.{BearerAuth, ErrorMsg} import org.mbari.raziel.etc.circe.CirceCodecs.given import scala.concurrent.{ExecutionContext, Future} -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* import sttp.tapir.server.ServerEndpoint import org.mbari.raziel.etc.jdk.Logging.given import sttp.tapir.model.UsernamePassword @@ -33,41 +33,41 @@ class AuthEndpoints(authController: AuthController, context: String = "config")( ec: ExecutionContext ) extends Endpoints: - val authEndpoint: Endpoint[Option[UsernamePassword], Option[String], ErrorMsg, BearerAuth, Any] = - baseEndpoint - .post - .in(context / "auth") - .securityIn(auth.basic[Option[UsernamePassword]](WWWAuthenticateChallenge.basic)) - .in(header[Option[String]]("X-Api-Key")) - .out(jsonBody[BearerAuth]) - .name("authenticate") - .description("Exchange an API key or user credentials for a JWT") - .tag("auth") - val authEndpointImpl: ServerEndpoint[Any, Future] = - authEndpoint - .serverSecurityLogic(userPwdOpt => - Future.successful(Right(userPwdOpt.map(up => BasicAuth(up.username, up.password.get)))) - ) - .serverLogic(basicAuthOpt => - xApiKeyOpt => + val authEndpoint: Endpoint[Option[UsernamePassword], Option[String], ErrorMsg, BearerAuth, Any] = + baseEndpoint + .post + .in(context / "auth") + .securityIn(auth.basic[Option[UsernamePassword]](WWWAuthenticateChallenge.basic)) + .in(header[Option[String]]("X-Api-Key")) + .out(jsonBody[BearerAuth]) + .name("authenticate") + .description("Exchange an API key or user credentials for a JWT") + .tag("auth") + val authEndpointImpl: ServerEndpoint[Any, Future] = + authEndpoint + .serverSecurityLogic(userPwdOpt => + Future.successful(Right(userPwdOpt.map(up => BasicAuth(up.username, up.password.get)))) + ) + .serverLogic(basicAuthOpt => + xApiKeyOpt => // log.atInfo.log(() => s"Found headers $xApiKeyOpt, $basicAuthOpt") - Future(authController.authenticate(xApiKeyOpt, basicAuthOpt)) - ) + Future(authController.authenticate(xApiKeyOpt, basicAuthOpt)) + ) - val verifyEndpoint: Endpoint[Option[String], Unit, ErrorMsg, Map[String, String], Any] = - baseEndpoint - .post - .in(context / "auth" / "verify") - .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) - .out(jsonBody[Map[String, String]]) - .name("verifyAuthentication") - .description("Verify the user's JWT token") - .tag("auth") - val verifyEndpointImpl: ServerEndpoint[Any, Future] = - verifyEndpoint - .serverSecurityLogic(tokenOpt => Future.successful(Right(tokenOpt))) - .serverLogic(tokenOpt => _ => Future(authController.verify(tokenOpt))) + val verifyEndpoint: Endpoint[Option[String], Unit, ErrorMsg, Map[String, String], Any] = + baseEndpoint + .post + .in(context / "auth" / "verify") + .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) + .out(jsonBody[Map[String, String]]) + .name("verifyAuthentication") + .description("Verify the user's JWT token") + .tag("auth") + val verifyEndpointImpl: ServerEndpoint[Any, Future] = + verifyEndpoint + .serverSecurityLogic(tokenOpt => Future.successful(Right(tokenOpt))) + .serverLogic(tokenOpt => _ => Future(authController.verify(tokenOpt))) - override val all: List[Endpoint[?, ?, ?, ?, ?]] = List(authEndpoint, verifyEndpoint) - override val allImpl: List[ServerEndpoint[Any, Future]] = - List(authEndpointImpl, verifyEndpointImpl) + override val all: List[Endpoint[?, ?, ?, ?, ?]] = List(authEndpoint, verifyEndpoint) + override val allImpl: List[ServerEndpoint[Any, Future]] = + List(authEndpointImpl, verifyEndpointImpl) diff --git a/src/main/scala/org/mbari/raziel/api/Endpoints.scala b/src/main/scala/org/mbari/raziel/api/Endpoints.scala index 5848560..c95b0d9 100644 --- a/src/main/scala/org/mbari/raziel/api/Endpoints.scala +++ b/src/main/scala/org/mbari/raziel/api/Endpoints.scala @@ -26,15 +26,15 @@ import sttp.tapir.json.circe.* import sttp.tapir.server.ServerEndpoint trait Endpoints: - val log = System.getLogger(getClass.getName) + val log = System.getLogger(getClass.getName) - def all: List[Endpoint[?, ?, ?, ?, ?]] - def allImpl: List[ServerEndpoint[Any, Future]] + def all: List[Endpoint[?, ?, ?, ?, ?]] + def allImpl: List[ServerEndpoint[Any, Future]] - val baseEndpoint = endpoint.errorOut( - oneOf[ErrorMsg]( - oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound])), - oneOfVariant(statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError])), - oneOfVariant(statusCode(StatusCode.Unauthorized).and(jsonBody[Unauthorized])) + val baseEndpoint: Endpoint[Unit, Unit, ErrorMsg, Unit, Any] = endpoint.errorOut( + oneOf[ErrorMsg]( + oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound])), + oneOfVariant(statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError])), + oneOfVariant(statusCode(StatusCode.Unauthorized).and(jsonBody[Unauthorized])) + ) ) - ) diff --git a/src/main/scala/org/mbari/raziel/api/EndpointsController.scala b/src/main/scala/org/mbari/raziel/api/EndpointsController.scala index cfc0f04..32970b7 100644 --- a/src/main/scala/org/mbari/raziel/api/EndpointsController.scala +++ b/src/main/scala/org/mbari/raziel/api/EndpointsController.scala @@ -23,31 +23,29 @@ import org.mbari.raziel.domain.{BearerAuth, ErrorMsg, Unauthorized} object EndpointsController: - private val jwtHelper = JwtHelper.default + private val jwtHelper = JwtHelper.default - private val securedEndpoints: List[EndpointConfig] = EndpointConfig.defaults - private val unsecuredEndpoints: List[EndpointConfig] = - EndpointConfig.defaults.map(_.copy(secret = None)) + private val securedEndpoints: List[EndpointConfig] = EndpointConfig.defaults + private val unsecuredEndpoints: List[EndpointConfig] = + EndpointConfig.defaults.map(_.copy(secret = None)) - private def authenticateRaw(authHeader: Option[String]): Boolean = - val token = authHeader - .flatMap(a => BearerAuth.parse(a)) - .map(_.accessToken) + private def authenticateRaw(authHeader: Option[String]): Boolean = + val token = authHeader + .flatMap(a => BearerAuth.parse(a)) + .map(_.accessToken) - authenticate(token) + authenticate(token) - private def authenticate(token: Option[String]): Boolean = - val auth = token.toRight(Unauthorized("JWT token is missing")) + private def authenticate(token: Option[String]): Boolean = + val auth = token.toRight(Unauthorized("JWT token is missing")) - val either = for - a <- auth - decodedJwt <- jwtHelper.verifyJwt(a) - yield decodedJwt + val either = for + a <- auth + decodedJwt <- jwtHelper.verifyJwt(a) + yield decodedJwt - either.isRight + either.isRight - def getEndpoints(token: Option[String]): List[EndpointConfig] = - if (authenticate(token)) - securedEndpoints - else - unsecuredEndpoints + def getEndpoints(token: Option[String]): List[EndpointConfig] = + if authenticate(token) then securedEndpoints + else unsecuredEndpoints diff --git a/src/main/scala/org/mbari/raziel/api/EndpointsEndpoints.scala b/src/main/scala/org/mbari/raziel/api/EndpointsEndpoints.scala index 19b706f..9852801 100644 --- a/src/main/scala/org/mbari/raziel/api/EndpointsEndpoints.scala +++ b/src/main/scala/org/mbari/raziel/api/EndpointsEndpoints.scala @@ -16,7 +16,7 @@ package org.mbari.raziel.api -import io.circe.generic.auto._ +import io.circe.generic.auto.* import org.mbari.raziel.domain.{BearerAuth, ErrorMsg} import org.mbari.raziel.domain.EndpointConfig import org.mbari.raziel.etc.circe.CirceCodecs.given @@ -157,28 +157,25 @@ import org.mbari.raziel.domain.SerializedEndpointConfig * Brian Schlining * @since 2021-12-23T11:00:00 */ -class EndpointsEndpoints(context: String = "config")(using ec: ExecutionContext) - extends org.mbari.raziel.api.Endpoints: +class EndpointsEndpoints(context: String = "config")(using ec: ExecutionContext) extends org.mbari.raziel.api.Endpoints: - given Schema[URL] = Schema.string + given Schema[URL] = Schema.string - val endpoints: Endpoint[Option[String], Unit, ErrorMsg, List[SerializedEndpointConfig], Any] = - baseEndpoint - .get - .in(context / "endpoints") - .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) - .out(jsonBody[List[SerializedEndpointConfig]]) - .name("listEndpoints") - .description( - "List available endpoints. Authorization header is optional. If defined it returns connection information for the endpoint." - ) - .tag("configuration") - val endpointsImpl: ServerEndpoint[Any, Future] = - endpoints - .serverSecurityLogic(tokenOpt => Future.successful(Right(tokenOpt))) - .serverLogic(tokenOpt => - _ => Future(Right(EndpointsController.getEndpoints(tokenOpt).map(_.external))) - ) + val endpoints: Endpoint[Option[String], Unit, ErrorMsg, List[SerializedEndpointConfig], Any] = + baseEndpoint + .get + .in(context / "endpoints") + .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) + .out(jsonBody[List[SerializedEndpointConfig]]) + .name("listEndpoints") + .description( + "List available endpoints. Authorization header is optional. If defined it returns connection information for the endpoint." + ) + .tag("configuration") + val endpointsImpl: ServerEndpoint[Any, Future] = + endpoints + .serverSecurityLogic(tokenOpt => Future.successful(Right(tokenOpt))) + .serverLogic(tokenOpt => _ => Future(Right(EndpointsController.getEndpoints(tokenOpt).map(_.external)))) - override val all: List[Endpoint[?, ?, ?, ?, ?]] = List(endpoints) - override val allImpl: List[ServerEndpoint[Any, Future]] = List(endpointsImpl) + override val all: List[Endpoint[?, ?, ?, ?, ?]] = List(endpoints) + override val allImpl: List[ServerEndpoint[Any, Future]] = List(endpointsImpl) diff --git a/src/main/scala/org/mbari/raziel/api/HealthController.scala b/src/main/scala/org/mbari/raziel/api/HealthController.scala index 7104baa..5b46146 100644 --- a/src/main/scala/org/mbari/raziel/api/HealthController.scala +++ b/src/main/scala/org/mbari/raziel/api/HealthController.scala @@ -19,7 +19,7 @@ package org.mbari.raziel.api import org.mbari.raziel.domain.HealthStatus import org.mbari.raziel.domain.ServiceStatus import org.mbari.raziel.services.HealthService -import org.mbari.raziel.services.HasHealth +import org.mbari.raziel.services.HealthServices import org.mbari.raziel.domain.ServerError import org.mbari.raziel.domain.ErrorMsg import scala.util.Try @@ -27,36 +27,35 @@ import scala.util.Success import scala.util.Failure import org.mbari.raziel.etc.zio.ZioUtil -class HealthController(services: Seq[HasHealth]): +class HealthController(services: Seq[HealthService]): - private val healthService = HealthService(services) + private val healthService = HealthServices(services) - def defaultHealthStatus: HealthStatus = HealthStatus.default + def defaultHealthStatus: HealthStatus = HealthStatus.default - def expectedServiceStatus: Seq[ServiceStatus] = services.map(s => ServiceStatus(s.name)) + def expectedServiceStatus: Seq[ServiceStatus] = services.map(s => ServiceStatus(s.name)) - def availableServiceStatus(): Either[ErrorMsg, Seq[ServiceStatus]] = - val app = - for healthStatuses <- healthService.fetchHealth() - yield healthStatuses - .filter(_.freeMemory > 0) - .map(hs => ServiceStatus(hs.application, Some(hs))) + def availableServiceStatus(): Either[ErrorMsg, Seq[ServiceStatus]] = + val app = + for healthStatuses <- healthService.fetchHealth() + yield healthStatuses + .filter(_.available) - Try(ZioUtil.unsafeRun(app)) match - case Success(s) => Right(s) - case Failure(e) => - Left(ServerError(e.getMessage)) + Try(ZioUtil.unsafeRun(app)) match + case Success(s) => Right(s) + case Failure(e) => + Left(ServerError(e.getMessage)) - def currentServiceStatus(): Either[ErrorMsg, Seq[ServiceStatus]] = - val app = - for healthStatuses <- healthService.fetchHealth() - yield healthStatuses - .map(hs => - val ss = if (hs.freeMemory <= 0) None else Some(hs) - ServiceStatus(hs.application, ss) - ) + def currentServiceStatus(): Either[ErrorMsg, Seq[ServiceStatus]] = + val app = + for healthStatuses <- healthService.fetchHealth() + yield healthStatuses + .map(serverStatus => + val hs = if serverStatus.available then serverStatus.healthStatus else None + ServiceStatus(serverStatus.name, hs) + ) - Try(ZioUtil.unsafeRun(app)) match - case Success(s) => Right(s) - case Failure(e) => - Left(ServerError(e.getMessage)) + Try(ZioUtil.unsafeRun(app)) match + case Success(s) => Right(s) + case Failure(e) => + Left(ServerError(e.getMessage)) diff --git a/src/main/scala/org/mbari/raziel/api/HealthEndpoints.scala b/src/main/scala/org/mbari/raziel/api/HealthEndpoints.scala index 023234b..8bf7cc3 100644 --- a/src/main/scala/org/mbari/raziel/api/HealthEndpoints.scala +++ b/src/main/scala/org/mbari/raziel/api/HealthEndpoints.scala @@ -31,65 +31,65 @@ class HealthEndpoints(controller: HealthController, context: String = "config")( ec: ExecutionContext ) extends org.mbari.raziel.api.Endpoints: - val defaultEndpoint: PublicEndpoint[Unit, ErrorMsg, HealthStatus, Any] = - baseEndpoint - .get - .in(context / "health") - .out(jsonBody[HealthStatus]) - .name("razielHealth") - .description("Get the health status of the server") - .tag("health") - val defaultImpl: ServerEndpoint[Any, Future] = - defaultEndpoint.serverLogic(Unit => Future(Right(controller.defaultHealthStatus))) + val defaultEndpoint: PublicEndpoint[Unit, ErrorMsg, HealthStatus, Any] = + baseEndpoint + .get + .in(context / "health") + .out(jsonBody[HealthStatus]) + .name("razielHealth") + .description("Get the health status of the server") + .tag("health") + val defaultImpl: ServerEndpoint[Any, Future] = + defaultEndpoint.serverLogic(Unit => Future(Right(controller.defaultHealthStatus))) - val expectedEndpoint: PublicEndpoint[Unit, ErrorMsg, Seq[ServiceStatus], Any] = - baseEndpoint - .get - .in(context / "health" / "expected") - .out(jsonBody[Seq[ServiceStatus]]) - .name("listExpectedServices") - .description( - "Get a list of the expected services. This returns services that are expected to be running, but might not be." - ) - .tag("health") - val expectedImpl: ServerEndpoint[Any, Future] = - expectedEndpoint.serverLogic(Unit => Future(Right(controller.expectedServiceStatus))) + val expectedEndpoint: PublicEndpoint[Unit, ErrorMsg, Seq[ServiceStatus], Any] = + baseEndpoint + .get + .in(context / "health" / "expected") + .out(jsonBody[Seq[ServiceStatus]]) + .name("listExpectedServices") + .description( + "Get a list of the expected services. This returns services that are expected to be running, but might not be." + ) + .tag("health") + val expectedImpl: ServerEndpoint[Any, Future] = + expectedEndpoint.serverLogic(Unit => Future(Right(controller.expectedServiceStatus))) - val availableEndpoint: PublicEndpoint[Unit, ErrorMsg, Seq[ServiceStatus], Any] = - baseEndpoint - .get - .in(context / "health" / "available") - .out(jsonBody[Seq[ServiceStatus]]) - .name("listAvailableServices") - .description( - "Get a list of the available services. Services that are down will not be included" - ) - .tag("health") - val availableImpl: ServerEndpoint[Any, Future] = - availableEndpoint.serverLogic(Unit => Future(controller.availableServiceStatus())) + val availableEndpoint: PublicEndpoint[Unit, ErrorMsg, Seq[ServiceStatus], Any] = + baseEndpoint + .get + .in(context / "health" / "available") + .out(jsonBody[Seq[ServiceStatus]]) + .name("listAvailableServices") + .description( + "Get a list of the available services. Services that are down will not be included" + ) + .tag("health") + val availableImpl: ServerEndpoint[Any, Future] = + availableEndpoint.serverLogic(Unit => Future(controller.availableServiceStatus())) - val statusEndpoint: PublicEndpoint[Unit, ErrorMsg, Seq[ServiceStatus], Any] = - baseEndpoint - .get - .in(context / "health" / "status") - .out(jsonBody[Seq[ServiceStatus]]) - .name("listAllServices") - .description( - "Get a list of the services. Services that are down will not include health status" - ) - .tag("health") - val statusImpl: ServerEndpoint[Any, Future] = - statusEndpoint.serverLogic(Unit => Future(controller.currentServiceStatus())) + val statusEndpoint: PublicEndpoint[Unit, ErrorMsg, Seq[ServiceStatus], Any] = + baseEndpoint + .get + .in(context / "health" / "status") + .out(jsonBody[Seq[ServiceStatus]]) + .name("listAllServices") + .description( + "Get a list of the services. Services that are down will not include health status" + ) + .tag("health") + val statusImpl: ServerEndpoint[Any, Future] = + statusEndpoint.serverLogic(Unit => Future(controller.currentServiceStatus())) - override def all: List[Endpoint[?, ?, ?, ?, ?]] = List( - defaultEndpoint, - expectedEndpoint, - availableEndpoint, - statusEndpoint - ) - val allImpl: List[ServerEndpoint[Any, Future]] = List( - defaultImpl, - expectedImpl, - availableImpl, - statusImpl - ) + override def all: List[Endpoint[?, ?, ?, ?, ?]] = List( + defaultEndpoint, + expectedEndpoint, + availableEndpoint, + statusEndpoint + ) + val allImpl: List[ServerEndpoint[Any, Future]] = List( + defaultImpl, + expectedImpl, + availableImpl, + statusImpl + ) diff --git a/src/main/scala/org/mbari/raziel/api/SwaggerEndpoints.scala b/src/main/scala/org/mbari/raziel/api/SwaggerEndpoints.scala index e4b3670..be76e24 100644 --- a/src/main/scala/org/mbari/raziel/api/SwaggerEndpoints.scala +++ b/src/main/scala/org/mbari/raziel/api/SwaggerEndpoints.scala @@ -27,10 +27,10 @@ case class SwaggerEndpoints( healthEndpoints: HealthEndpoints ): - val allImpl: List[ServerEndpoint[Any, Future]] = - SwaggerInterpreter() - .fromEndpoints[Future]( - authEndpoints.all ++ endpointsEndpoints.all ++ healthEndpoints.all, - AppConfig.Name, - AppConfig.Version - ) + val allImpl: List[ServerEndpoint[Any, Future]] = + SwaggerInterpreter() + .fromEndpoints[Future]( + authEndpoints.all ++ endpointsEndpoints.all ++ healthEndpoints.all, + AppConfig.Name, + AppConfig.Version + ) diff --git a/src/main/scala/org/mbari/raziel/domain/BasicAuth.scala b/src/main/scala/org/mbari/raziel/domain/BasicAuth.scala index f50ae16..3f57c66 100644 --- a/src/main/scala/org/mbari/raziel/domain/BasicAuth.scala +++ b/src/main/scala/org/mbari/raziel/domain/BasicAuth.scala @@ -31,24 +31,22 @@ case class BasicAuth(username: String, password: String) extends Auth(BasicAuth. object BasicAuth: - val TokenType = "Basic" + val TokenType = "Basic" - /** - * Parse a Basic Authorization header: "Basic " - * - * @param authorization - * The value portion of the Authorization header. - * @return - * The parse BasicAuth. None if it's no parsable - */ - def parse(authorization: String): Option[BasicAuth] = - val parts = authorization.split("\\s+") - if (parts.length == 2 && parts(0).toLowerCase == TokenType.toLowerCase) - val bytes = Base64.getDecoder.decode(parts(1)) - val decoded = new String(bytes, StandardCharsets.UTF_8) - val decodedParts = decoded.split(":") - if (decodedParts.length == 2) - Some(BasicAuth(decodedParts(0), decodedParts(1))) - else - None - else None + /** + * Parse a Basic Authorization header: "Basic " + * + * @param authorization + * The value portion of the Authorization header. + * @return + * The parse BasicAuth. None if it's no parsable + */ + def parse(authorization: String): Option[BasicAuth] = + val parts = authorization.split("\\s+") + if parts.length == 2 && parts(0).toLowerCase == TokenType.toLowerCase then + val bytes = Base64.getDecoder.decode(parts(1)) + val decoded = new String(bytes, StandardCharsets.UTF_8) + val decodedParts = decoded.split(":") + if decodedParts.length == 2 then Some(BasicAuth(decodedParts(0), decodedParts(1))) + else None + else None diff --git a/src/main/scala/org/mbari/raziel/domain/BearerAuth.scala b/src/main/scala/org/mbari/raziel/domain/BearerAuth.scala index 32e9883..f2c15e2 100644 --- a/src/main/scala/org/mbari/raziel/domain/BearerAuth.scala +++ b/src/main/scala/org/mbari/raziel/domain/BearerAuth.scala @@ -30,20 +30,18 @@ case class BearerAuth(accessToken: String) extends Auth(BearerAuth.TokenType) object BearerAuth: - val TokenType = "Bearer" + val TokenType = "Bearer" - /** - * Parse the value portion of an Authorization header into an Authorization object - * @param authorization - * The value portion of an Authorization header - * @return - * An Authorization object, None if it's not parseable - */ - def parse(authorization: String): Option[BearerAuth] = - val parts = authorization.split("\\s+") - if (parts.length == 2 && parts(0).toLowerCase == TokenType.toLowerCase) - Some(BearerAuth(parts(1))) - else - None + /** + * Parse the value portion of an Authorization header into an Authorization object + * @param authorization + * The value portion of an Authorization header + * @return + * An Authorization object, None if it's not parseable + */ + def parse(authorization: String): Option[BearerAuth] = + val parts = authorization.split("\\s+") + if parts.length == 2 && parts(0).toLowerCase == TokenType.toLowerCase then Some(BearerAuth(parts(1))) + else None - val Invalid: BearerAuth = BearerAuth("") + val Invalid: BearerAuth = BearerAuth("") diff --git a/src/main/scala/org/mbari/raziel/domain/EndpointConfig.scala b/src/main/scala/org/mbari/raziel/domain/EndpointConfig.scala index 54a2c4f..bd04647 100644 --- a/src/main/scala/org/mbari/raziel/domain/EndpointConfig.scala +++ b/src/main/scala/org/mbari/raziel/domain/EndpointConfig.scala @@ -45,16 +45,15 @@ case class EndpointConfig( internalUrl: URL ): - /** - * @return - * Endpoint info suitable for JSON serializtion - */ - lazy val external: SerializedEndpointConfig = - SerializedEndpointConfig(name, url, timeout.toMillis, secret, proxyPath) + /** + * @return + * Endpoint info suitable for JSON serializtion + */ + lazy val external: SerializedEndpointConfig = + SerializedEndpointConfig(name, url, timeout.toMillis, secret, proxyPath) /** - * Serialized version of EndpointConfig. Maps Duration to milliseconds, which is better for - * serialization. + * Serialized version of EndpointConfig. Maps Duration to milliseconds, which is better for serialization. * @param name */ case class SerializedEndpointConfig( @@ -64,27 +63,28 @@ case class SerializedEndpointConfig( secret: Option[String], proxyPath: String ): - lazy val internal: EndpointConfig = EndpointConfig( - name, - url, - Duration.ofMillis(timeoutMillis), - secret, - proxyPath, - url - ) // HACK, verify that this is OKs + lazy val internal: EndpointConfig = EndpointConfig( + name, + url, + Duration.ofMillis(timeoutMillis), + secret, + proxyPath, + url + ) // HACK, verify that this is OKs object EndpointConfig: - /** - * @return - * A list of M3 microservice [[EndpointConfig]]s as defined in application.conf - */ - def defaults: List[EndpointConfig] = - AppConfig.Annosaurus :: - AppConfig.Beholder :: - AppConfig.Charybdis :: - AppConfig.Panoptes :: - AppConfig.VampireSquid :: - AppConfig.VarsKbServer :: - AppConfig.VarsUserServer :: - Nil + /** + * @return + * A list of M3 microservice [[EndpointConfig]]s as defined in application.conf + */ + def defaults: List[EndpointConfig] = + (AppConfig.Annosaurus :: + AppConfig.Beholder :: + AppConfig.Charybdis :: + AppConfig.Oni :: + AppConfig.Panoptes :: + AppConfig.VampireSquid :: + AppConfig.VarsKbServer :: + AppConfig.VarsUserServer :: + Nil).flatten diff --git a/src/main/scala/org/mbari/raziel/domain/ErrorMsg.scala b/src/main/scala/org/mbari/raziel/domain/ErrorMsg.scala index a2e1ab2..7aefe14 100644 --- a/src/main/scala/org/mbari/raziel/domain/ErrorMsg.scala +++ b/src/main/scala/org/mbari/raziel/domain/ErrorMsg.scala @@ -17,8 +17,8 @@ package org.mbari.raziel.domain sealed trait ErrorMsg: - def message: String - def responseCode: Int + def message: String + def responseCode: Int /** * Just a simple class used to return a JSON error response diff --git a/src/main/scala/org/mbari/raziel/domain/HealthStatus.scala b/src/main/scala/org/mbari/raziel/domain/HealthStatus.scala index b830ee2..e9c851e 100644 --- a/src/main/scala/org/mbari/raziel/domain/HealthStatus.scala +++ b/src/main/scala/org/mbari/raziel/domain/HealthStatus.scala @@ -31,28 +31,28 @@ final case class HealthStatus( object HealthStatus: - /** - * @return - * a HealthStatus object with the current JVM stats - */ - def default: HealthStatus = - val runtime = Runtime.getRuntime - HealthStatus( - jdkVersion = Runtime.version.toString, - availableProcessors = runtime.availableProcessors, - freeMemory = runtime.freeMemory, - maxMemory = runtime.maxMemory, - totalMemory = runtime.totalMemory - ) + /** + * @return + * a HealthStatus object with the current JVM stats + */ + def default: HealthStatus = + val runtime = Runtime.getRuntime + HealthStatus( + jdkVersion = Runtime.version.toString, + availableProcessors = runtime.availableProcessors, + freeMemory = runtime.freeMemory, + maxMemory = runtime.maxMemory, + totalMemory = runtime.totalMemory + ) - def empty(application: String): HealthStatus = - HealthStatus( - jdkVersion = "", - availableProcessors = 0, - freeMemory = 0, - maxMemory = 0, - totalMemory = 0, - application = application, - version = "0.0.0", - description = "" - ) + def empty(application: String): HealthStatus = + HealthStatus( + jdkVersion = "", + availableProcessors = 0, + freeMemory = 0, + maxMemory = 0, + totalMemory = 0, + application = application, + version = "0.0.0", + description = "" + ) diff --git a/src/main/scala/org/mbari/raziel/domain/HealthStatusHelidon.scala b/src/main/scala/org/mbari/raziel/domain/HealthStatusHelidon.scala index ee931dc..8c424ca 100644 --- a/src/main/scala/org/mbari/raziel/domain/HealthStatusHelidon.scala +++ b/src/main/scala/org/mbari/raziel/domain/HealthStatusHelidon.scala @@ -22,28 +22,28 @@ import org.slf4j.LoggerFactory object HealthStatusHelidon: - private val log = LoggerFactory.getLogger(getClass) + private val log = LoggerFactory.getLogger(getClass) - def parseString(json: String): Option[HealthStatus] = - val either = for - j <- parse(json) - h <- parseJson(j) - yield h - either.toOption + def parseString(json: String): Option[HealthStatus] = + val either = for + j <- parse(json) + h <- parseJson(j) + yield h + either.toOption - def parseJson(json: Json): Either[Throwable, HealthStatus] = - val cursor = json.hcursor - val memJson = cursor - .downField("checks") - .values - .map(_.toSeq) - .getOrElse(Seq.empty) - .lastOption + def parseJson(json: Json): Either[Throwable, HealthStatus] = + val cursor = json.hcursor + val memJson = cursor + .downField("checks") + .values + .map(_.toSeq) + .getOrElse(Seq.empty) + .lastOption - for - doc <- memJson.toRight(new Exception("No memory check found")) - c = doc.hcursor - freeMemory <- c.downField("data").downField("freeBytes").as[Long] - maxMemory <- c.downField("data").downField("maxBytes").as[Long] - totalMemory <- c.downField("data").downField("totalBytes").as[Long] - yield HealthStatus("unknown", -1, freeMemory, maxMemory, totalMemory, "unknown") + for + doc <- memJson.toRight(new Exception("No memory check found")) + c = doc.hcursor + freeMemory <- c.downField("data").downField("freeBytes").as[Long] + maxMemory <- c.downField("data").downField("maxBytes").as[Long] + totalMemory <- c.downField("data").downField("totalBytes").as[Long] + yield HealthStatus("unknown", -1, freeMemory, maxMemory, totalMemory, "unknown") diff --git a/src/main/scala/org/mbari/raziel/domain/JwtAuthPayload.scala b/src/main/scala/org/mbari/raziel/domain/JwtAuthPayload.scala index d9ebc8b..88490fc 100644 --- a/src/main/scala/org/mbari/raziel/domain/JwtAuthPayload.scala +++ b/src/main/scala/org/mbari/raziel/domain/JwtAuthPayload.scala @@ -23,16 +23,16 @@ package org.mbari.raziel.domain */ case class JwtAuthPayload(username: String, email: String, affiliation: String): - /** - * @return - * The payload as a Map. Need to use with auth0's JWT library. - */ - def asMap(): Map[String, Any] = (productElementNames zip productIterator).toMap + /** + * @return + * The payload as a Map. Need to use with auth0's JWT library. + */ + def asMap(): Map[String, Any] = (productElementNames zip productIterator).toMap object JwtAuthPayload: - /** - * Create a JwtAuthPayload from a User's info. - */ - def fromUser(user: User): JwtAuthPayload = - JwtAuthPayload(user.username, user.email.getOrElse(""), user.affiliation.getOrElse("")) + /** + * Create a JwtAuthPayload from a User's info. + */ + def fromUser(user: User): JwtAuthPayload = + JwtAuthPayload(user.username, user.email.getOrElse(""), user.affiliation.getOrElse("")) diff --git a/src/main/scala/org/mbari/raziel/domain/ServiceStatus.scala b/src/main/scala/org/mbari/raziel/domain/ServiceStatus.scala index 7360662..cea77bf 100644 --- a/src/main/scala/org/mbari/raziel/domain/ServiceStatus.scala +++ b/src/main/scala/org/mbari/raziel/domain/ServiceStatus.scala @@ -17,4 +17,9 @@ package org.mbari.raziel.domain case class ServiceStatus(name: String, healthStatus: Option[HealthStatus] = None): - val status: String = if (healthStatus.isDefined) "UP" else "DOWN" + val status: String = if healthStatus.isDefined then "UP" else "DOWN" + + val available: Boolean = healthStatus.isDefined && healthStatus.get.freeMemory > 0 + + + diff --git a/src/main/scala/org/mbari/raziel/domain/User.scala b/src/main/scala/org/mbari/raziel/domain/User.scala index ebac027..fef072e 100644 --- a/src/main/scala/org/mbari/raziel/domain/User.scala +++ b/src/main/scala/org/mbari/raziel/domain/User.scala @@ -31,10 +31,10 @@ case class User( email: Option[String] ): - /** - * @param unencryptedPassword - * A users password, received via basic authentication It's checked to see if it's valid for the - * encrypted data from the database - */ - def authenticate(unencryptedPassword: String): Boolean = - BasicPasswordEncryptor().checkPassword(unencryptedPassword, password) + /** + * @param unencryptedPassword + * A users password, received via basic authentication It's checked to see if it's valid for the encrypted data + * from the database + */ + def authenticate(unencryptedPassword: String): Boolean = + BasicPasswordEncryptor().checkPassword(unencryptedPassword, password) diff --git a/src/main/scala/org/mbari/raziel/etc/auth0/JwtHelper.scala b/src/main/scala/org/mbari/raziel/etc/auth0/JwtHelper.scala index 011db2e..b88eccb 100644 --- a/src/main/scala/org/mbari/raziel/etc/auth0/JwtHelper.scala +++ b/src/main/scala/org/mbari/raziel/etc/auth0/JwtHelper.scala @@ -27,43 +27,43 @@ import scala.util.Try class JwtHelper(issuer: String, signingSecret: String, expiration: Duration): - private val algorithm = Algorithm.HMAC512(signingSecret) + private val algorithm = Algorithm.HMAC512(signingSecret) - val verifier = JWT - .require(algorithm) - .withIssuer(issuer) - .build() + val verifier = JWT + .require(algorithm) + .withIssuer(issuer) + .build() - /** - * @param payload - * A map of stuff to be encoded in the JWT - * @return - * A valid JWT - */ - def createJwt(payload: Map[String, Any]): String = - val now = Instant.now() - val expires = now.plus(expiration) - JWT - .create() - .withPayload(payload.asJava) - .withIssuer(issuer) - .withIssuedAt(Date.from(now)) - .withExpiresAt(Date.from(expires)) - .sign(algorithm) + /** + * @param payload + * A map of stuff to be encoded in the JWT + * @return + * A valid JWT + */ + def createJwt(payload: Map[String, Any]): String = + val now = Instant.now() + val expires = now.plus(expiration) + JWT + .create() + .withPayload(payload.asJava) + .withIssuer(issuer) + .withIssuedAt(Date.from(now)) + .withExpiresAt(Date.from(expires)) + .sign(algorithm) - /** - * @param token - * A JWT - * @return - * Either a decoded JWT or an exception from verifying the JWT - */ - def verifyJwt(token: String): Either[Throwable, DecodedJWT] = - Try(verifier.verify(token)).toEither + /** + * @param token + * A JWT + * @return + * Either a decoded JWT or an exception from verifying the JWT + */ + def verifyJwt(token: String): Either[Throwable, DecodedJWT] = + Try(verifier.verify(token)).toEither object JwtHelper: - /** - * The default JWT helper created from parameters in the application.conf - */ - lazy val default = - JwtHelper(AppConfig.Jwt.Issuer, AppConfig.Jwt.SigningSecret, AppConfig.Jwt.Expiration) + /** + * The default JWT helper created from parameters in the application.conf + */ + lazy val default = + JwtHelper(AppConfig.Jwt.Issuer, AppConfig.Jwt.SigningSecret, AppConfig.Jwt.Expiration) diff --git a/src/main/scala/org/mbari/raziel/etc/circe/CirceCodecs.scala b/src/main/scala/org/mbari/raziel/etc/circe/CirceCodecs.scala index 550b1e1..234ef0e 100644 --- a/src/main/scala/org/mbari/raziel/etc/circe/CirceCodecs.scala +++ b/src/main/scala/org/mbari/raziel/etc/circe/CirceCodecs.scala @@ -19,14 +19,7 @@ package org.mbari.raziel.etc.circe import io.circe.* import io.circe.generic.semiauto.* import java.net.{URI, URL} -import org.mbari.raziel.domain.{ - BearerAuth, - EndpointConfig, - ErrorMsg, - HealthStatus, - ServiceStatus, - User -} +import org.mbari.raziel.domain.{BearerAuth, EndpointConfig, ErrorMsg, HealthStatus, ServiceStatus, User} import org.mbari.raziel.util.HexUtil import scala.util.Try import java.time.Duration @@ -36,76 +29,76 @@ import org.mbari.raziel.domain.StatusMsg object CirceCodecs: - given Encoder[Array[Byte]] = new Encoder[Array[Byte]]: - final def apply(xs: Array[Byte]): Json = - Json.fromString(HexUtil.toHex(xs)) - given Decoder[Array[Byte]] = Decoder - .decodeString - .emapTry(str => Try(HexUtil.fromHex(str))) - - given Decoder[URL] = Decoder - .decodeString - .emapTry(str => Try(new URL(str))) - given Encoder[URL] = Encoder - .encodeString - .contramap(_.toString) - - given Decoder[URI] = Decoder - .decodeString - .emapTry(s => Try(URI.create(s))) - given Encoder[URI] = Encoder - .encodeString - .contramap[URI](_.toString) - - given Decoder[Duration] = Decoder - .decodeLong - .emapTry(lng => Try(Duration.ofMillis(lng))) - given Encoder[Duration] = Encoder - .encodeLong - .contramap(_.toMillis) - - given Decoder[User] = deriveDecoder - given Encoder[User] = deriveEncoder - - given Decoder[BearerAuth] = deriveDecoder - given Encoder[BearerAuth] = deriveEncoder - - given Decoder[ErrorMsg] = deriveDecoder - given Encoder[ErrorMsg] = deriveEncoder - - given Decoder[SerializedEndpointConfig] = deriveDecoder - given Encoder[SerializedEndpointConfig] = deriveEncoder - - given Decoder[HealthStatus] = deriveDecoder - given Encoder[HealthStatus] = deriveEncoder - - given Decoder[ServiceStatus] = deriveDecoder - given Encoder[ServiceStatus] = deriveEncoder - - given Decoder[StatusMsg] = deriveDecoder - given Encoder[StatusMsg] = deriveEncoder - - given Decoder[NotFound] = deriveDecoder - given Encoder[NotFound] = deriveEncoder - - given Decoder[ServerError] = deriveDecoder - given Encoder[ServerError] = deriveEncoder - - given Decoder[Unauthorized] = deriveDecoder - given Encoder[Unauthorized] = deriveEncoder - - private val printer = Printer.noSpaces.copy(dropNullValues = true) - - /** - * Convert a circe Json object to a JSON string - * @param value - * Any value with an implicit circe coder in scope - */ - extension (json: Json) def stringify: String = printer.print(json) - - /** - * Convert an object to a JSON string - * @param value - * Any value with an implicit circe coder in scope - */ - extension [T: Encoder](value: T) def stringify: String = Encoder[T].apply(value).stringify + given Encoder[Array[Byte]] = new Encoder[Array[Byte]]: + final def apply(xs: Array[Byte]): Json = + Json.fromString(HexUtil.toHex(xs)) + given Decoder[Array[Byte]] = Decoder + .decodeString + .emapTry(str => Try(HexUtil.fromHex(str))) + + given Decoder[URL] = Decoder + .decodeString + .emapTry(str => Try(URI.create(str).toURL)) + given Encoder[URL] = Encoder + .encodeString + .contramap(_.toString) + + given Decoder[URI] = Decoder + .decodeString + .emapTry(s => Try(URI.create(s))) + given Encoder[URI] = Encoder + .encodeString + .contramap[URI](_.toString) + + given Decoder[Duration] = Decoder + .decodeLong + .emapTry(lng => Try(Duration.ofMillis(lng))) + given Encoder[Duration] = Encoder + .encodeLong + .contramap(_.toMillis) + + given Decoder[User] = deriveDecoder + given Encoder[User] = deriveEncoder + + given Decoder[BearerAuth] = deriveDecoder + given Encoder[BearerAuth] = deriveEncoder + + given Decoder[ErrorMsg] = deriveDecoder + given Encoder[ErrorMsg] = deriveEncoder + + given Decoder[SerializedEndpointConfig] = deriveDecoder + given Encoder[SerializedEndpointConfig] = deriveEncoder + + given Decoder[HealthStatus] = deriveDecoder + given Encoder[HealthStatus] = deriveEncoder + + given Decoder[ServiceStatus] = deriveDecoder + given Encoder[ServiceStatus] = deriveEncoder + + given Decoder[StatusMsg] = deriveDecoder + given Encoder[StatusMsg] = deriveEncoder + + given Decoder[NotFound] = deriveDecoder + given Encoder[NotFound] = deriveEncoder + + given Decoder[ServerError] = deriveDecoder + given Encoder[ServerError] = deriveEncoder + + given Decoder[Unauthorized] = deriveDecoder + given Encoder[Unauthorized] = deriveEncoder + + private val printer = Printer.noSpaces.copy(dropNullValues = true) + + /** + * Convert a circe Json object to a JSON string + * @param value + * Any value with an implicit circe coder in scope + */ + extension (json: Json) def stringify: String = printer.print(json) + + /** + * Convert an object to a JSON string + * @param value + * Any value with an implicit circe coder in scope + */ + extension [T: Encoder](value: T) def stringify: String = Encoder[T].apply(value).stringify diff --git a/src/main/scala/org/mbari/raziel/etc/jdk/Logging.scala b/src/main/scala/org/mbari/raziel/etc/jdk/Logging.scala index 5609277..ad40e09 100644 --- a/src/main/scala/org/mbari/raziel/etc/jdk/Logging.scala +++ b/src/main/scala/org/mbari/raziel/etc/jdk/Logging.scala @@ -35,61 +35,60 @@ import java.util.function.Supplier */ object Logging: - trait Builder: - def logger: Logger - def level: Level - def throwable: Option[Throwable] + trait Builder: + def logger: Logger + def level: Level + def throwable: Option[Throwable] - case class LoggerBuilder( - logger: Logger, - level: Level = Level.OFF, - throwable: Option[Throwable] = None - ): + case class LoggerBuilder( + logger: Logger, + level: Level = Level.OFF, + throwable: Option[Throwable] = None + ): - def atTrace: LoggerBuilder = copy(level = Level.TRACE) - def atDebug: LoggerBuilder = copy(level = Level.DEBUG) - def atInfo: LoggerBuilder = copy(level = Level.INFO) - def atWarn: LoggerBuilder = copy(level = Level.WARNING) - def atError: LoggerBuilder = copy(level = Level.ERROR) + def atTrace: LoggerBuilder = copy(level = Level.TRACE) + def atDebug: LoggerBuilder = copy(level = Level.DEBUG) + def atInfo: LoggerBuilder = copy(level = Level.INFO) + def atWarn: LoggerBuilder = copy(level = Level.WARNING) + def atError: LoggerBuilder = copy(level = Level.ERROR) - def withCause(cause: Throwable): LoggerBuilder = copy(throwable = Some(cause)) + def withCause(cause: Throwable): LoggerBuilder = copy(throwable = Some(cause)) - def log(msg: String): Unit = - if (logger.isLoggable(level)) - throwable match - case Some(e) => logger.log(level, msg, e) - case None => logger.log(level, msg) + def log(msg: String): Unit = + if logger.isLoggable(level) then + throwable match + case Some(e) => logger.log(level, msg, e) + case None => logger.log(level, msg) - def log(fn: Supplier[String]): Unit = - if (logger.isLoggable(level)) - throwable match - case Some(e) => logger.log(level, fn, e) - case None => logger.log(level, fn) + def log(fn: Supplier[String]): Unit = + if logger.isLoggable(level) then + throwable match + case Some(e) => logger.log(level, fn, e) + case None => logger.log(level, fn) - given Conversion[Logger, LoggerBuilder] with - def apply(logger: Logger): LoggerBuilder = LoggerBuilder(logger) + given Conversion[Logger, LoggerBuilder] with + def apply(logger: Logger): LoggerBuilder = LoggerBuilder(logger) - case class TapLogBuilder[T]( - obj: T, - logger: Logger, - level: Level = Level.OFF, - throwable: Option[Throwable] = None - ) extends Builder: + case class TapLogBuilder[T]( + obj: T, + logger: Logger, + level: Level = Level.OFF, + throwable: Option[Throwable] = None + ) extends Builder: - def atTrace: TapLogBuilder[T] = copy(level = Level.TRACE) - def atDebug: TapLogBuilder[T] = copy(level = Level.DEBUG) - def atInfo: TapLogBuilder[T] = copy(level = Level.INFO) - def atWarn: TapLogBuilder[T] = copy(level = Level.WARNING) - def atError: TapLogBuilder[T] = copy(level = Level.ERROR) + def atTrace: TapLogBuilder[T] = copy(level = Level.TRACE) + def atDebug: TapLogBuilder[T] = copy(level = Level.DEBUG) + def atInfo: TapLogBuilder[T] = copy(level = Level.INFO) + def atWarn: TapLogBuilder[T] = copy(level = Level.WARNING) + def atError: TapLogBuilder[T] = copy(level = Level.ERROR) - def withCause(cause: Throwable): TapLogBuilder[T] = copy(throwable = Some(cause)) + def withCause(cause: Throwable): TapLogBuilder[T] = copy(throwable = Some(cause)) - def log(fn: T => String): T = - if (logger.isLoggable(level)) - throwable match - case Some(e) => logger.log(level, fn(obj), e) - case None => logger.log(level, fn(obj)) - obj + def log(fn: T => String): T = + if logger.isLoggable(level) then + throwable match + case Some(e) => logger.log(level, fn(obj), e) + case None => logger.log(level, fn(obj)) + obj - extension [T](obj: T)(using logger: Logger) - def tapLog: TapLogBuilder[T] = TapLogBuilder(obj, logger) + extension [T](obj: T)(using logger: Logger) def tapLog: TapLogBuilder[T] = TapLogBuilder(obj, logger) diff --git a/src/main/scala/org/mbari/raziel/etc/methanol/HttpClientSupport.scala b/src/main/scala/org/mbari/raziel/etc/methanol/HttpClientSupport.scala index e419bf7..663eb37 100644 --- a/src/main/scala/org/mbari/raziel/etc/methanol/HttpClientSupport.scala +++ b/src/main/scala/org/mbari/raziel/etc/methanol/HttpClientSupport.scala @@ -39,45 +39,45 @@ class HttpClientSupport( executor: Executor = Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors) ): - val client = Methanol - .newBuilder() - .autoAcceptEncoding(true) - .connectTimeout(timeout) - .executor(executor) - .interceptor(LoggingInterceptor) - .readTimeout(timeout) - .requestTimeout(timeout) - .userAgent(AppConfig.Name) - .build() + val client: Methanol = Methanol + .newBuilder() + .autoAcceptEncoding(true) + .connectTimeout(timeout) + .executor(executor) + .interceptor(LoggingInterceptor) + .readTimeout(timeout) + .requestTimeout(timeout) + .userAgent(AppConfig.Name) + .build() - /** - * Convert a [[HttpRequest]] to a [[Task]]. Used to compose IO. - * @param request - * The request to convert to a zio Task - * @tparam T - * The type of the response body. Requires an implicit circe [[Decoder]] is in scope. - * @return - * A [[Task]] that will resolve to the response body - */ - def requestObjectsZ[T]( - request: HttpRequest - )(implicit decoder: Decoder[T]): Task[T] = zio.ZIO.fromEither(requestObjects(request)) + /** + * Convert a [[HttpRequest]] to a [[Task]]. Used to compose IO. + * @param request + * The request to convert to a zio Task + * @tparam T + * The type of the response body. Requires an implicit circe [[Decoder]] is in scope. + * @return + * A [[Task]] that will resolve to the response body + */ + def requestObjectsZ[T]( + request: HttpRequest + )(implicit decoder: Decoder[T]): Task[T] = zio.ZIO.fromEither(requestObjects(request)) - def requestStringZ(request: HttpRequest): Task[String] = - zio.ZIO.fromEither(requestString(request)) + def requestStringZ(request: HttpRequest): Task[String] = + zio.ZIO.fromEither(requestString(request)) - def requestObjects[T: Decoder](request: HttpRequest): Either[Throwable, T] = - for - body <- requestString(request) - obj <- decode[T](body) - yield obj + def requestObjects[T: Decoder](request: HttpRequest): Either[Throwable, T] = + for + body <- requestString(request) + obj <- decode[T](body) + yield obj - def requestString(request: HttpRequest): Either[Throwable, String] = - for - response <- Try(client.send(request, BodyHandlers.ofString())).toEither - body <- if (response.statusCode() == 200) Right(response.body) - else - Left( - new RuntimeException(s"Unexpected response from ${request.uri}: ${response.body}") - ) - yield body + def requestString(request: HttpRequest): Either[Throwable, String] = + for + response <- Try(client.send(request, BodyHandlers.ofString())).toEither + body <- if response.statusCode() == 200 then Right(response.body) + else + Left( + new RuntimeException(s"Unexpected response from ${request.uri}: ${response.body}") + ) + yield body diff --git a/src/main/scala/org/mbari/raziel/etc/methanol/LoggingInterceptor.scala b/src/main/scala/org/mbari/raziel/etc/methanol/LoggingInterceptor.scala index 444b545..811371f 100644 --- a/src/main/scala/org/mbari/raziel/etc/methanol/LoggingInterceptor.scala +++ b/src/main/scala/org/mbari/raziel/etc/methanol/LoggingInterceptor.scala @@ -32,46 +32,46 @@ import java.util.stream.Collectors */ object LoggingInterceptor extends Methanol.Interceptor: - private val log = System.getLogger(getClass.getName) + private val log = System.getLogger(getClass.getName) - override def intercept[T](request: HttpRequest, chain: Chain[T]): HttpResponse[T] = - logRequest(request) - toLoggingChain(request, chain).forward(request) + override def intercept[T](request: HttpRequest, chain: Chain[T]): HttpResponse[T] = + logRequest(request) + toLoggingChain(request, chain).forward(request) - override def interceptAsync[T]( - request: HttpRequest, - chain: Chain[T] - ): CompletableFuture[HttpResponse[T]] = - logRequest(request) - toLoggingChain(request, chain).forwardAsync(request) + override def interceptAsync[T]( + request: HttpRequest, + chain: Chain[T] + ): CompletableFuture[HttpResponse[T]] = + logRequest(request) + toLoggingChain(request, chain).forwardAsync(request) - private def logRequest(request: HttpRequest): Unit = - log.log( - System.Logger.Level.DEBUG, - () => s""" Sent >>> + private def logRequest(request: HttpRequest): Unit = + log.log( + System.Logger.Level.DEBUG, + () => s""" Sent >>> |${request.method()} ${request.uri()} |${headersToString(request.headers())}""".stripMargin.trim() - ) + ) - private def toLoggingChain[T](request: HttpRequest, chain: Chain[T]): Chain[T] = + private def toLoggingChain[T](request: HttpRequest, chain: Chain[T]): Chain[T] = - val sentAt = Instant.now() - // format: off - chain.withBodyHandler(responseInfo => - log.log(System.Logger.Level.DEBUG, () => - s""" Received <<< ${request.method()} ${request.uri()} in ${Duration.between(sentAt, Instant.now()).toMillis()}ms - |${responseInfo.statusCode()} - |${headersToString(responseInfo.headers())}""".stripMargin.trim() + val sentAt = Instant.now() + // format: off + chain.withBodyHandler(responseInfo => + log.log(System.Logger.Level.DEBUG, () => + s""" Received <<< ${request.method()} ${request.uri()} in ${Duration.between(sentAt, Instant.now()).toMillis()}ms + |${responseInfo.statusCode()} + |${headersToString(responseInfo.headers())}""".stripMargin.trim() + ) + chain.bodyHandler().apply(responseInfo) ) - chain.bodyHandler().apply(responseInfo) - ) - // format: on + // format: on - private def headersToString(headers: HttpHeaders): String = - headers - .map() - .entrySet() - .stream() - .map(e => s"${e.getKey()}: ${String.join(", ", e.getValue())}") - .collect(Collectors.joining(System.lineSeparator())) - .trim() + private def headersToString(headers: HttpHeaders): String = + headers + .map() + .entrySet() + .stream() + .map(e => s"${e.getKey()}: ${String.join(", ", e.getValue())}") + .collect(Collectors.joining(System.lineSeparator())) + .trim() diff --git a/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLogger.scala b/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLogger.scala index 1ef1bac..458f6d2 100644 --- a/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLogger.scala +++ b/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLogger.scala @@ -23,37 +23,37 @@ import scala.beans.BeanProperty class Slf4jLogger(@BeanProperty val name: String) extends System.Logger: - private val logger = org.slf4j.LoggerFactory.getLogger(name) + private val logger = org.slf4j.LoggerFactory.getLogger(name) - override def isLoggable(level: Level): Boolean = - level match - case Level.OFF => false - case Level.TRACE => logger.isTraceEnabled - case Level.DEBUG => logger.isDebugEnabled - case Level.INFO => logger.isInfoEnabled - case Level.WARNING => logger.isWarnEnabled - case Level.ERROR => logger.isErrorEnabled - case _ => true + override def isLoggable(level: Level): Boolean = + level match + case Level.OFF => false + case Level.TRACE => logger.isTraceEnabled + case Level.DEBUG => logger.isDebugEnabled + case Level.INFO => logger.isInfoEnabled + case Level.WARNING => logger.isWarnEnabled + case Level.ERROR => logger.isErrorEnabled + case _ => true - override def log(level: Level, bundle: ResourceBundle, msg: String, thrown: Throwable): Unit = - if (isLoggable(level)) - level match - case Level.OFF => // Do nothing - case Level.TRACE => logger.trace(msg, thrown) - case Level.DEBUG => logger.debug(msg, thrown) - case Level.INFO => logger.info(msg, thrown) - case Level.WARNING => logger.warn(msg, thrown) - case Level.ERROR => logger.error(msg, thrown) - case _ => logger.info(msg, thrown) + override def log(level: Level, bundle: ResourceBundle, msg: String, thrown: Throwable): Unit = + if isLoggable(level) then + level match + case Level.OFF => // Do nothing + case Level.TRACE => logger.trace(msg, thrown) + case Level.DEBUG => logger.debug(msg, thrown) + case Level.INFO => logger.info(msg, thrown) + case Level.WARNING => logger.warn(msg, thrown) + case Level.ERROR => logger.error(msg, thrown) + case _ => logger.info(msg, thrown) - override def log(level: Level, bundle: ResourceBundle, format: String, params: Any*): Unit = - if (isLoggable(level)) - var msg = if params != null then MessageFormat.format(format, params: _*) else format - level match - case Level.OFF => // Do nothing - case Level.TRACE => logger.trace(msg) - case Level.DEBUG => logger.debug(msg) - case Level.INFO => logger.info(msg) - case Level.WARNING => logger.warn(msg) - case Level.ERROR => logger.error(msg) - case _ => logger.info(msg) + override def log(level: Level, bundle: ResourceBundle, format: String, params: Any*): Unit = + if isLoggable(level) then + var msg = if params != null then MessageFormat.format(format, params*) else format + level match + case Level.OFF => // Do nothing + case Level.TRACE => logger.trace(msg) + case Level.DEBUG => logger.debug(msg) + case Level.INFO => logger.info(msg) + case Level.WARNING => logger.warn(msg) + case Level.ERROR => logger.error(msg) + case _ => logger.info(msg) diff --git a/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLoggerFinder.scala b/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLoggerFinder.scala index 4867dc8..e4a0d54 100644 --- a/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLoggerFinder.scala +++ b/src/main/scala/org/mbari/raziel/etc/slf4j/Slf4jLoggerFinder.scala @@ -17,5 +17,5 @@ package org.mbari.raziel.etc.slf4j class Slf4jLoggerFinder extends System.LoggerFinder: - override def getLogger(name: String, module: Module): System.Logger = - new Slf4jLogger(name) + override def getLogger(name: String, module: Module): System.Logger = + new Slf4jLogger(name) diff --git a/src/main/scala/org/mbari/raziel/etc/xml/Converters.scala b/src/main/scala/org/mbari/raziel/etc/xml/Converters.scala index 46389ef..7dbb706 100644 --- a/src/main/scala/org/mbari/raziel/etc/xml/Converters.scala +++ b/src/main/scala/org/mbari/raziel/etc/xml/Converters.scala @@ -20,5 +20,5 @@ import org.w3c.dom.NodeList import org.w3c.dom.Node given Conversion[NodeList, List[Node]] with - def apply(nodeList: NodeList): List[Node] = - (0 until nodeList.getLength).map(nodeList.item).toList + def apply(nodeList: NodeList): List[Node] = + (0 until nodeList.getLength).map(nodeList.item).toList diff --git a/src/main/scala/org/mbari/raziel/etc/zio/ZioUtil.scala b/src/main/scala/org/mbari/raziel/etc/zio/ZioUtil.scala index ed45076..50c6809 100644 --- a/src/main/scala/org/mbari/raziel/etc/zio/ZioUtil.scala +++ b/src/main/scala/org/mbari/raziel/etc/zio/ZioUtil.scala @@ -22,36 +22,36 @@ import zio.Cause.Die object ZioUtil: - private val log = java.lang.System.getLogger(getClass.getName) + private val log = java.lang.System.getLogger(getClass.getName) - /** - * Run the given effect. Throws an exception if the effect fails. - * - * @param app - * The effect to run - * @return - * The result of the effect - */ - def unsafeRun[E, A](app: ZIO[Any, E, A]): A = - Unsafe.unsafe { implicit unsafe => - Runtime.default.unsafe.run(app).getOrThrowFiberFailure() - } + /** + * Run the given effect. Throws an exception if the effect fails. + * + * @param app + * The effect to run + * @return + * The result of the effect + */ + def unsafeRun[E, A](app: ZIO[Any, E, A]): A = + Unsafe.unsafe { implicit unsafe => + Runtime.default.unsafe.run(app).getOrThrowFiberFailure() + } - /** - * Run the given effect. - * - * @param app - * The effect to run - * @return - * The result of the effect. Some on success. None on Failure - */ - def safeRun[E, A](app: ZIO[Any, E, A]): Option[A] = - Unsafe.unsafe { implicit unsafe => - Runtime.default.unsafe.run(app) match - case Exit.Success(a) => Some(a) - case Exit.Failure(e) => - e match - case d: Die => log.atError.withCause(d.value).log(e.toString) - case _ => log.atError.log(e.toString) - None - } + /** + * Run the given effect. + * + * @param app + * The effect to run + * @return + * The result of the effect. Some on success. None on Failure + */ + def safeRun[E, A](app: ZIO[Any, E, A]): Option[A] = + Unsafe.unsafe { implicit unsafe => + Runtime.default.unsafe.run(app) match + case Exit.Success(a) => Some(a) + case Exit.Failure(e) => + e match + case d: Die => log.atError.withCause(d.value).log(e.toString) + case _ => log.atError.log(e.toString) + None + } diff --git a/src/main/scala/org/mbari/raziel/services/Annosaurus.scala b/src/main/scala/org/mbari/raziel/services/Annosaurus.scala index b77868b..1f17089 100644 --- a/src/main/scala/org/mbari/raziel/services/Annosaurus.scala +++ b/src/main/scala/org/mbari/raziel/services/Annosaurus.scala @@ -26,31 +26,17 @@ import org.mbari.raziel.etc.circe.CirceCodecs.given import org.mbari.raziel.etc.methanol.HttpClientSupport import zio.Task -class Annosaurus( - rootUrl: String, - timeout: Duration, - executor: Executor = Executors.newSingleThreadExecutor() -) extends HasHealth: - - private val httpClientSupport = new HttpClientSupport(timeout, executor) - - val name = "annosaurus" - - def health(): Task[HealthStatus] = - val request = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/health")) - .header("Accept", "application/json") - .GET() - .build() - httpClientSupport - .requestObjectsZ[HealthStatus](request) - object Annosaurus: - def default(using executor: Executor) = - new Annosaurus( - AppConfig.Annosaurus.internalUrl.toExternalForm, - AppConfig.Annosaurus.timeout, - executor - ) + def default(using executor: Executor): Option[HealthService] = + AppConfig + .Annosaurus + .map(config => + val uri = URI.create(s"${config.internalUrl.toExternalForm}/health") + new DefaultHealthService( + config.name, + uri, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/services/Beholder.scala b/src/main/scala/org/mbari/raziel/services/Beholder.scala index 091fb97..f1776c7 100644 --- a/src/main/scala/org/mbari/raziel/services/Beholder.scala +++ b/src/main/scala/org/mbari/raziel/services/Beholder.scala @@ -27,31 +27,17 @@ import java.net.URI import org.mbari.raziel.etc.circe.CirceCodecs.given import org.mbari.raziel.AppConfig -class Beholder( - rootUrl: String, - timeout: Duration, - executor: Executor = Executors.newSingleThreadExecutor() -) extends HasHealth: - - private val httpClientSupport = new HttpClientSupport(timeout, executor) - - override val name: String = "beholder" - - def health(): Task[HealthStatus] = - val request = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/health")) - .header("Accept", "application/json") - .GET() - .build() - httpClientSupport - .requestObjectsZ[HealthStatus](request) - object Beholder: - def default(using executor: Executor) = - new Beholder( - AppConfig.Beholder.internalUrl.toExternalForm(), - AppConfig.Beholder.timeout, - executor - ) + def default(using executor: Executor): Option[HealthService] = + AppConfig + .Beholder + .map(config => + val uri = URI.create(s"${config.internalUrl.toExternalForm}/health") + new DefaultHealthService( + config.name, + uri, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/services/Charybdis.scala b/src/main/scala/org/mbari/raziel/services/Charybdis.scala index 4e1b655..9e642f3 100644 --- a/src/main/scala/org/mbari/raziel/services/Charybdis.scala +++ b/src/main/scala/org/mbari/raziel/services/Charybdis.scala @@ -32,44 +32,50 @@ class Charybdis( rootUrl: String, timeout: Duration, executor: Executor = Executors.newSingleThreadExecutor() -) extends HasHealth: +) extends HealthService: - private val httpClientSupport = new HttpClientSupport(timeout, executor) + private val httpClientSupport = new HttpClientSupport(timeout, executor) - val name = "charybdis" + val name = "charybdis" - def health(): Task[HealthStatus] = + val healthUri: URI = URI.create(s"$rootUrl/health") - val request0 = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/health")) - .header("Accept", "application/json") - .GET() - .build() + def health(): Task[HealthStatus] = - val request1 = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/observe/health")) - .header("Accept", "application/json") - .GET() - .build() + val request0 = HttpRequest + .newBuilder() + .uri(healthUri) + .header("Accept", "application/json") + .GET() + .build() - for - // Try the new endpoint first, fall back to the old one - body <- httpClientSupport.requestStringZ(request1).orElse(httpClientSupport.requestStringZ(request0)) - healthStatus <- ZIO.fromEither( - HealthStatusHelidon - .parseString(body) - .map(Right(_)) - .getOrElse(Left(new Exception(s"Could not parse $body"))) - ) - yield healthStatus.copy(application = name, description = "Publication Dataset Server") + val request1 = HttpRequest + .newBuilder() + .uri(URI.create(s"$rootUrl/observe/health")) + .header("Accept", "application/json") + .GET() + .build() + + for + // Try the new endpoint first, fall back to the old one + body <- httpClientSupport.requestStringZ(request1).orElse(httpClientSupport.requestStringZ(request0)) + healthStatus <- ZIO.fromEither( + HealthStatusHelidon + .parseString(body) + .map(Right(_)) + .getOrElse(Left(new Exception(s"Could not parse $body"))) + ) + yield healthStatus.copy(application = name, description = "Publication Dataset Server") object Charybdis: - def default(using executor: Executor) = - new Charybdis( - AppConfig.Charybdis.internalUrl.toExternalForm, - AppConfig.Charybdis.timeout, - executor - ) + def default(using executor: Executor): Option[HealthService] = + AppConfig + .Charybdis + .map(config => + new Charybdis( + config.internalUrl.toExternalForm, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/services/DefaultHealthService.scala b/src/main/scala/org/mbari/raziel/services/DefaultHealthService.scala new file mode 100644 index 0000000..b1fa829 --- /dev/null +++ b/src/main/scala/org/mbari/raziel/services/DefaultHealthService.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2021 MBARI + * + * 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 org.mbari.raziel.services + +import org.mbari.raziel.domain.HealthStatus +import org.mbari.raziel.etc.methanol.HttpClientSupport +import zio.Task +import org.mbari.raziel.etc.circe.CirceCodecs.given + +import java.net.URI +import java.net.http.HttpRequest +import java.time.Duration +import java.util.concurrent.{Executor, Executors} + +class DefaultHealthService( + val name: String, + val healthUri: URI, + timeout: Duration, + executor: Executor = Executors.newSingleThreadExecutor() +) extends HealthService: + + private val httpClientSupport = new HttpClientSupport(timeout, executor) + + def health(): Task[HealthStatus] = + val request = HttpRequest + .newBuilder() + .uri(healthUri) + .header("Accept", "application/json") + .GET() + .build() + httpClientSupport + .requestObjectsZ[HealthStatus](request) diff --git a/src/main/scala/org/mbari/raziel/services/HasHealth.scala b/src/main/scala/org/mbari/raziel/services/HasHealth.scala deleted file mode 100644 index 649ef02..0000000 --- a/src/main/scala/org/mbari/raziel/services/HasHealth.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2021 MBARI - * - * 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 org.mbari.raziel.services - -import org.mbari.raziel.domain.HealthStatus -import zio.Task - -trait HasHealth: - - def name: String - - def health(): Task[HealthStatus] diff --git a/src/main/scala/org/mbari/raziel/services/HealthService.scala b/src/main/scala/org/mbari/raziel/services/HealthService.scala index dfcca88..091c37d 100644 --- a/src/main/scala/org/mbari/raziel/services/HealthService.scala +++ b/src/main/scala/org/mbari/raziel/services/HealthService.scala @@ -16,18 +16,15 @@ package org.mbari.raziel.services -import org.mbari.raziel.domain.ServiceStatus -import zio.Task import org.mbari.raziel.domain.HealthStatus -import zio.ZIO +import zio.Task + +import java.net.URI + +trait HealthService: + + def name: String -class HealthService(services: Seq[HasHealth]): + def healthUri: URI - def fetchHealth(): Task[Seq[HealthStatus]] = - for - healthStati <- - ZIO.collectAll( - services.map(s => s.health().orElse(ZIO.succeed(HealthStatus.empty(s.name)))) - ) - yield (healthStati :+ HealthStatus.default) - .sortBy(_.application) + def health(): Task[HealthStatus] diff --git a/src/main/scala/org/mbari/raziel/services/HealthServices.scala b/src/main/scala/org/mbari/raziel/services/HealthServices.scala new file mode 100644 index 0000000..f974571 --- /dev/null +++ b/src/main/scala/org/mbari/raziel/services/HealthServices.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2021 MBARI + * + * 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 org.mbari.raziel.services + +import org.mbari.raziel.AppConfig +import org.mbari.raziel.domain.ServiceStatus +import zio.Task +import org.mbari.raziel.domain.HealthStatus +import zio.ZIO + +import java.util.concurrent.Executor + +class HealthServices(services: Seq[HealthService]): + + def fetchHealth(): Task[Seq[ServiceStatus]] = + for healthStati <- + ZIO.collectAll( + services.map(s => s.health() + .map(hs => ServiceStatus(s.name, Some(hs))) // Rename the application to the service name + .orElse(ZIO.succeed(ServiceStatus(s.name, Some(HealthStatus.empty(s.name)))))) + ) + yield (healthStati :+ ServiceStatus(AppConfig.Name, Some(HealthStatus.default))) + .sortBy(_.name) + +object HealthServices: + + def init(using executor: Executor): Seq[HealthService] = List( + Annosaurus.default, + Beholder.default, + Charybdis.default, + Oni.default, + Panoptes.default, + VampireSquid.default, + VarsKbServer.default, + VarsUserServer.default + ).flatten diff --git a/src/main/scala/org/mbari/raziel/services/Oni.scala b/src/main/scala/org/mbari/raziel/services/Oni.scala new file mode 100644 index 0000000..350f94b --- /dev/null +++ b/src/main/scala/org/mbari/raziel/services/Oni.scala @@ -0,0 +1,42 @@ +/* + * Copyright 2021 MBARI + * + * 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 org.mbari.raziel.services + +import java.net.http.HttpRequest +import java.net.URI +import java.time.Duration +import java.util.concurrent.{Executor, Executors} +import org.mbari.raziel.AppConfig +import org.mbari.raziel.domain.HealthStatus +import org.mbari.raziel.etc.circe.CirceCodecs.given +import org.mbari.raziel.etc.methanol.HttpClientSupport +import zio.Task + +object Oni: + + def default(using executor: Executor): Option[HealthService] = + AppConfig + .Oni + .map(config => + val uri = URI.create(s"${config.internalUrl.toExternalForm}/health") + new DefaultHealthService( + config.name, + uri, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/services/Panoptes.scala b/src/main/scala/org/mbari/raziel/services/Panoptes.scala index 0eb586f..e922cdd 100644 --- a/src/main/scala/org/mbari/raziel/services/Panoptes.scala +++ b/src/main/scala/org/mbari/raziel/services/Panoptes.scala @@ -26,31 +26,17 @@ import org.mbari.raziel.etc.circe.CirceCodecs.given import org.mbari.raziel.etc.methanol.HttpClientSupport import zio.Task -class Panoptes( - rootUrl: String, - timeout: Duration, - executor: Executor = Executors.newSingleThreadExecutor() -) extends HasHealth: - - private val httpClientSupport = new HttpClientSupport(timeout, executor) - - val name = "panoptes" - - def health(): Task[HealthStatus] = - val request = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/health")) - .header("Accept", "application/json") - .GET() - .build() - httpClientSupport - .requestObjectsZ[HealthStatus](request) - object Panoptes: - def default(using executor: Executor) = - new Panoptes( - AppConfig.Panoptes.internalUrl.toExternalForm, - AppConfig.Panoptes.timeout, - executor - ) + def default(using executor: Executor): Option[HealthService] = + AppConfig + .Panoptes + .map(config => + val uri = URI.create(s"${config.internalUrl.toExternalForm}/health") + new DefaultHealthService( + config.name, + uri, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/services/VampireSquid.scala b/src/main/scala/org/mbari/raziel/services/VampireSquid.scala index 2ef238a..8bf2c8c 100644 --- a/src/main/scala/org/mbari/raziel/services/VampireSquid.scala +++ b/src/main/scala/org/mbari/raziel/services/VampireSquid.scala @@ -26,31 +26,17 @@ import org.mbari.raziel.etc.circe.CirceCodecs.given import org.mbari.raziel.etc.methanol.HttpClientSupport import zio.Task -class VampireSquid( - rootUrl: String, - timeout: Duration, - executor: Executor = Executors.newSingleThreadExecutor() -) extends HasHealth: - - private val httpClientSupport = new HttpClientSupport(timeout, executor) - - val name = "vampire-squid" - - def health(): Task[HealthStatus] = - val request = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/health")) - .header("Accept", "application/json") - .GET() - .build() - httpClientSupport - .requestObjectsZ[HealthStatus](request) - object VampireSquid: - def default(using executor: Executor) = - new VampireSquid( - AppConfig.VampireSquid.internalUrl.toExternalForm, - AppConfig.VampireSquid.timeout, - executor - ) + def default(using executor: Executor): Option[HealthService] = + AppConfig + .VampireSquid + .map(config => + val uri = URI.create(s"${config.internalUrl.toExternalForm}/health") + new DefaultHealthService( + config.name, + uri, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/services/VarsKbServer.scala b/src/main/scala/org/mbari/raziel/services/VarsKbServer.scala index bbf5dc9..1adb570 100644 --- a/src/main/scala/org/mbari/raziel/services/VarsKbServer.scala +++ b/src/main/scala/org/mbari/raziel/services/VarsKbServer.scala @@ -26,31 +26,17 @@ import org.mbari.raziel.etc.circe.CirceCodecs.given import org.mbari.raziel.etc.methanol.HttpClientSupport import zio.Task -class VarsKbServer( - rootUrl: String, - timeout: Duration, - executor: Executor = Executors.newSingleThreadExecutor() -) extends HasHealth: - - private val httpClientSupport = new HttpClientSupport(timeout, executor) - - val name = "vars-kb-server" - - def health(): Task[HealthStatus] = - val request = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/health")) - .header("Accept", "application/json") - .GET() - .build() - httpClientSupport - .requestObjectsZ[HealthStatus](request) - object VarsKbServer: - def default(using executor: Executor) = - new VarsKbServer( - AppConfig.VarsKbServer.internalUrl.toExternalForm, - AppConfig.VarsKbServer.timeout, - executor - ) + def default(using executor: Executor): Option[HealthService] = + AppConfig + .VarsKbServer + .map(config => + val uri = URI.create(s"${config.internalUrl.toExternalForm}/health") + new DefaultHealthService( + config.name, + uri, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/services/VarsUserServer.scala b/src/main/scala/org/mbari/raziel/services/VarsUserServer.scala index def20bb..88a036a 100644 --- a/src/main/scala/org/mbari/raziel/services/VarsUserServer.scala +++ b/src/main/scala/org/mbari/raziel/services/VarsUserServer.scala @@ -44,52 +44,58 @@ class VarsUserServer( rootUrl: String, timeout: Duration, executor: Executor = Executors.newSingleThreadExecutor() -) extends HasHealth: +) extends HealthService: - private val httpClientSupport = new HttpClientSupport(timeout, executor) + private val httpClientSupport = new HttpClientSupport(timeout, executor) - val name = "vars-user-server" + val name = "vars-user-server" - def health(): Task[HealthStatus] = - val request = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/health")) - .header("Accept", "application/json") - .GET() - .build() - httpClientSupport - .requestObjectsZ[HealthStatus](request) + val healthUri: URI = URI.create(s"$rootUrl/health") - object Users: + def health(): Task[HealthStatus] = + val request = HttpRequest + .newBuilder() + .uri(healthUri) + .header("Accept", "application/json") + .GET() + .build() + httpClientSupport + .requestObjectsZ[HealthStatus](request) - /** - * Find a user in the vars-user-server by thier username - * @param userName - * The username of the user to find - * @return - * A user if found, otherwise None - */ - def findByName(userName: String): Task[Option[User]] = - val request = HttpRequest - .newBuilder() - .uri(URI.create(s"$rootUrl/users/$userName")) - .header("Accept", "application/json") - .GET() - .build() - httpClientSupport - .requestObjectsZ[User](request) - .map(u => Option(u)) + object Users: + + /** + * Find a user in the vars-user-server by thier username + * @param userName + * The username of the user to find + * @return + * A user if found, otherwise None + */ + def findByName(userName: String): Task[Option[User]] = + val request = HttpRequest + .newBuilder() + .uri(URI.create(s"$rootUrl/users/$userName")) + .header("Accept", "application/json") + .GET() + .build() + httpClientSupport + .requestObjectsZ[User](request) + .map(u => Option(u)) object VarsUserServer: - /** - * Builds a VarsUserServer from the application configuration and the provided executor - * @param executor - * The executor to use for the HTTP requests - */ - def default(using executor: Executor) = - new VarsUserServer( - AppConfig.VarsUserServer.internalUrl.toExternalForm, - AppConfig.VarsUserServer.timeout, - executor - ) + /** + * Builds a VarsUserServer from the application configuration and the provided executor + * @param executor + * The executor to use for the HTTP requests + */ + def default(using executor: Executor): Option[VarsUserServer] = + AppConfig + .VarsUserServer + .map(config => + new VarsUserServer( + config.internalUrl.toExternalForm, + config.timeout, + executor + ) + ) diff --git a/src/main/scala/org/mbari/raziel/util/HexUtil.scala b/src/main/scala/org/mbari/raziel/util/HexUtil.scala index 917f227..fb730e5 100644 --- a/src/main/scala/org/mbari/raziel/util/HexUtil.scala +++ b/src/main/scala/org/mbari/raziel/util/HexUtil.scala @@ -18,14 +18,13 @@ package org.mbari.raziel.util object HexUtil: - def toHex(bytes: Array[Byte]): String = - val sb = new StringBuilder - for (b <- bytes) - sb.append(String.format("%02x", Byte.box(b))) - sb.toString + def toHex(bytes: Array[Byte]): String = + val sb = new StringBuilder + for b <- bytes do sb.append(String.format("%02x", Byte.box(b))) + sb.toString - def fromHex(hex: String): Array[Byte] = - hex - .grouped(2) - .toArray - .map(i => Integer.parseInt(i, 16).toByte) + def fromHex(hex: String): Array[Byte] = + hex + .grouped(2) + .toArray + .map(i => Integer.parseInt(i, 16).toByte)