From 4f4d18829bb7de3ff1c229f2c2906a5c33f14c5d Mon Sep 17 00:00:00 2001 From: akshayshirahatti-da <86774832+akshayshirahatti-da@users.noreply.github.com> Date: Thu, 6 Jan 2022 00:12:47 +0000 Subject: [PATCH] [Trigger-Service] Changes to use a typeconfig conf for trigger-service when provided. (#12217) * Changes to add the option of starting trigger service with typeconf/HOCON config CHANGELOG_BEGIN CHANGELOG_END * add tests for authorization config and fail on both config file and cli args * refactor and cleanup config loading and tests * Changes based on code review comments * Daml doc changes and making sure that we have defaults for most fields to mirror cli args CHANGELOG_BEGIN Trigger Service can now be configured with HOCON config file. - If a config file is provided we will choose to start the service using that, else we will fallback to cli arguments. - If both config file and cli args are provided we will error out. CHANGELOG_END * addressing some more code review comments * use scalatest inside properly --- docs/source/tools/trigger-service/index.rst | 100 ++++- .../com/daml/http/dbbackend/JdbcConfig.scala | 4 - .../scala/com/digitalasset/http/CliSpec.scala | 12 +- .../http/dbbackend/Connection.scala | 4 +- .../http/dbbackend/DBConfig.scala | 15 +- triggers/service/BUILD.bazel | 10 + .../daml/lf/engine/trigger/Cli.scala | 371 ++++++++++++++++++ .../lf/engine/trigger/ServiceConfig.scala | 241 +----------- .../daml/lf/engine/trigger/ServiceMain.scala | 2 +- .../trigger/TriggerServiceAppConf.scala | 138 +++++++ .../resources/trigger-service-minimal.conf | 6 + .../test-suite/resources/trigger-service.conf | 81 ++++ .../trigger/AuthorizationConfigTest.scala | 65 +++ ...ceConfigTest.scala => CliConfigTest.scala} | 4 +- .../com/daml/lf/engine/trigger/CliSpec.scala | 124 ++++++ .../trigger/TriggerServiceFixture.scala | 12 +- 16 files changed, 923 insertions(+), 266 deletions(-) create mode 100644 triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/Cli.scala create mode 100644 triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceAppConf.scala create mode 100644 triggers/service/src/test-suite/resources/trigger-service-minimal.conf create mode 100644 triggers/service/src/test-suite/resources/trigger-service.conf create mode 100644 triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/AuthorizationConfigTest.scala rename triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/{ServiceConfigTest.scala => CliConfigTest.scala} (97%) create mode 100644 triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/CliSpec.scala diff --git a/docs/source/tools/trigger-service/index.rst b/docs/source/tools/trigger-service/index.rst index a4bece5ecf..1ad2a0dcc6 100644 --- a/docs/source/tools/trigger-service/index.rst +++ b/docs/source/tools/trigger-service/index.rst @@ -23,14 +23,110 @@ Starting the Trigger Service In this example, it is assumed there is a sandbox ledger running on port 6865 on localhost. +.. code-block:: bash + + daml trigger-service --config trigger-service.conf + +where the corresponding config would be + +The required config would look like + +.. code-block:: none + + { + //dar file containing the trigger + dar-paths = [ + "./my-app.dar" + ] + + //IP address that Trigger service listens on. Defaults to 127.0.0.1. + address = "127.0.0.1" + //Trigger service port number. Defaults to 8088. A port number of 0 will let the system pick an ephemeral port. Consider specifying `port-file` with port number 0. + port = 8088 + //port-file = "dummy-port-file" + + ledger-api { + address = "localhost" + port = 6865 + } + + //Optional max inbound message size in bytes. Defaults to 4194304. + max-inbound-message-size = 4194304 + //Minimum time interval before restarting a failed trigger. Defaults to 5 seconds. + min-restart-interval = 5s + //Maximum time interval between restarting a failed trigger. Defaults to 60 seconds. + max-restart-interval = 60s + //Optional max HTTP entity upload size in bytes. Defaults to 4194304. + max-http-entity-upload-size = 4194304 + //Optional HTTP entity upload timeout. Defaults to 60 seconds. + http-entity-upload-timeout = 60s + //Use static time or wall-clock, default is wall-clock time. + time-provider-type = "wall-clock" + //Compiler config type to use , default or dev mode + compiler-config = "default" + //TTL in seconds used for commands emitted by the trigger. Defaults to 30s. + ttl = 60s + + //Initialize database and terminate. + init-db = "false" + + //Do not abort if there are existing tables in the database schema. EXPERT ONLY. Defaults to false. + allow-existing-schema = "false" + + trigger-store { + user = "postgres" + password = "password" + driver = "org.postgresql.Driver" + url = "jdbc:postgresql://localhost:5432/test?&ssl=true" + + // prefix for table names to avoid collisions, empty by default + table-prefix = "foo" + + // max pool size for the database connection pool + pool-size = 12 + //specifies the min idle connections for database connection pool. + min-idle = 4 + //specifies the idle timeout for the database connection pool. + idle-timeout = 12s + //specifies the connection timeout for database connection pool. + connection-timeout = 90s + } + + authorization { + //Sets both the internal and external auth URIs. + //auth-common-uri = "https://oauth2/common-uri" + + // Auth Client to redirect to login , defaults to no + auth-redirect = "yes" + + //Sets the internal auth URIs (used by the trigger service to connect directly to the middleware). Overrides value set by auth-common + auth-internal-uri = "https://oauth2/internal-uri" + //Sets the external auth URI (the one returned to the browser). overrides value set by auth-common. + auth-external-uri = "https://oauth2/external-uri" + //URI to the auth login flow callback endpoint `/cb`. By default constructed from the incoming login request. + auth-callback-uri = "https://oauth2/callback-uri" + + //Optional max number of pending authorization requests. Defaults to 250. + max-pending-authorizations = 250 + //Optional authorization timeout, defaults to 60 seconds + authorization-timeout = 60s + } + } + +The above starts the Trigger Service using a number of default parameters. Most notably, the HTTP port the Trigger Service listens on which defaults to 8088. +The above config file should list all available parameters, their defaults and descriptions. + +The trigger-service can also be started using cli-args as shown below, one can execute the command ``daml trigger-service --help`` to find all available parameters. + +.. note:: Configuration file is the recommended way to run trigger-service, running via cli-args is now deprecated + + .. code-block:: bash daml trigger-service --ledger-host localhost \ --ledger-port 6865 \ --wall-clock-time -The above starts the Trigger Service using a number of default parameters. Most notably, the HTTP port the Trigger Service listens on which defaults to 8088. To see all of the available parameters, their defaults and descriptions, one can execute the command ``daml trigger-service --help``. - Although as we'll see, the Trigger Service exposes an endpoint for end-users to upload DAR files to the service it is sometimes convenient to start the service pre-configured with a specific DAR. To do this, the ``--dar`` option is provided. .. code-block:: bash diff --git a/ledger-service/http-json-cli/src/main/scala/com/daml/http/dbbackend/JdbcConfig.scala b/ledger-service/http-json-cli/src/main/scala/com/daml/http/dbbackend/JdbcConfig.scala index 364c6f05a4..71e1d9107e 100644 --- a/ledger-service/http-json-cli/src/main/scala/com/daml/http/dbbackend/JdbcConfig.scala +++ b/ledger-service/http-json-cli/src/main/scala/com/daml/http/dbbackend/JdbcConfig.scala @@ -21,10 +21,6 @@ private[http] object JdbcConfig extends dbutils.ConfigCompanion[JdbcConfig, DBConfig.JdbcConfigDefaults]("JdbcConfig") with StrictLogging { - final val MinIdle = 8 - final val IdleTimeout = 10000L // ms, minimum according to log, defaults to 600s - final val ConnectionTimeout = 5000L - implicit val showInstance: Show[JdbcConfig] = Show.shows { a => import a._, baseConfig._ s"JdbcConfig(driver=$driver, url=$url, user=$user, start-mode=$dbStartupMode)" diff --git a/ledger-service/http-json/src/test/scala/com/digitalasset/http/CliSpec.scala b/ledger-service/http-json/src/test/scala/com/digitalasset/http/CliSpec.scala index 9d94e9209d..604c5f0280 100644 --- a/ledger-service/http-json/src/test/scala/com/digitalasset/http/CliSpec.scala +++ b/ledger-service/http-json/src/test/scala/com/digitalasset/http/CliSpec.scala @@ -8,11 +8,13 @@ import org.scalatest.matchers.should.Matchers import com.daml.dbutils import com.daml.http.dbbackend.{DbStartupMode, JdbcConfig} +import scala.concurrent.duration._ + object CliSpec { private val poolSize = 10 private val minIdle = 4 - private val connectionTimeout = 5000L - private val idleTimeout = 1000L + private val connectionTimeout = 5000.millis + private val idleTimeout = 10000.millis private val tablePrefix = "foo" } final class CliSpec extends AnyFreeSpec with Matchers { @@ -40,7 +42,8 @@ final class CliSpec extends AnyFreeSpec with Matchers { ) val jdbcConfigString = "driver=org.postgresql.Driver,url=jdbc:postgresql://localhost:5432/test?&ssl=true,user=postgres,password=password," + - s"poolSize=$poolSize,minIdle=$minIdle,connectionTimeout=$connectionTimeout,idleTimeout=$idleTimeout,tablePrefix=$tablePrefix" + s"poolSize=$poolSize,minIdle=$minIdle,connectionTimeout=${connectionTimeout.toMillis}," + + s"idleTimeout=${idleTimeout.toMillis},tablePrefix=$tablePrefix" val sharedOptions = Seq("--ledger-host", "localhost", "--ledger-port", "6865", "--http-port", "7500") @@ -121,7 +124,8 @@ final class CliSpec extends AnyFreeSpec with Matchers { "DbStartupMode" - { val jdbcConfigShared = "driver=org.postgresql.Driver,url=jdbc:postgresql://localhost:5432/test?&ssl=true,user=postgres,password=password," + - s"poolSize=$poolSize,minIdle=$minIdle,connectionTimeout=$connectionTimeout,idleTimeout=$idleTimeout,tablePrefix=$tablePrefix" + s"poolSize=$poolSize,minIdle=$minIdle,connectionTimeout=${connectionTimeout.toMillis}," + + s"idleTimeout=${idleTimeout.toMillis},tablePrefix=$tablePrefix" "should get the CreateOnly startup mode from the string" in { val jdbcConfigString = s"$jdbcConfigShared,start-mode=create-only" diff --git a/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/Connection.scala b/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/Connection.scala index 73290ac28f..21414cbf42 100644 --- a/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/Connection.scala +++ b/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/Connection.scala @@ -63,9 +63,9 @@ object ConnectionPool { c.setUsername(user) c.setPassword(password) c.setMinimumIdle(jc.minIdle) - c.setConnectionTimeout(jc.connectionTimeout) + c.setConnectionTimeout(jc.connectionTimeout.toMillis) c.setMaximumPoolSize(poolSize) - c.setIdleTimeout(jc.idleTimeout) + c.setIdleTimeout(jc.idleTimeout.toMillis) new HikariDataSource(c) } } diff --git a/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/DBConfig.scala b/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/DBConfig.scala index 47277827aa..1628730afe 100644 --- a/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/DBConfig.scala +++ b/libs-scala/db-utils/src/main/scala/com/digitalasset/http/dbbackend/DBConfig.scala @@ -12,6 +12,7 @@ import scalaz.syntax.traverse._ import scalaz.{Show, StateT, \/} import java.io.File +import scala.concurrent.duration._ import scala.util.Try object DBConfig { @@ -28,8 +29,8 @@ final case class JdbcConfig( password: String, poolSize: Int, minIdle: Int = JdbcConfig.MinIdle, - connectionTimeout: Long = JdbcConfig.ConnectionTimeout, - idleTimeout: Long = JdbcConfig.IdleTimeout, + connectionTimeout: FiniteDuration = JdbcConfig.ConnectionTimeout, + idleTimeout: FiniteDuration = JdbcConfig.IdleTimeout, tablePrefix: String = "", ) @@ -105,8 +106,8 @@ object JdbcConfig with StrictLogging { final val MinIdle = 8 - final val IdleTimeout = 10000L // ms, minimum according to log, defaults to 600s - final val ConnectionTimeout = 5000L + final val IdleTimeout = 10000.millis // minimum according to log, defaults to 600s + final val ConnectionTimeout = 5000.millis @scala.deprecated("do I need this?", since = "SC") implicit val showInstance: Show[JdbcConfig] = @@ -163,8 +164,10 @@ object JdbcConfig tablePrefix <- optionalStringField("tablePrefix").map(_ getOrElse "") maxPoolSize <- optionalIntField("poolSize").map(_ getOrElse PoolSize.Production) minIdle <- optionalIntField("minIdle").map(_ getOrElse MinIdle) - connTimeout <- optionalLongField("connectionTimeout").map(_ getOrElse ConnectionTimeout) - idleTimeout <- optionalLongField("idleTimeout").map(_ getOrElse IdleTimeout) + connTimeout <- optionalLongField("connectionTimeout") + .map(x => x.map(_.millis) getOrElse ConnectionTimeout) + idleTimeout <- optionalLongField("idleTimeout") + .map(x => x.map(_.millis) getOrElse IdleTimeout) } yield JdbcConfig( driver = driver, url = url, diff --git a/triggers/service/BUILD.bazel b/triggers/service/BUILD.bazel index 32e8e8a17b..a29a0b9cb6 100644 --- a/triggers/service/BUILD.bazel +++ b/triggers/service/BUILD.bazel @@ -41,6 +41,8 @@ da_scala_library( "@maven//:org_typelevel_cats_effect", "@maven//:org_typelevel_cats_free", "@maven//:org_typelevel_cats_kernel", + "@maven//:com_github_pureconfig_pureconfig_core", + "@maven//:com_github_pureconfig_pureconfig_generic", ], scala_runtime_deps = [ "@maven//:com_typesafe_akka_akka_slf4j", @@ -201,9 +203,14 @@ da_scala_test_suite( ["src/test-suite/scala/**/*.scala"], exclude = ["**/*Oracle*"], ), + data = [ + ":src/test-suite/resources/trigger-service.conf", + ":src/test-suite/resources/trigger-service-minimal.conf", + ], scala_deps = [ "@maven//:io_spray_spray_json", "@maven//:com_typesafe_akka_akka_http_core", + "@maven//:com_typesafe_akka_akka_parsing", "@maven//:org_scalatest_scalatest_core", "@maven//:org_scalatest_scalatest_matchers_core", "@maven//:org_scalatest_scalatest_shouldmatchers", @@ -214,9 +221,11 @@ da_scala_test_suite( deps = [ ":trigger-service", ":trigger-service-tests", + "//bazel_tools/runfiles:scala_runfiles", "//daml-lf/archive:daml_lf_1.dev_archive_proto_java", "//daml-lf/archive:daml_lf_archive_reader", "//daml-lf/data", + "//daml-lf/interpreter", "//language-support/scala/bindings-akka", "//ledger-api/rs-grpc-bridge", "//ledger-api/testing-utils", @@ -234,6 +243,7 @@ da_scala_test_suite( "//libs-scala/ports", "//libs-scala/postgresql-testing", "//libs-scala/resources", + "//triggers/service/auth:middleware-api", "//triggers/service/auth:oauth2-test-server", "@maven//:eu_rekawek_toxiproxy_toxiproxy_java_2_1_3", "@maven//:org_flywaydb_flyway_core", diff --git a/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/Cli.scala b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/Cli.scala new file mode 100644 index 0000000000..64e6c5ca8e --- /dev/null +++ b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/Cli.scala @@ -0,0 +1,371 @@ +// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf.engine.trigger + +import akka.http.scaladsl.model.Uri +import com.daml.lf.speedy.Compiler +import com.daml.platform.services.time.TimeProviderType + +import java.io.File +import java.nio.file.{Path, Paths} +import java.time.Duration +import com.daml.cliopts + +import scala.concurrent.duration.FiniteDuration +import com.daml.auth.middleware.api.{Client => AuthClient} +import com.daml.dbutils.{DBConfig, JdbcConfig} +import com.typesafe.scalalogging.StrictLogging +import pureconfig.ConfigSource +import pureconfig.error.ConfigReaderFailures + +import scala.concurrent.duration +import scalaz.syntax.std.option._ + +private[trigger] final case class Cli( + configFile: Option[File], + // For convenience, we allow passing DARs on startup + // as opposed to uploading them dynamically. + darPaths: List[Path], + address: String, + httpPort: Int, + ledgerHost: String, + ledgerPort: Int, + authInternalUri: Option[Uri], + authExternalUri: Option[Uri], + authBothUri: Option[Uri], + authRedirectToLogin: AuthClient.RedirectToLogin, + authCallbackUri: Option[Uri], + maxInboundMessageSize: Int, + minRestartInterval: FiniteDuration, + maxRestartInterval: FiniteDuration, + maxAuthCallbacks: Int, + authCallbackTimeout: FiniteDuration, + maxHttpEntityUploadSize: Long, + httpEntityUploadTimeout: FiniteDuration, + timeProviderType: TimeProviderType, + commandTtl: Duration, + init: Boolean, + jdbcConfig: Option[JdbcConfig], + portFile: Option[Path], + allowExistingSchema: Boolean, + compilerConfig: Compiler.Config, +) extends StrictLogging { + + def loadFromConfigFile: Option[Either[ConfigReaderFailures, TriggerServiceAppConf]] = + configFile.map(cf => ConfigSource.file(cf).load[TriggerServiceAppConf]) + + def loadFromCliArgs: ServiceConfig = { + ServiceConfig( + darPaths = darPaths, + address = address, + httpPort = httpPort, + ledgerHost = ledgerHost, + ledgerPort = ledgerPort, + authInternalUri = authInternalUri, + authExternalUri = authExternalUri, + authBothUri = authBothUri, + authRedirectToLogin = authRedirectToLogin, + authCallbackUri = authCallbackUri, + maxInboundMessageSize = maxInboundMessageSize, + minRestartInterval = minRestartInterval, + maxRestartInterval = maxRestartInterval, + maxAuthCallbacks = maxAuthCallbacks, + authCallbackTimeout = authCallbackTimeout, + maxHttpEntityUploadSize = maxHttpEntityUploadSize, + httpEntityUploadTimeout = httpEntityUploadTimeout, + timeProviderType = timeProviderType, + commandTtl = commandTtl, + init = init, + jdbcConfig = jdbcConfig, + portFile = portFile, + allowExistingSchema = allowExistingSchema, + compilerConfig = compilerConfig, + ) + } + + def loadConfig: Option[ServiceConfig] = + loadFromConfigFile.cata( + { + case Right(cfg) => Some(cfg.toServiceConfig) + case Left(ex) => + logger.error( + s"Error loading trigger service config from file ${configFile}", + ex.prettyPrint(), + ) + None + }, + Some(loadFromCliArgs), + ) +} + +private[trigger] object Cli { + + val DefaultHttpPort: Int = 8088 + val DefaultMaxInboundMessageSize: Int = RunnerConfig.DefaultMaxInboundMessageSize + val DefaultMinRestartInterval: FiniteDuration = FiniteDuration(5, duration.SECONDS) + val DefaultMaxRestartInterval: FiniteDuration = FiniteDuration(60, duration.SECONDS) + // Adds up to ~1GB with DefaultMaxInboundMessagesSize + val DefaultMaxAuthCallbacks: Int = 250 + val DefaultAuthCallbackTimeout: FiniteDuration = FiniteDuration(1, duration.MINUTES) + val DefaultMaxHttpEntityUploadSize: Long = RunnerConfig.DefaultMaxInboundMessageSize.toLong + val DefaultHttpEntityUploadTimeout: FiniteDuration = FiniteDuration(1, duration.MINUTES) + val DefaultCompilerConfig: Compiler.Config = Compiler.Config.Default + val DefaultCommandTtl: FiniteDuration = FiniteDuration(30, duration.SECONDS) + + private[trigger] def redirectToLogin(value: String): AuthClient.RedirectToLogin = { + value.toLowerCase match { + case "yes" => AuthClient.RedirectToLogin.Yes + case "no" => AuthClient.RedirectToLogin.No + case "auto" => AuthClient.RedirectToLogin.Auto + case s => + throw new IllegalArgumentException(s"value '$s' is not one of 'yes', 'no', or 'auto'.") + } + } + + implicit val redirectToLoginRead: scopt.Read[AuthClient.RedirectToLogin] = + scopt.Read.reads(redirectToLogin) + + private[trigger] val Default = Cli( + configFile = None, + darPaths = Nil, + address = cliopts.Http.defaultAddress, + httpPort = DefaultHttpPort, + ledgerHost = null, + ledgerPort = 0, + authInternalUri = None, + authExternalUri = None, + authBothUri = None, + authRedirectToLogin = AuthClient.RedirectToLogin.No, + authCallbackUri = None, + maxInboundMessageSize = DefaultMaxInboundMessageSize, + minRestartInterval = DefaultMinRestartInterval, + maxRestartInterval = DefaultMaxRestartInterval, + maxAuthCallbacks = Cli.DefaultMaxAuthCallbacks, + authCallbackTimeout = Cli.DefaultAuthCallbackTimeout, + maxHttpEntityUploadSize = DefaultMaxHttpEntityUploadSize, + httpEntityUploadTimeout = DefaultHttpEntityUploadTimeout, + timeProviderType = TimeProviderType.WallClock, + commandTtl = Duration.ofSeconds(30L), + init = false, + jdbcConfig = None, + portFile = None, + allowExistingSchema = false, + compilerConfig = DefaultCompilerConfig, + ) + + @SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) // scopt builders + private class OptionParser(supportedJdbcDriverNames: Set[String]) + extends scopt.OptionParser[Cli]("trigger-service") { + head("trigger-service") + + opt[Option[File]]('c', "config") + .text( + "The application config file, this is the recommended way to run the service, individual cli-args are now deprecated" + ) + .valueName("") + .action((file, cli) => cli.copy(configFile = file)) + + opt[String]("dar") + .optional() + .unbounded() + .action((f, c) => c.copy(darPaths = Paths.get(f) :: c.darPaths)) + .text("Path to the dar file containing the trigger.") + + cliopts.Http.serverParse(this, serviceName = "Trigger")( + address = (f, c) => c.copy(address = f(c.address)), + httpPort = (f, c) => c.copy(httpPort = f(c.httpPort)), + defaultHttpPort = Some(DefaultHttpPort), + portFile = Some((f, c) => c.copy(portFile = f(c.portFile))), + ) + + opt[String]("ledger-host") + .optional() + .action((t, c) => c.copy(ledgerHost = t)) + .text("Ledger hostname.") + + opt[Int]("ledger-port") + .optional() + .action((t, c) => c.copy(ledgerPort = t)) + .text("Ledger port.") + + opt[String]("auth") + .optional() + .action((t, c) => c.copy(authBothUri = Some(Uri(t)))) + .text( + "Sets both the internal and external auth URIs. Incompatible with --auth-internal and --auth-external." + ) + + opt[String]("auth-internal") + .optional() + .action((t, c) => c.copy(authInternalUri = Some(Uri(t)))) + .text( + "Sets the internal auth URIs (used by the trigger service to connect directly to the middleware). Incompatible with --auth." + ) + + opt[String]("auth-external") + .optional() + .action((t, c) => c.copy(authExternalUri = Some(Uri(t)))) + .text( + "Sets the external auth URI (the one returned to the browser). Incompatible with --auth." + ) + + opt[AuthClient.RedirectToLogin]("auth-redirect") + .optional() + .action((x, c) => c.copy(authRedirectToLogin = x)) + .text( + "Redirect to auth middleware login endpoint when unauthorized. One of 'yes', 'no', or 'auto'." + ) + + opt[String]("auth-callback") + .optional() + .action((t, c) => c.copy(authCallbackUri = Some(Uri(t)))) + .text( + "URI to the auth login flow callback endpoint `/cb`. By default constructed from the incoming login request." + ) + + opt[Int]("max-inbound-message-size") + .action((x, c) => c.copy(maxInboundMessageSize = x)) + .optional() + .text( + s"Optional max inbound message size in bytes. Defaults to ${DefaultMaxInboundMessageSize}." + ) + + opt[Long]("min-restart-interval") + .action((x, c) => c.copy(minRestartInterval = FiniteDuration(x, duration.SECONDS))) + .optional() + .text( + s"Minimum time interval before restarting a failed trigger. Defaults to ${DefaultMinRestartInterval.toSeconds} seconds." + ) + + opt[Long]("max-restart-interval") + .action((x, c) => c.copy(maxRestartInterval = FiniteDuration(x, duration.SECONDS))) + .optional() + .text( + s"Maximum time interval between restarting a failed trigger. Defaults to ${DefaultMaxRestartInterval.toSeconds} seconds." + ) + + opt[Int]("max-pending-authorizations") + .action((x, c) => c.copy(maxAuthCallbacks = x)) + .optional() + .text( + s"Optional max number of pending authorization requests. Defaults to ${DefaultMaxAuthCallbacks}." + ) + + opt[Long]("authorization-timeout") + .action((x, c) => c.copy(authCallbackTimeout = FiniteDuration(x, duration.SECONDS))) + .optional() + .text( + s"Optional authorization timeout. Defaults to ${DefaultAuthCallbackTimeout.toSeconds} seconds." + ) + + opt[Long]("max-http-entity-upload-size") + .action((x, c) => c.copy(maxHttpEntityUploadSize = x)) + .optional() + .text(s"Optional max HTTP entity upload size. Defaults to ${DefaultMaxHttpEntityUploadSize}.") + + opt[Long]("http-entity-upload-timeout") + .action((x, c) => c.copy(httpEntityUploadTimeout = FiniteDuration(x, duration.SECONDS))) + .optional() + .text( + s"Optional HTTP entity upload timeout. Defaults to ${DefaultHttpEntityUploadTimeout.toSeconds} seconds." + ) + + opt[Unit]('s', "static-time") + .optional() + .action((_, c) => c.copy(timeProviderType = TimeProviderType.Static)) + .text("Use static time. When not specified, wall-clock time is used.") + + opt[Unit]('w', "wall-clock-time") + .optional() + .text( + "[DEPRECATED] Wall-clock time is the default. This flag has no effect. Use `-s` to enable static time." + ) + + opt[Long]("ttl") + .action { (t, c) => + c.copy(commandTtl = Duration.ofSeconds(t)) + } + .text("TTL in seconds used for commands emitted by the trigger. Defaults to 30s.") + + opt[Unit]("dev-mode-unsafe") + .action((_, c) => c.copy(compilerConfig = Compiler.Config.Dev)) + .optional() + .text( + "Turns on development mode. Development mode allows development versions of Daml-LF language." + ) + .hidden() + + implicit val jcd: DBConfig.JdbcConfigDefaults = DBConfig.JdbcConfigDefaults( + supportedJdbcDrivers = supportedJdbcDriverNames, + defaultDriver = Some("org.postgresql.Driver"), + ) + + opt[Map[String, String]]("jdbc") + .action { (x, c) => + c.copy(jdbcConfig = + Some( + JdbcConfig + .create(x) + .fold(e => throw new IllegalArgumentException(e), identity) + ) + ) + } + .optional() + .text(JdbcConfig.help()) + .text( + "JDBC configuration parameters. If omitted the service runs without a database. " + + JdbcConfig.help() + ) + + opt[Boolean]("allow-existing-schema") + .action((x, c) => c.copy(allowExistingSchema = x)) + .text( + "Do not abort if there are existing tables in the database schema. EXPERT ONLY. Defaults to false." + ) + + checkConfig { cfg => + if ( + (cfg.authBothUri.nonEmpty && (cfg.authInternalUri.nonEmpty || cfg.authExternalUri.nonEmpty)) + || (cfg.authInternalUri.nonEmpty != cfg.authExternalUri.nonEmpty) + ) + failure("You must specify either just --auth or both --auth-internal and --auth-external.") + else + success + } + + checkConfig { cfg => + if (cfg.configFile.isEmpty && (cfg.ledgerHost == null || cfg.ledgerPort == 0)) + failure( + "Missing required values i.e --ledger-host and/or --ledger-port values for cli args are missing" + ) + else + success + } + + checkConfig { cfg => + if (cfg.configFile.isDefined && (cfg.ledgerHost != null || cfg.ledgerPort != 0)) + Left("Found both config file and cli opts for the app, please provide only one of them") + else Right(()) + } + + cmd("init-db") + .action((_, c) => c.copy(init = true)) + .text("Initialize database and terminate.") + + help("help").text("Print this usage text") + + } + + def parse(args: Array[String], supportedJdbcDriverNames: Set[String]): Option[Cli] = { + new OptionParser(supportedJdbcDriverNames).parse(args, Default) + } + + def parseConfig( + args: Array[String], + supportedJdbcDriverNames: Set[String], + ): Option[ServiceConfig] = { + val cli = parse(args, supportedJdbcDriverNames) + cli.flatMap(_.loadConfig) + } +} diff --git a/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceConfig.scala b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceConfig.scala index faa530b79f..66ea292417 100644 --- a/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceConfig.scala +++ b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceConfig.scala @@ -5,16 +5,13 @@ package com.daml.lf.engine.trigger import com.daml.lf.speedy.Compiler -import java.nio.file.{Path, Paths} +import java.nio.file.Path import java.time.Duration - import akka.http.scaladsl.model.Uri -import com.daml.cliopts import com.daml.platform.services.time.TimeProviderType import com.daml.auth.middleware.api.{Client => AuthClient} -import com.daml.dbutils.{DBConfig, JdbcConfig} +import com.daml.dbutils.JdbcConfig -import scala.concurrent.duration import scala.concurrent.duration.FiniteDuration private[trigger] final case class ServiceConfig( @@ -45,237 +42,3 @@ private[trigger] final case class ServiceConfig( allowExistingSchema: Boolean, compilerConfig: Compiler.Config, ) - -private[trigger] object ServiceConfig { - private val DefaultHttpPort: Int = 8088 - val DefaultMaxInboundMessageSize: Int = RunnerConfig.DefaultMaxInboundMessageSize - private val DefaultMinRestartInterval: FiniteDuration = FiniteDuration(5, duration.SECONDS) - val DefaultMaxRestartInterval: FiniteDuration = FiniteDuration(60, duration.SECONDS) - // Adds up to ~1GB with DefaultMaxInboundMessagesSize - val DefaultMaxAuthCallbacks: Int = 250 - val DefaultAuthCallbackTimeout: FiniteDuration = FiniteDuration(1, duration.MINUTES) - val DefaultMaxHttpEntityUploadSize: Long = RunnerConfig.DefaultMaxInboundMessageSize.toLong - val DefaultHttpEntityUploadTimeout: FiniteDuration = FiniteDuration(1, duration.MINUTES) - val DefaultCompilerConfig: Compiler.Config = Compiler.Config.Default - - implicit val redirectToLoginRead: scopt.Read[AuthClient.RedirectToLogin] = scopt.Read.reads { - _.toLowerCase match { - case "yes" => AuthClient.RedirectToLogin.Yes - case "no" => AuthClient.RedirectToLogin.No - case "auto" => AuthClient.RedirectToLogin.Auto - case s => throw new IllegalArgumentException(s"'$s' is not one of 'yes', 'no', or 'auto'.") - } - } - - @SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) // scopt builders - private class OptionParser(supportedJdbcDriverNames: Set[String]) - extends scopt.OptionParser[ServiceConfig]("trigger-service") { - head("trigger-service") - - opt[String]("dar") - .optional() - .unbounded() - .action((f, c) => c.copy(darPaths = Paths.get(f) :: c.darPaths)) - .text("Path to the dar file containing the trigger.") - - cliopts.Http.serverParse(this, serviceName = "Trigger")( - address = (f, c) => c.copy(address = f(c.address)), - httpPort = (f, c) => c.copy(httpPort = f(c.httpPort)), - defaultHttpPort = Some(DefaultHttpPort), - portFile = Some((f, c) => c.copy(portFile = f(c.portFile))), - ) - - opt[String]("ledger-host") - .required() - .action((t, c) => c.copy(ledgerHost = t)) - .text("Ledger hostname.") - - opt[Int]("ledger-port") - .required() - .action((t, c) => c.copy(ledgerPort = t)) - .text("Ledger port.") - - opt[String]("auth") - .optional() - .action((t, c) => c.copy(authBothUri = Some(Uri(t)))) - .text( - "Sets both the internal and external auth URIs. Incompatible with --auth-internal and --auth-external." - ) - - opt[String]("auth-internal") - .optional() - .action((t, c) => c.copy(authInternalUri = Some(Uri(t)))) - .text( - "Sets the internal auth URIs (used by the trigger service to connect directly to the middleware). Incompatible with --auth." - ) - - opt[String]("auth-external") - .optional() - .action((t, c) => c.copy(authExternalUri = Some(Uri(t)))) - .text( - "Sets the external auth URI (the one returned to the browser). Incompatible with --auth." - ) - - opt[AuthClient.RedirectToLogin]("auth-redirect") - .optional() - .action((x, c) => c.copy(authRedirectToLogin = x)) - .text( - "Redirect to auth middleware login endpoint when unauthorized. One of 'yes', 'no', or 'auto'." - ) - - opt[String]("auth-callback") - .optional() - .action((t, c) => c.copy(authCallbackUri = Some(Uri(t)))) - .text( - "URI to the auth login flow callback endpoint `/cb`. By default constructed from the incoming login request." - ) - - opt[Int]("max-inbound-message-size") - .action((x, c) => c.copy(maxInboundMessageSize = x)) - .optional() - .text( - s"Optional max inbound message size in bytes. Defaults to ${DefaultMaxInboundMessageSize}." - ) - - opt[Long]("min-restart-interval") - .action((x, c) => c.copy(minRestartInterval = FiniteDuration(x, duration.SECONDS))) - .optional() - .text( - s"Minimum time interval before restarting a failed trigger. Defaults to ${DefaultMinRestartInterval.toSeconds} seconds." - ) - - opt[Long]("max-restart-interval") - .action((x, c) => c.copy(maxRestartInterval = FiniteDuration(x, duration.SECONDS))) - .optional() - .text( - s"Maximum time interval between restarting a failed trigger. Defaults to ${DefaultMaxRestartInterval.toSeconds} seconds." - ) - - opt[Int]("max-pending-authorizations") - .action((x, c) => c.copy(maxAuthCallbacks = x)) - .optional() - .text( - s"Optional max number of pending authorization requests. Defaults to ${DefaultMaxAuthCallbacks}." - ) - - opt[Long]("authorization-timeout") - .action((x, c) => c.copy(authCallbackTimeout = FiniteDuration(x, duration.SECONDS))) - .optional() - .text( - s"Optional authorization timeout. Defaults to ${DefaultAuthCallbackTimeout.toSeconds} seconds." - ) - - opt[Long]("max-http-entity-upload-size") - .action((x, c) => c.copy(maxHttpEntityUploadSize = x)) - .optional() - .text(s"Optional max HTTP entity upload size. Defaults to ${DefaultMaxHttpEntityUploadSize}.") - - opt[Long]("http-entity-upload-timeout") - .action((x, c) => c.copy(httpEntityUploadTimeout = FiniteDuration(x, duration.SECONDS))) - .optional() - .text( - s"Optional HTTP entity upload timeout. Defaults to ${DefaultHttpEntityUploadTimeout.toSeconds} seconds." - ) - - opt[Unit]('s', "static-time") - .optional() - .action((_, c) => c.copy(timeProviderType = TimeProviderType.Static)) - .text("Use static time. When not specified, wall-clock time is used.") - - opt[Unit]('w', "wall-clock-time") - .optional() - .text( - "[DEPRECATED] Wall-clock time is the default. This flag has no effect. Use `-s` to enable static time." - ) - - opt[Long]("ttl") - .action { (t, c) => - c.copy(commandTtl = Duration.ofSeconds(t)) - } - .text("TTL in seconds used for commands emitted by the trigger. Defaults to 30s.") - - opt[Unit]("dev-mode-unsafe") - .action((_, c) => c.copy(compilerConfig = Compiler.Config.Dev)) - .optional() - .text( - "Turns on development mode. Development mode allows development versions of Daml-LF language." - ) - .hidden() - - implicit val jcd: DBConfig.JdbcConfigDefaults = DBConfig.JdbcConfigDefaults( - supportedJdbcDrivers = supportedJdbcDriverNames, - defaultDriver = Some("org.postgresql.Driver"), - ) - - opt[Map[String, String]]("jdbc") - .action { (x, c) => - c.copy(jdbcConfig = - Some( - JdbcConfig - .create(x) - .fold(e => throw new IllegalArgumentException(e), identity) - ) - ) - } - .optional() - .text(JdbcConfig.help()) - .text( - "JDBC configuration parameters. If omitted the service runs without a database. " - + JdbcConfig.help() - ) - - opt[Boolean]("allow-existing-schema") - .action((x, c) => c.copy(allowExistingSchema = x)) - .text( - "Do not abort if there are existing tables in the database schema. EXPERT ONLY. Defaults to false." - ) - - checkConfig { cfg => - if ( - (cfg.authBothUri.nonEmpty && (cfg.authInternalUri.nonEmpty || cfg.authExternalUri.nonEmpty)) - || (cfg.authInternalUri.nonEmpty != cfg.authExternalUri.nonEmpty) - ) - failure("You must specify either just --auth or both --auth-internal and --auth-external.") - else - success - } - - cmd("init-db") - .action((_, c) => c.copy(init = true)) - .text("Initialize database and terminate.") - - help("help").text("Print this usage text") - - } - - def parse(args: Array[String], supportedJdbcDriverNames: Set[String]): Option[ServiceConfig] = - new OptionParser(supportedJdbcDriverNames).parse( - args, - ServiceConfig( - darPaths = Nil, - address = cliopts.Http.defaultAddress, - httpPort = DefaultHttpPort, - ledgerHost = null, - ledgerPort = 0, - authInternalUri = None, - authExternalUri = None, - authBothUri = None, - authRedirectToLogin = AuthClient.RedirectToLogin.No, - authCallbackUri = None, - maxInboundMessageSize = DefaultMaxInboundMessageSize, - minRestartInterval = DefaultMinRestartInterval, - maxRestartInterval = DefaultMaxRestartInterval, - maxAuthCallbacks = DefaultMaxAuthCallbacks, - authCallbackTimeout = DefaultAuthCallbackTimeout, - maxHttpEntityUploadSize = DefaultMaxHttpEntityUploadSize, - httpEntityUploadTimeout = DefaultHttpEntityUploadTimeout, - timeProviderType = TimeProviderType.WallClock, - commandTtl = Duration.ofSeconds(30L), - init = false, - jdbcConfig = None, - portFile = None, - allowExistingSchema = false, - compilerConfig = DefaultCompilerConfig, - ), - ) -} diff --git a/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceMain.scala b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceMain.scala index 2ec4726b9b..d22c53731f 100644 --- a/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceMain.scala +++ b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/ServiceMain.scala @@ -87,7 +87,7 @@ object ServiceMain { } def main(args: Array[String]): Unit = { - ServiceConfig.parse( + Cli.parseConfig( args, DbTriggerDao.supportedJdbcDriverNames(JdbcDrivers.availableJdbcDriverNames), ) match { diff --git a/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceAppConf.scala b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceAppConf.scala new file mode 100644 index 0000000000..f8a75cd7a5 --- /dev/null +++ b/triggers/service/src/main/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceAppConf.scala @@ -0,0 +1,138 @@ +// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf.engine.trigger + +import akka.http.scaladsl.model.Uri +import com.daml.dbutils.JdbcConfig +import com.daml.lf.speedy.Compiler +import com.daml.platform.services.time.TimeProviderType +import pureconfig.{ConfigReader, ConvertHelpers} +import com.daml.auth.middleware.api.{Client => AuthClient} +import pureconfig.error.FailureReason +import pureconfig.generic.semiauto.deriveReader + +import java.nio.file.Path +import java.time.Duration +import scala.concurrent.duration.FiniteDuration + +private[trigger] object LedgerApiConfig { + implicit val ledgerApiCfgReader: ConfigReader[LedgerApiConfig] = + deriveReader[LedgerApiConfig] +} +private[trigger] final case class LedgerApiConfig(address: String, port: Int) + +private[trigger] object AuthorizationConfig { + final case object AuthConfigFailure extends FailureReason { + val description = + "You must specify either just auth-common-uri or both auth-internal-uri and auth-external-uri" + } + + def isValid(ac: AuthorizationConfig): Boolean = { + (ac.authCommonUri.isDefined && ac.authExternalUri.isEmpty && ac.authInternalUri.isEmpty) || + (ac.authCommonUri.isEmpty && ac.authExternalUri.nonEmpty && ac.authInternalUri.nonEmpty) + } + + implicit val uriCfgReader: ConfigReader[Uri] = + ConfigReader.fromString[Uri](ConvertHelpers.catchReadError(s => Uri(s))) + + implicit val redirectToLoginCfgReader: ConfigReader[AuthClient.RedirectToLogin] = + ConfigReader.fromString[AuthClient.RedirectToLogin]( + ConvertHelpers.catchReadError(s => Cli.redirectToLogin(s)) + ) + + implicit val authCfgReader: ConfigReader[AuthorizationConfig] = + deriveReader[AuthorizationConfig].emap { ac => + Either.cond(isValid(ac), ac, AuthConfigFailure) + } +} +private[trigger] final case class AuthorizationConfig( + authInternalUri: Option[Uri] = None, + authExternalUri: Option[Uri] = None, + authCommonUri: Option[Uri] = None, + authRedirect: AuthClient.RedirectToLogin = AuthClient.RedirectToLogin.No, + authCallbackUri: Option[Uri] = None, + maxPendingAuthorizations: Int = Cli.DefaultMaxAuthCallbacks, + authCallbackTimeout: FiniteDuration = Cli.DefaultAuthCallbackTimeout, +) + +private[trigger] object TriggerServiceAppConf { + implicit val compilerCfgReader: ConfigReader[Compiler.Config] = + ConfigReader.fromString[Compiler.Config](ConvertHelpers.catchReadError { s => + s.toLowerCase() match { + case "default" => Compiler.Config.Default + case "dev" => Compiler.Config.Dev + case s => + throw new IllegalArgumentException( + s"Value '$s' for compiler-config is not one of 'default' or 'dev'" + ) + } + }) + implicit val timeProviderTypeCfgReader: ConfigReader[TimeProviderType] = + ConfigReader.fromString[TimeProviderType](ConvertHelpers.catchReadError { s => + s.toLowerCase() match { + case "static" => TimeProviderType.Static + case "wall-clock" => TimeProviderType.WallClock + case s => + throw new IllegalArgumentException( + s"Value '$s' for time-provider-type is not one of 'static' or 'wall-clock'" + ) + } + }) + implicit val jdbcCfgReader: ConfigReader[JdbcConfig] = deriveReader[JdbcConfig] + implicit val serviceCfgReader: ConfigReader[TriggerServiceAppConf] = + deriveReader[TriggerServiceAppConf] +} + +/* An intermediate config representation allowing us to define our HOCON config in a more modular fashion, + this eventually gets mapped to `ServiceConfig` + */ +private[trigger] final case class TriggerServiceAppConf( + darPaths: List[Path] = Nil, + address: String = "127.0.0.1", + port: Int = Cli.DefaultHttpPort, + portFile: Option[Path] = None, + ledgerApi: LedgerApiConfig, + authorization: AuthorizationConfig = AuthorizationConfig(), + maxInboundMessageSize: Int = Cli.DefaultMaxInboundMessageSize, + minRestartInterval: FiniteDuration = Cli.DefaultMinRestartInterval, + maxRestartInterval: FiniteDuration = Cli.DefaultMaxRestartInterval, + maxHttpEntityUploadSize: Long = Cli.DefaultMaxHttpEntityUploadSize, + httpEntityUploadTimeout: FiniteDuration = Cli.DefaultHttpEntityUploadTimeout, + timeProviderType: TimeProviderType = TimeProviderType.WallClock, + ttl: FiniteDuration = Cli.DefaultCommandTtl, + initDb: Boolean = false, + triggerStore: Option[JdbcConfig] = None, + allowExistingSchema: Boolean = false, + compilerConfig: Compiler.Config = Compiler.Config.Default, +) { + def toServiceConfig: ServiceConfig = { + ServiceConfig( + darPaths = darPaths, + address = address, + httpPort = port, + ledgerHost = ledgerApi.address, + ledgerPort = ledgerApi.port, + authInternalUri = authorization.authInternalUri, + authExternalUri = authorization.authExternalUri, + authBothUri = authorization.authCommonUri, + authRedirectToLogin = authorization.authRedirect, + authCallbackUri = authorization.authCallbackUri, + maxInboundMessageSize = maxInboundMessageSize, + minRestartInterval = minRestartInterval, + maxRestartInterval = maxRestartInterval, + maxAuthCallbacks = authorization.maxPendingAuthorizations, + authCallbackTimeout = authorization.authCallbackTimeout, + maxHttpEntityUploadSize = maxHttpEntityUploadSize, + httpEntityUploadTimeout = httpEntityUploadTimeout, + timeProviderType = timeProviderType, + commandTtl = + Duration.ofSeconds(ttl.toSeconds), // mapping from FiniteDuration to java.time.Duration + init = initDb, + jdbcConfig = triggerStore, + portFile = portFile, + allowExistingSchema = allowExistingSchema, + compilerConfig = compilerConfig, + ) + } +} diff --git a/triggers/service/src/test-suite/resources/trigger-service-minimal.conf b/triggers/service/src/test-suite/resources/trigger-service-minimal.conf new file mode 100644 index 0000000000..2795a9d5e9 --- /dev/null +++ b/triggers/service/src/test-suite/resources/trigger-service-minimal.conf @@ -0,0 +1,6 @@ +{ + ledger-api { + address = "127.0.0.1" + port = 5041 + } +} diff --git a/triggers/service/src/test-suite/resources/trigger-service.conf b/triggers/service/src/test-suite/resources/trigger-service.conf new file mode 100644 index 0000000000..7536e9e811 --- /dev/null +++ b/triggers/service/src/test-suite/resources/trigger-service.conf @@ -0,0 +1,81 @@ +{ + //dar file containing the trigger + dar-paths = [ + "./my-app.dar" + ] + + //IP address that Trigger service listens on. Defaults to 127.0.0.1. + address = "127.0.0.1" + //Trigger service port number. Defaults to 8088. A port number of 0 will let the system pick an ephemeral port. Consider specifying `port-file` option with port number 0. + port = 8088 + port-file = "port-file" + + ledger-api { + address = "127.0.0.1" + port = 5041 + } + + + //Optional max inbound message size in bytes. Defaults to 4194304. + max-inbound-message-size = 4194304 + //Minimum time interval before restarting a failed trigger. Defaults to 5 seconds. + min-restart-interval = 5s + //Maximum time interval between restarting a failed trigger. Defaults to 60 seconds. + max-restart-interval = 60s + //Optional max HTTP entity upload size in bytes. Defaults to 4194304. + max-http-entity-upload-size = 4194304 + //Optional HTTP entity upload timeout. Defaults to 60 seconds. + http-entity-upload-timeout = 60s + //Use static time or wall-clock, default is wall-clock time. + time-provider-type = "static" + //Compiler config type to use , default or dev mode + compiler-config = "dev" + //TTL in seconds used for commands emitted by the trigger. Defaults to 30s. + ttl = 60s + + //Initialize database and terminate. + init-db = "true" + + //Do not abort if there are existing tables in the database schema. EXPERT ONLY. Defaults to false. + allow-existing-schema = "true" + + trigger-store { + user = "postgres" + password = "password" + driver = "org.postgresql.Driver" + url = "jdbc:postgresql://localhost:5432/test?&ssl=true" + + // prefix for table names to avoid collisions, empty by default + table-prefix = "foo" + + // max pool size for the database connection pool + pool-size = 12 + //specifies the min idle connections for database connection pool. + min-idle = 4 + //specifies the idle timeout for the database connection pool. + idle-timeout = 12s + //specifies the connection timeout for database connection pool. + connection-timeout = 90s + } + + authorization { + //Sets both the internal and external auth URIs. + //auth-common-uri = "https://oauth2/common-uri" + + // Auth Client to redirect to login , defaults to no + auth-redirect = "yes" + + //Sets the internal auth URIs (used by the trigger service to connect directly to the middleware). Overrides value set by auth-common + auth-internal-uri = "https://oauth2/internal-uri" + //Sets the external auth URI (the one returned to the browser). overrides value set by auth-common. + auth-external-uri = "https://oauth2/external-uri" + //URI to the auth login flow callback endpoint `/cb`. By default constructed from the incoming login request. + auth-callback-uri = "https://oauth2/callback-uri" + + //Optional max number of pending authorization requests. Defaults to 250. + max-pending-authorizations = 250 + //Optional authorization timeout, defaults to 60 seconds + authorization-timeout = 60s + } + +} diff --git a/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/AuthorizationConfigTest.scala b/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/AuthorizationConfigTest.scala new file mode 100644 index 0000000000..da93353f8b --- /dev/null +++ b/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/AuthorizationConfigTest.scala @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf.engine.trigger + +import org.scalatest.Succeeded +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec +import pureconfig.ConfigSource +import pureconfig.error.{ConfigReaderFailures, ConvertFailure} +import org.scalatest.Assertion + +class AuthorizationConfigTest extends AsyncWordSpec with Matchers { + + private def validateFailure(ex: ConfigReaderFailures): Assertion = { + ex.head match { + case ConvertFailure(reason, _, _) => + reason shouldBe a[AuthorizationConfig.AuthConfigFailure.type] + reason.description shouldBe AuthorizationConfig.AuthConfigFailure.description + case _ => fail("Unexpected failure type expected `AuthConfigFailure`") + } + } + + "should error on specifying both authCommonUri and authInternalUri/authExternalUri" in { + val invalidConfigs = List( + """ + |{ + | auth-common-uri = "https://oauth2/common-uri" + | auth-internal-uri = "https://oauth2/internal-uri" + |} + |""".stripMargin, + """ + |{ + | auth-common-uri = "https://oauth2/common-uri" + | auth-external-uri = "https://oauth2/external-uri" + |} + |""".stripMargin, + ) + + invalidConfigs.foreach { c => + ConfigSource.string(c).load[AuthorizationConfig] match { + case Right(_) => + fail("Should fail on supplying both auth-common and auth-internal/auth-external uris") + case Left(ex) => + validateFailure(ex) + } + } + Succeeded + } + + "should error on specifying only authInternalUri and no authExternalUri" in { + ConfigSource + .string(""" + |{ + | auth-internal-uri = "https://oauth2/internal-uri" + |} + |""".stripMargin) + .load[AuthorizationConfig] match { + case Right(_) => fail("Should fail on only auth-internal uris") + case Left(ex) => + validateFailure(ex) + } + } + +} diff --git a/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/ServiceConfigTest.scala b/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/CliConfigTest.scala similarity index 97% rename from triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/ServiceConfigTest.scala rename to triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/CliConfigTest.scala index 2a7767841a..4da4494fd4 100644 --- a/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/ServiceConfigTest.scala +++ b/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/CliConfigTest.scala @@ -8,9 +8,9 @@ import org.scalatest.OptionValues import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -class ServiceConfigTest extends AnyWordSpec with Matchers with OptionValues { +class CliConfigTest extends AnyWordSpec with Matchers with OptionValues { "parse" should { - import ServiceConfig.parse + import Cli.parse import com.daml.cliopts.Http.defaultAddress val baseOpts = Array("--ledger-host", "localhost", "--ledger-port", "9999") diff --git a/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/CliSpec.scala b/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/CliSpec.scala new file mode 100644 index 0000000000..c4cbf4262a --- /dev/null +++ b/triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/CliSpec.scala @@ -0,0 +1,124 @@ +// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf.engine.trigger + +import akka.http.scaladsl.model.Uri +import com.daml.auth.middleware.api.{Client => AuthClient} +import com.daml.bazeltools.BazelRunfiles.requiredResource +import com.daml.dbutils.JdbcConfig +import com.daml.lf.speedy.Compiler +import com.daml.platform.services.time.TimeProviderType +import org.scalatest.Inside.inside +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec +import pureconfig.error.{CannotReadFile, ConfigReaderFailures} + +import java.nio.file.Paths +import scala.concurrent.duration._ + +class CliSpec extends AsyncWordSpec with Matchers { + + val minimalConf = TriggerServiceAppConf(ledgerApi = LedgerApiConfig("127.0.0.1", 5041)) + val confFile = "triggers/service/src/test-suite/resources/trigger-service.conf" + def loadCli(file: String): Cli = { + Cli.parse(Array("--config", file), Set()).getOrElse(fail("Could not load Cli on parse")) + } + + "should pickup the config file provided" in { + val file = requiredResource(confFile) + val cli = loadCli(file.getAbsolutePath) + cli.configFile should not be empty + } + + "should be able to successfully load the config based on the file provided" in { + val expectedAuthCfg = AuthorizationConfig( + authInternalUri = Some(Uri("https://oauth2/internal-uri")), + authExternalUri = Some(Uri("https://oauth2/external-uri")), + authCallbackUri = Some(Uri("https://oauth2/callback-uri")), + authRedirect = AuthClient.RedirectToLogin.Yes, + ) + val expectedJdbcConfig = JdbcConfig( + url = "jdbc:postgresql://localhost:5432/test?&ssl=true", + driver = "org.postgresql.Driver", + user = "postgres", + password = "password", + poolSize = 12, + idleTimeout = 12.seconds, + connectionTimeout = 90.seconds, + tablePrefix = "foo", + minIdle = 4, + ) + val file = requiredResource(confFile) + val cli = loadCli(file.getAbsolutePath) + cli.configFile should not be empty + val cfg = cli.loadFromConfigFile + inside(cfg) { case Some(Right(c)) => + c shouldBe minimalConf.copy( + darPaths = List(Paths.get("./my-app.dar")), + portFile = Some(Paths.get("port-file")), + authorization = expectedAuthCfg, + triggerStore = Some(expectedJdbcConfig), + timeProviderType = TimeProviderType.Static, + compilerConfig = Compiler.Config.Dev, + initDb = true, + ttl = 60.seconds, + allowExistingSchema = true, + ) + } + } + + "should take default values on loading minimal config" in { + val file = + requiredResource("triggers/service/src/test-suite/resources/trigger-service-minimal.conf") + val cli = loadCli(file.getAbsolutePath) + cli.configFile should not be empty + val cfg = cli.loadFromConfigFile + inside(cfg) { case Some(Right(c)) => + c shouldBe minimalConf + } + } + + "parse should raise error on non-existent config file" in { + val cli = loadCli("missingFile.conf") + cli.configFile should not be empty + val cfg = cli.loadFromConfigFile + inside(cfg) { case Some(Left(ConfigReaderFailures(head))) => + head shouldBe a[CannotReadFile] + } + + //parseConfig for non-existent file should return a None + Cli.parseConfig( + Array( + "--config", + "missingFile.conf", + ), + Set(), + ) shouldBe None + } + + "should load config from cli args when no conf file is specified" in { + Cli + .parseConfig( + Array("--ledger-host", "localhost", "--ledger-port", "9999"), + Set(), + ) shouldBe Some(Cli.Default.copy(ledgerHost = "localhost", ledgerPort = 9999)) + .map(_.loadFromCliArgs) + } + + "should fail to load config from cli args on missing required params" in { + Cli + .parseConfig( + Array("--ledger-host", "localhost"), + Set(), + ) shouldBe None + } + + "should fail to load config on supplying both cli args and config file" in { + Cli + .parseConfig( + Array("--config", confFile, "--ledger-host", "localhost", "--ledger-port", "9999"), + Set(), + ) shouldBe None + } +} diff --git a/triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceFixture.scala b/triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceFixture.scala index 4c6e57af56..67f5f45deb 100644 --- a/triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceFixture.scala +++ b/triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceFixture.scala @@ -520,20 +520,20 @@ trait TriggerServiceFixture toxiSandboxPort.value, TimeProviderType.Static, java.time.Duration.ofSeconds(30), - ServiceConfig.DefaultMaxInboundMessageSize, + Cli.DefaultMaxInboundMessageSize, ) val restartConfig = TriggerRestartConfig( minRestartInterval, - ServiceConfig.DefaultMaxRestartInterval, + Cli.DefaultMaxRestartInterval, ) for { r <- ServiceMain.startServer( host.getHostName, Port.Dynamic.value, - ServiceConfig.DefaultMaxAuthCallbacks, - ServiceConfig.DefaultAuthCallbackTimeout, - ServiceConfig.DefaultMaxHttpEntityUploadSize, - ServiceConfig.DefaultHttpEntityUploadTimeout, + Cli.DefaultMaxAuthCallbacks, + Cli.DefaultAuthCallbackTimeout, + Cli.DefaultMaxHttpEntityUploadSize, + Cli.DefaultHttpEntityUploadTimeout, authConfig, AuthClient.RedirectToLogin.Yes, authCallback,