diff --git a/.gitignore b/.gitignore index f226d5cc9..1a7ae0466 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,4 @@ testem.log # System Files .DS_Store Thumbs.db -/bakery/dashboard/dashboard.zip +/http/baker-http-dashboard/dashboard.zip diff --git a/README.md b/README.md index 775193ccb..9a3c593d6 100644 --- a/README.md +++ b/README.md @@ -74,16 +74,16 @@ Applying Baker will only be successful if you make sure that: To get started with SBT, simply add the following to your build.sbt file: ``` -libraryDependencies += "com.ing.baker" %% "baker-recipe-dsl" % "3.0.3" -libraryDependencies += "com.ing.baker" %% "baker-runtime" % "3.0.3" -libraryDependencies += "com.ing.baker" %% "baker-compiler" % "3.0.3" +libraryDependencies += "com.ing.baker" %% "baker-recipe-dsl" % version +libraryDependencies += "com.ing.baker" %% "baker-runtime" % version +libraryDependencies += "com.ing.baker" %% "baker-compiler" % version ``` From 1.3.x to 2.0.x we cross compile to both Scala 2.11 and 2.12. Earlier releases are only available for Scala 2.11. -From 3.0.x we support only Scala 2.12. +From 3.x.x we support only Scala 2.12. # How to contribute? diff --git a/bakery/README.MD b/bakery/README.MD new file mode 100644 index 000000000..ad89c5eef --- /dev/null +++ b/bakery/README.MD @@ -0,0 +1,9 @@ +The `bakery` directory contains modules for setting up a bakery cluster. + +Bakery is a way to host a baker service using akka-runtime and expose it using API endpoints. This allows you to separate +the running of an akka cluster and managing of the persistence from using baker for your business process. + +A bakery team can provide baker clusters as a service (which requires specific knowledge about akka clusters). +A client team can focus on creating recipes based on actual business processes. + +The `bakery-state` module is the entry point for bakery. diff --git a/bakery/client/src/test/java/com/ing/bakery/client/JBakerClientSpec.java b/bakery/client/src/test/java/com/ing/bakery/client/JBakerClientSpec.java deleted file mode 100644 index c873bc18b..000000000 --- a/bakery/client/src/test/java/com/ing/bakery/client/JBakerClientSpec.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ing.bakery.client; - -import com.google.common.collect.ImmutableList; -import com.ing.baker.runtime.javadsl.Baker; -import com.ing.bakery.javadsl.BakerClient; - -import java.util.Optional; -import java.util.concurrent.ExecutionException; - -public class JBakerClientSpec { - - public void shouldCompileTheRecipeWithoutIssues() throws ExecutionException, InterruptedException { - Baker baker = BakerClient.build( - ImmutableList.of("bakeryhost1"), - "/api/bakery", - ImmutableList.of(), - "", - ImmutableList.of(), - Optional.empty(), - true).get(); - } -} diff --git a/bakery/dashboard/src/app/home/home.component.html b/bakery/dashboard/src/app/home/home.component.html deleted file mode 100644 index 238c26748..000000000 --- a/bakery/dashboard/src/app/home/home.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - - - - - - - - -
Version{{ bakeryVersion }}
State version{{ stateVersion }}
-
diff --git a/bakery/dashboard/src/assets/settings/settings.json b/bakery/dashboard/src/assets/settings/settings.json deleted file mode 100644 index f36215181..000000000 --- a/bakery/dashboard/src/assets/settings/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "apiUrl" : "http://localhost:4200/api/bakery", - "title" : "Bakery OSS", - "bakeryVersion" : "123", - "stateVersion" : "456" -} diff --git a/bakery/dashboard/src/proxy.conf.json b/bakery/dashboard/src/proxy.conf.json deleted file mode 100644 index 0ae6c83ab..000000000 --- a/bakery/dashboard/src/proxy.conf.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "/api/bakery": { - "target": "http://localhost:8080", - "secure": false, - "logLevel": "debug", - "changeOrigin": true - } -} diff --git a/bakery/integration-tests/src/test/resources/kubernetes/webshop-baker.yaml b/bakery/integration-tests/src/test/resources/kubernetes/webshop-baker.yaml index f9683359e..f8b6c942a 100644 --- a/bakery/integration-tests/src/test/resources/kubernetes/webshop-baker.yaml +++ b/bakery/integration-tests/src/test/resources/kubernetes/webshop-baker.yaml @@ -114,7 +114,7 @@ data: baker { recipe-poll-interval: 5 seconds event-sink { - class: "com.ing.bakery.baker.KafkaEventSink", + class: "com.ing.bakery.components.KafkaEventSink", bootstrap-servers: "kafka-event-sink:9092", topic: "events" } diff --git a/bakery/baker-state-k8s/src/main/resources/reference.conf b/bakery/interaction-k8s-interaction-manager/src/main/resources/reference.conf similarity index 100% rename from bakery/baker-state-k8s/src/main/resources/reference.conf rename to bakery/interaction-k8s-interaction-manager/src/main/resources/reference.conf diff --git a/bakery/baker-state-k8s/src/main/scala/com/ing/bakery/baker/KubernetesInteractions.scala b/bakery/interaction-k8s-interaction-manager/src/main/scala/com/ing/bakery/baker/KubernetesInteractions.scala similarity index 98% rename from bakery/baker-state-k8s/src/main/scala/com/ing/bakery/baker/KubernetesInteractions.scala rename to bakery/interaction-k8s-interaction-manager/src/main/scala/com/ing/bakery/baker/KubernetesInteractions.scala index 4d2c48eda..8812e6e42 100644 --- a/bakery/baker-state-k8s/src/main/scala/com/ing/bakery/baker/KubernetesInteractions.scala +++ b/bakery/interaction-k8s-interaction-manager/src/main/scala/com/ing/bakery/baker/KubernetesInteractions.scala @@ -7,6 +7,7 @@ import akka.{Done, NotUsed} import cats.effect.{ContextShift, IO, Resource, Timer} import cats.implicits.catsSyntaxApplicativeError import com.ing.baker.runtime.akka.internal.DynamicInteractionManager +import com.ing.bakery.components.RemoteInteractionDiscovery import com.ing.bakery.interaction.RemoteInteractionClient import com.typesafe.config.Config import com.typesafe.scalalogging.LazyLogging diff --git a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala b/bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala similarity index 97% rename from bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala rename to bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala index e11a91ed1..88b9b2afb 100644 --- a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala +++ b/bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala @@ -2,6 +2,10 @@ package com.ing.bakery.baker import akka.actor.ActorSystem import cats.effect.{IO, Resource} +import com.ing.baker.http.DashboardConfiguration +import com.ing.baker.http.client.scaladsl.BakerClient +import com.ing.baker.http.server.common.RecipeLoader +import com.ing.baker.http.server.scaladsl.Http4sBakerServer import com.ing.baker.il.CompiledRecipe import com.ing.baker.runtime.akka.{AkkaBaker, AkkaBakerConfig} import com.ing.baker.runtime.common.BakerException.NoSuchProcessException @@ -9,12 +13,12 @@ import com.ing.baker.runtime.common.{BakerException, SensoryEventStatus} import com.ing.baker.runtime.model.{InteractionInstance, InteractionManager} import com.ing.baker.runtime.scaladsl.{Baker, EventInstance, InteractionInstanceInput} import com.ing.baker.types._ -import com.ing.bakery.baker.mocks.KubeApiServer +import com.ing.bakery.components.InteractionRegistry import com.ing.bakery.mocks.{EventListener, RemoteInteraction} import com.ing.bakery.recipe.Events.{ItemsReserved, OrderPlaced} import com.ing.bakery.recipe.Ingredients.{Item, OrderId, ReservedItems} import com.ing.bakery.recipe.{ItemReservationRecipe, SimpleRecipe, SimpleRecipe2} -import com.ing.bakery.scaladsl.BakerClient +import com.ing.bakery.baker.mocks.KubeApiServer import com.ing.bakery.testing.BakeryFunSpec import com.typesafe.config.ConfigFactory import org.mockserver.integration.ClientAndServer @@ -474,7 +478,13 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { _ <- Resource.eval(eventListener.eventSink.attach(baker)) _ <- Resource.eval(RecipeLoader.loadRecipesIntoBaker(getResourceDirectoryPathSafe, baker)) - server <- BakerService.resource(baker, executionContext, InetSocketAddress.createUnresolved("127.0.0.1", 0), "/api/bakery", "/opt/docker/dashboard", loggingEnabled = true) + server <- Http4sBakerServer.resource( + baker, + executionContext, + InetSocketAddress.createUnresolved("127.0.0.1", 0), + apiUrlPrefix = "/api/bakery", + dashboardConfiguration = DashboardConfiguration(enabled = true, applicationName = "StateRuntimeSpec", clusterInformation = Map.empty), + loggingEnabled = true) client <- BakerClient.resource(server.baseUri, "/api/bakery", executionContext) } yield Context( diff --git a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/TestInteractionRegistry.scala b/bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/TestInteractionRegistry.scala similarity index 96% rename from bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/TestInteractionRegistry.scala rename to bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/TestInteractionRegistry.scala index 14e5350f7..d5dc6cc90 100644 --- a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/TestInteractionRegistry.scala +++ b/bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/TestInteractionRegistry.scala @@ -4,6 +4,7 @@ import akka.actor.ActorSystem import cats.effect.{IO, Resource} import com.ing.baker.runtime.model.InteractionManager import com.ing.bakery.baker.mocks.KubeApiServer +import com.ing.bakery.components.{BaseInteractionRegistry, LocalhostInteractions} import com.ing.bakery.interaction.BaseRemoteInteractionClient import com.ing.bakery.mocks.RemoteInteraction import com.typesafe.config.{Config, ConfigValueFactory} diff --git a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/mocks/KubeApiServer.scala b/bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/mocks/KubeApiServer.scala similarity index 100% rename from bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/mocks/KubeApiServer.scala rename to bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/mocks/KubeApiServer.scala diff --git a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/mocks/WatchEvent.scala b/bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/mocks/WatchEvent.scala similarity index 100% rename from bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/mocks/WatchEvent.scala rename to bakery/interaction-k8s-interaction-manager/src/test/scala/com/ing/bakery/baker/mocks/WatchEvent.scala diff --git a/bakery/state/src/it/resources/application.conf b/bakery/state/src/it/resources/application.conf index d289272bd..94abfc03a 100644 --- a/bakery/state/src/it/resources/application.conf +++ b/bakery/state/src/it/resources/application.conf @@ -43,10 +43,9 @@ baker { localhost.ports = [ 8081 ] } event-sink { - class: "" + class = "" } api-logging-enabled = false - dashboard-path = "/Users/hr29bv/work/baker/bakery/dashboard/dist" } inmemory-read-journal { diff --git a/bakery/state/src/main/resources/reference.conf b/bakery/state/src/main/resources/reference.conf index a33e44f0a..ae6d61c35 100644 --- a/bakery/state/src/main/resources/reference.conf +++ b/bakery/state/src/main/resources/reference.conf @@ -9,9 +9,9 @@ baker { encryption.enabled = off metrics-port = 9095 + api-host = "0.0.0.0" api-port = 8080 api-url-prefix = "/api/bakery" - dashboard-path = "/opt/docker/dashboard" api-logging-enabled = false bake-timeout = 30 seconds @@ -29,7 +29,7 @@ baker { } interactions { - class = "com.ing.bakery.baker.BaseInteractionRegistry" + class = "com.ing.bakery.components.BaseInteractionRegistry" localhost { port = 8081 api-url-prefix = "/api/bakery/interactions" @@ -155,10 +155,10 @@ akka { liveness-path = "health/alive" liveness-checks { cluster-health = "akka.sensors.ClusterHealthCheck" - name = "com.ing.bakery.baker.WatcherReadinessCheck" + name = "com.ing.bakery.components.WatcherReadinessCheck" } readiness-checks { - name = "com.ing.bakery.baker.BakerReadinessCheck" + name = "com.ing.bakery.components.BakerReadinessCheck" } } } diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/Bakery.scala b/bakery/state/src/main/scala/com/ing/bakery/Bakery.scala similarity index 81% rename from bakery/state/src/main/scala/com/ing/bakery/baker/Bakery.scala rename to bakery/state/src/main/scala/com/ing/bakery/Bakery.scala index 6b37bfa99..6e64dae83 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/Bakery.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/Bakery.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.bakery import akka.actor.ActorSystem import akka.cluster.Cluster @@ -7,6 +7,7 @@ import com.ing.baker.runtime.akka.{AkkaBaker, AkkaBakerConfig} import com.ing.baker.runtime.model.InteractionManager import com.ing.baker.runtime.recipe_manager.{ActorBasedRecipeManager, RecipeManager} import com.ing.baker.runtime.scaladsl.Baker +import com.ing.bakery.components.{Cassandra, EventSink, InteractionRegistry, Watcher} import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.scalalogging.LazyLogging import io.prometheus.client.CollectorRegistry @@ -15,16 +16,16 @@ import org.http4s.metrics.prometheus.Prometheus import java.io.File import scala.concurrent.ExecutionContext -case class Bakery(baker: Baker, - executionContext: ExecutionContext, - system: ActorSystem) +case class AkkaBakery(baker: Baker, system: ActorSystem) { + def executionContext: ExecutionContext = system.dispatcher +} object Bakery extends LazyLogging { - def resource(optionalConfig: Option[Config], - externalContext: Option[Any] = None, - interactionManager: Option[InteractionManager[IO]] = None, - recipeManager: Option[RecipeManager] = None) : Resource[IO, Bakery] = { + def akkaBakery(optionalConfig: Option[Config], + externalContext: Option[Any] = None, + interactionManager: Option[InteractionManager[IO]] = None, + recipeManager: Option[RecipeManager] = None) : Resource[IO, AkkaBakery] = { val configPath = sys.env.getOrElse("CONFIG_DIRECTORY", "/opt/docker/conf") val config = optionalConfig.getOrElse(ConfigFactory.load(ConfigFactory.parseFile(new File(s"$configPath/application.conf")))) val bakerConfig = config.getConfig("baker") @@ -55,9 +56,7 @@ object Bakery extends LazyLogging { bakerActorProvider = AkkaBakerConfig.bakerProviderFrom(config), timeouts = AkkaBakerConfig.Timeouts.apply(config), bakerValidationSettings = AkkaBakerConfig.BakerValidationSettings.from(config))(system)) - _ <- Resource.make(IO { - baker - })(baker => IO.fromFuture(IO(baker.gracefulShutdown()))) + _ <- Resource.make(IO {baker})(baker => IO.fromFuture(IO(baker.gracefulShutdown()))) _ <- Resource.eval(eventSink.attach(baker)) _ <- Resource.eval(IO.async[Unit] { callback => //If using local Baker the registerOnMemberUp is never called, should onl be used during local testing. @@ -70,7 +69,7 @@ object Bakery extends LazyLogging { } }) - } yield Bakery(baker, system.dispatcher, system) + } yield AkkaBakery(baker, system) } } diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/ClosableBakery.scala b/bakery/state/src/main/scala/com/ing/bakery/ClosableBakery.scala similarity index 62% rename from bakery/state/src/main/scala/com/ing/bakery/baker/ClosableBakery.scala rename to bakery/state/src/main/scala/com/ing/bakery/ClosableBakery.scala index 77c926f4a..be1e666b1 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/ClosableBakery.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/ClosableBakery.scala @@ -1,20 +1,18 @@ -package com.ing.bakery.baker +package com.ing.bakery -import java.io.Closeable import akka.actor.ActorSystem import cats.effect.IO import com.ing.baker.runtime.model.InteractionManager import com.ing.baker.runtime.recipe_manager.RecipeManager import com.ing.baker.runtime.scaladsl.Baker -import com.ing.bakery.baker.Bakery.resource +import com.ing.bakery.Bakery.akkaBakery import com.typesafe.config.Config -import scala.concurrent.ExecutionContext +import java.io.Closeable class ClosableBakery(baker: Baker, - executionContext: ExecutionContext, system: ActorSystem, - close: IO[Unit]) extends Bakery(baker, executionContext, system) with Closeable { + close: IO[Unit]) extends AkkaBakery(baker, system) with Closeable { override def close(): Unit = close.unsafeRunSync() } @@ -22,13 +20,12 @@ object ClosableBakery { /** * Create bakery instance as external context * @param externalContext optional external context in which Bakery is running, e.g. Spring context - * @return */ def instance(optionalConfig: Option[Config], externalContext: Option[Any], interactionManager: Option[InteractionManager[IO]] = None, recipeManager: Option[RecipeManager] = None): ClosableBakery = { - val (baker: Bakery, close: IO[Unit]) = resource(optionalConfig, externalContext, interactionManager, recipeManager).allocated.unsafeRunSync() - new ClosableBakery(baker.baker, baker.executionContext, baker.system, close) + val (baker: AkkaBakery, close: IO[Unit]) = akkaBakery(optionalConfig, externalContext, interactionManager, recipeManager).allocated.unsafeRunSync() + new ClosableBakery(baker.baker, baker.system, close) } -} \ No newline at end of file +} diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/Main.scala b/bakery/state/src/main/scala/com/ing/bakery/Main.scala similarity index 59% rename from bakery/state/src/main/scala/com/ing/bakery/baker/Main.scala rename to bakery/state/src/main/scala/com/ing/bakery/Main.scala index 2b8f346f8..2e5987524 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/Main.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/Main.scala @@ -1,13 +1,16 @@ -package com.ing.bakery.baker - -import java.io.File -import java.net.InetSocketAddress +package com.ing.bakery import cats.effect.{ExitCode, IO, IOApp} +import com.ing.baker.http.DashboardConfiguration +import com.ing.baker.http.server.common.RecipeLoader +import com.ing.baker.http.server.scaladsl.{Http4sBakerServer, Http4sBakerServerConfiguration} +import com.ing.bakery.components.BakerReadinessCheck import com.ing.bakery.metrics.MetricService import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.LazyLogging +import java.io.File +import java.net.InetSocketAddress import scala.concurrent.duration.Duration object Main extends IOApp with LazyLogging { @@ -16,31 +19,26 @@ object Main extends IOApp with LazyLogging { val configPath = sys.env.getOrElse("CONFIG_DIRECTORY", "/opt/docker/conf") val config = ConfigFactory.load(ConfigFactory.parseFile(new File(s"$configPath/application.conf"))) - val bakerConfig = config.getConfig("baker") - val apiPort = bakerConfig.getInt("api-port") - val metricsPort = bakerConfig.getInt("metrics-port") - val apiUrlPrefix = bakerConfig.getString("api-url-prefix") - val dashboardPath = bakerConfig.getString("dashboard-path") - val loggingEnabled = bakerConfig.getBoolean("api-logging-enabled") + val metricsPort = config.getInt("baker.metrics-port") (for { - bakery <- Bakery.resource(Some(config)) + bakery <- Bakery.akkaBakery(Some(config)) _ <- MetricService.resource(InetSocketAddress.createUnresolved("0.0.0.0", metricsPort), bakery.executionContext) - bakerService <- BakerService.resource( + bakerService <- Http4sBakerServer.resource( bakery.baker, - bakery.executionContext, - InetSocketAddress.createUnresolved("0.0.0.0", apiPort), - apiUrlPrefix, dashboardPath, loggingEnabled) + Http4sBakerServerConfiguration.fromConfig(config), + DashboardConfiguration.fromConfig(config), + bakery.executionContext) } yield (bakery, bakerService)) .use { case (bakery, bakerService) => logger.info(s"Bakery started at ${bakerService.address}/${bakerService.baseUri}, enabling the readiness in Akka management") BakerReadinessCheck.enable() - RecipeLoader.pollRecipesUpdates(configPath, bakery, + RecipeLoader.pollRecipesUpdates(configPath, bakery.baker, Duration.fromNanos(config.getDuration("baker.recipe-poll-interval").toNanos)) - }.as(ExitCode.Success) + }.as(ExitCode.Success) } } diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/BakerService.scala b/bakery/state/src/main/scala/com/ing/bakery/baker/BakerService.scala deleted file mode 100644 index d8b65560a..000000000 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/BakerService.scala +++ /dev/null @@ -1,203 +0,0 @@ -package com.ing.bakery.baker - -import cats.data.OptionT -import cats.effect.{Blocker, ContextShift, IO, Resource, Sync, Timer} -import cats.implicits._ -import com.ing.baker.runtime.common.BakerException -import com.ing.baker.runtime.scaladsl.{Baker, BakerResult, EncodedRecipe, EventInstance, InteractionExecutionResult} -import com.ing.baker.runtime.serialization.InteractionExecution -import com.ing.baker.runtime.serialization.InteractionExecutionJsonCodecs._ -import com.ing.baker.runtime.serialization.JsonDecoders._ -import com.ing.baker.runtime.serialization.JsonEncoders._ -import com.typesafe.scalalogging.LazyLogging -import io.circe._ -import io.circe.generic.auto._ -import io.prometheus.client.CollectorRegistry -import org.http4s._ -import org.http4s.circe._ -import org.http4s.dsl.io._ -import org.http4s.implicits._ -import org.http4s.metrics.prometheus.Prometheus -import org.http4s.server.blaze.BlazeServerBuilder -import org.http4s.server.middleware.{CORS, Logger, Metrics} -import org.http4s.server.{Router, Server} -import org.slf4j.LoggerFactory - -import java.io.File -import java.net.InetSocketAddress -import java.nio.charset.Charset -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContext, Future} - -object BakerService { - - def resource(baker: Baker, ec: ExecutionContext, hostname: InetSocketAddress, apiUrlPrefix: String, dashboardPath: String, loggingEnabled: Boolean) - (implicit sync: Sync[IO], cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, Server[IO]] = { - - val bakeryRequestClassifier: Request[IO] => Option[String] = { request => - val uriPath = request.uri.path - val p = uriPath.takeRight(uriPath.length - apiUrlPrefix.length) - - if (p.startsWith("/app")) Some(p) // cardinality is low, we don't care - else if (p.startsWith("/instances")) { - val action = p.split('/') // /instances///... - we don't want ID here - if (action.length >= 4) Some(s"/instances/${action(3)}") else Some("/instances/state") - } else None - } - - val apiLoggingAction: Option[String => IO[Unit]] = if (loggingEnabled) { - val apiLogger = LoggerFactory.getLogger("API") - Some(s => IO(apiLogger.info(s))) - } else None - - for { - metrics <- Prometheus.metricsOps[IO](CollectorRegistry.defaultRegistry, "http_api") - blocker <- Blocker[IO] - server <- BlazeServerBuilder[IO](ec) - .bindSocketAddress(hostname) - .withHttpApp( - CORS.policy - .withAllowOriginAll - .withAllowCredentials(true) - .withMaxAge(1.day)( - Logger.httpApp( - logHeaders = loggingEnabled, - logBody = loggingEnabled, - logAction = apiLoggingAction) { - - def dashboardFile(request: Request[IO], filename: String): OptionT[IO, Response[IO]] = - StaticFile.fromFile(new File(dashboardPath + "/" + filename), blocker, Some(request)) - - def index(request: Request[IO]) = dashboardFile(request, "index.html").getOrElseF(NotFound()) - - Router( - apiUrlPrefix -> Metrics[IO](metrics, classifierF = bakeryRequestClassifier)(routes(baker)), - "/" -> HttpRoutes.of[IO] { - case request => - if (dashboardPath.isEmpty) NotFound() - else dashboardFile(request, request.pathInfo).getOrElseF(index(request)) - } - ) orNotFound - })).resource - } yield server - } - - def routes(baker: Baker)(implicit cs: ContextShift[IO], timer: Timer[IO]): HttpRoutes[IO] = - new BakerService(baker).routes -} - -final class BakerService private(baker: Baker)(implicit cs: ContextShift[IO], timer: Timer[IO]) extends LazyLogging { - - object CorrelationId extends OptionalQueryParamDecoderMatcher[String]("correlationId") - - private class RegExpValidator(regexp: String) { - def unapply(str: String): Option[String] = if (str.matches(regexp)) Some(str) else None - } - - private object RecipeId extends RegExpValidator("[A-Za-z0-9]+") - - private object RecipeInstanceId extends RegExpValidator("[A-Za-z0-9-]+") - - private object InteractionName extends RegExpValidator("[A-Za-z0-9_]+") - - implicit val recipeDecoder: EntityDecoder[IO, EncodedRecipe] = jsonOf[IO, EncodedRecipe] - - implicit val eventInstanceDecoder: EntityDecoder[IO, EventInstance] = jsonOf[IO, EventInstance] - implicit val interactionExecutionRequestDecoder: EntityDecoder[IO, InteractionExecution.ExecutionRequest] = jsonOf[IO, InteractionExecution.ExecutionRequest] - implicit val bakerResultEntityEncoder: EntityEncoder[IO, BakerResult] = jsonEncoderOf[IO, BakerResult] - - def routes: HttpRoutes[IO] = app <+> instance - - private def callBaker[A](f: => Future[A])(implicit encoder: Encoder[A]): IO[Response[IO]] = - callBakerIO(IO.fromFuture(IO(f))) - - private def callBakerIO[A](io: => IO[A])(implicit encoder: Encoder[A]): IO[Response[IO]] = - io.attempt.flatMap { - case Left(e: BakerException) => Ok(BakerResult(e)) - case Left(e) => - logger.error(s"Unexpected exception happened when calling Baker", e) - InternalServerError(s"No other exception but BakerExceptions should be thrown here: ${e.getCause}") - case Right(()) => Ok(BakerResult.Ack) - case Right(a) => Ok(BakerResult(a)) - } - - private def app: HttpRoutes[IO] = Router("/app" -> - HttpRoutes.of[IO] { - case GET -> Root / "health" => Ok() - - case GET -> Root / "interactions" => callBaker(baker.getAllInteractions) - - case GET -> Root / "interactions" / InteractionName(name) => callBaker(baker.getInteraction(name)) - - case req@POST -> Root / "interactions" / "execute" => - for { - executionRequest <- req.as[InteractionExecution.ExecutionRequest] - result <- callBakerIO( - IO.fromFuture(IO(baker.executeSingleInteraction(executionRequest.id, executionRequest.ingredients))) - .map(_.toSerializationInteractionExecutionResult)) - } yield result - - case req@POST -> Root / "recipes" => - for { - encodedRecipe <- req.as[EncodedRecipe] - recipe <- RecipeLoader.fromBytes(encodedRecipe.base64.getBytes(Charset.forName("UTF-8"))) - result <- callBaker(baker.addRecipe(recipe, validate = true)) - } yield result - - case GET -> Root / "recipes" => callBaker(baker.getAllRecipes) - - case GET -> Root / "recipes" / RecipeId(recipeId) => callBaker(baker.getRecipe(recipeId)) - - case GET -> Root / "recipes" / RecipeId(recipeId) / "visual" => callBaker(baker.getRecipeVisual(recipeId)) - }) - - private def instance: HttpRoutes[IO] = Router("/instances" -> HttpRoutes.of[IO] { - - case GET -> Root => callBaker(baker.getAllRecipeInstancesMetadata) - - case GET -> Root / RecipeInstanceId(recipeInstanceId) => callBaker(baker.getRecipeInstanceState(recipeInstanceId)) - - case GET -> Root / RecipeInstanceId(recipeInstanceId) / "events" => callBaker(baker.getEvents(recipeInstanceId)) - - case GET -> Root / RecipeInstanceId(recipeInstanceId) / "ingredients" => callBaker(baker.getIngredients(recipeInstanceId)) - - case GET -> Root / RecipeInstanceId(recipeInstanceId) / "visual" => callBaker(baker.getVisualState(recipeInstanceId)) - - case POST -> Root / RecipeInstanceId(recipeInstanceId) / "bake" / RecipeId(recipeId) => callBaker(baker.bake(recipeId, recipeInstanceId)) - - case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "fire-and-resolve-when-received" :? CorrelationId(maybeCorrelationId) => - for { - event <- req.as[EventInstance] - result <- callBaker(baker.fireEventAndResolveWhenReceived(recipeInstanceId, event, maybeCorrelationId)) - } yield result - - case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "fire-and-resolve-when-completed" :? CorrelationId(maybeCorrelationId) => - for { - event <- req.as[EventInstance] - result <- callBaker(baker.fireEventAndResolveWhenCompleted(recipeInstanceId, event, maybeCorrelationId)) - } yield result - - case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "fire-and-resolve-on-event" / onEvent :? CorrelationId(maybeCorrelationId) => - for { - event <- req.as[EventInstance] - result <- callBaker(baker.fireEventAndResolveOnEvent(recipeInstanceId, event, onEvent, maybeCorrelationId)) - } yield result - - case POST -> Root / RecipeInstanceId(recipeInstanceId) / "interaction" / InteractionName(interactionName) / "retry" => - for { - result <- callBaker(baker.retryInteraction(recipeInstanceId, interactionName)) - } yield result - - case POST -> Root / RecipeInstanceId(recipeInstanceId) / "interaction" / InteractionName(interactionName) / "stop-retrying" => - for { - result <- callBaker(baker.stopRetryingInteraction(recipeInstanceId, interactionName)) - } yield result - - case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "interaction" / InteractionName(interactionName) / "resolve" => - for { - event <- req.as[EventInstance] - result <- callBaker(baker.resolveInteraction(recipeInstanceId, interactionName, event)) - } yield result - }) - -} diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/BakeryExecutorJava.scala b/bakery/state/src/main/scala/com/ing/bakery/baker/BakeryExecutorJava.scala deleted file mode 100644 index 049c6b7f2..000000000 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/BakeryExecutorJava.scala +++ /dev/null @@ -1,95 +0,0 @@ -package com.ing.bakery.baker - -import com.ing.baker.runtime.common.BakerException -import com.ing.baker.runtime.scaladsl.{BakerResult, EncodedRecipe, EventInstance} -import com.ing.baker.runtime.serialization.JsonDecoders._ -import com.ing.baker.runtime.serialization.JsonEncoders._ -import com.typesafe.scalalogging.LazyLogging -import io.circe.Encoder -import io.circe.generic.auto._ -import io.circe.parser.parse - -import java.nio.charset.Charset -import java.util.{Optional, UUID} -import java.util.concurrent.{CompletableFuture => JFuture} -import scala.compat.java8.FutureConverters.FutureOps -import scala.concurrent.{ExecutionContext, Future} - -class BakeryExecutorJava(bakery: Bakery) extends LazyLogging { - - implicit val executionContext: ExecutionContext = bakery.executionContext - - private def callBaker[A](f: => Future[A])(implicit encoder: Encoder[A]): Future[String] = { - f.map { - case () => BakerResult.Ack - case a => BakerResult(a) - }.recover { - case e: BakerException => BakerResult(e) - case e: Throwable => - val errorId = UUID.randomUUID().toString - logger.error(s"Unexpected exception happened when calling Baker (id='$errorId').", e) - BakerResult(BakerException.UnexpectedException(errorId)) - }.map(bakerResultEncoder.apply(_).noSpaces) - } - - private def callBakerJava[A](f: => Future[A])(implicit encoder: Encoder[A]): JFuture[String] = { - callBaker(f)(encoder).toJava.toCompletableFuture - } - - def appGetAllInteractions: JFuture[String] = callBakerJava(bakery.baker.getAllInteractions) - - def appGetInteraction(interactionName: String): JFuture[String] = callBakerJava(bakery.baker.getInteraction(interactionName)) - - def appAddRecipe(recipe: String): JFuture[String] = { - (for { - json <- parse(recipe).toOption - encodedRecipe <- json.as[EncodedRecipe].toOption - } yield RecipeLoader.fromBytes(encodedRecipe.base64.getBytes(Charset.forName("UTF-8"))).unsafeToFuture()) - .map(_.flatMap(recipe => callBaker(bakery.baker.addRecipe(recipe, validate = false)))) - .getOrElse(Future.failed(new IllegalStateException("Error adding recipe"))) - }.toJava.toCompletableFuture - - def appGetRecipe(recipeId: String): JFuture[String] = callBakerJava(bakery.baker.getRecipe(recipeId)) - - def appGetAllRecipes: JFuture[String] = callBakerJava(bakery.baker.getAllRecipes) - - def appGetVisualRecipe(recipeId: String): JFuture[String] = callBakerJava(bakery.baker.getRecipeVisual(recipeId)) - - def instanceGet(recipeInstanceId: String): JFuture[String] = callBakerJava(bakery.baker.getRecipeInstanceState(recipeInstanceId)) - - def instanceGetEvents(recipeInstanceId: String): JFuture[String] = callBakerJava(bakery.baker.getEvents(recipeInstanceId)) - - def instanceGetIngredients(recipeInstanceId: String): JFuture[String] = callBakerJava(bakery.baker.getIngredients(recipeInstanceId)) - - def instanceGetVisual(recipeInstanceId: String): JFuture[String] = callBakerJava(bakery.baker.getVisualState(recipeInstanceId)) - - def instanceBake(recipeId: String, recipeInstanceId: String): JFuture[String] = callBakerJava(bakery.baker.bake(recipeId, recipeInstanceId)) - - private def toOption[T](opt: Optional[T]): Option[T] = if (opt.isPresent) Some(opt.get()) else None - - private def parseEventAndExecute[A](eventJson: String, f: EventInstance => Future[A])(implicit encoder: Encoder[A]): JFuture[String] = (for { - json <- parse(eventJson) - eventInstance <- json.as[EventInstance] - } yield { - callBaker(f(eventInstance)) - }).getOrElse(Future.failed(new IllegalArgumentException("Can't process event"))).toJava.toCompletableFuture - - def instanceFireAndResolveWhenReceived(recipeInstanceId: String, eventJson: String, maybeCorrelationId: Optional[String]): JFuture[String] = - parseEventAndExecute(eventJson, bakery.baker.fireEventAndResolveWhenReceived(recipeInstanceId, _, toOption(maybeCorrelationId))) - - def instanceFireAndResolveWhenCompleted(recipeInstanceId: String, eventJson: String, maybeCorrelationId: Optional[String]): JFuture[String] = - parseEventAndExecute(eventJson, bakery.baker.fireEventAndResolveWhenCompleted(recipeInstanceId, _, toOption(maybeCorrelationId))) - - def instancefireAndResolveOnEvent(recipeInstanceId: String, eventJson: String, event: String, maybeCorrelationId: Optional[String]): JFuture[String] = - parseEventAndExecute(eventJson, bakery.baker.fireEventAndResolveOnEvent(recipeInstanceId, _, event, toOption(maybeCorrelationId))) - - def instanceInteractionRetry(recipeInstanceId: String, interactionName: String): JFuture[String] = - callBakerJava(bakery.baker.retryInteraction(recipeInstanceId, interactionName)) - - def instanceInteractionStopRetrying(recipeInstanceId: String, interactionName: String): JFuture[String] = - callBakerJava(bakery.baker.stopRetryingInteraction(recipeInstanceId, interactionName)) - - def instanceInteractionResolve(recipeInstanceId: String, interactionName: String, eventJson: String): JFuture[String] = - parseEventAndExecute(eventJson, bakery.baker.resolveInteraction(recipeInstanceId, interactionName, _)) - -} diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/BakerReadinessCheck.scala b/bakery/state/src/main/scala/com/ing/bakery/components/BakerReadinessCheck.scala similarity index 89% rename from bakery/state/src/main/scala/com/ing/bakery/baker/BakerReadinessCheck.scala rename to bakery/state/src/main/scala/com/ing/bakery/components/BakerReadinessCheck.scala index e9afe20e5..31b9be10b 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/BakerReadinessCheck.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/components/BakerReadinessCheck.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.bakery.components import scala.concurrent.Future diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/Cassandra.scala b/bakery/state/src/main/scala/com/ing/bakery/components/Cassandra.scala similarity index 97% rename from bakery/state/src/main/scala/com/ing/bakery/baker/Cassandra.scala rename to bakery/state/src/main/scala/com/ing/bakery/components/Cassandra.scala index 86b9b6762..2b982982a 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/Cassandra.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/components/Cassandra.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.bakery.components import akka.actor.ActorSystem import cats.effect.{Async, ContextShift, IO, Resource, Timer} diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/EventSink.scala b/bakery/state/src/main/scala/com/ing/bakery/components/EventSink.scala similarity index 99% rename from bakery/state/src/main/scala/com/ing/bakery/baker/EventSink.scala rename to bakery/state/src/main/scala/com/ing/bakery/components/EventSink.scala index 16d3b2e3a..6e1790372 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/EventSink.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/components/EventSink.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.bakery.components import cats.effect.{ContextShift, IO, Resource, Timer} import com.ing.baker.runtime.akka.AkkaBaker diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/InteractionRegistry.scala b/bakery/state/src/main/scala/com/ing/bakery/components/InteractionRegistry.scala similarity index 97% rename from bakery/state/src/main/scala/com/ing/bakery/baker/InteractionRegistry.scala rename to bakery/state/src/main/scala/com/ing/bakery/components/InteractionRegistry.scala index 6b235f4b8..332872d57 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/InteractionRegistry.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/components/InteractionRegistry.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.bakery.components import akka.actor.ActorSystem import cats.Traverse @@ -134,7 +134,7 @@ trait RemoteInteractionDiscovery extends LazyLogging { case _: IOException => logger.info(s"Can't connect to interactions @ ${uri.toString}, the container may still be starting...") case _ => - logger.error(s"Failed to list interactions @ ${uri.toString}", e) + logger.warn(s"Failed to list interactions @ ${uri.toString}. The list of available interactions will not be updated.", e) } IO.sleep(times) *> attempt(count - 1, times) case Right(a) => IO(a) diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/LocalhostInteractions.scala b/bakery/state/src/main/scala/com/ing/bakery/components/LocalhostInteractions.scala similarity index 97% rename from bakery/state/src/main/scala/com/ing/bakery/baker/LocalhostInteractions.scala rename to bakery/state/src/main/scala/com/ing/bakery/components/LocalhostInteractions.scala index ec3a7886f..ea089afa5 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/LocalhostInteractions.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/components/LocalhostInteractions.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.bakery.components import akka.actor.ActorSystem import cats.effect.{ContextShift, IO, Resource, Timer} diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/Watcher.scala b/bakery/state/src/main/scala/com/ing/bakery/components/Watcher.scala similarity index 97% rename from bakery/state/src/main/scala/com/ing/bakery/baker/Watcher.scala rename to bakery/state/src/main/scala/com/ing/bakery/components/Watcher.scala index 51a8ed921..902125f0a 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/Watcher.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/components/Watcher.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.bakery.components import akka.actor.ActorSystem import cats.effect.{ContextShift, IO, Resource, Timer} diff --git a/bakery/state/src/test/scala/com/ing/bakery/baker/TestWatcher.scala b/bakery/state/src/test/scala/com/ing/bakery/baker/TestWatcher.scala index 89757f70a..37b70e835 100644 --- a/bakery/state/src/test/scala/com/ing/bakery/baker/TestWatcher.scala +++ b/bakery/state/src/test/scala/com/ing/bakery/baker/TestWatcher.scala @@ -3,6 +3,7 @@ import akka.actor.ActorSystem import cats.effect.{IO, Resource, Timer} import com.typesafe.config.Config import cats.implicits._ +import com.ing.bakery.components.{Cassandra, Watcher} import scala.concurrent.duration._ object TestWatcher { diff --git a/bakery/state/src/test/scala/com/ing/bakery/baker/WatcherSpec.scala b/bakery/state/src/test/scala/com/ing/bakery/baker/WatcherSpec.scala index a88890afb..2e5512925 100644 --- a/bakery/state/src/test/scala/com/ing/bakery/baker/WatcherSpec.scala +++ b/bakery/state/src/test/scala/com/ing/bakery/baker/WatcherSpec.scala @@ -4,6 +4,7 @@ import akka.actor.ActorSystem import cats.effect.{ContextShift, IO, Resource, Timer} import com.ing.baker.runtime.akka.internal.CachingInteractionManager import com.ing.baker.runtime.akka.{AkkaBaker, AkkaBakerConfig} +import com.ing.bakery.components.{Watcher, WatcherReadinessCheck} import com.ing.bakery.mocks.EventListener import com.ing.bakery.testing.BakeryFunSpec import com.typesafe.config.ConfigFactory diff --git a/bakery/state/src/test/scala/com/ing/bakery/mocks/EventListener.scala b/bakery/state/src/test/scala/com/ing/bakery/mocks/EventListener.scala index 817b243cb..c31122bbe 100644 --- a/bakery/state/src/test/scala/com/ing/bakery/mocks/EventListener.scala +++ b/bakery/state/src/test/scala/com/ing/bakery/mocks/EventListener.scala @@ -2,7 +2,7 @@ package com.ing.bakery.mocks import cats.effect.{ContextShift, IO} import com.ing.baker.runtime.common.{BakerEvent, EventInstance} -import com.ing.bakery.baker.EventSink +import com.ing.bakery.components.EventSink import scala.collection.mutable diff --git a/build.sbt b/build.sbt index 8b81e5a18..c1c46c16c 100644 --- a/build.sbt +++ b/build.sbt @@ -53,7 +53,7 @@ val dockerSettings: Seq[Setting[_]] = Seq( Docker / maintainer := "The Bakery Team", dockerBaseImage := "adoptopenjdk/openjdk11", dockerUpdateLatest := true, // todo only for master branch - Docker / version := "local", // used by smoke tests for locally built images + Docker / version := "local" // used by smoke tests for locally built images ) val dependencyOverrideSettings: Seq[Setting[_]] = Seq( @@ -78,13 +78,12 @@ val dependencyOverrideSettings: Seq[Setting[_]] = Seq( jawnParser, nettyHandler, bouncyCastleBcprov, - bouncyCastleBcpkix, + bouncyCastleBcpkix ) ) lazy val noPublishSettings: Seq[Setting[_]] = Seq( publish := {}, - publishLocal := {}, publishArtifact := false ) @@ -242,7 +241,7 @@ lazy val `baker-recipe-dsl`: Project = project.in(file("core/recipe-dsl")) compileDeps( javaxInject, paranamer, - scalaReflect(scalaVersion.value), + scalaReflect(scalaVersion.value) ) ++ testDeps( scalaTest, @@ -263,28 +262,10 @@ lazy val `baker-recipe-compiler`: Project = project.in(file("core/recipe-compile .dependsOn(`baker-recipe-dsl`, `baker-intermediate-language`, testScope(`baker-recipe-dsl`)) -lazy val `bakery-interaction-protocol`: Project = project.in(file("bakery/interaction-protocol")) - .settings(defaultModuleSettings) - .settings(scalaPBSettings) - .settings( - moduleName := "bakery-interaction-protocol", - libraryDependencies ++= Seq( - http4s, - http4sDsl, - http4sServer, - http4sClient, - http4sCirce, - http4sPrometheus, - prometheus, - prometheusJmx - ) - ) - .dependsOn(`baker-interface`) - -lazy val `bakery-client`: Project = project.in(file("bakery/client")) +lazy val `baker-http-client`: Project = project.in(file("http/baker-http-client")) .settings(defaultModuleSettings) .settings( - moduleName := "bakery-client", + moduleName := "baker-http-client", libraryDependencies ++= Seq( http4s, http4sDsl, @@ -302,38 +283,131 @@ lazy val `bakery-client`: Project = project.in(file("bakery/client")) ) .dependsOn(`baker-interface`) -val npmBuildTask = taskKey[File]("Dashboard build") -lazy val `bakery-dashboard`: Project = project.in(file("bakery/dashboard")) +lazy val `baker-http-server`: Project = project.in(file("http/baker-http-server")) + .settings(defaultModuleSettings) + .settings(yPartialUnificationSetting) + .settings( + moduleName := "baker-http-server", + libraryDependencies ++= Seq( + slf4jApi, + logback, + http4s, + http4sDsl, + http4sCirce, + http4sServer, + http4sPrometheus, + prometheus, + prometheusJmx + ) ++ testDeps(mockitoScala, mockitoScalaTest, catsEffectTesting) + ) + .dependsOn( + `baker-interface`, + `baker-http-dashboard`, + testScope(`baker-recipe-compiler`) + ) + +val npmInputFiles = taskKey[Set[File]]("List of files which are used by the npmBuildTask. Used to determine if something has changed and an npm build needs to be redone.") +val npmBuildTask = taskKey[File]("Uses NPM to build the dashboard into the dist directory") +val zipDistDirectory = taskKey[File]("Creates a zip file of the dashboard files") +val staticDashboardFilePrefix = settingKey[String]("Prefix for static files of dashboard in jar.") +val distDirectory = settingKey[File]("dist directory. This is like /target but for npm builds.") +val dashboardZipArtifact = settingKey[Artifact]("Creates the artifact object") +val dashboardFilesList = taskKey[Seq[File]]("List of static dashboard files") +val dashboardFilesIndex = taskKey[File]("Creates an index of dashboard resources.") +val prefixedDashboardResources = taskKey[Seq[File]]("Create resources containing dashboard files, prefixed.") + +lazy val `baker-http-dashboard`: Project = project.in(file("http/baker-http-dashboard")) .enablePlugins(UniversalPlugin) .settings(defaultModuleSettings) .settings( - name := "bakery-dashboard", + name := "baker-http-dashboard", maintainer := "The Bakery Team", - Universal / packageName := s"bakery-dashboard", - Universal / mappings ++= Seq(file("dashboard.zip") -> "dashboard.zip"), + libraryDependencies ++= Seq(typeSafeConfig) ++ testDeps( + scalaTest, + logback + ), + Universal / packageName := name.value, + Universal / mappings += file("dashboard.zip") -> "dashboard.zip", + staticDashboardFilePrefix := "dashboard_static", + distDirectory := baseDirectory.value / "dist", + npmInputFiles := { + val sources = baseDirectory.value / "src" ** "*" + val projectConfiguration = baseDirectory.value * "*.json" + (sources.get() ++ projectConfiguration.get()).toSet + }, npmBuildTask := { - val processBuilder = Process("./npm-build.sh", file("bakery/dashboard")) - val process = processBuilder.run() - if(process.exitValue() != 0) throw new Error(s"NPM failed with exit value ${process.exitValue()}") - file("bakery/dashboard/dashboard.zip") + // Caches the npm ./npm-build.sh execution. Invalidation is done if either + // - anything is different in the baseDirectory / src, or files in the baseDirectory / *.json (compared using hash of file contents) + // - dist directory doesn't contain the same files as previously. + val cachedFunction = FileFunction.cached(streams.value.cacheDirectory / "npmBuild", inStyle = FileInfo.hash) { (in: Set[File]) => + val processBuilder = Process("./npm-build.sh", baseDirectory.value) + val process = processBuilder.run() + if (process.exitValue() != 0) throw new Error(s"NPM failed with exit value ${process.exitValue()}") + val outputFiles = (distDirectory.value ** "*").get().toSet + outputFiles + } + cachedFunction(npmInputFiles.value) + distDirectory.value + }, + zipDistDirectory := { + val inputDirectory = npmBuildTask.value + val targetZipFile = target.value / "dashboard.zip" + IO.zip( + sources = (inputDirectory ** "*").get().map(f => (f, inputDirectory.relativize(f).get.toString)), + outputZip = targetZipFile, + time = None) + targetZipFile + }, + prefixedDashboardResources := { + val outputFolder = (Compile / resourceManaged).value / staticDashboardFilePrefix.value + IO.copyDirectory(npmBuildTask.value, outputFolder) + (outputFolder ** "*").get() + }, + dashboardFilesList := { + val distDir = npmBuildTask.value + (distDir ** "*") + .filter(_.isFile).get() }, - Compile / doc / sources := Seq.empty, - Compile / packageDoc / mappings := Seq(), - Compile / packageDoc / publishArtifact := false, - Compile / packageSrc / publishArtifact := false, - Compile / packageBin / publishArtifact := false, - Universal / packageBin := npmBuildTask.value, - addArtifact(Artifact("dashboard", "zip", "zip"), npmBuildTask), - publish := (publish dependsOn (Universal / packageBin)).value, - publishLocal := (publishLocal dependsOn (Universal / packageBin)).value + dashboardFilesIndex := { + val distDir = npmBuildTask.value + val resultFile = (Compile / resourceManaged).value / "dashboard_static_index" + IO.write(resultFile, dashboardFilesList.value + .map(file => s"${staticDashboardFilePrefix.value}/${distDir.relativize(file).get.toString}").mkString("\n")) + resultFile + }, + dashboardZipArtifact := Artifact(name.value, "zip", "zip"), + sourceDirectory := baseDirectory.value / "src-scala", + // Note: resourceGenerators is not run by task compile. It is run by task package or run. + Compile / resourceGenerators += prefixedDashboardResources.taskValue, + Compile / resourceGenerators += Def.task { Seq(dashboardFilesIndex.value) }.taskValue, + cleanFiles += distDirectory.value, + addArtifact(dashboardZipArtifact, zipDistDirectory) + ) + +lazy val `bakery-interaction-protocol`: Project = project.in(file("bakery/interaction-protocol")) + .settings(defaultModuleSettings) + .settings(scalaPBSettings) + .settings( + moduleName := "bakery-interaction-protocol", + libraryDependencies ++= Seq( + http4s, + http4sDsl, + http4sServer, + http4sClient, + http4sCirce, + http4sPrometheus, + prometheus, + prometheusJmx + ) ) + .dependsOn(`baker-interface`) -lazy val `bakery-state-k8s`: Project = project.in(file("bakery/baker-state-k8s")) +lazy val `bakery-interaction-k8s-interaction-manager`: Project = project.in(file("bakery/interaction-k8s-interaction-manager")) .settings(defaultModuleSettings) .settings(yPartialUnificationSetting) .settings( - moduleName := "bakery-state-k8s", + moduleName := "bakery-interaction-k8s-interaction-manager", libraryDependencies ++= Seq( skuber ) ++ testDeps( @@ -352,14 +426,15 @@ lazy val `bakery-state-k8s`: Project = project.in(file("bakery/baker-state-k8s") ) .dependsOn( `baker-akka-runtime`, - `bakery-client`, `baker-interface`, `bakery-interaction-protocol`, `baker-recipe-compiler`, `baker-recipe-dsl`, `baker-intermediate-language`, - `bakery-dashboard`, - `bakery-state` % "compile->compile;test->test" + `bakery-state`, + testScope(`bakery-state`), + testScope(`baker-http-client`), + testScope(`baker-http-server`) ) @@ -372,9 +447,6 @@ lazy val `bakery-state`: Project = project.in(file("bakery/state")) dockerExposedPorts ++= Seq(8080), Docker / packageName := "bakery-state", dockerBaseImage := "adoptopenjdk/openjdk11", - Universal / mappings ++= - directory(s"${(`bakery-dashboard` / baseDirectory).value.getAbsolutePath}/dist") - .map(t => (t._1, t._2.replace("dist", "dashboard"))), moduleName := "bakery-state", libraryDependencies ++= Seq( slf4jApi, @@ -387,10 +459,6 @@ lazy val `bakery-state`: Project = project.in(file("bakery/state")) akkaDiscovery, akkaDiscoveryKube, akkaPki, - http4s, - http4sDsl, - http4sCirce, - http4sServer, kafkaClient ) ++ testDeps( slf4jApi, @@ -408,13 +476,13 @@ lazy val `bakery-state`: Project = project.in(file("bakery/state")) ) .dependsOn( `baker-akka-runtime`, - `bakery-client`, `baker-interface`, `bakery-interaction-protocol`, `baker-recipe-compiler`, `baker-recipe-dsl`, `baker-intermediate-language`, - `bakery-dashboard` + `baker-http-server`, + `baker-http-dashboard` ) lazy val `bakery-interaction`: Project = project.in(file("bakery/interaction")) @@ -429,7 +497,7 @@ lazy val `bakery-interaction`: Project = project.in(file("bakery/interaction")) http4sCirce, circe, catsEffect, - catsCore, + catsCore ) ++ testDeps( scalaTest, logback @@ -463,11 +531,21 @@ lazy val `bakery-interaction-spring`: Project = project.in(file("bakery/interact lazy val baker: Project = project.in(file(".")) .settings(defaultModuleSettings) .settings( - crossScalaVersions := Nil, + crossScalaVersions := Nil + ) + .aggregate( + // Core + `baker-types`, `baker-akka-runtime`, `baker-recipe-compiler`, `baker-recipe-dsl`, `baker-intermediate-language`, + `baker-interface`, `baker-annotations`, `baker-test`, + // Http + `baker-http-client`, `baker-http-server`, `baker-http-dashboard`, + // Bakery + `bakery-state`, `bakery-interaction`, `bakery-interaction-spring`, `bakery-interaction-protocol`, + `bakery-interaction-k8s-interaction-manager`, + // Examples + `baker-example`, `bakery-client-example`, `interaction-example-make-payment-and-ship-items`, + `interaction-example-reserve-items`, `bakery-kafka-listener-example` ) - .aggregate(`baker-types`, `baker-akka-runtime`, `baker-recipe-compiler`, `baker-recipe-dsl`, `baker-intermediate-language`, - `bakery-client`, `bakery-state`, `bakery-interaction`, `bakery-interaction-spring`, `bakery-interaction-protocol`, - `bakery-state-k8s`, `baker-interface`, `bakery-dashboard`, `baker-annotations`, `baker-test`) lazy val `baker-example`: Project = project .in(file("examples/baker-example")) @@ -475,6 +553,7 @@ lazy val `baker-example`: Project = project .settings(commonSettings) .settings(noPublishSettings) .settings(yPartialUnificationSetting) + .settings(crossBuildSettings) .settings( moduleName := "baker-example", libraryDependencies ++= @@ -493,7 +572,8 @@ lazy val `baker-example`: Project = project scalaCheck, mockitoScala, junitInterface, - slf4jApi + slf4jApi, + akkaTestKit ) ) .settings(dockerSettings) @@ -506,8 +586,10 @@ lazy val `baker-example`: Project = project lazy val `bakery-client-example`: Project = project .in(file("examples/bakery-client-example")) .enablePlugins(JavaAppPackaging) - .settings(defaultModuleSettings) + .settings(commonSettings) + .settings(noPublishSettings) .settings(yPartialUnificationSetting) + .settings(crossBuildSettings) .settings( moduleName := "bakery-client-example", libraryDependencies ++= @@ -529,11 +611,12 @@ lazy val `bakery-client-example`: Project = project .settings( Docker / packageName := "bakery-client-example" ) - .dependsOn(`baker-types`, `bakery-client`, `baker-recipe-compiler`, `baker-recipe-dsl`) + .dependsOn(`baker-types`, `baker-http-client`, `baker-recipe-compiler`, `baker-recipe-dsl`) lazy val `bakery-kafka-listener-example`: Project = project .in(file("examples/bakery-kafka-listener-example")) .enablePlugins(JavaAppPackaging) + .settings(noPublishSettings) .settings(defaultModuleSettings) .settings(yPartialUnificationSetting) .settings( @@ -555,11 +638,12 @@ lazy val `bakery-kafka-listener-example`: Project = project .settings( Docker / packageName := "bakery-kafka-listener-example" ) - .dependsOn(`baker-types`, `bakery-client`, `baker-recipe-compiler`, `baker-recipe-dsl`) + .dependsOn(`baker-types`, `baker-http-client`, `baker-recipe-compiler`, `baker-recipe-dsl`) lazy val `interaction-example-reserve-items`: Project = project.in(file("examples/bakery-interaction-examples/reserve-items")) .enablePlugins(JavaAppPackaging) .enablePlugins(bakery.sbt.BuildInteractionDockerImageSBTPlugin) + .settings(noPublishSettings) .settings(defaultModuleSettings) .settings(yPartialUnificationSetting) .settings( @@ -580,6 +664,7 @@ lazy val `interaction-example-reserve-items`: Project = project.in(file("example lazy val `interaction-example-make-payment-and-ship-items`: Project = project.in(file("examples/bakery-interaction-examples/make-payment-and-ship-items")) .enablePlugins(JavaAppPackaging) .enablePlugins(bakery.sbt.BuildInteractionDockerImageSBTPlugin) + .settings(noPublishSettings) .settings(defaultModuleSettings) .settings(yPartialUnificationSetting) .settings( @@ -613,7 +698,7 @@ lazy val `bakery-integration-tests`: Project = project.in(file("bakery/integrati ) ) .dependsOn( - `bakery-client`, + `baker-http-client`, `bakery-client-example`, `interaction-example-make-payment-and-ship-items`, `interaction-example-reserve-items`) diff --git a/core/README.MD b/core/README.MD new file mode 100644 index 000000000..8cba1b018 --- /dev/null +++ b/core/README.MD @@ -0,0 +1,3 @@ +The `core` directory contains all modules pertaining to baker. + +It contains the recipe DSLs, as well as the runtime implementations (in-memory based on `cats`, as well as the akka-based runtime). diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala index 0f1c0d6fd..66139e52e 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala @@ -19,6 +19,11 @@ import scala.concurrent.{Await, Future} class Baker(private val baker: scaladsl.Baker) extends common.Baker[CompletableFuture] with JavaApi with AutoCloseable { + /** + * Get underlying baker instance, which provides the scala api. + */ + def getScalaBaker: scaladsl.Baker = baker + override type SensoryEventResultType = SensoryEventResult override type EventResolutionsType = EventResolutions diff --git a/core/baker-test/src/main/scala/com/ing/baker/test/RecipeAssert.scala b/core/baker-test/src/main/scala/com/ing/baker/test/RecipeAssert.scala index 58d893e44..4b39939a1 100644 --- a/core/baker-test/src/main/scala/com/ing/baker/test/RecipeAssert.scala +++ b/core/baker-test/src/main/scala/com/ing/baker/test/RecipeAssert.scala @@ -116,14 +116,7 @@ object RecipeAssert { private implicit def toScala(duration: java.time.Duration): Duration = Duration.fromNanos(duration.toNanos) - // hack for now as there is no way to convert java baker to scala baker private implicit def toScala(baker: javadsl.Baker): scaladsl.Baker = { - val field = classOf[javadsl.Baker].getDeclaredField("baker") - try { - field.setAccessible(true) - field.get(baker).asInstanceOf[scaladsl.Baker] - } finally { - field.setAccessible(false) - } + baker.getScalaBaker } } diff --git a/examples/baker-example/src/main/resources/application.conf b/examples/baker-example/src/main/resources/application.conf index 70cd23342..2cbb30706 100644 --- a/examples/baker-example/src/main/resources/application.conf +++ b/examples/baker-example/src/main/resources/application.conf @@ -36,15 +36,6 @@ baker { seed-nodes = [ "akka://"${service.actorSystemName}"@"${service.seedHost}":"${service.seedPort}] } - - event-sink { - class: "com.ing.bakery.baker.KafkaEventSink", - bootstrap-servers: "kafka-event-sink:9092", - bootstrap-servers: ${?KAFKA_EVENT_SINK_BOOTSTRAP_SERVERS}, - topic: "events", - topic: ${?KAFKA_EVENT_SINK_TOPIC} - } - } cassandra-journal.contact-points.0 = "127.0.0.1" diff --git a/examples/baker-example/src/test/java/webshop/JWebshopRecipeTests.java b/examples/baker-example/src/test/java/webshop/JWebshopRecipeTests.java index e5b37c9a1..84f32c582 100644 --- a/examples/baker-example/src/test/java/webshop/JWebshopRecipeTests.java +++ b/examples/baker-example/src/test/java/webshop/JWebshopRecipeTests.java @@ -1,15 +1,12 @@ package webshop; -import akka.actor.ActorSystem; import com.google.common.collect.ImmutableList; import com.ing.baker.compiler.RecipeCompiler; import com.ing.baker.il.CompiledRecipe; -import com.ing.baker.runtime.akka.AkkaBaker; import com.ing.baker.runtime.inmemory.InMemoryBaker; import com.ing.baker.runtime.javadsl.Baker; import com.ing.baker.runtime.javadsl.EventInstance; import com.ing.baker.runtime.javadsl.EventMoment; -import com.typesafe.config.ConfigFactory; import org.junit.Test; import scala.Console; import webshop.simple.*; @@ -54,10 +51,7 @@ public void shouldRunSimpleInstance() throws ExecutionException, InterruptedExce new ShipItemsInstance()); // Setup the Baker - Baker baker = AkkaBaker.java( - ConfigFactory.load(), - ActorSystem.apply("BakerActorSystem"), - implementations); + Baker baker = InMemoryBaker.java(implementations); // Create the sensory events List items = new ArrayList<>(2); @@ -136,4 +130,6 @@ public void shouldRunSimpleInstanceMockitoSample() throws InterruptedException, blockedResult.contains("ShippingAddressReceived") && blockedResult.contains("ShippingConfirmed")); } + + } diff --git a/examples/baker-example/src/test/resources/application.conf b/examples/baker-example/src/test/resources/application.conf index 385178f20..fdeab6144 100644 --- a/examples/baker-example/src/test/resources/application.conf +++ b/examples/baker-example/src/test/resources/application.conf @@ -1,5 +1,3 @@ -include "baker.conf" - akka { log-config-on-start = off @@ -25,6 +23,14 @@ akka { logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" } +akka { + remote.artery { + transport = tcp + canonical.hostname = "127.0.0.1" + canonical.port = 0 + } +} + baker { actor.retention-check-interval = 100 milliseconds encryption { @@ -35,13 +41,6 @@ baker { localhost.port = 8081 } - event-sink { - class: "com.ing.bakery.baker.KafkaEventSink", - bootstrap-servers: "kafka-event-sink:9092", - bootstrap-servers: ${?KAFKA_EVENT_SINK_BOOTSTRAP_SERVERS}, - topic: "events", - topic: ${?KAFKA_EVENT_SINK_TOPIC} - } recipe-manager-type = "actor" actor.provider = "local" allow-adding-recipe-without-requiring-instances = false diff --git a/examples/baker-example/src/test/scala/webshop/simple/WebshopRecipeSpec.scala b/examples/baker-example/src/test/scala/webshop/simple/WebshopRecipeSpec.scala index 161f759a7..088848f8b 100644 --- a/examples/baker-example/src/test/scala/webshop/simple/WebshopRecipeSpec.scala +++ b/examples/baker-example/src/test/scala/webshop/simple/WebshopRecipeSpec.scala @@ -1,36 +1,50 @@ package webshop.simple import akka.actor.ActorSystem +import akka.testkit.TestKit import cats.effect.{ContextShift, IO} import com.ing.baker.compiler.RecipeCompiler import com.ing.baker.runtime.akka.AkkaBaker import com.ing.baker.runtime.akka.internal.CachingInteractionManager import com.ing.baker.runtime.common.RecipeRecord import com.ing.baker.runtime.scaladsl.{Baker, EventInstance, InteractionInstance} -import org.scalatest.flatspec.AsyncFlatSpec +import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike import java.util.UUID import scala.concurrent.{ExecutionContext, Future} -class WebshopRecipeSpec extends AsyncFlatSpec with Matchers { +class WebshopRecipeSpec extends TestKit(ActorSystem("baker-webshop-system")) with Matchers with AnyWordSpecLike with BeforeAndAfterAll { - "The WebshopRecipeReflection" should "compile the recipe without errors" in { - RecipeCompiler.compileRecipe(SimpleWebshopRecipeReflection.recipe) - Future.successful(succeed) + implicit val ec : ExecutionContext = system.dispatcher + + override def afterAll(): Unit = { + TestKit.shutdownActorSystem(system) } - "The WebshopRecipe" should "compile the recipe without errors" in { - RecipeCompiler.compileRecipe(SimpleWebshopRecipe.recipe) - Future.successful(succeed) + "The WebshopRecipeReflection" should { + "compile the recipe without errors" in { + RecipeCompiler.compileRecipe(SimpleWebshopRecipeReflection.recipe) + Future.successful(succeed) + } } - it should "visualize the recipe" in { - val compiled = RecipeCompiler.compileRecipe(SimpleWebshopRecipe.recipe) - val viz: String = compiled.getRecipeVisualization - println(Console.GREEN + s"Recipe visualization, paste this into webgraphviz.com:") - println(viz + Console.RESET) - Future.successful(succeed) + "The WebshopRecipe" should { + "compile the recipe without errors" in { + RecipeCompiler.compileRecipe(SimpleWebshopRecipe.recipe) + Future.successful(succeed) + } + } + + it should { + "visualize the recipe" in { + val compiled = RecipeCompiler.compileRecipe(SimpleWebshopRecipe.recipe) + val viz: String = compiled.getRecipeVisualization + println(Console.GREEN + s"Recipe visualization, paste this into webgraphviz.com:") + println(viz + Console.RESET) + Future.successful(succeed) + } } trait ReserveItems { @@ -57,41 +71,42 @@ class WebshopRecipeSpec extends AsyncFlatSpec with Matchers { } } - it should "reserve items in happy conditions" in { - val system: ActorSystem = ActorSystem("baker-webshop-system") - implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) - - val reserveItemsInstance: InteractionInstance = - InteractionInstance.unsafeFrom(new ReserveItemsMock) - val baker: Baker = AkkaBaker.localDefault(system, CachingInteractionManager(reserveItemsInstance)) - - val compiled = RecipeCompiler.compileRecipe(SimpleWebshopRecipe.recipe) - val recipeInstanceId: String = UUID.randomUUID().toString - - val orderId: String = "order-id" - val items: List[String] = List("item1", "item2") - - val orderPlaced = EventInstance - .unsafeFrom(SimpleWebshopRecipeReflection.OrderPlaced(orderId, items)) - val paymentMade = EventInstance - .unsafeFrom(SimpleWebshopRecipeReflection.PaymentMade()) - - - for { - recipeId <- baker.addRecipe(RecipeRecord.of(compiled)) - _ <- baker.bake(recipeId, recipeInstanceId) - _ <- baker.fireEventAndResolveWhenCompleted( - recipeInstanceId, orderPlaced) - _ <- baker.fireEventAndResolveWhenCompleted( - recipeInstanceId, paymentMade) - state <- baker.getRecipeInstanceState(recipeInstanceId) - provided = state - .ingredients - .find(_._1 == "reservedItems") - .map(_._2.as[List[String]]) - .map(_.mkString(", ")) - .getOrElse("No reserved items") - - } yield provided shouldBe items.mkString(", ") + it should { + "reserve items in happy conditions" in { + implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + + val reserveItemsInstance: InteractionInstance = + InteractionInstance.unsafeFrom(new ReserveItemsMock) + val baker: Baker = AkkaBaker.localDefault(system, CachingInteractionManager(reserveItemsInstance)) + + val compiled = RecipeCompiler.compileRecipe(SimpleWebshopRecipe.recipe) + val recipeInstanceId: String = UUID.randomUUID().toString + + val orderId: String = "order-id" + val items: List[String] = List("item1", "item2") + + val orderPlaced = EventInstance + .unsafeFrom(SimpleWebshopRecipeReflection.OrderPlaced(orderId, items)) + val paymentMade = EventInstance + .unsafeFrom(SimpleWebshopRecipeReflection.PaymentMade()) + + + for { + recipeId <- baker.addRecipe(RecipeRecord.of(compiled)) + _ <- baker.bake(recipeId, recipeInstanceId) + _ <- baker.fireEventAndResolveWhenCompleted( + recipeInstanceId, orderPlaced) + _ <- baker.fireEventAndResolveWhenCompleted( + recipeInstanceId, paymentMade) + state <- baker.getRecipeInstanceState(recipeInstanceId) + provided = state + .ingredients + .find(_._1 == "reservedItems") + .map(_._2.as[List[String]]) + .map(_.mkString(", ")) + .getOrElse("No reserved items") + + } yield provided shouldBe items.mkString(", ") + } } } diff --git a/examples/bakery-client-example/src/main/scala/webshop/webservice/Main.scala b/examples/bakery-client-example/src/main/scala/webshop/webservice/Main.scala index d30e77c30..4e3829b4f 100644 --- a/examples/bakery-client-example/src/main/scala/webshop/webservice/Main.scala +++ b/examples/bakery-client-example/src/main/scala/webshop/webservice/Main.scala @@ -5,7 +5,7 @@ import java.util.concurrent.Executors import cats.effect.{ExitCode, IO, IOApp} import cats.implicits._ import com.ing.baker.compiler.RecipeCompiler -import com.ing.bakery.scaladsl.BakerClient +import com.ing.baker.http.client.scaladsl.BakerClient import com.typesafe.config.ConfigFactory import org.http4s.Uri import org.http4s.server.blaze.BlazeServerBuilder diff --git a/http/README.md b/http/README.md new file mode 100644 index 000000000..127a01243 --- /dev/null +++ b/http/README.md @@ -0,0 +1,5 @@ +The `http` directory contains the modules for exposing or consuming baker using http(s). This allows for baker to be called +from other APIs (see the `bakery` directory) or even from non-jvm applications (for example javascript). + +The `http` directory also contains the `baker-http-dashboard` project which uses the endpoints exposed by the `baker-http-server` library to allow you +to view added recipes, inspect and execute interactions manually, or inspect the status of process instances (executed recipes). diff --git a/bakery/client/src/main/resources/reference.conf b/http/baker-http-client/src/main/resources/reference.conf similarity index 100% rename from bakery/client/src/main/resources/reference.conf rename to http/baker-http-client/src/main/resources/reference.conf diff --git a/bakery/client/src/main/scala/com/ing/bakery/common/FailoverState.scala b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/FailoverState.scala similarity index 87% rename from bakery/client/src/main/scala/com/ing/bakery/common/FailoverState.scala rename to http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/FailoverState.scala index 538b1e498..91dec0ba9 100644 --- a/bakery/client/src/main/scala/com/ing/bakery/common/FailoverState.scala +++ b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/FailoverState.scala @@ -1,6 +1,6 @@ -package com.ing.bakery.common +package com.ing.baker.http.client.common -import com.ing.bakery.scaladsl.EndpointConfig +import com.ing.baker.http.client.scaladsl.EndpointConfig import com.typesafe.scalalogging.LazyLogging import org.http4s.Uri @@ -26,4 +26,4 @@ sealed class FailoverState(var endpoint: EndpointConfig) extends LazyLogging { } def uri: Uri = endpoint.hosts(currentPosition.get()) -} \ No newline at end of file +} diff --git a/bakery/client/src/main/scala/com/ing/bakery/common/FailoverUtils.scala b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/FailoverUtils.scala similarity index 96% rename from bakery/client/src/main/scala/com/ing/bakery/common/FailoverUtils.scala rename to http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/FailoverUtils.scala index 1c019075b..06789ee9f 100644 --- a/bakery/client/src/main/scala/com/ing/bakery/common/FailoverUtils.scala +++ b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/FailoverUtils.scala @@ -1,11 +1,11 @@ -package com.ing.bakery.common +package com.ing.baker.http.client.common import cats.effect.{ContextShift, IO, Timer} import cats.implicits._ +import com.ing.baker.http.client.scaladsl.{EndpointConfig, ResponseError} import com.ing.baker.runtime.common.BakerException import com.ing.baker.runtime.common.BakerException.NoSuchProcessException import com.ing.baker.runtime.scaladsl.BakerResult -import com.ing.bakery.scaladsl.{EndpointConfig, ResponseError} import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.LazyLogging import io.circe.Decoder diff --git a/bakery/client/src/main/scala/com/ing/bakery/common/KeystoreConfig.scala b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/KeystoreConfig.scala similarity index 97% rename from bakery/client/src/main/scala/com/ing/bakery/common/KeystoreConfig.scala rename to http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/KeystoreConfig.scala index b937948af..538a8ee00 100644 --- a/bakery/client/src/main/scala/com/ing/bakery/common/KeystoreConfig.scala +++ b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/KeystoreConfig.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.common +package com.ing.baker.http.client.common import java.io.{File, FileInputStream, InputStream} import java.security.KeyStore diff --git a/bakery/client/src/main/scala/com/ing/bakery/common/TLSConfig.scala b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/TLSConfig.scala similarity index 94% rename from bakery/client/src/main/scala/com/ing/bakery/common/TLSConfig.scala rename to http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/TLSConfig.scala index 03e47ef87..5b7f5f874 100644 --- a/bakery/client/src/main/scala/com/ing/bakery/common/TLSConfig.scala +++ b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/common/TLSConfig.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.common +package com.ing.baker.http.client.common import java.security.SecureRandom diff --git a/bakery/client/src/main/scala/com/ing/bakery/javadsl/BakerClient.scala b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/javadsl/BakerClient.scala similarity index 92% rename from bakery/client/src/main/scala/com/ing/bakery/javadsl/BakerClient.scala rename to http/baker-http-client/src/main/scala/com/ing/baker/http/client/javadsl/BakerClient.scala index d47543ac3..319ba7dd9 100644 --- a/bakery/client/src/main/scala/com/ing/bakery/javadsl/BakerClient.scala +++ b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/javadsl/BakerClient.scala @@ -1,10 +1,9 @@ -package com.ing.bakery.javadsl +package com.ing.baker.http.client.javadsl import cats.effect.{ContextShift, IO, Timer} +import com.ing.baker.http.client.common.TLSConfig +import com.ing.baker.http.client.scaladsl.{BakerClient => ScalaClient, EndpointConfig} import com.ing.baker.runtime.javadsl.{Baker => JavaBaker} -import com.ing.bakery.common.TLSConfig -import com.ing.bakery.scaladsl -import com.ing.bakery.scaladsl.EndpointConfig import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.{Request, Uri} @@ -51,7 +50,7 @@ object BakerClient { sslContext = sslContext) .resource .map { client => - new scaladsl.BakerClient( + new ScalaClient( client = client, EndpointConfig(hosts.asScala.map(Uri.unsafeFromString).toIndexedSeq, apiUrlPrefix, apiLoggingEnabled), if (fallbackHosts.size == 0) None @@ -66,4 +65,4 @@ object BakerClient { FutureConverters.toJava(future).toCompletableFuture } -} \ No newline at end of file +} diff --git a/bakery/client/src/main/scala/com/ing/bakery/scaladsl/BakerClient.scala b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/scaladsl/BakerClient.scala similarity index 98% rename from bakery/client/src/main/scala/com/ing/bakery/scaladsl/BakerClient.scala rename to http/baker-http-client/src/main/scala/com/ing/baker/http/client/scaladsl/BakerClient.scala index d35436fcc..6acd3ec0d 100644 --- a/bakery/client/src/main/scala/com/ing/bakery/scaladsl/BakerClient.scala +++ b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/scaladsl/BakerClient.scala @@ -1,8 +1,7 @@ -package com.ing.bakery.scaladsl +package com.ing.baker.http.client.scaladsl import cats.effect.{ContextShift, IO, Resource, Timer} import com.ing.baker.il.RecipeVisualStyle -import com.ing.baker.runtime.common.BakerException.SingleInteractionExecutionFailedException import com.ing.baker.runtime.common.{BakerException, RecipeRecord, SensoryEventStatus, Utils} import com.ing.baker.runtime.scaladsl.{BakerEvent, BakerResult, EncodedRecipe, EventInstance, EventMoment, EventResolutions, IngredientInstance, InteractionExecutionResult, InteractionInstanceDescriptor, RecipeEventMetadata, RecipeInformation, RecipeInstanceMetadata, RecipeInstanceState, SensoryEventResult, Baker => ScalaBaker} import com.ing.baker.runtime.serialization.InteractionExecution @@ -10,8 +9,8 @@ import com.ing.baker.runtime.serialization.InteractionExecutionJsonCodecs._ import com.ing.baker.runtime.serialization.JsonDecoders._ import com.ing.baker.runtime.serialization.JsonEncoders._ import com.ing.baker.types.Value -import com.ing.bakery.common.FailoverUtils._ -import com.ing.bakery.common.{FailoverState, TLSConfig} +import com.ing.baker.http.client.common.FailoverUtils._ +import com.ing.baker.http.client.common.{FailoverState, TLSConfig} import com.typesafe.scalalogging.LazyLogging import io.circe.Decoder import org.http4s.Method._ diff --git a/bakery/client/src/test/resources/logback-test.xml b/http/baker-http-client/src/test/resources/logback-test.xml similarity index 100% rename from bakery/client/src/test/resources/logback-test.xml rename to http/baker-http-client/src/test/resources/logback-test.xml diff --git a/bakery/client/src/test/resources/test-certs/client.jks b/http/baker-http-client/src/test/resources/test-certs/client.jks similarity index 100% rename from bakery/client/src/test/resources/test-certs/client.jks rename to http/baker-http-client/src/test/resources/test-certs/client.jks diff --git a/bakery/client/src/test/resources/test-certs/server.jks b/http/baker-http-client/src/test/resources/test-certs/server.jks similarity index 100% rename from bakery/client/src/test/resources/test-certs/server.jks rename to http/baker-http-client/src/test/resources/test-certs/server.jks diff --git a/bakery/client/src/test/scala/com/ing/bakery/client/BakerClientSpec.scala b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/BakerClientSpec.scala similarity index 92% rename from bakery/client/src/test/scala/com/ing/bakery/client/BakerClientSpec.scala rename to http/baker-http-client/src/test/scala/com/ing/baker/http/client/BakerClientSpec.scala index d558def34..16253f733 100644 --- a/bakery/client/src/test/scala/com/ing/bakery/client/BakerClientSpec.scala +++ b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/BakerClientSpec.scala @@ -1,18 +1,18 @@ -package com.ing.bakery.client +package com.ing.baker.http.client import cats.effect.concurrent.{MVar, MVar2} import cats.effect.{IO, Resource} +import com.ing.baker.http.client.common.{KeystoreConfig, TLSConfig} +import com.ing.baker.http.client.javadsl.{BakerClient => JavaClient} +import com.ing.baker.http.client.scaladsl.{EndpointConfig, BakerClient => ScalaClient} import com.ing.baker.runtime.scaladsl.BakerResult import com.ing.baker.runtime.serialization.JsonEncoders._ -import com.ing.bakery.common.{KeystoreConfig, TLSConfig} -import com.ing.bakery.javadsl -import com.ing.bakery.scaladsl.{BakerClient, EndpointConfig} +import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.io._ import org.http4s.implicits._ import org.http4s.server.Router import org.http4s.server.blaze._ -import org.http4s._ import org.scalatest.ConfigMap import java.net.InetSocketAddress @@ -89,7 +89,7 @@ class BakerClientSpec extends BakeryFunSpec { val host = Uri.unsafeFromString(s"https://localhost:${context.serverAddress.getPort}/") val testHeader = Header("X-Test", "Foo") val filter: Request[IO] => Request[IO] = _.putHeaders(testHeader) - BakerClient.resource(host, "/api/bakery", executionContext, List(filter), Some(clientTLSConfig)).use { client => + ScalaClient.resource(host, "/api/bakery", executionContext, List(filter), Some(clientTLSConfig)).use { client => for { _ <- IO.fromFuture(IO(client.getAllRecipeInstancesMetadata)) headers <- context.receivedHeaders @@ -102,7 +102,7 @@ class BakerClientSpec extends BakeryFunSpec { val uri2 = Uri.unsafeFromString(s"https://invaliddomainname:445") val uri3 = uri1 / "nowWorking" - BakerClient.resourceBalanced( + ScalaClient.resourceBalanced( endpointConfig = EndpointConfig(IndexedSeq(uri3, uri2, uri1)), executionContext = executionContext, filters = List.empty, @@ -120,7 +120,7 @@ class BakerClientSpec extends BakeryFunSpec { val filter: java.util.function.Function[Request[IO], Request[IO]] = _.putHeaders(testHeader) for { client <- IO.fromFuture(IO(FutureConverters.toScala( - javadsl.BakerClient.build(List(host).asJava, "/api/bakery", + JavaClient.build(List(host).asJava, "/api/bakery", List().asJava, "", List(filter).asJava, java.util.Optional.of(clientTLSConfig), true)))) _ <- IO.fromFuture(IO(FutureConverters.toScala(client.getAllRecipeInstancesMetadata))) headers <- context.receivedHeaders diff --git a/bakery/client/src/test/scala/com/ing/bakery/client/BakeryFunSpec.scala b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/BakeryFunSpec.scala similarity index 98% rename from bakery/client/src/test/scala/com/ing/bakery/client/BakeryFunSpec.scala rename to http/baker-http-client/src/test/scala/com/ing/baker/http/client/BakeryFunSpec.scala index 34f699b69..2817ad42f 100644 --- a/bakery/client/src/test/scala/com/ing/bakery/client/BakeryFunSpec.scala +++ b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/BakeryFunSpec.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.client +package com.ing.baker.http.client import cats.effect.{ContextShift, IO, Resource, Timer} import org.scalactic.source @@ -68,4 +68,4 @@ abstract class BakeryFunSpec extends FixtureAsyncFunSpecLike { override def withFixture(test: OneArgAsyncTest): FutureOutcome = test.apply(argumentsBuilder(test.configMap)) -} \ No newline at end of file +} diff --git a/bakery/client/src/test/scala/com/ing/bakery/common/FailoverStateSpec.scala b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/common/FailoverStateSpec.scala similarity index 96% rename from bakery/client/src/test/scala/com/ing/bakery/common/FailoverStateSpec.scala rename to http/baker-http-client/src/test/scala/com/ing/baker/http/client/common/FailoverStateSpec.scala index 762278b14..619fbbd4a 100644 --- a/bakery/client/src/test/scala/com/ing/bakery/common/FailoverStateSpec.scala +++ b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/common/FailoverStateSpec.scala @@ -1,6 +1,6 @@ -package com.ing.bakery.common +package com.ing.baker.http.client.common -import com.ing.bakery.scaladsl.EndpointConfig +import com.ing.baker.http.client.scaladsl.EndpointConfig import org.http4s.Uri import org.scalatest.funspec.AnyFunSpec @@ -122,4 +122,4 @@ class FailoverStateSpec extends AnyFunSpec { } -} \ No newline at end of file +} diff --git a/bakery/client/src/test/scala/com/ing/bakery/common/FailoverUtilsSpec.scala b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/common/FailoverUtilsSpec.scala similarity index 97% rename from bakery/client/src/test/scala/com/ing/bakery/common/FailoverUtilsSpec.scala rename to http/baker-http-client/src/test/scala/com/ing/baker/http/client/common/FailoverUtilsSpec.scala index 951a8bedd..e68e87014 100644 --- a/bakery/client/src/test/scala/com/ing/bakery/common/FailoverUtilsSpec.scala +++ b/http/baker-http-client/src/test/scala/com/ing/baker/http/client/common/FailoverUtilsSpec.scala @@ -1,10 +1,10 @@ -package com.ing.bakery.common +package com.ing.baker.http.client.common import cats.effect.{ContextShift, IO, Resource, Timer} +import com.ing.baker.http.client.scaladsl.EndpointConfig import com.ing.baker.runtime.common.BakerException.NoSuchProcessException import com.ing.baker.runtime.scaladsl.BakerResult import com.ing.baker.runtime.serialization.JsonEncoders._ -import com.ing.bakery.scaladsl.EndpointConfig import org.http4s.Method.GET import org.http4s._ import org.http4s.circe.jsonEncoderOf @@ -27,7 +27,7 @@ import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} @nowarn class FailoverUtilsSpec extends FixtureAsyncFunSpec { - import FailoverUtils._ + import com.ing.baker.http.client.common.FailoverUtils._ implicit val ec: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) @@ -177,4 +177,4 @@ class FailoverUtilsSpec extends FixtureAsyncFunSpec { } } } -} \ No newline at end of file +} diff --git a/bakery/dashboard/.browserslistrc b/http/baker-http-dashboard/.browserslistrc similarity index 100% rename from bakery/dashboard/.browserslistrc rename to http/baker-http-dashboard/.browserslistrc diff --git a/bakery/dashboard/.editorconfig b/http/baker-http-dashboard/.editorconfig similarity index 100% rename from bakery/dashboard/.editorconfig rename to http/baker-http-dashboard/.editorconfig diff --git a/bakery/dashboard/.eslintrc.json b/http/baker-http-dashboard/.eslintrc.json similarity index 100% rename from bakery/dashboard/.eslintrc.json rename to http/baker-http-dashboard/.eslintrc.json diff --git a/bakery/dashboard/.gitignore b/http/baker-http-dashboard/.gitignore similarity index 100% rename from bakery/dashboard/.gitignore rename to http/baker-http-dashboard/.gitignore diff --git a/bakery/dashboard/.nvmrc b/http/baker-http-dashboard/.nvmrc similarity index 100% rename from bakery/dashboard/.nvmrc rename to http/baker-http-dashboard/.nvmrc diff --git a/bakery/dashboard/README.md b/http/baker-http-dashboard/README.md similarity index 100% rename from bakery/dashboard/README.md rename to http/baker-http-dashboard/README.md diff --git a/bakery/dashboard/angular.json b/http/baker-http-dashboard/angular.json similarity index 100% rename from bakery/dashboard/angular.json rename to http/baker-http-dashboard/angular.json diff --git a/bakery/dashboard/e2e/protractor.conf.js b/http/baker-http-dashboard/e2e/protractor.conf.js similarity index 100% rename from bakery/dashboard/e2e/protractor.conf.js rename to http/baker-http-dashboard/e2e/protractor.conf.js diff --git a/bakery/dashboard/e2e/src/app.e2e-spec.ts b/http/baker-http-dashboard/e2e/src/app.e2e-spec.ts similarity index 100% rename from bakery/dashboard/e2e/src/app.e2e-spec.ts rename to http/baker-http-dashboard/e2e/src/app.e2e-spec.ts diff --git a/bakery/dashboard/e2e/src/app.po.ts b/http/baker-http-dashboard/e2e/src/app.po.ts similarity index 100% rename from bakery/dashboard/e2e/src/app.po.ts rename to http/baker-http-dashboard/e2e/src/app.po.ts diff --git a/bakery/dashboard/e2e/tsconfig.json b/http/baker-http-dashboard/e2e/tsconfig.json similarity index 100% rename from bakery/dashboard/e2e/tsconfig.json rename to http/baker-http-dashboard/e2e/tsconfig.json diff --git a/bakery/dashboard/karma.conf.js b/http/baker-http-dashboard/karma.conf.js similarity index 100% rename from bakery/dashboard/karma.conf.js rename to http/baker-http-dashboard/karma.conf.js diff --git a/bakery/dashboard/npm-build.sh b/http/baker-http-dashboard/npm-build.sh similarity index 74% rename from bakery/dashboard/npm-build.sh rename to http/baker-http-dashboard/npm-build.sh index b10bc8ac7..8e61b24bb 100755 --- a/bakery/dashboard/npm-build.sh +++ b/http/baker-http-dashboard/npm-build.sh @@ -4,5 +4,4 @@ npm install npx ng build #npm run test #npm run lint -cd dist -zip -r ../dashboard.zip * + diff --git a/bakery/dashboard/package.json b/http/baker-http-dashboard/package.json similarity index 98% rename from bakery/dashboard/package.json rename to http/baker-http-dashboard/package.json index 8fd8aec54..6f7d03a91 100644 --- a/bakery/dashboard/package.json +++ b/http/baker-http-dashboard/package.json @@ -36,7 +36,7 @@ "@angular-eslint/eslint-plugin-template": "14.0.2", "@angular-eslint/schematics": "14.0.2", "@angular-eslint/template-parser": "14.0.2", - "@angular/cli": "^14.0.6", + "@angular/cli": "^14.1.1", "@angular/compiler-cli": "^14.0.6", "@angular/language-service": "^14.0.6", "@angular/localize": "^14.0.6", diff --git a/http/baker-http-dashboard/src-scala/main/resources/reference.conf b/http/baker-http-dashboard/src-scala/main/resources/reference.conf new file mode 100644 index 000000000..13482dc4e --- /dev/null +++ b/http/baker-http-dashboard/src-scala/main/resources/reference.conf @@ -0,0 +1,6 @@ +baker.dashboard { + enabled = true + application-name = "Baker OSS" + cluster-information { + } +} diff --git a/http/baker-http-dashboard/src-scala/main/scala/com/ing/baker/http/Dashboard.scala b/http/baker-http-dashboard/src-scala/main/scala/com/ing/baker/http/Dashboard.scala new file mode 100644 index 000000000..806375b04 --- /dev/null +++ b/http/baker-http-dashboard/src-scala/main/scala/com/ing/baker/http/Dashboard.scala @@ -0,0 +1,38 @@ +package com.ing.baker.http + +import scala.io.Source +import scala.util.Try +import scala.util.matching.Regex + +object Dashboard { + private val DASHBOARD_PREFIX = "dashboard_static/" + + /** + * List of static files of the dashboard. + */ + lazy val files : Seq[String] = + Try(Source.fromResource("dashboard_static_index").getLines().map(_.replace(DASHBOARD_PREFIX, "")).toIndexedSeq) + .getOrElse(throw new IllegalStateException("Expected list of dashboard files to be available under 'dashboard_static_index")) + + /** + * Http paths that should serve the index page. + */ + val indexPattern: Regex = "^(/)?|(/recipes)|(/interactions)|(/instances(/.+)?)$".r + + /** + * Get URL to resource from filename. Do not specify the dashboard prefix, as it is automatically added. + * Uses a whitelist of files to prevent any unauthorized access of resources by a malicious user. + */ + def safeGetResourcePath(fileName: String) : Option[String] = + files.find(_ == fileName).map(DASHBOARD_PREFIX + _) + + def dashboardConfigJson(apiPath: String, dashboardConfiguration: DashboardConfiguration) : String = + s"""{ + | "applicationName": "${dashboardConfiguration.applicationName}", + | "apiPath": "${apiPath}", + | "clusterInformation": { + | ${dashboardConfiguration.clusterInformation.map{ case (key, value) => s""" "$key": "$value""""}.mkString(",\n ")} + | } + |} + |""".stripMargin +} diff --git a/http/baker-http-dashboard/src-scala/main/scala/com/ing/baker/http/DashboardConfiguration.scala b/http/baker-http-dashboard/src-scala/main/scala/com/ing/baker/http/DashboardConfiguration.scala new file mode 100644 index 000000000..a3d38dc7a --- /dev/null +++ b/http/baker-http-dashboard/src-scala/main/scala/com/ing/baker/http/DashboardConfiguration.scala @@ -0,0 +1,33 @@ +package com.ing.baker.http + +import com.typesafe.config.Config + +import scala.annotation.nowarn +import scala.collection.immutable.Map +import scala.collection.JavaConverters._ + +case class DashboardConfiguration(enabled: Boolean, + applicationName: String, + clusterInformation: Map[String, String]) + +object DashboardConfiguration { + @nowarn + def fromConfig(config: Config) : DashboardConfiguration = { + val dashboardConfig = config.getConfig("baker.dashboard") + val clusterInformation : Map[String, String] = { + if (dashboardConfig.hasPath("cluster-information")) + dashboardConfig + .getConfig("cluster-information") + .entrySet().asScala + .map(entry => (entry.getKey, entry.getValue.unwrapped().toString)).toMap + else Map.empty + } + + DashboardConfiguration( + enabled = dashboardConfig.getBoolean("enabled"), + applicationName = dashboardConfig.getString("application-name"), + clusterInformation = clusterInformation + ) + } +} + diff --git a/http/baker-http-dashboard/src-scala/test/scala/com/ing/baker/http/DashboardAndConfigurationSpec.scala b/http/baker-http-dashboard/src-scala/test/scala/com/ing/baker/http/DashboardAndConfigurationSpec.scala new file mode 100644 index 000000000..3db1b38ee --- /dev/null +++ b/http/baker-http-dashboard/src-scala/test/scala/com/ing/baker/http/DashboardAndConfigurationSpec.scala @@ -0,0 +1,55 @@ +package com.ing.baker.http + +import org.scalatest.flatspec._ +import org.scalatest.matchers._ + +import scala.io.Source +import scala.util.Try + + +class DashboardAndConfigurationSpec extends AnyFlatSpec with should.Matchers { + + "The dashboard object" should "list the static files" in { + Dashboard.files.find(_ == "index.html") should not be empty + } + + "The safe get resource url" should "return a valid resource" in { + val path = Dashboard.safeGetResourcePath("index.html") + path should not be empty + Try(Source.fromResource(path.get)).toOption should not be empty + } + + "The versionJson" should "return a valid response" in { + val configuration = DashboardConfiguration( + enabled = true, + applicationName = "application name", + clusterInformation = Map( + "version1" -> "1.0", + "version2" -> "2.0" + ) + ) + Dashboard.dashboardConfigJson("/test/path", configuration).replace(" ", "") shouldEqual + """{ + | "applicationName": "application name", + | "apiPath": "/test/path", + | "clusterInformation": { + | "version1": "1.0", + | "version2": "2.0" + | } + | } + |""".stripMargin.replace(" ", "") + } + + "The dashboard object" should "match correct urls" in { + //TODO Revert this commit once scala-212 is no longer supported. + "".matches(Dashboard.indexPattern.regex) shouldBe true + "/".matches(Dashboard.indexPattern.regex) shouldBe true + "/recipes".matches(Dashboard.indexPattern.regex) shouldBe true + "/interactions".matches(Dashboard.indexPattern.regex) shouldBe true + "/instances".matches(Dashboard.indexPattern.regex) shouldBe true + "/instances/instance-id".matches(Dashboard.indexPattern.regex) shouldBe true + "/instanceand".matches(Dashboard.indexPattern.regex) shouldBe false + "/instance/".matches(Dashboard.indexPattern.regex) shouldBe false + } + +} diff --git a/bakery/dashboard/src/app/app-routing.module.ts b/http/baker-http-dashboard/src/app/app-routing.module.ts similarity index 100% rename from bakery/dashboard/src/app/app-routing.module.ts rename to http/baker-http-dashboard/src/app/app-routing.module.ts diff --git a/bakery/dashboard/src/app/app.component.css b/http/baker-http-dashboard/src/app/app.component.css similarity index 100% rename from bakery/dashboard/src/app/app.component.css rename to http/baker-http-dashboard/src/app/app.component.css diff --git a/bakery/dashboard/src/app/app.component.html b/http/baker-http-dashboard/src/app/app.component.html similarity index 100% rename from bakery/dashboard/src/app/app.component.html rename to http/baker-http-dashboard/src/app/app.component.html diff --git a/bakery/dashboard/src/app/app.component.ts b/http/baker-http-dashboard/src/app/app.component.ts similarity index 94% rename from bakery/dashboard/src/app/app.component.ts rename to http/baker-http-dashboard/src/app/app.component.ts index 1dbfba483..80df3c651 100644 --- a/bakery/dashboard/src/app/app.component.ts +++ b/http/baker-http-dashboard/src/app/app.component.ts @@ -9,7 +9,7 @@ import {wasmFolder} from "@hpcc-js/wasm"; "templateUrl": "./app.component.html" }) export class AppComponent implements OnDestroy, OnInit { - title = AppSettingsService.settings.title; + title = AppSettingsService.settings.applicationName; mobileQuery: MediaQueryList; private readonly mobileQueryListener: () => void; diff --git a/bakery/dashboard/src/app/app.module.ts b/http/baker-http-dashboard/src/app/app.module.ts similarity index 100% rename from bakery/dashboard/src/app/app.module.ts rename to http/baker-http-dashboard/src/app/app.module.ts diff --git a/bakery/dashboard/src/app/app.settings.ts b/http/baker-http-dashboard/src/app/app.settings.ts similarity index 64% rename from bakery/dashboard/src/app/app.settings.ts rename to http/baker-http-dashboard/src/app/app.settings.ts index c7cdd6a55..4b0630ee7 100644 --- a/bakery/dashboard/src/app/app.settings.ts +++ b/http/baker-http-dashboard/src/app/app.settings.ts @@ -1,13 +1,13 @@ import {HttpClient} from "@angular/common/http"; import {Injectable} from "@angular/core"; +import {Value} from "./baker-value.api"; -const SETTINGS_LOCATION = "assets/settings/settings.json"; +const SETTINGS_LOCATION = "/dashboard_config"; export interface AppSettings { - apiUrl: string; - title: string; - bakeryVersion: string; - stateVersion: string; + applicationName: string; + apiPath: string; + clusterInformation: { [key: string]: string }; } @Injectable() @@ -27,5 +27,11 @@ export class AppSettingsService { }). catch((response: any) => reject(Error(`Could not load file '${SETTINGS_LOCATION}': ${JSON.stringify(response)}`))); }); + //// For testing purposes: + // AppSettingsService.settings = { + // "applicationName": "Test", + // "apiPath": "/api/bakery", + // "clusterInformation": {} + // }; } } diff --git a/bakery/dashboard/src/app/baker-conversion.service.ts b/http/baker-http-dashboard/src/app/baker-conversion.service.ts similarity index 100% rename from bakery/dashboard/src/app/baker-conversion.service.ts rename to http/baker-http-dashboard/src/app/baker-conversion.service.ts diff --git a/bakery/dashboard/src/app/baker-types.api.ts b/http/baker-http-dashboard/src/app/baker-types.api.ts similarity index 100% rename from bakery/dashboard/src/app/baker-types.api.ts rename to http/baker-http-dashboard/src/app/baker-types.api.ts diff --git a/bakery/dashboard/src/app/baker-value.api.ts b/http/baker-http-dashboard/src/app/baker-value.api.ts similarity index 100% rename from bakery/dashboard/src/app/baker-value.api.ts rename to http/baker-http-dashboard/src/app/baker-value.api.ts diff --git a/bakery/dashboard/src/app/bakery.api.ts b/http/baker-http-dashboard/src/app/bakery.api.ts similarity index 93% rename from bakery/dashboard/src/app/bakery.api.ts rename to http/baker-http-dashboard/src/app/bakery.api.ts index 343704138..faee0e345 100644 --- a/bakery/dashboard/src/app/bakery.api.ts +++ b/http/baker-http-dashboard/src/app/bakery.api.ts @@ -9,11 +9,17 @@ export interface Recipe { errors: string[]; } +export interface RecipeBodyCompiledRecipe { + name: string; + recipeId: string; + validationErrors: string[]; +} + export interface RecipeBody { - compiledRecipe: Recipe; + compiledRecipe: RecipeBodyCompiledRecipe; recipeCreatedTime: number; - validate: boolean; errors: string[]; + validate: boolean; } export interface Recipes { diff --git a/bakery/dashboard/src/app/bakery.service.ts b/http/baker-http-dashboard/src/app/bakery.service.ts similarity index 97% rename from bakery/dashboard/src/app/bakery.service.ts rename to http/baker-http-dashboard/src/app/bakery.service.ts index b5f4ef49e..a378d71af 100644 --- a/bakery/dashboard/src/app/bakery.service.ts +++ b/http/baker-http-dashboard/src/app/bakery.service.ts @@ -23,7 +23,7 @@ import {Injectable} from "@angular/core"; @Injectable({"providedIn": "root"}) export class BakeryService { - private baseUrl = AppSettingsService.settings.apiUrl; + private baseUrl = AppSettingsService.settings.apiPath; httpOptions = { "headers": new HttpHeaders({"Content-Type": "application/json"}) @@ -40,7 +40,7 @@ export class BakeryService { pipe(map(recipes => Object.values(recipes.body) .map(response => { const row: Recipe = { - "errors": response.compiledRecipe.errors, + "errors": response.errors, "name": response.compiledRecipe.name, "recipeCreatedTime": response.recipeCreatedTime, "recipeId": response.compiledRecipe.recipeId, diff --git a/bakery/dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.html b/http/baker-http-dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.html similarity index 100% rename from bakery/dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.html rename to http/baker-http-dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.html diff --git a/bakery/dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.ts b/http/baker-http-dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.ts similarity index 100% rename from bakery/dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.ts rename to http/baker-http-dashboard/src/app/generic/visualize-recipe/visualize-recipe.component.ts diff --git a/bakery/dashboard/src/app/generic/visualize-recipe/visualize-recipe.scss b/http/baker-http-dashboard/src/app/generic/visualize-recipe/visualize-recipe.scss similarity index 100% rename from bakery/dashboard/src/app/generic/visualize-recipe/visualize-recipe.scss rename to http/baker-http-dashboard/src/app/generic/visualize-recipe/visualize-recipe.scss diff --git a/http/baker-http-dashboard/src/app/home/home.component.html b/http/baker-http-dashboard/src/app/home/home.component.html new file mode 100644 index 000000000..a7360efa6 --- /dev/null +++ b/http/baker-http-dashboard/src/app/home/home.component.html @@ -0,0 +1,5 @@ +
+
+    {{clusterInformation}}
+  
+
diff --git a/bakery/dashboard/src/app/home/home.component.spec.ts b/http/baker-http-dashboard/src/app/home/home.component.spec.ts similarity index 100% rename from bakery/dashboard/src/app/home/home.component.spec.ts rename to http/baker-http-dashboard/src/app/home/home.component.spec.ts diff --git a/bakery/dashboard/src/app/home/home.component.ts b/http/baker-http-dashboard/src/app/home/home.component.ts similarity index 55% rename from bakery/dashboard/src/app/home/home.component.ts rename to http/baker-http-dashboard/src/app/home/home.component.ts index 41d094834..5b28af886 100644 --- a/bakery/dashboard/src/app/home/home.component.ts +++ b/http/baker-http-dashboard/src/app/home/home.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit, Renderer2} from "@angular/core"; +import {Component, OnInit} from "@angular/core"; import {AppSettingsService} from "../app.settings"; /** @title Bakery DashboardComponent */ @@ -9,15 +9,13 @@ import {AppSettingsService} from "../app.settings"; }) export class HomeComponent implements OnInit { - bakeryVersion: string; - stateVersion: string; + clusterInformation: string; constructor () { } ngOnInit (): void { - this.bakeryVersion = AppSettingsService.settings.bakeryVersion; - this.stateVersion = AppSettingsService.settings.stateVersion; + this.clusterInformation = JSON.stringify(AppSettingsService.settings.clusterInformation); } } diff --git a/bakery/dashboard/src/app/home/home.css b/http/baker-http-dashboard/src/app/home/home.css similarity index 100% rename from bakery/dashboard/src/app/home/home.css rename to http/baker-http-dashboard/src/app/home/home.css diff --git a/bakery/dashboard/src/app/instances/instances.component.html b/http/baker-http-dashboard/src/app/instances/instances.component.html similarity index 100% rename from bakery/dashboard/src/app/instances/instances.component.html rename to http/baker-http-dashboard/src/app/instances/instances.component.html diff --git a/bakery/dashboard/src/app/instances/instances.component.spec.ts b/http/baker-http-dashboard/src/app/instances/instances.component.spec.ts similarity index 100% rename from bakery/dashboard/src/app/instances/instances.component.spec.ts rename to http/baker-http-dashboard/src/app/instances/instances.component.spec.ts diff --git a/bakery/dashboard/src/app/instances/instances.component.ts b/http/baker-http-dashboard/src/app/instances/instances.component.ts similarity index 100% rename from bakery/dashboard/src/app/instances/instances.component.ts rename to http/baker-http-dashboard/src/app/instances/instances.component.ts diff --git a/bakery/dashboard/src/app/instances/instances.css b/http/baker-http-dashboard/src/app/instances/instances.css similarity index 100% rename from bakery/dashboard/src/app/instances/instances.css rename to http/baker-http-dashboard/src/app/instances/instances.css diff --git a/bakery/dashboard/src/app/interactions/interaction-definition/interaction-definition.component.html b/http/baker-http-dashboard/src/app/interactions/interaction-definition/interaction-definition.component.html similarity index 100% rename from bakery/dashboard/src/app/interactions/interaction-definition/interaction-definition.component.html rename to http/baker-http-dashboard/src/app/interactions/interaction-definition/interaction-definition.component.html diff --git a/bakery/dashboard/src/app/interactions/interaction-definition/interaction-definition.component.scss b/http/baker-http-dashboard/src/app/interactions/interaction-definition/interaction-definition.component.scss similarity index 100% rename from bakery/dashboard/src/app/interactions/interaction-definition/interaction-definition.component.scss rename to http/baker-http-dashboard/src/app/interactions/interaction-definition/interaction-definition.component.scss diff --git a/bakery/dashboard/src/app/interactions/interaction-definition/interaction-definition.component.ts b/http/baker-http-dashboard/src/app/interactions/interaction-definition/interaction-definition.component.ts similarity index 100% rename from bakery/dashboard/src/app/interactions/interaction-definition/interaction-definition.component.ts rename to http/baker-http-dashboard/src/app/interactions/interaction-definition/interaction-definition.component.ts diff --git a/bakery/dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.html b/http/baker-http-dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.html similarity index 100% rename from bakery/dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.html rename to http/baker-http-dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.html diff --git a/bakery/dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.scss b/http/baker-http-dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.scss similarity index 100% rename from bakery/dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.scss rename to http/baker-http-dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.scss diff --git a/bakery/dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.ts b/http/baker-http-dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.ts similarity index 100% rename from bakery/dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.ts rename to http/baker-http-dashboard/src/app/interactions/interaction-manual-test/interaction-manual-test.component.ts diff --git a/bakery/dashboard/src/app/interactions/interactions.component.html b/http/baker-http-dashboard/src/app/interactions/interactions.component.html similarity index 100% rename from bakery/dashboard/src/app/interactions/interactions.component.html rename to http/baker-http-dashboard/src/app/interactions/interactions.component.html diff --git a/bakery/dashboard/src/app/interactions/interactions.component.spec.ts b/http/baker-http-dashboard/src/app/interactions/interactions.component.spec.ts similarity index 100% rename from bakery/dashboard/src/app/interactions/interactions.component.spec.ts rename to http/baker-http-dashboard/src/app/interactions/interactions.component.spec.ts diff --git a/bakery/dashboard/src/app/interactions/interactions.component.ts b/http/baker-http-dashboard/src/app/interactions/interactions.component.ts similarity index 100% rename from bakery/dashboard/src/app/interactions/interactions.component.ts rename to http/baker-http-dashboard/src/app/interactions/interactions.component.ts diff --git a/bakery/dashboard/src/app/interactions/interactions.css b/http/baker-http-dashboard/src/app/interactions/interactions.css similarity index 100% rename from bakery/dashboard/src/app/interactions/interactions.css rename to http/baker-http-dashboard/src/app/interactions/interactions.css diff --git a/bakery/dashboard/src/app/interactions/interactions.scss b/http/baker-http-dashboard/src/app/interactions/interactions.scss similarity index 100% rename from bakery/dashboard/src/app/interactions/interactions.scss rename to http/baker-http-dashboard/src/app/interactions/interactions.scss diff --git a/bakery/dashboard/src/app/notfound/notfound.component.html b/http/baker-http-dashboard/src/app/notfound/notfound.component.html similarity index 100% rename from bakery/dashboard/src/app/notfound/notfound.component.html rename to http/baker-http-dashboard/src/app/notfound/notfound.component.html diff --git a/bakery/dashboard/src/app/notfound/notfound.component.ts b/http/baker-http-dashboard/src/app/notfound/notfound.component.ts similarity index 100% rename from bakery/dashboard/src/app/notfound/notfound.component.ts rename to http/baker-http-dashboard/src/app/notfound/notfound.component.ts diff --git a/bakery/dashboard/src/app/notfound/notfound.css b/http/baker-http-dashboard/src/app/notfound/notfound.css similarity index 100% rename from bakery/dashboard/src/app/notfound/notfound.css rename to http/baker-http-dashboard/src/app/notfound/notfound.css diff --git a/bakery/dashboard/src/app/recipes/recipes.component.html b/http/baker-http-dashboard/src/app/recipes/recipes.component.html similarity index 100% rename from bakery/dashboard/src/app/recipes/recipes.component.html rename to http/baker-http-dashboard/src/app/recipes/recipes.component.html diff --git a/bakery/dashboard/src/app/recipes/recipes.component.spec.ts b/http/baker-http-dashboard/src/app/recipes/recipes.component.spec.ts similarity index 100% rename from bakery/dashboard/src/app/recipes/recipes.component.spec.ts rename to http/baker-http-dashboard/src/app/recipes/recipes.component.spec.ts diff --git a/bakery/dashboard/src/app/recipes/recipes.component.ts b/http/baker-http-dashboard/src/app/recipes/recipes.component.ts similarity index 100% rename from bakery/dashboard/src/app/recipes/recipes.component.ts rename to http/baker-http-dashboard/src/app/recipes/recipes.component.ts diff --git a/bakery/dashboard/src/app/recipes/recipes.scss b/http/baker-http-dashboard/src/app/recipes/recipes.scss similarity index 100% rename from bakery/dashboard/src/app/recipes/recipes.scss rename to http/baker-http-dashboard/src/app/recipes/recipes.scss diff --git a/bakery/dashboard/src/assets/.gitkeep b/http/baker-http-dashboard/src/assets/.gitkeep similarity index 100% rename from bakery/dashboard/src/assets/.gitkeep rename to http/baker-http-dashboard/src/assets/.gitkeep diff --git a/bakery/dashboard/src/assets/@hpcc-js/wasm/dist/graphvizlib.wasm b/http/baker-http-dashboard/src/assets/@hpcc-js/wasm/dist/graphvizlib.wasm similarity index 100% rename from bakery/dashboard/src/assets/@hpcc-js/wasm/dist/graphvizlib.wasm rename to http/baker-http-dashboard/src/assets/@hpcc-js/wasm/dist/graphvizlib.wasm diff --git a/bakery/dashboard/src/custom-theme.scss b/http/baker-http-dashboard/src/custom-theme.scss similarity index 100% rename from bakery/dashboard/src/custom-theme.scss rename to http/baker-http-dashboard/src/custom-theme.scss diff --git a/bakery/dashboard/src/environments/environment.prod.ts b/http/baker-http-dashboard/src/environments/environment.prod.ts similarity index 100% rename from bakery/dashboard/src/environments/environment.prod.ts rename to http/baker-http-dashboard/src/environments/environment.prod.ts diff --git a/bakery/dashboard/src/environments/environment.ts b/http/baker-http-dashboard/src/environments/environment.ts similarity index 100% rename from bakery/dashboard/src/environments/environment.ts rename to http/baker-http-dashboard/src/environments/environment.ts diff --git a/bakery/dashboard/src/favicon.ico b/http/baker-http-dashboard/src/favicon.ico similarity index 100% rename from bakery/dashboard/src/favicon.ico rename to http/baker-http-dashboard/src/favicon.ico diff --git a/bakery/dashboard/src/index.html b/http/baker-http-dashboard/src/index.html similarity index 100% rename from bakery/dashboard/src/index.html rename to http/baker-http-dashboard/src/index.html diff --git a/bakery/dashboard/src/main.ts b/http/baker-http-dashboard/src/main.ts similarity index 100% rename from bakery/dashboard/src/main.ts rename to http/baker-http-dashboard/src/main.ts diff --git a/bakery/dashboard/src/polyfills.ts b/http/baker-http-dashboard/src/polyfills.ts similarity index 100% rename from bakery/dashboard/src/polyfills.ts rename to http/baker-http-dashboard/src/polyfills.ts diff --git a/http/baker-http-dashboard/src/proxy.conf.json b/http/baker-http-dashboard/src/proxy.conf.json new file mode 100644 index 000000000..154f50676 --- /dev/null +++ b/http/baker-http-dashboard/src/proxy.conf.json @@ -0,0 +1,10 @@ +{ + // Will not work against https if self-signed. Use it to test against a non-https (e.g. localhost) baker-http-server. + // Sometimes failures will be cached. It can be helpful to test it in incognito mode in the browser. + "/api/bakery": { + "target": "http://localhost:8080", + "secure": false, + "logLevel": "debug", + "changeOrigin": true + } +} diff --git a/bakery/dashboard/src/styles.scss b/http/baker-http-dashboard/src/styles.scss similarity index 100% rename from bakery/dashboard/src/styles.scss rename to http/baker-http-dashboard/src/styles.scss diff --git a/bakery/dashboard/src/test.ts b/http/baker-http-dashboard/src/test.ts similarity index 100% rename from bakery/dashboard/src/test.ts rename to http/baker-http-dashboard/src/test.ts diff --git a/bakery/dashboard/tsconfig.app.json b/http/baker-http-dashboard/tsconfig.app.json similarity index 100% rename from bakery/dashboard/tsconfig.app.json rename to http/baker-http-dashboard/tsconfig.app.json diff --git a/bakery/dashboard/tsconfig.json b/http/baker-http-dashboard/tsconfig.json similarity index 100% rename from bakery/dashboard/tsconfig.json rename to http/baker-http-dashboard/tsconfig.json diff --git a/bakery/dashboard/tsconfig.spec.json b/http/baker-http-dashboard/tsconfig.spec.json similarity index 100% rename from bakery/dashboard/tsconfig.spec.json rename to http/baker-http-dashboard/tsconfig.spec.json diff --git a/http/baker-http-server/src/main/resources/reference.conf b/http/baker-http-server/src/main/resources/reference.conf new file mode 100644 index 000000000..ec1f8296d --- /dev/null +++ b/http/baker-http-server/src/main/resources/reference.conf @@ -0,0 +1,6 @@ +baker { + api-host = "0.0.0.0" + api-port = 8080 + api-url-prefix = "/api/bakery" + api-logging-enabled = false +} diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/RecipeLoader.scala b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/common/RecipeLoader.scala similarity index 91% rename from bakery/state/src/main/scala/com/ing/bakery/baker/RecipeLoader.scala rename to http/baker-http-server/src/main/scala/com/ing/baker/http/server/common/RecipeLoader.scala index 537221ae4..ef0be97ab 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/RecipeLoader.scala +++ b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/common/RecipeLoader.scala @@ -1,4 +1,4 @@ -package com.ing.bakery.baker +package com.ing.baker.http.server.common import cats.effect.{ContextShift, IO, Timer} import cats.implicits._ @@ -20,9 +20,9 @@ import scala.util.Try object RecipeLoader extends LazyLogging { - def pollRecipesUpdates(path: String, bakery: Bakery, duration: FiniteDuration) + def pollRecipesUpdates(path: String, baker: Baker, duration: FiniteDuration) (implicit timer: Timer[IO], cs: ContextShift[IO]): IO[Unit] = { - def pollRecipes: IO[Unit] = loadRecipesIntoBaker(path, bakery.baker) >> IO.sleep(duration) >> IO.defer(pollRecipes) + def pollRecipes: IO[Unit] = loadRecipesIntoBaker(path, baker) >> IO.sleep(duration) >> IO.defer(pollRecipes) pollRecipes } @@ -30,7 +30,7 @@ object RecipeLoader extends LazyLogging { def loadRecipesIntoBaker(path: String, baker: Baker)(implicit cs: ContextShift[IO]): IO[Unit] = for { recipes <- RecipeLoader.loadRecipes(path) - _ <- recipes.traverse { record => + _ <- recipes.traverse { record => IO.fromFuture(IO(baker.addRecipe(record))) } } yield () @@ -51,7 +51,7 @@ object RecipeLoader extends LazyLogging { bytes } - private[baker] def loadRecipes(path: String): IO[List[RecipeRecord]] = { + def loadRecipes(path: String): IO[List[RecipeRecord]] = { def recipeFiles(path: String): IO[List[File]] = IO { val d = new File(path) diff --git a/http/baker-http-server/src/main/scala/com/ing/baker/http/server/javadsl/BakerWithHttpResponse.scala b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/javadsl/BakerWithHttpResponse.scala new file mode 100644 index 000000000..f2eb1eb4f --- /dev/null +++ b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/javadsl/BakerWithHttpResponse.scala @@ -0,0 +1,110 @@ +package com.ing.baker.http.server.javadsl + +import com.ing.baker.http.server.common.RecipeLoader +import com.ing.baker.runtime.common.BakerException +import com.ing.baker.runtime.scaladsl.{Baker, BakerResult, EncodedRecipe, EventInstance} +import com.ing.baker.runtime.serialization.JsonDecoders._ +import com.ing.baker.runtime.serialization.JsonEncoders._ +import com.typesafe.scalalogging.LazyLogging +import io.circe.Encoder +import io.circe.generic.auto._ +import io.circe.parser.parse + +import java.nio.charset.Charset +import java.util.concurrent.{CompletableFuture => JFuture} +import java.util.{Optional, UUID} +import scala.compat.java8.FutureConverters.FutureOps +import scala.concurrent.{ExecutionContext, Future} + +/** + * A wrapper around baker which calls the specified baker instance, and returns the BakerResult according to the bakery protocol. + * Useful when making your own controller. + * + * @param baker baker methods to wrap + * @param ec execution context to use + */ +class BakerWithHttpResponse(val baker: Baker, ec: ExecutionContext) extends LazyLogging { + implicit val executionContext: ExecutionContext = ec + + def appGetAllInteractions: JFuture[String] = baker.getAllInteractions.toBakerResult + + def appGetInteraction(interactionName: String): JFuture[String] = baker.getInteraction(interactionName).toBakerResult + + def appAddRecipe(recipe: String): JFuture[String] = { + (for { + json <- parse(recipe).toOption + encodedRecipe <- json.as[EncodedRecipe].toOption + } yield RecipeLoader.fromBytes(encodedRecipe.base64.getBytes(Charset.forName("UTF-8"))).unsafeToFuture()) + .map(_.flatMap(recipe => baker.addRecipe(recipe, validate = false).toBakerResultScalaFuture)) + .getOrElse(Future.failed(new IllegalStateException("Error adding recipe"))) + }.toJava.toCompletableFuture + + def appGetRecipe(recipeId: String): JFuture[String] = baker.getRecipe(recipeId).toBakerResult + + def appGetAllRecipes: JFuture[String] = baker.getAllRecipes.toBakerResult + + def appGetVisualRecipe(recipeId: String): JFuture[String] = baker.getRecipeVisual(recipeId).toBakerResult + + def bake(recipeId: String, recipeInstanceId: String): JFuture[String] = baker.bake(recipeId, recipeInstanceId).toBakerResult + + /** + * Do calls for a specific instance. + */ + def instance(recipeInstanceId: String) : InstanceResponseMapper = new InstanceResponseMapper(recipeInstanceId) + + class InstanceResponseMapper(recipeInstanceId: String) { + def get(): JFuture[String] = baker.getRecipeInstanceState(recipeInstanceId).toBakerResult + + def getEvents: JFuture[String] = baker.getEvents(recipeInstanceId).toBakerResult + + def getIngredients: JFuture[String] = baker.getIngredients(recipeInstanceId).toBakerResult + + def getVisual: JFuture[String] = baker.getVisualState(recipeInstanceId).toBakerResult + + def fireAndResolveWhenReceived(eventJson: String, maybeCorrelationId: Optional[String]): JFuture[String] = + parseEventAndExecute(eventJson, baker.fireEventAndResolveWhenReceived(recipeInstanceId, _, toOption(maybeCorrelationId))) + + def fireAndResolveWhenCompleted(eventJson: String, maybeCorrelationId: Optional[String]): JFuture[String] = + parseEventAndExecute(eventJson, baker.fireEventAndResolveWhenCompleted(recipeInstanceId, _, toOption(maybeCorrelationId))) + + def fireAndResolveOnEvent(eventJson: String, event: String, maybeCorrelationId: Optional[String]): JFuture[String] = + parseEventAndExecute(eventJson, baker.fireEventAndResolveOnEvent(recipeInstanceId, _, event, toOption(maybeCorrelationId))) + + def retryInteraction(interactionName: String): JFuture[String] = + baker.retryInteraction(recipeInstanceId, interactionName).toBakerResult + + def stopRetryingInteraction(interactionName: String): JFuture[String] = + baker.stopRetryingInteraction(recipeInstanceId, interactionName).toBakerResult + + def resolveInteraction(interactionName: String, eventJson: String): JFuture[String] = + parseEventAndExecute(eventJson, baker.resolveInteraction(recipeInstanceId, interactionName, _)) + } + + private def toOption[T](opt: Optional[T]): Option[T] = if (opt.isPresent) Some(opt.get()) else None + + private def parseEventAndExecute[A](eventJson: String, f: EventInstance => Future[A])(implicit encoder: Encoder[A]): JFuture[String] = (for { + json <- parse(eventJson) + eventInstance <- json.as[EventInstance] + } yield { + f(eventInstance).toBakerResultScalaFuture + }).getOrElse(Future.failed(new IllegalArgumentException("Can't process event"))).toJava.toCompletableFuture + + private implicit class BakerResultHelperJavaFuture[A](f: => Future[A])(implicit encoder: Encoder[A]) { + def toBakerResult: JFuture[String] = f.toBakerResultScalaFuture.toJava.toCompletableFuture + } + + private implicit class BakerResultHelperScalaFuture[A](f: => Future[A])(implicit encoder: Encoder[A]) { + def toBakerResultScalaFuture(implicit encoder: Encoder[A]): Future[String] = { + f.map { + case () => BakerResult.Ack + case a => BakerResult(a) + }.recover { + case e: BakerException => BakerResult(e) + case e: Throwable => + val errorId = UUID.randomUUID().toString + logger.error(s"Unexpected exception happened when calling Baker (id='$errorId').", e) + BakerResult(BakerException.UnexpectedException(errorId)) + }.map(bakerResultEncoder.apply(_).noSpaces) + } + } +} diff --git a/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServer.scala b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServer.scala new file mode 100644 index 000000000..22fc6e4b1 --- /dev/null +++ b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServer.scala @@ -0,0 +1,280 @@ +package com.ing.baker.http.server.scaladsl + +import cats.data.OptionT +import cats.effect.{Blocker, ContextShift, IO, Resource, Sync, Timer} +import cats.implicits._ +import com.ing.baker.http.{Dashboard, DashboardConfiguration} +import com.ing.baker.http.server.common.RecipeLoader +import com.ing.baker.runtime.common.BakerException +import com.ing.baker.runtime.scaladsl.{Baker, BakerResult, EncodedRecipe, EventInstance} +import com.ing.baker.runtime.javadsl.{Baker => JBaker} +import com.ing.baker.runtime.serialization.InteractionExecution +import com.ing.baker.runtime.serialization.InteractionExecutionJsonCodecs._ +import com.ing.baker.runtime.serialization.JsonDecoders._ +import com.ing.baker.runtime.serialization.JsonEncoders._ +import com.typesafe.config.{Config, ConfigFactory} +import com.typesafe.scalalogging.LazyLogging +import io.circe._ +import io.circe.generic.auto._ +import io.prometheus.client.CollectorRegistry +import org.http4s._ +import org.http4s.circe._ +import org.http4s.dsl.io._ +import org.http4s.headers.{`Content-Length`, `Content-Type`} +import org.http4s.implicits._ +import org.http4s.metrics.MetricsOps +import org.http4s.metrics.prometheus.Prometheus +import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.server.middleware.{CORS, Logger, Metrics} +import org.http4s.server.{Router, Server} +import org.slf4j.LoggerFactory + +import java.io.Closeable +import java.net.InetSocketAddress +import java.nio.charset.Charset +import java.util.concurrent.CompletableFuture +import scala.compat.java8.FutureConverters +import scala.concurrent.duration.DurationInt +import scala.concurrent.{ExecutionContext, Future} + +object Http4sBakerServer { + + def resource(baker: Baker, ec: ExecutionContext, hostname: InetSocketAddress, apiUrlPrefix: String, + dashboardConfiguration: DashboardConfiguration, loggingEnabled: Boolean) + (implicit sync: Sync[IO], cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, Server[IO]] = { + + + val apiLoggingAction: Option[String => IO[Unit]] = if (loggingEnabled) { + val apiLogger = LoggerFactory.getLogger("API") + Some(s => IO(apiLogger.info(s))) + } else None + + for { + metrics <- Prometheus.metricsOps[IO](CollectorRegistry.defaultRegistry, "http_api") + blocker <- Blocker[IO] + server <- BlazeServerBuilder[IO](ec) + .bindSocketAddress(hostname) + .withHttpApp( + CORS.policy + .withAllowOriginAll + .withAllowCredentials(true) + .withMaxAge(1.day)( + Logger.httpApp( + logHeaders = loggingEnabled, + logBody = loggingEnabled, + logAction = apiLoggingAction)( + routes(baker, apiUrlPrefix, metrics, dashboardConfiguration, blocker).orNotFound))) + .resource + } yield server + } + + def resource(baker: Baker, + http4sBakerServerConfiguration: Http4sBakerServerConfiguration, + dashboardConfiguration: DashboardConfiguration, + ec: ExecutionContext = ExecutionContext.global) + (implicit sync: Sync[IO], cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, Server[IO]] = + resource(baker, ec, + hostname = InetSocketAddress.createUnresolved(http4sBakerServerConfiguration.apiHost, http4sBakerServerConfiguration.apiPort), + apiUrlPrefix = http4sBakerServerConfiguration.apiUrlPrefix, + dashboardConfiguration = dashboardConfiguration, + loggingEnabled = http4sBakerServerConfiguration.loggingEnabled + ) + + def java(baker: JBaker, + http4sBakerServerConfiguration: Http4sBakerServerConfiguration, + dashboardConfiguration: DashboardConfiguration, + ): CompletableFuture[ClosableBakerServer] = { + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) + implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + val serverStarted = resource(baker.getScalaBaker, http4sBakerServerConfiguration, dashboardConfiguration) + .allocated + .unsafeToFuture() + .map { + case (server: Server[IO], closeEffect: IO[Unit]) => new ClosableBakerServer(server, closeEffect) + }(ExecutionContext.global) + FutureConverters.toJava(serverStarted).toCompletableFuture + } + + def java(baker: JBaker): CompletableFuture[ClosableBakerServer] = { + val config : Config = ConfigFactory.load() + java(baker, Http4sBakerServerConfiguration.fromConfig(config), DashboardConfiguration.fromConfig(config)) + } + + class ClosableBakerServer(val server : Server[IO], closeEffect : IO[Unit]) extends Closeable { + override def close(): Unit = closeEffect.unsafeRunSync() + } + def routes(baker: Baker, apiUrlPrefix: String, metrics: MetricsOps[IO], + dashboardConfiguration: DashboardConfiguration, blocker: Blocker) + (implicit sync: Sync[IO], cs: ContextShift[IO], timer: Timer[IO]): HttpRoutes[IO] = { + val dashboardRoutesOrEmpty: HttpRoutes[IO] = + if (dashboardConfiguration.enabled) dashboardRoutes(apiUrlPrefix, dashboardConfiguration, blocker) + else HttpRoutes.empty + + new Http4sBakerServer(baker).routesWithPrefixAndMetrics(apiUrlPrefix, metrics) <+> dashboardRoutesOrEmpty + } + + + private def dashboardRoutes(apiUrlPrefix: String, dashboardConfiguration: DashboardConfiguration, blocker: Blocker) + (implicit sync: Sync[IO], cs: ContextShift[IO]): HttpRoutes[IO] = + HttpRoutes.of[IO] { + case GET -> Root / "dashboard_config" => + val bodyText = Dashboard.dashboardConfigJson(apiUrlPrefix, dashboardConfiguration) + IO(Response[IO]( + status = Ok, + body = fs2.Stream(bodyText).through(fs2.text.utf8Encode), + headers = Headers( + `Content-Type`(MediaType.text.plain, org.http4s.Charset.`UTF-8`), + `Content-Length`.unsafeFromLong(bodyText.length) + ) + )) + //TODO: Change to Dashboard.indexPattern.matches(req.pathInfo) once support for scala_2.12 is removed. + case req if req.method == GET && req.pathInfo.matches(Dashboard.indexPattern.regex) => dashboardFile(req, blocker, "index.html").getOrElseF(NotFound()) + case req if req.method == GET && Dashboard.files.contains(req.pathInfo.substring(1)) => + dashboardFile(req, blocker, req.pathInfo.substring(1)).getOrElseF(NotFound()) + } + + private def dashboardFile(request: Request[IO], blocker: Blocker, filename: String) + (implicit sync: Sync[IO], cs: ContextShift[IO]): OptionT[IO, Response[IO]] = { + OptionT.fromOption(Dashboard.safeGetResourcePath(filename))(sync) + .flatMap(resourcePath => StaticFile.fromResource(resourcePath, blocker, Some(request))) + } +} + +final class Http4sBakerServer private(baker: Baker)(implicit cs: ContextShift[IO]) extends LazyLogging { + + object CorrelationId extends OptionalQueryParamDecoderMatcher[String]("correlationId") + + private class RegExpValidator(regexp: String) { + def unapply(str: String): Option[String] = if (str.matches(regexp)) Some(str) else None + } + + private object RecipeId extends RegExpValidator("[A-Za-z0-9]+") + + private object RecipeInstanceId extends RegExpValidator("[A-Za-z0-9-]+") + + private object InteractionName extends RegExpValidator("[A-Za-z0-9_]+") + + implicit val recipeDecoder: EntityDecoder[IO, EncodedRecipe] = jsonOf[IO, EncodedRecipe] + + implicit val eventInstanceDecoder: EntityDecoder[IO, EventInstance] = jsonOf[IO, EventInstance] + implicit val interactionExecutionRequestDecoder: EntityDecoder[IO, InteractionExecution.ExecutionRequest] = jsonOf[IO, InteractionExecution.ExecutionRequest] + implicit val bakerResultEntityEncoder: EntityEncoder[IO, BakerResult] = jsonEncoderOf[IO, BakerResult] + + def routesWithPrefixAndMetrics(apiUrlPrefix: String, metrics: MetricsOps[IO]) + (implicit timer: Timer[IO]): HttpRoutes[IO] = + Router( + apiUrlPrefix -> Metrics[IO](metrics, classifierF = metricsClassifier(apiUrlPrefix))(routes), + ) + + def routes: HttpRoutes[IO] = app <+> instance + + private def app: HttpRoutes[IO] = Router("/app" -> + HttpRoutes.of[IO] { + case GET -> Root / "health" => Ok() + + case GET -> Root / "interactions" => baker.getAllInteractions.toBakerResultResponseIO + + case GET -> Root / "interactions" / InteractionName(name) => baker.getInteraction(name).toBakerResultResponseIO + + case req@POST -> Root / "interactions" / "execute" => + for { + executionRequest <- req.as[InteractionExecution.ExecutionRequest] + result <- + IO.fromFuture(IO(baker.executeSingleInteraction(executionRequest.id, executionRequest.ingredients))) + .map(_.toSerializationInteractionExecutionResult) + .toBakerResultResponseIO + } yield result + + case req@POST -> Root / "recipes" => + for { + encodedRecipe <- req.as[EncodedRecipe] + recipe <- RecipeLoader.fromBytes(encodedRecipe.base64.getBytes(Charset.forName("UTF-8"))) + result <- baker.addRecipe(recipe, validate = true).toBakerResultResponseIO + } yield result + + case GET -> Root / "recipes" => baker.getAllRecipes.toBakerResultResponseIO + + case GET -> Root / "recipes" / RecipeId(recipeId) => baker.getRecipe(recipeId).toBakerResultResponseIO + + case GET -> Root / "recipes" / RecipeId(recipeId) / "visual" => baker.getRecipeVisual(recipeId).toBakerResultResponseIO + }) + + private def instance: HttpRoutes[IO] = Router("/instances" -> HttpRoutes.of[IO] { + + case GET -> Root => baker.getAllRecipeInstancesMetadata.toBakerResultResponseIO + + case GET -> Root / RecipeInstanceId(recipeInstanceId) => baker.getRecipeInstanceState(recipeInstanceId).toBakerResultResponseIO + + case GET -> Root / RecipeInstanceId(recipeInstanceId) / "events" => baker.getEvents(recipeInstanceId).toBakerResultResponseIO + + case GET -> Root / RecipeInstanceId(recipeInstanceId) / "ingredients" => baker.getIngredients(recipeInstanceId).toBakerResultResponseIO + + case GET -> Root / RecipeInstanceId(recipeInstanceId) / "visual" => baker.getVisualState(recipeInstanceId).toBakerResultResponseIO + + case POST -> Root / RecipeInstanceId(recipeInstanceId) / "bake" / RecipeId(recipeId) => baker.bake(recipeId, recipeInstanceId).toBakerResultResponseIO + + case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "fire-and-resolve-when-received" :? CorrelationId(maybeCorrelationId) => + for { + event <- req.as[EventInstance] + result <- baker.fireEventAndResolveWhenReceived(recipeInstanceId, event, maybeCorrelationId).toBakerResultResponseIO + } yield result + + case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "fire-and-resolve-when-completed" :? CorrelationId(maybeCorrelationId) => + for { + event <- req.as[EventInstance] + result <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, event, maybeCorrelationId).toBakerResultResponseIO + } yield result + + case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "fire-and-resolve-on-event" / onEvent :? CorrelationId(maybeCorrelationId) => + for { + event <- req.as[EventInstance] + result <- baker.fireEventAndResolveOnEvent(recipeInstanceId, event, onEvent, maybeCorrelationId).toBakerResultResponseIO + } yield result + + case POST -> Root / RecipeInstanceId(recipeInstanceId) / "interaction" / InteractionName(interactionName) / "retry" => + for { + result <- baker.retryInteraction(recipeInstanceId, interactionName).toBakerResultResponseIO + } yield result + + case POST -> Root / RecipeInstanceId(recipeInstanceId) / "interaction" / InteractionName(interactionName) / "stop-retrying" => + for { + result <- baker.stopRetryingInteraction(recipeInstanceId, interactionName).toBakerResultResponseIO + } yield result + + case req@POST -> Root / RecipeInstanceId(recipeInstanceId) / "interaction" / InteractionName(interactionName) / "resolve" => + for { + event <- req.as[EventInstance] + result <- baker.resolveInteraction(recipeInstanceId, interactionName, event).toBakerResultResponseIO + } yield result + }) + + def metricsClassifier(apiUrlPrefix: String): Request[IO] => Option[String] = { request => + val uriPath = request.uri.path + val p = uriPath.takeRight(uriPath.length - apiUrlPrefix.length) + + if (p.startsWith("/app")) Some(p) // cardinality is low, we don't care + else if (p.startsWith("/instances")) { + val action = p.split('/') // /instances///... - we don't want ID here + if (action.length >= 4) Some(s"/instances/${action(3)}") else Some("/instances/state") + } else None + } + + + private implicit class BakerResultFutureHelper[A](f: => Future[A]) { + def toBakerResultResponseIO(implicit encoder: Encoder[A]): IO[Response[IO]] = + IO.fromFuture(IO(f)).toBakerResultResponseIO + } + + private implicit class BakerResultIOHelper[A](io: => IO[A]) { + def toBakerResultResponseIO(implicit encoder: Encoder[A]): IO[Response[IO]] = + io.attempt.flatMap { + case Left(e: BakerException) => Ok(BakerResult(e)) + case Left(e) => + logger.error(s"Unexpected exception happened when calling Baker", e) + InternalServerError(s"No other exception but BakerExceptions should be thrown here: ${e.getCause}") + case Right(()) => Ok(BakerResult.Ack) + case Right(a) => Ok(BakerResult(a)) + } + } + +} diff --git a/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerConfiguration.scala b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerConfiguration.scala new file mode 100644 index 000000000..99e425dad --- /dev/null +++ b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerConfiguration.scala @@ -0,0 +1,21 @@ +package com.ing.baker.http.server.scaladsl + +import com.typesafe.config.Config + +case class Http4sBakerServerConfiguration(apiHost: String, + apiPort: Int, + apiUrlPrefix: String, + loggingEnabled: Boolean) + +object Http4sBakerServerConfiguration { + def fromConfig(config: Config) : Http4sBakerServerConfiguration = { + val bakerConfig = config.getConfig("baker") + + Http4sBakerServerConfiguration( + apiHost = bakerConfig.getString("api-host"), + apiPort = bakerConfig.getInt("api-port"), + apiUrlPrefix = bakerConfig.getString("api-url-prefix"), + loggingEnabled = bakerConfig.getBoolean("api-logging-enabled") + ) + } +} diff --git a/bakery/state/src/test/resources/recipes/ItemReservation.recipe b/http/baker-http-server/src/test/resources/recipes/ItemReservation.recipe similarity index 100% rename from bakery/state/src/test/resources/recipes/ItemReservation.recipe rename to http/baker-http-server/src/test/resources/recipes/ItemReservation.recipe diff --git a/bakery/state/src/test/resources/recipes/ItemReservationBlocking.recipe b/http/baker-http-server/src/test/resources/recipes/ItemReservationBlocking.recipe similarity index 100% rename from bakery/state/src/test/resources/recipes/ItemReservationBlocking.recipe rename to http/baker-http-server/src/test/resources/recipes/ItemReservationBlocking.recipe diff --git a/bakery/state/src/test/scala/com/ing/bakery/baker/RecipeLoaderSpec.scala b/http/baker-http-server/src/test/scala/com/ing/baker/http/server/common/RecipeLoaderSpec.scala similarity index 98% rename from bakery/state/src/test/scala/com/ing/bakery/baker/RecipeLoaderSpec.scala rename to http/baker-http-server/src/test/scala/com/ing/baker/http/server/common/RecipeLoaderSpec.scala index a72e211de..f3a697582 100644 --- a/bakery/state/src/test/scala/com/ing/bakery/baker/RecipeLoaderSpec.scala +++ b/http/baker-http-server/src/test/scala/com/ing/baker/http/server/common/RecipeLoaderSpec.scala @@ -1,8 +1,5 @@ -package com.ing.bakery.baker +package com.ing.baker.http.server.common -import java.io.{File, FileInputStream} -import java.nio.file.{Files, Paths} -import java.util.Base64 import com.ing.baker.compiler.RecipeCompiler import com.ing.baker.il.CompiledRecipe import com.ing.baker.recipe.annotations.{FiresEvent, RecipeInstanceId, RequiresIngredient} @@ -15,6 +12,9 @@ import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import java.io.File +import java.nio.file.{Files, Paths} +import java.util.Base64 import scala.concurrent.duration._ class RecipeLoaderSpec extends AnyFunSuite with Matchers with BeforeAndAfterAll { diff --git a/http/baker-http-server/src/test/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerConfigurationSpec.scala b/http/baker-http-server/src/test/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerConfigurationSpec.scala new file mode 100644 index 000000000..4f7f31204 --- /dev/null +++ b/http/baker-http-server/src/test/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerConfigurationSpec.scala @@ -0,0 +1,17 @@ +package com.ing.baker.http.server.scaladsl + +import com.typesafe.config.ConfigFactory +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should + +class Http4sBakerServerConfigurationSpec extends AnyFlatSpec with should.Matchers { + + "Http4sBakerServiceConfiguration" should "load by default" in { + Http4sBakerServerConfiguration.fromConfig(ConfigFactory.load()) shouldBe Http4sBakerServerConfiguration( + "0.0.0.0", + 8080, + "/api/bakery", + loggingEnabled = false + ) + } +} diff --git a/http/baker-http-server/src/test/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerSpec.scala b/http/baker-http-server/src/test/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerSpec.scala new file mode 100644 index 000000000..8a9420ba4 --- /dev/null +++ b/http/baker-http-server/src/test/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServerSpec.scala @@ -0,0 +1,99 @@ +package com.ing.baker.http.server.scaladsl + +import cats.effect.testing.scalatest.AsyncIOSpec +import cats.effect.{Blocker, IO, Resource} +import com.ing.baker.http.DashboardConfiguration +import com.ing.baker.runtime.scaladsl.Baker +import io.prometheus.client.CollectorRegistry +import org.http4s.headers.{`Content-Length`, `Content-Type`} +import org.http4s.implicits._ +import org.http4s.metrics.MetricsOps +import org.http4s.metrics.prometheus.Prometheus +import org.http4s._ +import org.mockito.Mockito.{times, verify, when} +import org.mockito.MockitoSugar.mock +import org.scalatest.flatspec.AsyncFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.concurrent.Future + +class Http4sBakerServerSpec extends AsyncFlatSpec with AsyncIOSpec with Matchers{ + + val bakerMock: Baker = mock[Baker] + + val dashboardConfigurationEnabled: DashboardConfiguration = + DashboardConfiguration(enabled = true, applicationName = "TestCluster", clusterInformation = Map.empty) + val dashboardConfigurationDisabled: DashboardConfiguration = + dashboardConfigurationEnabled.copy(enabled = false) + + def check[A](actual: IO[Response[IO]], + expectedStatus: Status, + expectedBody: Option[A])( + implicit ev: EntityDecoder[IO, A] + ): Boolean = { + val actualResp = actual.unsafeRunSync() + val statusCheck = actualResp.status == expectedStatus + val bodyCheck = expectedBody.fold[Boolean]( + // Verify Response's body is empty. + actualResp.body.compile.toVector.unsafeRunSync().isEmpty)( + expected => actualResp.as[A].unsafeRunSync() == expected + ) + statusCheck && bodyCheck + } + + private def routes(metrics: MetricsOps[IO], + blocker: Blocker, + dashboardConfiguration: DashboardConfiguration): HttpRoutes[IO] = Http4sBakerServer.routes( + baker = bakerMock, + apiUrlPrefix = "/api/test", + metrics = metrics, + dashboardConfiguration = dashboardConfiguration, + blocker = blocker + ) + + private def doRequest(request : Request[IO], + dashboardConfiguration: DashboardConfiguration = dashboardConfigurationEnabled) : Response[IO] = { + val routesResource: Resource[IO, HttpRoutes[IO]] = for { + metrics <- Prometheus.metricsOps[IO](CollectorRegistry.defaultRegistry, "test") + blocker <- Blocker[IO] + } yield routes(metrics, blocker, dashboardConfiguration) + + routesResource.use(_.orNotFound.run(request)).unsafeRunSync() + } + + "the routes" should "give 404 for non-existent urls" in { + val response = doRequest(Request(method = Method.GET, uri = uri"/non-existent")) + response.status shouldEqual Status.NotFound + } + + "the routes" should "serve the dashboard index file if dashboard is enabled" in { + val response = doRequest(Request(method = Method.GET, uri = uri"/")) + response.status shouldEqual Status.Ok + response.headers.get(`Content-Type`).toString shouldEqual "Some(Content-Type: text/html)" + } + + "the routes" should "serve the other static files if dashboard is enabled" in { + val response = doRequest(Request(method = Method.GET, uri = uri"/main.js")) + response.status shouldEqual Status.Ok + response.headers.get(`Content-Type`).toString shouldEqual "Some(Content-Type: application/javascript)" + } + + "the routes" should "give 404 if dashboard is disabled" in { + val response = doRequest(Request(method = Method.GET, uri = uri"/"), dashboardConfigurationDisabled) + response.status shouldEqual Status.NotFound + } + + "the routes" should "give dashboard_config" in { + val response = doRequest(Request(method = Method.GET, uri = uri"/dashboard_config")) + response.status shouldEqual Status.Ok + response.headers.get(`Content-Length`).toString shouldEqual "Some(Content-Length: 104)" + } + + "the routes" should "call the underlying baker implementation" in { + when(bakerMock.getAllInteractions).thenReturn(Future.successful(List.empty)) + val response = doRequest(Request(method = Method.GET, uri = uri"/api/test/app/interactions")) + verify(bakerMock, times(1)).getAllInteractions + response.status shouldEqual Status.Ok + } + +} diff --git a/project/BuildInteractionDockerImageSBTPlugin.scala b/project/BuildInteractionDockerImageSBTPlugin.scala index a6d50af10..a2f372a5f 100644 --- a/project/BuildInteractionDockerImageSBTPlugin.scala +++ b/project/BuildInteractionDockerImageSBTPlugin.scala @@ -161,6 +161,7 @@ object BuildInteractionDockerImageSBTPlugin extends sbt.AutoPlugin { |import org.springframework.context.annotation.AnnotationConfigApplicationContext | |import scala.collection.JavaConverters._ + |import scala.annotation.nowarn |import scala.concurrent.ExecutionContext.Implicits.global | |/** @@ -168,7 +169,7 @@ object BuildInteractionDockerImageSBTPlugin extends sbt.AutoPlugin { | */ |object Main extends App with LazyLogging{ | - | + | @nowarn | def getImplementations(configurationClassString: String) : List[InteractionInstance] = { | val configClass = Class.forName(configurationClassString) | logger.info("Class found: " + configClass) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 976efdcc7..b88b5ec6b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -27,7 +27,7 @@ object Dependencies { val scalaJava8Compat100 = "org.scala-lang.modules" %% "scala-java8-compat" % "1.0.0" val scalaJava8Compat091 = "org.scala-lang.modules" %% "scala-java8-compat" % "0.9.1" - val scalaTest = "org.scalatest" %% "scalatest" % "3.2.11" + val scalaTest = "org.scalatest" %% "scalatest" % "3.2.12" val mockitoScala = "org.mockito" %% "mockito-scala" % mockitoScalaVersion val mockitoScalaTest = "org.mockito" %% "mockito-scala-scalatest" % mockitoScalaVersion val mockServer = "org.mock-server" % "mockserver-netty" % "5.13.2" @@ -98,6 +98,7 @@ object Dependencies { val circeGenericExtras = "io.circe" %% "circe-generic-extras" % circeVersion val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion + val catsEffectTesting = "com.codecommit" %% "cats-effect-testing-scalatest" % "0.5.4" val catsCore = "org.typelevel" %% "cats-core" % catsCoreVersion val console4Cats = "dev.profunktor" %% "console4cats" % "0.8.0" val catsRetry = "com.github.cb372" %% "cats-retry" % "2.1.1" diff --git a/project/Publish.scala b/project/Publish.scala index 774e4dd40..d4704b6bb 100644 --- a/project/Publish.scala +++ b/project/Publish.scala @@ -16,7 +16,7 @@ object Publish { val SuppressJavaDocsAndSources = Seq( doc / sources := Seq(), packageDoc / publishArtifact := false, - packageSrc / publishArtifact := false + packageSrc / publishArtifact := true ) val StableToAzureFeed = Seq( @@ -84,4 +84,4 @@ object Publish { pushChanges ) ) -} \ No newline at end of file +} diff --git a/version.sbt b/version.sbt index ef3dcf062..8d0e33a2a 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "3.5.1-SNAPSHOT" \ No newline at end of file +ThisBuild / version := "3.6.0-SNAPSHOT"