From d9d1f0a3fc650c3b70ad4b40c95070911f5f5a8c Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Wed, 27 Feb 2019 12:37:37 +0000 Subject: [PATCH] Add a config strategy to load from environment variables first. --- config/checkstyle-config.xml | 2 +- config/checkstyle-suppressions.xml | 2 +- .../config/EnvFirstConfigLoadingStrategy.java | 79 +++++++++++++++++++ .../com/typesafe/config/impl/ConfigTest.scala | 47 +++++++++++ .../typesafe/config/impl/PublicApiTest.scala | 29 +++++++ 5 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 config/src/main/java/com/typesafe/config/EnvFirstConfigLoadingStrategy.java diff --git a/config/checkstyle-config.xml b/config/checkstyle-config.xml index b5b71d2b..0fc2c197 100644 --- a/config/checkstyle-config.xml +++ b/config/checkstyle-config.xml @@ -1,7 +1,7 @@ + "https://checkstyle.org/dtds/configuration_1_3.dtd"> diff --git a/config/checkstyle-suppressions.xml b/config/checkstyle-suppressions.xml index 21fb8e51..6fa3fdc0 100644 --- a/config/checkstyle-suppressions.xml +++ b/config/checkstyle-suppressions.xml @@ -1,6 +1,6 @@ + "https://checkstyle.org/dtds/configuration_1_3.dtd"> + * Environment variables are mangled in the following way after stripping the prefix: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Env VarConfig
_   [1 underscore]. [dot]
__  [2 underscore]- [dash]
___ [3 underscore]_ [underscore]
+ * + *

+ * A variable like: {@code CONFIG_a_b__c___d} + * is translated to a config key: {@code a.b-c_d} + * + *

+ * The prefix may be altered by defining the VM property {@code config.env_var_prefix} + */ +public class EnvFirstConfigLoadingStrategy extends DefaultConfigLoadingStrategy { + + protected static Map env = new HashMap(System.getenv()); + + @Override + public Config parseApplicationConfig(ConfigParseOptions parseOptions) { + String envVarPrefix = System.getProperty("config.env_var_prefix"); + if (envVarPrefix == null) // fallback to default + envVarPrefix = "CONFIG_"; + + Map defaultsFromEnv = new HashMap(); + for (String key : env.keySet()) { + if (key.startsWith(envVarPrefix)) { + StringBuilder builder = new StringBuilder(); + + String strippedPrefix = key.substring(envVarPrefix.length(), key.length()); + + int underscores = 0; + for (char c : strippedPrefix.toCharArray()) { + if (c == '_') { + underscores++; + } else { + switch (underscores) { + case 1: builder.append('.'); + break; + case 2: builder.append('-'); + break; + case 3: builder.append('_'); + break; + } + underscores = 0; + builder.append(c); + } + } + + String propertyKey = builder.toString(); + defaultsFromEnv.put(propertyKey, env.get(key)); + } + } + + return ConfigFactory.parseMap(defaultsFromEnv).withFallback(super.parseApplicationConfig(parseOptions)); + } +} diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index 1bf031ba..f5084c79 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -1090,6 +1090,53 @@ class ConfigTest extends TestUtils { assertEquals(10, resolved.getInt("bar.nested.a.q")) } + @Test + def testLoadWithEnvSubstitutions() { + TestEnvFirstStrategy.putEnvVar("CONFIG_42___a", "1") + TestEnvFirstStrategy.putEnvVar("CONFIG_a_b_c", "2") + TestEnvFirstStrategy.putEnvVar("CONFIG_a__c", "3") + TestEnvFirstStrategy.putEnvVar("CONFIG_a___c", "4") + + TestEnvFirstStrategy.putEnvVar("CONFIG_akka_version", "foo") + TestEnvFirstStrategy.putEnvVar("CONFIG_akka_event__handler__dispatcher_max__pool__size", "10") + + System.setProperty("config.strategy", classOf[EnvFirstConfigLoadingStrategy].getCanonicalName) + + try { + val loader02 = new TestClassLoader(this.getClass().getClassLoader(), + Map("reference.conf" -> resourceFile("test02.conf").toURI.toURL())) + + val loader04 = new TestClassLoader(this.getClass().getClassLoader(), + Map("reference.conf" -> resourceFile("test04.conf").toURI.toURL())) + + val conf02 = withContextClassLoader(loader02) { + ConfigFactory.load() + } + + val conf04 = withContextClassLoader(loader04) { + ConfigFactory.load() + } + + assertEquals(1, conf02.getInt("42_a")) + assertEquals(2, conf02.getInt("a.b.c")) + assertEquals(3, conf02.getInt("a-c")) + assertEquals(4, conf02.getInt("a_c")) + + assertEquals("foo", conf04.getString("akka.version")) + assertEquals(10, conf04.getInt("akka.event-handler-dispatcher.max-pool-size")) + } finally { + System.clearProperty("config.strategy") + + TestEnvFirstStrategy.removeEnvVar("CONFIG_42___a") + TestEnvFirstStrategy.removeEnvVar("CONFIG_a_b_c") + TestEnvFirstStrategy.removeEnvVar("CONFIG_a__c") + TestEnvFirstStrategy.removeEnvVar("CONFIG_a___c") + + TestEnvFirstStrategy.removeEnvVar("CONFIG_akka_version") + TestEnvFirstStrategy.removeEnvVar("CONFIG_akka_event__handler__dispatcher_max__pool__size") + } + } + @Test def renderRoundTrip() { val allBooleans = true :: false :: Nil diff --git a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala index d25d4365..136ea690 100644 --- a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala @@ -653,6 +653,28 @@ class PublicApiTest extends TestUtils { } } + @Test + def supportsEnvFirstConfigLoadingStrategy(): Unit = { + assertEquals("config.strategy is not set", null, System.getProperty("config.strategy")) + + TestEnvFirstStrategy.putEnvVar("CONFIG_a", "5") + System.setProperty("config.strategy", classOf[EnvFirstConfigLoadingStrategy].getCanonicalName) + + try { + val loaderA1 = new TestClassLoader(this.getClass().getClassLoader(), + Map("reference.conf" -> resourceFile("a_1.conf").toURI.toURL())) + + val configA1 = withContextClassLoader(loaderA1) { + ConfigFactory.load() + } + + assertEquals(5, configA1.getInt("a")) + } finally { + System.clearProperty("config.strategy") + TestEnvFirstStrategy.removeEnvVar("CONFIG_a") + } + } + @Test def usesContextClassLoaderForApplicationConf() { val loaderA1 = new TestClassLoader(this.getClass().getClassLoader(), @@ -1145,4 +1167,11 @@ object TestStrategy { private var invocations = 0 def getIncovations() = invocations def increment() = invocations += 1 +} + +object TestEnvFirstStrategy extends EnvFirstConfigLoadingStrategy { + def putEnvVar(key: String, value: String) = + EnvFirstConfigLoadingStrategy.env.put(key, value) + def removeEnvVar(key: String) = + EnvFirstConfigLoadingStrategy.env.remove(key) } \ No newline at end of file