diff --git a/distage/distage-extension-config/.js/src/main/scala/izumi/distage/config/model/AppConfig.scala b/distage/distage-extension-config/.js/src/main/scala/izumi/distage/config/model/AppConfig.scala new file mode 100644 index 0000000000..c0a4cc28b2 --- /dev/null +++ b/distage/distage-extension-config/.js/src/main/scala/izumi/distage/config/model/AppConfig.scala @@ -0,0 +1,12 @@ +package izumi.distage.config.model + +import izumi.distage.config.DistageConfigImpl + +final case class AppConfig( + config: DistageConfigImpl +) + +object AppConfig { + val empty: AppConfig = AppConfig(Map.empty[String, String]) + def provided(config: DistageConfigImpl): AppConfig = AppConfig(config) +} diff --git a/distage/distage-extension-config/.js/src/main/scala/izumi/distage/config/model/AppConfigSyntax.scala b/distage/distage-extension-config/.js/src/main/scala/izumi/distage/config/model/AppConfigSyntax.scala deleted file mode 100644 index ab1d91b0a8..0000000000 --- a/distage/distage-extension-config/.js/src/main/scala/izumi/distage/config/model/AppConfigSyntax.scala +++ /dev/null @@ -1,5 +0,0 @@ -package izumi.distage.config.model - -trait AppConfigSyntax { - val empty: AppConfig = AppConfig(Map.empty[String, String]) -} diff --git a/distage/distage-extension-config/.jvm/src/main/scala/izumi/distage/config/model/AppConfig.scala b/distage/distage-extension-config/.jvm/src/main/scala/izumi/distage/config/model/AppConfig.scala new file mode 100644 index 0000000000..0ed8dde920 --- /dev/null +++ b/distage/distage-extension-config/.jvm/src/main/scala/izumi/distage/config/model/AppConfig.scala @@ -0,0 +1,67 @@ +package izumi.distage.config.model + +import com.typesafe.config.{Config, ConfigFactory} +import izumi.distage.config.DistageConfigImpl + +import java.io.File + +final case class AppConfig( + config: DistageConfigImpl, + shared: List[ConfigLoadResult.Success], + roles: List[LoadedRoleConfigs], +) + +object AppConfig { + val empty: AppConfig = AppConfig(ConfigFactory.empty(), List.empty, List.empty) + def provided(config: DistageConfigImpl): AppConfig = AppConfig(config, List.empty, List.empty) +} + +sealed trait GenericConfigSource + +object GenericConfigSource { + case class ConfigFile(file: File) extends GenericConfigSource + + case object ConfigDefault extends GenericConfigSource +} + +case class RoleConfig(role: String, active: Boolean, configSource: GenericConfigSource) + +case class LoadedRoleConfigs(roleConfig: RoleConfig, loaded: Seq[ConfigLoadResult.Success]) + +sealed trait ConfigLoadResult { + def clue: String + + def src: ConfigSource + + def toEither: Either[ConfigLoadResult.Failure, ConfigLoadResult.Success] +} + +object ConfigLoadResult { + case class Success(clue: String, src: ConfigSource, config: Config) extends ConfigLoadResult { + override def toEither: Either[ConfigLoadResult.Failure, ConfigLoadResult.Success] = Right(this) + } + + case class Failure(clue: String, src: ConfigSource, failure: Throwable) extends ConfigLoadResult { + override def toEither: Either[ConfigLoadResult.Failure, ConfigLoadResult.Success] = Left(this) + } +} + +sealed trait ConfigSource + +object ConfigSource { + final case class Resource(name: String, kind: ResourceConfigKind) extends ConfigSource { + override def toString: String = s"resource:$name" + } + + final case class File(file: java.io.File) extends ConfigSource { + override def toString: String = s"file:$file" + } +} + +sealed trait ResourceConfigKind + +object ResourceConfigKind { + case object Primary extends ResourceConfigKind + + case object Development extends ResourceConfigKind +} diff --git a/distage/distage-extension-config/.jvm/src/main/scala/izumi/distage/config/model/AppConfigSyntax.scala b/distage/distage-extension-config/.jvm/src/main/scala/izumi/distage/config/model/AppConfigSyntax.scala deleted file mode 100644 index 50111341e4..0000000000 --- a/distage/distage-extension-config/.jvm/src/main/scala/izumi/distage/config/model/AppConfigSyntax.scala +++ /dev/null @@ -1,7 +0,0 @@ -package izumi.distage.config.model - -import com.typesafe.config.ConfigFactory - -trait AppConfigSyntax { - val empty: AppConfig = AppConfig(ConfigFactory.empty()) -} diff --git a/distage/distage-extension-config/.jvm/src/test/scala/izumi/distage/config/ConfigTest.scala b/distage/distage-extension-config/.jvm/src/test/scala/izumi/distage/config/ConfigTest.scala index bcc38a4b75..1570a1c9c6 100644 --- a/distage/distage-extension-config/.jvm/src/test/scala/izumi/distage/config/ConfigTest.scala +++ b/distage/distage-extension-config/.jvm/src/test/scala/izumi/distage/config/ConfigTest.scala @@ -20,7 +20,7 @@ final class ConfigTest extends AnyWordSpec { } def mkModule(config: Config): AppConfigModule = { - val appConfig = AppConfig(config) + val appConfig = AppConfig(config, List.empty, List.empty) new AppConfigModule(appConfig) } diff --git a/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala b/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala index c69db5fc05..8f6bd3e44a 100644 --- a/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala +++ b/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala @@ -5,11 +5,9 @@ import izumi.distage.model.definition.ModuleDef class AppConfigModule(appConfig: AppConfig) extends ModuleDef { make[AppConfig].fromValue(appConfig) - - def this(config: DistageConfigImpl) = this(AppConfig(config)) } object AppConfigModule { def apply(appConfig: AppConfig): AppConfigModule = new AppConfigModule(appConfig) - def apply(config: DistageConfigImpl): AppConfigModule = new AppConfigModule(config) + def apply(config: DistageConfigImpl): AppConfigModule = new AppConfigModule(AppConfig.provided(config)) } diff --git a/distage/distage-extension-config/src/main/scala/izumi/distage/config/model/AppConfig.scala b/distage/distage-extension-config/src/main/scala/izumi/distage/config/model/AppConfig.scala deleted file mode 100644 index 58382b4da9..0000000000 --- a/distage/distage-extension-config/src/main/scala/izumi/distage/config/model/AppConfig.scala +++ /dev/null @@ -1,7 +0,0 @@ -package izumi.distage.config.model - -import izumi.distage.config.DistageConfigImpl - -final case class AppConfig(config: DistageConfigImpl) - -object AppConfig extends AppConfigSyntax {} diff --git a/distage/distage-framework/.js/src/main/scala/izumi/distage/roles/RoleAppMain.scala b/distage/distage-framework/.js/src/main/scala/izumi/distage/roles/RoleAppMain.scala index b5dc556bea..509ebdf49c 100644 --- a/distage/distage-framework/.js/src/main/scala/izumi/distage/roles/RoleAppMain.scala +++ b/distage/distage-framework/.js/src/main/scala/izumi/distage/roles/RoleAppMain.scala @@ -1,6 +1,7 @@ package izumi.distage.roles import distage.Injector +import distage.config.AppConfig import izumi.distage.framework.config.PlanningOptions import izumi.distage.model.Locator import izumi.distage.model.definition.{Activation, Axis, Module, ModuleDef} @@ -9,6 +10,7 @@ import izumi.distage.plugins.PluginConfig import izumi.distage.roles.RoleAppMain.ArgV import izumi.distage.roles.launcher.AppResourceProvider.AppResource import izumi.distage.roles.launcher.AppShutdownStrategy +import izumi.distage.roles.launcher.ActivationParser import izumi.functional.lifecycle.Lifecycle import izumi.functional.quasi.QuasiIO import izumi.fundamentals.platform.cli.model.raw.{RawAppArgs, RawEntrypointParams, RawRoleParams, RequiredRoles} @@ -107,7 +109,12 @@ abstract class RoleAppMain[F[_]]( ) ++ new ModuleDef { make[RawAppArgs].fromValue(RawAppArgs(RawEntrypointParams.empty, additionalRoles.requiredRoles)) make[PlanningOptions].fromValue(planningOptions()) - make[Activation].named("roleapp").fromValue(activation()) + make[ActivationParser].from[ActivationParser.Impl] + make[Activation].named("entrypoint").fromValue(activation()) + make[Activation].named("roleapp").from { + (parser: ActivationParser, config: AppConfig) => + parser.parseActivation(config) + } } } diff --git a/distage/distage-framework/.js/src/main/scala/izumi/distage/roles/launcher/ActivationParser.scala b/distage/distage-framework/.js/src/main/scala/izumi/distage/roles/launcher/ActivationParser.scala new file mode 100644 index 0000000000..a9334c98e9 --- /dev/null +++ b/distage/distage-framework/.js/src/main/scala/izumi/distage/roles/launcher/ActivationParser.scala @@ -0,0 +1,23 @@ +package izumi.distage.roles.launcher + +import distage.config.AppConfig +import izumi.distage.model.definition.{Activation, Id} +import scala.annotation.unused + +trait ActivationParser extends AbstractActivationParser {} + +object ActivationParser { + + class Impl( + defaultActivations: Activation@Id("default"), + additionalActivations: Activation@Id("additional"), + activation: Activation@Id("entrypoint"), + ) extends ActivationParser { + def parseActivation(@unused config: AppConfig): Activation = { + defaultActivations ++ + additionalActivations ++ + activation + } + } + +} diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/CheckableApp.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/CheckableApp.scala index 2398c823f4..818fd2efb2 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/CheckableApp.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/CheckableApp.scala @@ -173,7 +173,7 @@ abstract class RoleCheckableApp[F[_]](override implicit val tagK: TagK[F]) exten if (cfg.origin().resource() eq null) { throw new DIConfigReadException(s"Couldn't find a config resource with name `$resourceName` - file not found", null) } - AppConfig(cfg) + AppConfig(cfg, List.empty, List.empty) } } } diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/model/PlanCheckInput.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/model/PlanCheckInput.scala index c35f6dedc4..f37cadf1fa 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/model/PlanCheckInput.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/model/PlanCheckInput.scala @@ -1,8 +1,8 @@ package izumi.distage.framework.model import distage.Injector -import izumi.distage.framework.services.ConfigLoader -import izumi.distage.framework.services.ConfigLoader.ConfigLocation +import izumi.distage.framework.services.ConfigMerger.ConfigMergerImpl +import izumi.distage.framework.services.{ConfigArgsProvider, ConfigLoader, ConfigLocationProvider} import izumi.distage.model.definition.ModuleBase import izumi.distage.model.plan.Roots import izumi.distage.model.reflection.DIKey @@ -26,7 +26,11 @@ object PlanCheckInput { module: ModuleBase, roots: Roots, roleNames: Set[String] = Set.empty, - configLoader: ConfigLoader = new ConfigLoader.LocalFSImpl(IzLogger(), ConfigLocation.Default, ConfigLoader.Args.empty), + configLoader: ConfigLoader = { + val logger = IzLogger() + val merger = new ConfigMergerImpl(logger) + new ConfigLoader.LocalFSImpl(logger, merger, ConfigLocationProvider.Default, ConfigArgsProvider.Empty) + }, appPlugins: LoadedPlugins = LoadedPlugins.empty, bsPlugins: LoadedPlugins = LoadedPlugins.empty, )(implicit effectType: TagK[F], diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigArgsProvider.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigArgsProvider.scala new file mode 100644 index 0000000000..a45e84f0ad --- /dev/null +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigArgsProvider.scala @@ -0,0 +1,48 @@ +package izumi.distage.framework.services + +import izumi.distage.config.model.{GenericConfigSource, RoleConfig} +import izumi.distage.roles.RoleAppMain +import izumi.distage.roles.model.meta.RolesInfo +import izumi.fundamentals.platform.cli.model.raw.RawAppArgs + +import java.io.File +import scala.annotation.nowarn + +trait ConfigArgsProvider { + def args(): ConfigLoader.Args +} + +object ConfigArgsProvider { + + object Empty extends ConfigArgsProvider { + override def args(): ConfigLoader.Args = ConfigLoader.Args(None, List.empty) + } + + @nowarn("msg=Unused import") + class Default( + parameters: RawAppArgs, + rolesInfo: RolesInfo, + ) extends ConfigArgsProvider { + override def args(): ConfigLoader.Args = { + import scala.collection.compat.* + + val specifiedRoleConfigs: Map[String, Option[File]] = parameters.roles.iterator + .map(roleParams => roleParams.role -> roleParams.roleParameters.findValue(RoleAppMain.Options.configParam).asFile) + .toMap + + val roleConfigs = rolesInfo.availableRoleNames.toList.map { + roleName => + val source = specifiedRoleConfigs.get(roleName).flatten match { + case Some(value) => + GenericConfigSource.ConfigFile(value) + case _ => + GenericConfigSource.ConfigDefault + } + RoleConfig(roleName, rolesInfo.requiredRoleNames.contains(roleName), source) + } + val maybeGlobalConfig = parameters.globalParameters.findValue(RoleAppMain.Options.configParam).asFile + + ConfigLoader.Args(maybeGlobalConfig, roleConfigs) + } + } +} diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigLoader.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigLoader.scala index b8d9b04966..f903bbffee 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigLoader.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigLoader.scala @@ -1,20 +1,18 @@ package izumi.distage.framework.services -import com.typesafe.config.{Config, ConfigFactory, ConfigResolveOptions} -import izumi.distage.config.model.AppConfig -import izumi.distage.framework.services.ConfigLoader.LocalFSImpl.{ConfigLoaderException, ConfigSource, ResourceConfigKind} +import com.typesafe.config.{Config, ConfigFactory} +import izumi.distage.config.model.* import izumi.distage.model.definition.Id import izumi.distage.model.exceptions.DIException -import izumi.distage.roles.RoleAppMain -import izumi.distage.roles.model.meta.RolesInfo -import izumi.fundamentals.platform.cli.model.raw.RawAppArgs +import izumi.functional.IzEither.* +import izumi.fundamentals.platform.exceptions.IzThrowable.* import izumi.fundamentals.platform.resources.IzResources import izumi.fundamentals.platform.resources.IzResources.{LoadablePathReference, UnloadablePathReference} import izumi.fundamentals.platform.strings.IzString.* import izumi.logstage.api.IzLogger import java.io.{File, FileNotFoundException} -import scala.jdk.CollectionConverters.* +import scala.annotation.nowarn import scala.util.{Failure, Success, Try} /** @@ -51,184 +49,116 @@ import scala.util.{Failure, Success, Try} */ trait ConfigLoader extends AbstractConfigLoader {} +@nowarn("msg=Unused import") object ConfigLoader { - def empty: ConfigLoader = () => AppConfig(ConfigFactory.empty()) + def empty: ConfigLoader = () => AppConfig(ConfigFactory.empty(), List.empty, List.empty) import scala.collection.compat.* - trait ConfigLocation { - def forRole(roleName: String): Seq[ConfigSource] = ConfigLocation.defaultConfigReferences(roleName) - def forBase(filename: String): Seq[ConfigSource] = ConfigLocation.defaultConfigReferences(filename) - def defaultBaseConfigs: Seq[String] = ConfigLocation.defaultBaseConfigs - } - object ConfigLocation { - object Default extends ConfigLocation - - def defaultBaseConfigs: Seq[String] = Seq("application", "common") - - def defaultConfigReferences(name: String): Seq[ConfigSource] = { - Seq( - ConfigSource.Resource(s"$name.conf", ResourceConfigKind.Primary), - ConfigSource.Resource(s"$name-reference.conf", ResourceConfigKind.Primary), - ConfigSource.Resource(s"$name-reference-dev.conf", ResourceConfigKind.Development), - ) - } - } - final case class Args( global: Option[File], - role: Map[String, Option[File]], + configs: List[RoleConfig], ) - object Args { - def makeConfigLoaderArgs( - parameters: RawAppArgs, - rolesInfo: RolesInfo, - ): ConfigLoader.Args = { - val maybeGlobalConfig = parameters.globalParameters.findValue(RoleAppMain.Options.configParam).asFile - val emptyRoleConfigs = rolesInfo.availableRoleNames.map(_ -> None).toMap - val specifiedRoleConfigs = parameters.roles.iterator - .map(roleParams => roleParams.role -> roleParams.roleParameters.findValue(RoleAppMain.Options.configParam).asFile) - .toMap - ConfigLoader.Args(maybeGlobalConfig, (emptyRoleConfigs ++ specifiedRoleConfigs).view.toMap) - } - - def empty: ConfigLoader.Args = ConfigLoader.Args(None, Map.empty) - } + final class ConfigLoaderException(message: String, val failures: List[Throwable]) extends DIException(message) open class LocalFSImpl( logger: IzLogger @Id("early"), - configLocation: ConfigLocation, - configArgs: ConfigLoader.Args, + merger: ConfigMerger, + configLocation: ConfigLocationProvider, + args: ConfigArgsProvider, ) extends ConfigLoader { protected def resourceClassLoader: ClassLoader = getClass.getClassLoader def loadConfig(): AppConfig = { - val commonReferenceConfigs = configLocation.defaultBaseConfigs.flatMap(configLocation.forBase) - val commonExplicitConfigs = configArgs.global.map(ConfigSource.File.apply).toList - - val (roleReferenceConfigs, roleExplicitConfigs) = (configArgs.role: Iterable[(String, Option[File])]).partitionMap { - case (role, None) => Left(configLocation.forRole(role)) - case (_, Some(file)) => Right(ConfigSource.File(file)) + val configArgs = args.args() + + val maybeLoadedRoleConfigs = configArgs.configs.map { + rc => + val loaded = rc.configSource match { + case GenericConfigSource.ConfigFile(file) => + Seq(loadConfigSource(ConfigSource.File(file))) + case GenericConfigSource.ConfigDefault => + configLocation.forRole(rc.role).map(loadConfigSource) + } + (rc, loaded) } - val allConfigs = (roleExplicitConfigs.iterator ++ commonExplicitConfigs ++ roleReferenceConfigs.iterator.flatten ++ commonReferenceConfigs).toList - - val (cfgInfo, loaded) = loadConfigSources(allConfigs) - - logger.info(s"Using system properties with fallback ${cfgInfo.niceList() -> "config files"}") - - val (good, bad) = loaded.partition(_._2.isSuccess) - - if (bad.nonEmpty) { - val failuresList = bad.collect { - case (s, Failure(f)) => - s"$s: $f" - } - val failures = failuresList.niceList() - - logger.error(s"Failed to load configs: $failures") - throw new ConfigLoaderException(s"Failed to load configs: failures=$failures", failuresList) - } else { - val folded = foldConfigs(good.collect { - case (src, Success(c)) => src -> c - }) - - val config = ConfigFactory - .systemProperties() - .withFallback(folded) - .resolve() + val commonExplicitConfigs = configArgs.global.map(ConfigSource.File.apply).toList + val commonReferenceConfigs = configLocation.commonReferenceConfigs.toList + val commonConfigs = commonReferenceConfigs ++ commonExplicitConfigs + val loaded = for { + loadedCommonConfigs <- commonConfigs.map(loadConfigSource).map(_.toEither).biSequenceScalar + loadedRoleConfigs <- maybeLoadedRoleConfigs.map { + case (rc, loaded) => + loaded.map { + lr => + lr.toEither + }.biSequenceScalar match { + case Left(value) => + Left(value) + case Right(value) => + Right(LoadedRoleConfigs(rc, value)) + } + }.biSequence + } yield { + (loadedCommonConfigs, loadedRoleConfigs) + } - AppConfig(config) + loaded match { + case Left(value) => + val failures = value.map(f => s"Failed to load ${f.src} ${f.clue}: ${f.failure.stacktraceString}") + logger.error(s"Cannot load configuration: ${failures.toList.niceList() -> "failures"}") + throw new ConfigLoaderException(s"Cannot load configuration: ${failures.toList.niceList()}", value.map(_.failure).toList) + case Right((shared, role)) => + val merged = merger.merge(shared, role) + AppConfig(merged, shared, role) } - } - protected def loadConfigSources(allConfigs: List[ConfigSource]): (List[String], List[(ConfigSource, Try[Config])]) = { - allConfigs.map(loadConfigSource).unzip } - protected def loadConfigSource(configSource: ConfigSource): (String, (ConfigSource, Try[Config])) = configSource match { - case r: ConfigSource.Resource => - def tryLoadResource(): Try[Config] = { - Try(ConfigFactory.parseResources(resourceClassLoader, r.name)).flatMap { - cfg => - if (cfg.origin().resource() eq null) { - Failure(new FileNotFoundException(s"Couldn't find config file $r")) - } else Success(cfg) + protected def loadConfigSource(configSource: ConfigSource): ConfigLoadResult = { + configSource match { + case r: ConfigSource.Resource => + def tryLoadResource(): Try[Config] = { + Try(ConfigFactory.parseResources(resourceClassLoader, r.name)).flatMap { + cfg => + if (cfg.origin().resource() eq null) { + Failure(new FileNotFoundException(s"Couldn't find config file $r")) + } else Success(cfg) + } } - } - - IzResources(resourceClassLoader).getPath(r.name) match { - case Some(LoadablePathReference(path, _)) => - s"$r (available: $path)" -> (r -> tryLoadResource()) - case Some(UnloadablePathReference(path)) => - s"$r (exists: $path)" -> (r -> tryLoadResource()) - case None => - s"$r (missing)" -> (r -> Success(ConfigFactory.empty())) - } - - case f: ConfigSource.File => - if (f.file.exists()) { - s"$f (exists: ${f.file.getCanonicalPath})" -> (f -> Try(ConfigFactory.parseFile(f.file)).flatMap { - cfg => if (cfg.origin().filename() ne null) Success(cfg) else Failure(new FileNotFoundException(s"Couldn't find config file $f")) - }) - } else { - s"$f (missing)" -> (f -> Success(ConfigFactory.empty())) - } - } - protected def foldConfigs(roleConfigs: IterableOnce[(ConfigSource, Config)]): Config = { - roleConfigs.iterator.foldLeft(ConfigFactory.empty()) { - case (acc, (src, cfg)) => - verifyConfigs(src, cfg, acc) - acc.withFallback(cfg) - } - } - - protected def verifyConfigs(src: ConfigSource, cfg: Config, acc: Config): Unit = { - val duplicateKeys = getKeys(acc) intersect getKeys(cfg) - if (duplicateKeys.nonEmpty) { - src match { - case ConfigSource.Resource(_, ResourceConfigKind.Development) => - logger.debug(s"Some keys in supplied ${src -> "development config"} duplicate already defined keys: ${duplicateKeys.niceList() -> "keys" -> null}") - case _ => - logger.warn(s"Some keys in supplied ${src -> "config"} duplicate already defined keys: ${duplicateKeys.niceList() -> "keys" -> null}") - } - } - } + IzResources(resourceClassLoader).getPath(r.name) match { + case Some(LoadablePathReference(path, _)) => + doLoad(s"$r (available: $path)", configSource, tryLoadResource()) + case Some(UnloadablePathReference(path)) => + doLoad(s"$r (exists: $path)", configSource, tryLoadResource()) + case None => + doLoad(s"$r (missing)", configSource, Success(ConfigFactory.empty())) + } - protected def getKeys(c: Config): collection.Set[String] = { - if (c.isResolved) { - c.entrySet().asScala.map(_.getKey) - } else { - Try { - c.resolve(ConfigResolveOptions.defaults().setAllowUnresolved(true)) - }.toOption.filter(_.isResolved) match { - case Some(value) => value.entrySet().asScala.map(_.getKey) - case None => Set.empty - } + case f: ConfigSource.File => + if (f.file.exists()) { + doLoad( + s"$f (exists: ${f.file.getCanonicalPath})", + configSource, + Try(ConfigFactory.parseFile(f.file)).flatMap { + cfg => if (cfg.origin().filename() ne null) Success(cfg) else Failure(new FileNotFoundException(s"Couldn't find config file $f")) + }, + ) + } else { + doLoad(s"$f (missing)", configSource, Failure(new FileNotFoundException(f.file.getCanonicalPath))) + } } } - } - - object LocalFSImpl { - sealed trait ResourceConfigKind - object ResourceConfigKind { - case object Primary extends ResourceConfigKind - case object Development extends ResourceConfigKind - } - - sealed trait ConfigSource - object ConfigSource { - final case class Resource(name: String, kind: ResourceConfigKind) extends ConfigSource { - override def toString: String = s"resource:$name" - } - final case class File(file: java.io.File) extends ConfigSource { - override def toString: String = s"file:$file" + private def doLoad(clue: String, source: ConfigSource, loader: => Try[Config]): ConfigLoadResult = { + loader match { + case Failure(exception) => ConfigLoadResult.Failure(clue, source, exception) + case Success(value) => ConfigLoadResult.Success(clue, source, value) } } - final class ConfigLoaderException(message: String, val failures: List[String]) extends DIException(message) } } diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigLocationProvider.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigLocationProvider.scala new file mode 100644 index 0000000000..fd5f8b40b4 --- /dev/null +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigLocationProvider.scala @@ -0,0 +1,31 @@ +package izumi.distage.framework.services + +import izumi.distage.config.model.{ConfigSource, ResourceConfigKind} + +trait ConfigLocationProvider { + def forRole(roleName: String): Seq[ConfigSource] + + def commonReferenceConfigs: Seq[ConfigSource] +} + +object ConfigLocationProvider { + object Default extends ConfigLocationProvider { + def forRole(roleName: String): Seq[ConfigSource] = { + ConfigLocationProvider.defaultConfigReferences(roleName) + } + + def commonReferenceConfigs: Seq[ConfigSource] = { + ConfigLocationProvider.defaultBaseConfigs.flatMap(ConfigLocationProvider.defaultConfigReferences) + } + } + + private def defaultBaseConfigs: Seq[String] = Seq("application", "common") + + private def defaultConfigReferences(name: String): Seq[ConfigSource] = { + Seq( + ConfigSource.Resource(s"$name.conf", ResourceConfigKind.Primary), + ConfigSource.Resource(s"$name-reference.conf", ResourceConfigKind.Primary), + ConfigSource.Resource(s"$name-reference-dev.conf", ResourceConfigKind.Development), + ) + } +} diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigMerger.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigMerger.scala new file mode 100644 index 0000000000..28864dd4b3 --- /dev/null +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ConfigMerger.scala @@ -0,0 +1,72 @@ +package izumi.distage.framework.services + +import com.typesafe.config.{Config, ConfigFactory, ConfigResolveOptions} +import izumi.distage.config.model.* +import izumi.distage.model.definition.Id +import izumi.fundamentals.platform.strings.IzString.* +import izumi.logstage.api.IzLogger + +import scala.jdk.CollectionConverters.* +import scala.util.Try + +trait ConfigMerger { + def merge(shared: List[ConfigLoadResult.Success], role: List[LoadedRoleConfigs]): Config + def mergeFilter(shared: List[ConfigLoadResult.Success], role: List[LoadedRoleConfigs], filter: LoadedRoleConfigs => Boolean): Config + def foldConfigs(roleConfigs: Iterable[ConfigLoadResult.Success]): Config +} + +object ConfigMerger { + class ConfigMergerImpl(logger: IzLogger @Id("early")) extends ConfigMerger { + override def merge(shared: List[ConfigLoadResult.Success], role: List[LoadedRoleConfigs]): Config = { + mergeFilter(shared, role, _.roleConfig.active) + } + + override def mergeFilter(shared: List[ConfigLoadResult.Success], role: List[LoadedRoleConfigs], filter: LoadedRoleConfigs => Boolean): Config = { + val cfgInfo = (shared ++ role.flatMap(_.loaded)).map(c => c.clue) + logger.info(s"Using system properties with fallback ${cfgInfo.niceList() -> "config files"}") + + val toMerge = shared ++ role.filter(filter).flatMap(_.loaded) + + val folded = foldConfigs(toMerge) + + ConfigFactory + .systemProperties() + .withFallback(folded) + .resolve() + } + + def foldConfigs(roleConfigs: Iterable[ConfigLoadResult.Success]): Config = { + roleConfigs.iterator.foldLeft(ConfigFactory.empty()) { + case (acc, loaded) => + verifyConfigs(loaded, acc) + acc.withFallback(loaded.config) + } + } + + protected def verifyConfigs(loaded: ConfigLoadResult.Success, acc: Config): Unit = { + val duplicateKeys = getKeys(acc) intersect getKeys(loaded.config) + if (duplicateKeys.nonEmpty) { + loaded.src match { + case ConfigSource.Resource(_, ResourceConfigKind.Development) => + logger.debug(s"Some keys in supplied ${loaded.src -> "development config"} duplicate already defined keys: ${duplicateKeys.niceList() -> "keys" -> null}") + case _ => + logger.warn(s"Some keys in supplied ${loaded.src -> "config"} duplicate already defined keys: ${duplicateKeys.niceList() -> "keys" -> null}") + } + } + } + + protected def getKeys(c: Config): collection.Set[String] = { + if (c.isResolved) { + c.entrySet().asScala.map(_.getKey) + } else { + Try { + c.resolve(ConfigResolveOptions.defaults().setAllowUnresolved(true)) + }.toOption.filter(_.isResolved) match { + case Some(value) => value.entrySet().asScala.map(_.getKey) + case None => Set.empty + } + } + } + + } +} diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootArgsModule.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootArgsModule.scala index cb405eefbf..b47ab780c5 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootArgsModule.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootArgsModule.scala @@ -1,5 +1,6 @@ package izumi.distage.roles +import distage.config.AppConfig import izumi.distage.model.definition.ModuleDef import izumi.distage.modules.DefaultModule import izumi.distage.roles.RoleAppMain.ArgV @@ -38,8 +39,8 @@ class RoleAppBootArgsModule[F[_]: TagK: DefaultModule]( make[RoleAppActivationParser].from[RoleAppActivationParser.Impl] make[ActivationParser].from[ActivationParser.Impl] make[Activation].named("roleapp").from { - (parser: ActivationParser) => - parser.parseActivation() + (parser: ActivationParser, config: AppConfig) => + parser.parseActivation(config) } make[AppArgsInterceptor].from[AppArgsInterceptor.Impl] diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootConfigModule.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootConfigModule.scala index a7072a8127..58a8a48b53 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootConfigModule.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/RoleAppBootConfigModule.scala @@ -1,15 +1,17 @@ package izumi.distage.roles import izumi.distage.config.model.AppConfig -import izumi.distage.framework.services.ConfigLoader +import izumi.distage.framework.services.{ConfigArgsProvider, ConfigLoader, ConfigLocationProvider, ConfigMerger} import izumi.distage.model.definition.ModuleDef import izumi.distage.modules.DefaultModule import izumi.reflect.TagK class RoleAppBootConfigModule[F[_]: TagK: DefaultModule]() extends ModuleDef { make[ConfigLoader].from[ConfigLoader.LocalFSImpl] - make[ConfigLoader.ConfigLocation].from(ConfigLoader.ConfigLocation.Default) - make[ConfigLoader.Args].from(ConfigLoader.Args.makeConfigLoaderArgs _) + make[ConfigMerger].from[ConfigMerger.ConfigMergerImpl] + make[ConfigLocationProvider].from(ConfigLocationProvider.Default) + // make[ConfigLoader.Args].from(ConfigLoader.Args.makeConfigLoaderArgs _) + make[ConfigArgsProvider].from[ConfigArgsProvider.Default] make[AppConfig].from { (configLoader: ConfigLoader) => configLoader.loadConfig() diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala index 4500bca660..ba54f008fc 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala @@ -1,19 +1,19 @@ package izumi.distage.roles.bundled import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions} +import distage.config.AppConfig +import distage.{BootstrapModuleDef, Plan} import izumi.distage.config.model.ConfTag -import izumi.distage.framework.services.RoleAppPlanner +import izumi.distage.framework.services.{ConfigMerger, RoleAppPlanner} import izumi.distage.model.definition.Id -import izumi.functional.quasi.QuasiIO +import izumi.distage.model.plan.ExecutableOp import izumi.distage.model.plan.operations.OperationOrigin -import izumi.distage.model.plan.{ExecutableOp, Plan} -import izumi.distage.roles.bundled.ConfigWriter.{ConfigPath, ConfigurableComponent, ExtractConfigPath, WriteReference} +import izumi.distage.roles.bundled.ConfigWriter.{ConfigPath, ExtractConfigPath, WriteReference} import izumi.distage.roles.model.meta.{RoleBinding, RolesInfo} import izumi.distage.roles.model.{RoleDescriptor, RoleTask} -import izumi.functional.Value +import izumi.functional.quasi.QuasiIO import izumi.fundamentals.platform.cli.model.raw.RawEntrypointParams import izumi.fundamentals.platform.cli.model.schema.{ParserDef, RoleParserSchema} -import scala.annotation.unused import izumi.fundamentals.platform.resources.ArtifactVersion import izumi.logstage.api.IzLogger import izumi.logstage.api.logger.LogRouter @@ -21,7 +21,7 @@ import izumi.logstage.distage.LogstageModule import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} -import scala.annotation.nowarn +import scala.annotation.{nowarn, unused} import scala.collection.compat.immutable.ArraySeq import scala.util.Try @@ -30,6 +30,7 @@ final class ConfigWriter[F[_]]( launcherVersion: ArtifactVersion @Id("launcher-version"), roleInfo: RolesInfo, roleAppPlanner: RoleAppPlanner, + appConfig: AppConfig, F: QuasiIO[F], ) extends RoleTask[F] with BundledTask { @@ -38,6 +39,7 @@ final class ConfigWriter[F[_]]( // should've been unnecessary after https://github.com/7mind/izumi/issues/779 // but, the contents of the MainAppModule (including `"activation"` config read) are not accessible here from `RoleAppPlanner` yet... private[this] val _HackyMandatorySection = ConfigPath("activation") + private val configMerger = new ConfigMerger.ConfigMergerImpl(logger) override def start(roleParameters: RawEntrypointParams, @unused freeArgs: Vector[String]): F[Unit] = { F.maybeSuspend { @@ -56,29 +58,32 @@ final class ConfigWriter[F[_]]( logger.info(s"Going to process ${roleInfo.availableRoleBindings.size -> "roles"}") - val commonComponent = ConfigurableComponent("common", Some(launcherVersion), None) - val commonConfig = buildConfig(options, commonComponent) - - if (!options.includeCommon) { - writeConfig(options, commonComponent, None, commonConfig) - } + val index = appConfig.roles.map(c => (c.roleConfig.role, c)).toMap + assert(roleInfo.availableRoleNames == index.keySet) roleInfo.availableRoleBindings.foreach { role => - val component = ConfigurableComponent(role.descriptor.id, role.descriptor.artifact.map(_.version), Some(commonConfig)) - val refConfig = buildConfig(options, component) - val versionedComponent = if (options.useLauncherVersion) { - component.copy(version = Some(ArtifactVersion(launcherVersion.version))) - } else { - component - } - try { - writeConfig(options, versionedComponent, None, refConfig) - minimizedConfig(refConfig, role) + val roleId = role.descriptor.id + val roleVersion = if (options.useLauncherVersion) { + Some(launcherVersion.version) + } else { + role.descriptor.artifact.map(_.version).map(_.version) + } + val subLogger = logger("role" -> roleId) + val fileNameFull = outputFileName(roleId, roleVersion, options.asJson, Some("full")) + + val loaded = index(roleId) + + // TODO: mergeFilter considers system properties, we might want to AVOID that in configwriter + val mergedRoleConfig = configMerger.mergeFilter(appConfig.shared, List(loaded), _ => true) + writeConfig(options, fileNameFull, mergedRoleConfig, subLogger) + + minimizedConfig(mergedRoleConfig, role) .foreach { cfg => - writeConfig(options, versionedComponent, Some("minimized"), cfg) + val fileNameMinimized = outputFileName(roleId, roleVersion, options.asJson, Some("minimized")) + writeConfig(options, fileNameMinimized, cfg, subLogger) } } catch { case exception: Throwable => @@ -88,36 +93,30 @@ final class ConfigWriter[F[_]]( } } - private[this] def buildConfig(config: WriteReference, cmp: ConfigurableComponent): Config = { - val referenceConfig = s"${cmp.roleId}-reference.conf" - logger.info(s"[${cmp.roleId}] Resolving $referenceConfig... with ${config.includeCommon -> "shared sections"}") - - val reference = Value(ConfigFactory.parseResourcesAnySyntax(referenceConfig)) - .mut(cmp.parent.filter(_ => config.includeCommon))(_.withFallback(_)) - .get - .resolve() - - if (reference.isEmpty) { - logger.warn(s"[${cmp.roleId}] Reference config is empty.") - } - - val resolved = ConfigFactory - .systemProperties() - .withFallback(reference) - .resolve() + private[this] def outputFileName(service: String, version: Option[String], asJson: Boolean, suffix: Option[String]): String = { + val extension = if (asJson) "json" else "conf" + val vstr = version.getOrElse("0.0.0-UNKNOWN") + val suffixStr = suffix.fold("")("-" + _) - val filtered = cleanupEffectiveAppConfig(resolved, reference) - filtered.checkValid(reference) - filtered + s"$service$suffixStr-$vstr.$extension" } - private[this] def minimizedConfig(config: Config, role: RoleBinding): Option[Config] = { + private[this] def minimizedConfig(roleConfig: Config, role: RoleBinding): Option[Config] = { val roleDIKey = role.binding.key - val bootstrapOverride = new LogstageModule(LogRouter.nullRouter, setupStaticLogRouter = false) + val roleConfigs = appConfig.roles.map(lrc => lrc.copy(roleConfig = lrc.roleConfig.copy(active = lrc.roleConfig.role == role.descriptor.id))) + + // TODO: mergeFilter considers system properties, we might want to AVOID that in configwriter + // TODO: here we accept all the role configs regardless of them being active or not, that might resolve cross-role conflicts in unpredictable manner + val fullConfig = configMerger.mergeFilter(appConfig.shared, roleConfigs, _ => true) + val correctedAppConfig = appConfig.copy(config = fullConfig, roles = roleConfigs) + + val bootstrapOverride = new BootstrapModuleDef { + include(new LogstageModule(LogRouter.nullRouter, setupStaticLogRouter = false)) + } val plans = roleAppPlanner - .reboot(bootstrapOverride) + .reboot(bootstrapOverride, Some(correctedAppConfig)) .makePlan(Set(roleDIKey)) def getConfig(plan: Plan): Iterator[ConfigPath] = { @@ -130,46 +129,70 @@ final class ConfigWriter[F[_]]( getConfig(plans.app).toSet + _HackyMandatorySection if (plans.app.stepsUnordered.exists(_.target == roleDIKey)) { - Some(ConfigWriter.minimized(resolvedConfig, config)) + Some(ConfigWriter.minimized(resolvedConfig, roleConfig)) } else { logger.warn(s"$roleDIKey is not in the refined plan") None } } - private[this] def writeConfig(config: WriteReference, cmp: ConfigurableComponent, suffix: Option[String], typesafeConfig: Config): Try[Unit] = { - val fileName = outputFileName(cmp.roleId, cmp.version, config.asJson, suffix) - val target = Paths.get(config.targetDir, fileName) - + // private[this] def buildConfig(config: WriteReference, cmp: ConfigurableComponent): Config = { + // val referenceConfig = s"${cmp.roleId}-reference.conf" + // logger.info(s"[${cmp.roleId}] Resolving $referenceConfig... with ${config.includeCommon -> "shared sections"}") + // + // val reference = Value(ConfigFactory.parseResourcesAnySyntax(referenceConfig)) + // .mut(cmp.parent.filter(_ => config.includeCommon))(_.withFallback(_)) + // .get + // .resolve() + // + // if (reference.isEmpty) { + // logger.warn(s"[${cmp.roleId}] Reference config is empty.") + // } + // + // val resolved = ConfigFactory + // .systemProperties() + // .withFallback(reference) + // .resolve() + // + // val filtered = cleanupEffectiveAppConfig(resolved, reference) + // filtered.checkValid(reference) + // filtered + // } + // + // + // + // // TODO: sdk? + // @nowarn("msg=Unused import") + // private[this] def cleanupEffectiveAppConfig(effectiveAppConfig: Config, reference: Config): Config = { + // import scala.collection.compat._ + // import scala.jdk.CollectionConverters._ + // + // ConfigFactory.parseMap(effectiveAppConfig.root().unwrapped().asScala.view.filterKeys(reference.hasPath).toMap.asJava) + // } + // + // private[this] def outputFileName(service: String, version: Option[ArtifactVersion], asJson: Boolean, suffix: Option[String]): String = { + // val extension = if (asJson) "json" else "conf" + // val vstr = version.map(_.version).getOrElse("0.0.0-UNKNOWN") + // val suffixStr = suffix.fold("")("-" + _) + // + // s"$service$suffixStr-$vstr.$extension" + // } + // + + private[this] def writeConfig(options: WriteReference, fileName: String, typesafeConfig: Config, subLogger: IzLogger): Try[Unit] = { + val configRenderOptions = ConfigRenderOptions.defaults.setOriginComments(false).setComments(false) + + val target = Paths.get(options.targetDir, fileName) Try { - val cfg = typesafeConfig.root().render(configRenderOptions.setJson(config.asJson)) + val cfg = typesafeConfig.root().render(configRenderOptions.setJson(options.asJson)) val bytes = cfg.getBytes(StandardCharsets.UTF_8) Files.write(target, bytes) - logger.info(s"[${cmp.roleId}] Reference config saved -> $target (${bytes.size} bytes)") + subLogger.info(s"Reference config saved -> $target (${bytes.size} bytes)") }.recover { case error: Throwable => - logger.error(s"[${cmp.roleId -> "component id" -> null}] Can't write reference config to $target, $error") + subLogger.error(s"Can't write reference config to $target, $error") } } - - // TODO: sdk? - @nowarn("msg=Unused import") - private[this] def cleanupEffectiveAppConfig(effectiveAppConfig: Config, reference: Config): Config = { - import scala.collection.compat._ - import scala.jdk.CollectionConverters._ - - ConfigFactory.parseMap(effectiveAppConfig.root().unwrapped().asScala.view.filterKeys(reference.hasPath).toMap.asJava) - } - - private[this] def outputFileName(service: String, version: Option[ArtifactVersion], asJson: Boolean, suffix: Option[String]): String = { - val extension = if (asJson) "json" else "conf" - val vstr = version.map(_.version).getOrElse("0.0.0-UNKNOWN") - val suffixStr = suffix.fold("")("-" + _) - - s"$service$suffixStr-$vstr.$extension" - } - - private[this] final val configRenderOptions = ConfigRenderOptions.defaults.setOriginComments(false).setComments(false) } object ConfigWriter extends RoleDescriptor { @@ -220,8 +243,8 @@ object ConfigWriter extends RoleDescriptor { @nowarn("msg=Unused import") def minimized(requiredPaths: Set[ConfigPath], source: Config): Config = { - import scala.collection.compat._ - import scala.jdk.CollectionConverters._ + import scala.collection.compat.* + import scala.jdk.CollectionConverters.* val paths = requiredPaths.map(_.toPath) diff --git a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/ActivationParser.scala b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/ActivationParser.scala index d87a6b06e7..fe81b75cf9 100644 --- a/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/ActivationParser.scala +++ b/distage/distage-framework/.jvm/src/main/scala/izumi/distage/roles/launcher/ActivationParser.scala @@ -20,7 +20,6 @@ object ActivationParser { class Impl( parser: RoleAppActivationParser, parameters: RawAppArgs, - config: AppConfig, activationInfo: ActivationInfo, defaultActivations: Activation @Id("default"), additionalActivations: Activation @Id("additional"), @@ -28,10 +27,11 @@ object ActivationParser { warnUnsetActivations: Boolean @Id("distage.roles.activation.warn-unset"), ) extends ActivationParser { - def parseActivation(): Activation = { + def parseActivation(config: AppConfig): Activation = { val cmdChoices = parameters.globalParameters.findValues(RoleAppMain.Options.use).map(AxisPoint parseAxisPoint _.value) val cmdActivations = parser.parseActivation(cmdChoices, activationInfo) + // println(s"PARSEACT: ${config.config}") val configChoices = if (config.config.hasPath(configActivationSection)) { ActivationConfig.diConfigReader.decodeConfig(configActivationSection)(config.config).activation.map(AxisPoint(_)) } else Iterable.empty diff --git a/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala b/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala index 659fae08d1..0f013bfd71 100644 --- a/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala +++ b/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala @@ -5,6 +5,7 @@ import cats.effect.unsafe.IORuntime import com.github.pshirshov.test.plugins.{StaticTestMainLogIO2, StaticTestRole} import com.github.pshirshov.test3.plugins.Fixture3 import com.typesafe.config.ConfigFactory +import distage.config.AppConfig import distage.plugins.{PluginBase, PluginDef} import distage.{DIKey, Injector, Locator, LocatorRef} import izumi.distage.framework.config.PlanningOptions @@ -15,6 +16,7 @@ import izumi.distage.model.definition.{Activation, BootstrapModule, Lifecycle} import izumi.distage.modules.DefaultModule import izumi.distage.plugins.PluginConfig import izumi.distage.roles.DebugProperties +import izumi.distage.roles.launcher.ActivationParser import izumi.distage.roles.test.fixtures.* import izumi.distage.roles.test.fixtures.Fixture.* import izumi.distage.roles.test.fixtures.roles.TestRole00 @@ -179,6 +181,9 @@ class RoleAppTest extends AnyWordSpec with WithProperties { bsModule = BootstrapModule.empty, bootloader = Injector.bootloader[Identity](BootstrapModule.empty, Activation.empty, DefaultModule.empty, PlannerInput(definition, Activation.empty, roots)), logger = logger, + parser = new ActivationParser { + override def parseActivation(config: AppConfig): Activation = ??? + }, ) val plans = roleAppPlanner.makePlan(roots) @@ -216,6 +221,9 @@ class RoleAppTest extends AnyWordSpec with WithProperties { bsModule = BootstrapModule.empty, bootloader = Injector.bootloader[Identity](BootstrapModule.empty, Activation.empty, DefaultModule.empty, PlannerInput(definition, Activation.empty, roots)), logger = logger, + parser = new ActivationParser { + override def parseActivation(config: AppConfig): Activation = ??? + }, ) val plans = roleAppPlanner.makePlan(roots) @@ -257,6 +265,9 @@ class RoleAppTest extends AnyWordSpec with WithProperties { bsModule = BootstrapModule.empty, bootloader = Injector.bootloader[Identity](BootstrapModule.empty, Activation.empty, DefaultModule.empty, PlannerInput(definition, Activation.empty, roots)), logger = logger, + parser = new ActivationParser { + override def parseActivation(config: AppConfig): Activation = ??? + }, ) val plans = roleAppPlanner.makePlan(roots) @@ -289,14 +300,14 @@ class RoleAppTest extends AnyWordSpec with WithProperties { TestEntrypoint.main(Array("-ll", logLevel, "-u", "axiscomponentaxis:incorrect", ":configwriter", "-t", targetPath)) } - val cwCfg = cfg("configwriter", version) + val cwCfg = cfg("configwriter-full", version) val cwCfgMin = cfg("configwriter-minimized", version) assert(cwCfg.exists(), s"$cwCfg exists") assert(cwCfgMin.exists(), s"$cwCfgMin exists") assert(cwCfg.length() > cwCfgMin.length()) - val role0Cfg = cfg("testrole00", version) + val role0Cfg = cfg("testrole00-full", version) val role0CfgMin = cfg("testrole00-minimized", version) assert(role0Cfg.exists(), s"$role0Cfg exists") @@ -316,11 +327,11 @@ class RoleAppTest extends AnyWordSpec with WithProperties { assert(role0CfgMinParsed.hasPath("testservice")) assert(role0CfgMinParsed.getString("testservice2.strval") == "xxx") - assert(role0CfgMinParsed.getString("testservice.overridenInt") == "111") + assert(role0CfgMinParsed.getString("testservice.overridenInt") == "222") assert(role0CfgMinParsed.getInt("testservice.systemPropInt") == 265) assert(role0CfgMinParsed.getList("testservice.systemPropList").unwrapped().asScala.toList == List("111", "222")) - val role4Cfg = cfg("testrole04", version) + val role4Cfg = cfg("testrole04-full", version) val role4CfgMin = cfg("testrole04-minimized", version) assert(role4Cfg.exists(), s"$role4Cfg exists") diff --git a/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala b/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala index 32660c903e..31d71bf3e3 100644 --- a/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala +++ b/distage/distage-framework/.jvm/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala @@ -51,7 +51,7 @@ object roles { override def start(roleParameters: RawEntrypointParams, freeArgs: Vector[String]): Lifecycle[F, Unit] = Lifecycle.make(QuasiIO[F].maybeSuspend { logger.info(s"[TestRole00] started: $roleParameters, $freeArgs, $dummies, $conflict") - assert(conf.overridenInt == 111) + assert(conf.overridenInt == 222, s"Common value is 111, role-specific value is 222, found ${conf.overridenInt}") }) { _ => QuasiIO[F].maybeSuspend { @@ -179,4 +179,3 @@ class FailingRole02[F[_]: QuasiIO]( object FailingRole02 extends RoleDescriptor { override final val id = "failingrole02" } - diff --git a/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala b/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala index d1cea0ca2e..120de4ca41 100644 --- a/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala +++ b/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala @@ -1,20 +1,22 @@ package izumi.distage.framework.services -import distage.{Injector, PlannerInput} +import distage.config.{AppConfig, AppConfigModule} +import distage.{BootstrapModuleDef, Injector, PlannerInput} import izumi.distage.framework.config.PlanningOptions import izumi.distage.framework.services.RoleAppPlanner.AppStartupPlans -import izumi.distage.model.definition.{Activation, BootstrapModule, Id} -import izumi.functional.quasi.{QuasiAsync, QuasiIO, QuasiIORunner} +import izumi.distage.model.definition.{Activation, BootstrapModule, Id, ModuleBase} import izumi.distage.model.plan.{Plan, Roots} -import izumi.distage.model.recursive.{BootConfig, Bootloader, BootstrappedApp} +import izumi.distage.model.recursive.{BootConfig, Bootloader} import izumi.distage.model.reflection.DIKey import izumi.distage.modules.DefaultModule +import izumi.distage.roles.launcher.ActivationParser +import izumi.functional.quasi.{QuasiAsync, QuasiIO, QuasiIORunner} import izumi.fundamentals.platform.functional.Identity import izumi.logstage.api.IzLogger import izumi.reflect.TagK trait RoleAppPlanner { - def reboot(bsModule: BootstrapModule): RoleAppPlanner + def reboot(bsModule: BootstrapModule, config: Option[AppConfig]): RoleAppPlanner def makePlan(appMainRoots: Set[DIKey]): AppStartupPlans } @@ -32,6 +34,7 @@ object RoleAppPlanner { bsModule: BootstrapModule @Id("roleapp"), bootloader: Bootloader @Id("roleapp"), logger: IzLogger, + parser: ActivationParser, )(implicit defaultModule: DefaultModule[F] ) extends RoleAppPlanner { self => @@ -42,30 +45,37 @@ object RoleAppPlanner { DIKey.get[QuasiAsync[F]], ) - override def reboot(bsOverride: BootstrapModule): RoleAppPlanner = { - new RoleAppPlanner.Impl[F](options, activation, bsModule overriddenBy bsOverride, bootloader, logger) + override def reboot(bsOverride: BootstrapModule, config: Option[AppConfig]): RoleAppPlanner = { + val configOverride = new BootstrapModuleDef { + config.foreach(cfg => include(AppConfigModule(cfg))) + } + val updatedBsModule = bsModule overriddenBy bsOverride overriddenBy configOverride + + val activation = config.map(parser.parseActivation).getOrElse(this.activation) + + new RoleAppPlanner.Impl[F](options, activation, updatedBsModule, bootloader, logger, parser) } override def makePlan(appMainRoots: Set[DIKey]): AppStartupPlans = { logger.trace(s"Application will use: ${appMainRoots -> "app roots"} and $activation") - // TODO: why .module doesn't work within for-comprehension?.. - def log(runtimeBsApp: BootstrappedApp): Either[Nothing, Unit] = Right { - logger.trace(s"Bootstrap plan:\n${runtimeBsApp.plan.render() -> "bootstrap dump" -> null}") - logger.trace(s"App module: ${runtimeBsApp.module -> "app module" -> null}") - } - val out = for { bootstrapped <- bootloader.boot( BootConfig( - bootstrap = _ => bsModule, + bootstrap = _ => + bsModule overriddenBy new BootstrapModuleDef { + make[RoleAppPlanner].fromValue(self) + }, activation = _ => activation, roots = _ => Roots(runtimeGcRoots), ) ) runtimeKeys = bootstrapped.plan.keys - _ <- log(bootstrapped) + _ <- Right { + logger.trace(s"Bootstrap plan:\n${bootstrapped.plan.render() -> "bootstrap dump" -> null}") + logger.trace(s"App module: ${(bootstrapped.module: ModuleBase) -> "app module" -> null}") + } appPlan <- bootstrapped.injector.plan(PlannerInput(bootstrapped.module.drop(runtimeKeys), activation, appMainRoots)) } yield { diff --git a/distage/distage-framework/src/main/scala/izumi/distage/roles/launcher/AbstractActivationParser.scala b/distage/distage-framework/src/main/scala/izumi/distage/roles/launcher/AbstractActivationParser.scala index 1e00fa9dd7..8d5dd1b96a 100644 --- a/distage/distage-framework/src/main/scala/izumi/distage/roles/launcher/AbstractActivationParser.scala +++ b/distage/distage-framework/src/main/scala/izumi/distage/roles/launcher/AbstractActivationParser.scala @@ -1,7 +1,8 @@ package izumi.distage.roles.launcher +import distage.config.AppConfig import izumi.distage.model.definition.Activation trait AbstractActivationParser { - def parseActivation(): Activation + def parseActivation(config: AppConfig): Activation } diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/TestPlanner.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/TestPlanner.scala index e3885390ad..10c8ed8edb 100644 --- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/TestPlanner.scala +++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/TestPlanner.scala @@ -212,14 +212,13 @@ class TestPlanner[F[_]: TagK: DefaultModule]( val activationParser = new ActivationParser.Impl( roleAppActivationParser, RawAppArgs.empty, - config, env.activationInfo, env.activation, Activation.empty, lateLogger, warnUnset, ) - val configActivation = activationParser.parseActivation() + val configActivation = activationParser.parseActivation(config) configActivation ++ env.activation } diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/BootstrapFactory.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/BootstrapFactory.scala index f9591efd89..1fcbce99ec 100644 --- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/BootstrapFactory.scala +++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/BootstrapFactory.scala @@ -3,13 +3,11 @@ package izumi.distage.testkit.runner.impl.services import distage.config.AppConfig import izumi.distage.framework.config.PlanningOptions import izumi.distage.framework.model.ActivationInfo -import izumi.distage.framework.services.ConfigLoader.ConfigLocation -import izumi.distage.framework.services.{ConfigLoader, ModuleProvider} +import izumi.distage.framework.services.ModuleProvider import izumi.distage.model.definition.Activation import izumi.distage.roles.launcher.AppShutdownInitiator import izumi.distage.roles.model.meta.RolesInfo import izumi.fundamentals.platform.cli.model.raw.RawAppArgs -import izumi.logstage.api.IzLogger import izumi.logstage.api.logger.LogRouter import izumi.reflect.TagK @@ -22,22 +20,10 @@ trait BootstrapFactory { activationInfo: ActivationInfo, activation: Activation, ): ModuleProvider - - def makeConfigLoader(configBaseName: String, logger: IzLogger): ConfigLoader - - protected def makeConfigLocation(configBaseName: String): ConfigLocation } object BootstrapFactory { object Impl extends BootstrapFactory { - override protected def makeConfigLocation(configBaseName: String): ConfigLocation = { - ConfigLocation.Default - } - - override def makeConfigLoader(configBaseName: String, logger: IzLogger): ConfigLoader = { - new ConfigLoader.LocalFSImpl(logger, makeConfigLocation(configBaseName), ConfigLoader.Args(None, Map(configBaseName -> None))) - } - override def makeModuleProvider[F[_]: TagK]( options: PlanningOptions, config: AppConfig, diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/TestConfigLoader.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/TestConfigLoader.scala index 71280516d4..a4c24378a2 100644 --- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/TestConfigLoader.scala +++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/runner/impl/services/TestConfigLoader.scala @@ -1,6 +1,8 @@ package izumi.distage.testkit.runner.impl.services -import izumi.distage.config.model.AppConfig +import izumi.distage.config.model.{AppConfig, GenericConfigSource, RoleConfig} +import izumi.distage.framework.services.ConfigMerger.ConfigMergerImpl +import izumi.distage.framework.services.{ConfigArgsProvider, ConfigLoader, ConfigLocationProvider} import izumi.distage.testkit.model.TestEnvironment import izumi.logstage.api.IzLogger @@ -20,13 +22,12 @@ object TestConfigLoader { .computeIfAbsent( (env.configBaseName, env.bootstrapFactory, env.configOverrides), _ => { - val configLoader = env.bootstrapFactory - .makeConfigLoader(env.configBaseName, envLogger) + val configLoader = makeConfigLoader(env.configBaseName, envLogger) .map { appConfig => env.configOverrides match { case Some(overrides) => - AppConfig(overrides.config.withFallback(appConfig.config).resolve()) + AppConfig.provided(overrides.config.withFallback(appConfig.config).resolve()) case None => appConfig } @@ -35,5 +36,14 @@ object TestConfigLoader { }, ) } + + protected def makeConfigLoader(configBaseName: String, logger: IzLogger): ConfigLoader = { + val provider = new ConfigArgsProvider { + override def args(): ConfigLoader.Args = ConfigLoader.Args(None, List(RoleConfig(configBaseName, active = true, GenericConfigSource.ConfigDefault))) + } + val merger = new ConfigMergerImpl(logger) + new ConfigLoader.LocalFSImpl(logger, merger, ConfigLocationProvider.Default, provider) + } + } } diff --git a/doc/microsite/src/main/tut/distage/distage-framework.md b/doc/microsite/src/main/tut/distage/distage-framework.md index 42e4458a97..70e8f2e44f 100644 --- a/doc/microsite/src/main/tut/distage/distage-framework.md +++ b/doc/microsite/src/main/tut/distage/distage-framework.md @@ -276,7 +276,7 @@ val module = new ConfigModuleDef { makeConfig[OtherConf]("conf").named("other") // add config instance - make[AppConfig].from(AppConfig( + make[AppConfig].from(AppConfig.provided( ConfigFactory.parseString( """conf { | name = "John"