sksamuel / hoplite

A boilerplate-free Kotlin config library for loading configuration files as data classes
Apache License 2.0
923 stars 74 forks source link

Environment variables in HOCON files will not be resolved in some cases #397

Open luedi opened 11 months ago

luedi commented 11 months ago

I'm using the standard Kotlin application.conf behavior to use environment variables to override config values. So my HOCON file looks like this (simplified):

{
  prod = {
    host = "myprodhost:myport"
    host = ${?FIXED_PREFIX_MY_PROD_HOST}
  }
  dev = {
    host = "mydevhost:mydevport"
    host = ${?FIXED_PREFIX_MY_DEV_HOST}
  }
  anotherProperty = "stage_independent"
  anotherProperty = ${?FIXED_PREFIX_ANOTHER_PROPE>RTY}
}

Then I implemented a configuration class which is (very simplified) like this:

enum class Stage {
    DEV,
    PROD
}

// this interface is needed to be able to process the configuration
sealed interface Configuration

data class StageConfig(
    val host: String,
) : Configuration

object HostConfiguration {

    data class HostConfig(
        val stages: Map<Stage, StageConfig>,
        val anotherProperty: String,
    )

    private val hostConfig: HostConfig

    init {
        // by the way: how can i avoid this and read the nested configuration direct into HostConfig???
        data class InternalHostConfig(
            val prod: Configuration,
            val dev: Configuration,
            val anotherProperty: String,
        )

        val config = ConfigLoaderBuilder.default()
            .addPropertySource(PropertySource.resource("/host.conf"))
            .build()
            .loadConfigOrThrow<InternalHostConfig>()

        hostConfig = HostConfig(
            mapOf(
                Stage.PROD to (config.prod as StageConfig),
                Stage.DEV to (config.dev as StageConfig)
            ),
            config.anoherProperty,
        )
    }

    fun stageConfig(stage: Stage): StageConfig = hostConfig.stages[stage]!!

    fun anotherProperty() = hostConfig.anotherProperty
}

At least I wrote some unit tests for my configuration with the aid of Kotest. Kotest has a feature to inject environment variables into a test (which works fine). This tests runs successful when starting them from the IntelliJ editor pane but are failing when running them in a Gradle build or with the Kotest runner. In this cases config.host contains "myprodhost:myport". Here is the, again very simpolified code:

class HostConfigurationKtTest : FunSpec({

    test("Environment variables should be used when present") {
        withEnvironment(
            mapof(
                "FIXED_PREFIX_MY_PROD_HOST" to "host_url_prod",
            )
        ) {
            // This is successful
            System.getenv("FIXED_PREFIX_MY_PROD_HOST") shouldBe "host_url_prod"

            val prodConfig = HostConfiguration.stageConfig(Stage.PROD)
            // This will fail
            prodConfig.host shouldBe "broker_url_prod"
        }
    }
})

I've read about the EnvironmentVariablesOverridePropertySource but i can't prefix environment variables with config.override. In our production environment the environment variables are generated by a tool which follows a specific naming convention which i can't override. For example, if i declare a variable MY_HOST in the tool, a prefix FIXED_PREFIX is automatically added.

Interesting, when I change my code like you do in the EnvironmentVariablesOverridePropertySourceTest the test fails always.

I used hopelite because my real configuration is much more complicated and the library helps me to save a lot of work in opposite to use the standard application.conf.

Before I forget, my environment is:

IntelliJ IDEA Ultimate 2023.3 Kotlin 1.9.21 targeting Java 21 Kotest 5.8.0 hopelite 2.7.5