Skip to content

Commit

Permalink
Add capability to lookup environment variables in configuration files
Browse files Browse the repository at this point in the history
  • Loading branch information
joschi committed Feb 28, 2015
1 parent 09ab6b0 commit f61215e
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 9 deletions.
47 changes: 46 additions & 1 deletion docs/source/manual/core.rst
Expand Up @@ -216,7 +216,46 @@ Dropwizard then calls your ``Application`` subclass to initialize your applicati
file. If it does not exist or is not an array setting, it will get added as a simple string setting, including
the ',' characters as part of the string.
.. _man-core-environments:
.. _man-core-environment-variables:
Environment variables
---------------------
The ``dropwizard-configuration`` module also provides the capabilities to substitute configuration settings with the
value of environment variables using a ``SubstitutingSourceProvider`` and ``EnvironmentVariableSubstitutor``.
.. code-block:: java
public class MyApplication extends Application<MyConfiguration> {
// [...]
@Override
public void initialize(Bootstrap<MyConfiguration> bootstrap) {
// Enable variable substitution with environment variables
bootstrap.setConfigurationSourceProvider(
new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
new EnvironmentVariableSubstitutor()
)
);
}
// [...]
}
The configuration settings which should be substituted need to be explicitly written in the configuration file and
follow the substitution rules of StrSubstitutor_ from the Apache Commons Lang library.
.. code-block:: yaml
mySetting: ${DW_MY_SETTING}
defaultSetting: ${DW_DEFAULT_SETTING:-default value}
In general ``SubstitutingSourceProvider`` isn't restricted to substitute environment variables but can be used to replace
variables in the configuration source with arbitrary values by passing a custom ``StrSubstitutor`` implementation.
.. _StrSubstitutor: https://commons.apache.org/proper/commons-lang/javadocs/api-release/org/apache/commons/lang3/text/StrSubstitutor.html
.. _man-core-ssl:
SSL
---
Expand All @@ -238,6 +277,9 @@ command you need). There is a test keystore you can use in the
keyStorePassword: example
validateCerts: false
.. _man-core-bootstrapping:
Bootstrapping
=============
Expand All @@ -247,6 +289,9 @@ run as a server, it must first go through a bootstrapping phase. This phase corr
:ref:`man-core-commands`, or register Jackson modules to allow you to include custom types as part
of your configuration class.
.. _man-core-environments:
Environments
============
Expand Down
21 changes: 20 additions & 1 deletion dropwizard-configuration/pom.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand Down Expand Up @@ -33,4 +34,22 @@
<version>3.3.2</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<!-- Required for tests related to environment variables -->
<configuration>
<forkMode>always</forkMode>
<environmentVariables>
<TEST>test_value</TEST>
<TEST_SUFFIX>2</TEST_SUFFIX>
<TEST2>alternative</TEST2>
</environmentVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,48 @@
package io.dropwizard.configuration;

import org.apache.commons.lang3.text.StrLookup;

/**
* A custom {@link org.apache.commons.lang3.text.StrLookup} implementation using environment variables as lookup source.
*/
public class EnvironmentVariableLookup extends StrLookup {
private final boolean strict;

/**
* Create a new instance with strict behavior.
*/
public EnvironmentVariableLookup() {
this(true);
}

/**
* Create a new instance.
*
* @param strict {@code true} if looking up undefined environment variables should throw a
* {@link UndefinedEnvironmentVariableException}, {@code false} otherwise.
* @throws UndefinedEnvironmentVariableException if the environment variable doesn't exist and strict behavior
* is enabled.
*/
public EnvironmentVariableLookup(boolean strict) {
this.strict = strict;
}

/**
* {@inheritDoc}
*
* @throws UndefinedEnvironmentVariableException if the environment variable doesn't exist and strict behavior
* is enabled.
*/
@Override
public String lookup(String key) {
String value = System.getenv(key);

if (value == null && strict) {
throw new UndefinedEnvironmentVariableException("The environment variable '" + key
+ "' is not defined; could not substitute the expression '${"
+ key + "}'.");
}

return value;
}
}
@@ -0,0 +1,28 @@
package io.dropwizard.configuration;

import org.apache.commons.lang3.text.StrSubstitutor;

/**
* A custom {@link StrSubstitutor} using environment variables as lookup source.
*/
public class EnvironmentVariableSubstitutor extends StrSubstitutor {
public EnvironmentVariableSubstitutor() {
this(true, false);
}

public EnvironmentVariableSubstitutor(boolean strict) {
this(strict, false);
}

/**
* @param strict {@code true} if looking up undefined environment variables should throw a
* {@link UndefinedEnvironmentVariableException}, {@code false} otherwise.
* @param substitutionInVariables a flag whether substitution is done in variable names.
* @see io.dropwizard.configuration.EnvironmentVariableLookup#EnvironmentVariableLookup(boolean)
* @see org.apache.commons.lang3.text.StrSubstitutor#setEnableSubstitutionInVariables(boolean)
*/
public EnvironmentVariableSubstitutor(boolean strict, boolean substitutionInVariables) {
super(new EnvironmentVariableLookup(strict));
this.setEnableSubstitutionInVariables(substitutionInVariables);
}
}
@@ -0,0 +1,42 @@
package io.dropwizard.configuration;

import com.google.common.io.ByteStreams;
import org.apache.commons.lang3.text.StrSubstitutor;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import static com.google.common.base.Preconditions.checkNotNull;

/**
* A delegating {@link ConfigurationSourceProvider} which replaces variables in the underlying configuration
* source according to the rules of a custom {@link org.apache.commons.lang3.text.StrSubstitutor}.
*/
public class SubstitutingSourceProvider implements ConfigurationSourceProvider {
private final ConfigurationSourceProvider delegate;
private final StrSubstitutor substitutor;

/**
* Create a new instance.
*
* @param delegate The underlying {@link io.dropwizard.configuration.ConfigurationSourceProvider}.
* @param substitutor The custom {@link org.apache.commons.lang3.text.StrSubstitutor} implementation.
*/
public SubstitutingSourceProvider(ConfigurationSourceProvider delegate, StrSubstitutor substitutor) {
this.delegate = checkNotNull(delegate);
this.substitutor = checkNotNull(substitutor);
}

/**
* {@inheritDoc}
*/
@Override
public InputStream open(String path) throws IOException {
String config = new String(ByteStreams.toByteArray(delegate.open(path)), StandardCharsets.UTF_8);
String substituted = substitutor.replace(config);

return new ByteArrayInputStream(substituted.getBytes(StandardCharsets.UTF_8));
}
}
@@ -0,0 +1,7 @@
package io.dropwizard.configuration;

public class UndefinedEnvironmentVariableException extends RuntimeException {
public UndefinedEnvironmentVariableException(String errorMessage) {
super(errorMessage);
}
}
@@ -0,0 +1,34 @@
package io.dropwizard.configuration;

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assume.assumeThat;

public class EnvironmentVariableLookupTest {
@Test(expected = UndefinedEnvironmentVariableException.class)
public void defaultConstructorEnablesStrict() {
assumeThat(System.getenv("nope"), nullValue());

EnvironmentVariableLookup lookup = new EnvironmentVariableLookup();
lookup.lookup("nope");
}

@Test
public void lookupReplacesWithEnvironmentVariables() {
EnvironmentVariableLookup lookup = new EnvironmentVariableLookup(false);

// Let's hope this doesn't break on Windows
assertThat(lookup.lookup("TEST")).isEqualTo(System.getenv("TEST"));
assertThat(lookup.lookup("nope")).isNull();
}

@Test(expected = UndefinedEnvironmentVariableException.class)
public void lookupThrowsExceptionInStrictMode() {
assumeThat(System.getenv("nope"), nullValue());

EnvironmentVariableLookup lookup = new EnvironmentVariableLookup(true);
lookup.lookup("nope");
}
}
@@ -0,0 +1,61 @@
package io.dropwizard.configuration;

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assume.assumeThat;

public class EnvironmentVariableSubstitutorTest {
@Test
public void defaultConstructorDisablesSubstitutionInVariables() {
EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor();
assertThat(substitutor.isEnableSubstitutionInVariables()).isFalse();
}

@Test(expected = UndefinedEnvironmentVariableException.class)
public void defaultConstructorEnablesStrict() {
assumeThat(System.getenv("DOES_NOT_EXIST"), nullValue());

EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor();
substitutor.replace("${DOES_NOT_EXIST}");
}

@Test
public void constructorEnablesSubstitutionInVariables() {
EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(true, true);
assertThat(substitutor.isEnableSubstitutionInVariables()).isTrue();
}

@Test
public void substitutorUsesEnvironmentVariableLookup() {
EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor();
assertThat(substitutor.getVariableResolver()).isInstanceOf(EnvironmentVariableLookup.class);
}

@Test
public void substitutorReplacesWithEnvironmentVariables() {
EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(false);

assertThat(substitutor.replace("${TEST}")).isEqualTo(System.getenv("TEST"));
assertThat(substitutor.replace("no replacement")).isEqualTo("no replacement");
assertThat(substitutor.replace("${DOES_NOT_EXIST}")).isEqualTo("${DOES_NOT_EXIST}");
assertThat(substitutor.replace("${DOES_NOT_EXIST:-default}")).isEqualTo("default");
}

@Test(expected = UndefinedEnvironmentVariableException.class)
public void substitutorThrowsExceptionInStrictMode() {
assumeThat(System.getenv("DOES_NOT_EXIST"), nullValue());

EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(true);
substitutor.replace("${DOES_NOT_EXIST}");
}

@Test
public void substitutorReplacesRecursively() {
EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(false, true);

assertThat(substitutor.replace("$${${TEST}}")).isEqualTo("${test_value}");
assertThat(substitutor.replace("${TEST${TEST_SUFFIX}}")).isEqualTo(System.getenv("TEST2"));
}
}
@@ -0,0 +1,64 @@
package io.dropwizard.configuration;

import com.google.common.io.ByteStreams;
import org.apache.commons.lang3.text.StrLookup;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import static org.assertj.core.api.Assertions.assertThat;

public class SubstitutingSourceProviderTest {
@Test
public void shouldSubstituteCorrectly() throws IOException {
StrLookup dummyLookup = new StrLookup() {
@Override
public String lookup(String key) {
return "baz";
}
};
SubstitutingSourceProvider provider = new SubstitutingSourceProvider(new DummySourceProvider(), new StrSubstitutor(dummyLookup));
String results = new String(ByteStreams.toByteArray(provider.open("foo: ${bar}")), StandardCharsets.UTF_8);

assertThat(results).isEqualTo("foo: baz");
}

@Test
public void shouldSubstituteOnlyExistingVariables() throws IOException {
StrLookup dummyLookup = new StrLookup() {
@Override
public String lookup(String key) {
return null;
}
};
SubstitutingSourceProvider provider = new SubstitutingSourceProvider(new DummySourceProvider(), new StrSubstitutor(dummyLookup));
String results = new String(ByteStreams.toByteArray(provider.open("foo: ${bar}")), StandardCharsets.UTF_8);

assertThat(results).isEqualTo("foo: ${bar}");
}

@Test
public void shouldSubstituteWithDefaultValue() throws IOException {
StrLookup dummyLookup = new StrLookup() {
@Override
public String lookup(String key) {
return null;
}
};
SubstitutingSourceProvider provider = new SubstitutingSourceProvider(new DummySourceProvider(), new StrSubstitutor(dummyLookup));
String results = new String(ByteStreams.toByteArray(provider.open("foo: ${bar:-default}")), StandardCharsets.UTF_8);

assertThat(results).isEqualTo("foo: default");
}

private static class DummySourceProvider implements ConfigurationSourceProvider {
@Override
public InputStream open(String s) throws IOException {
return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8));
}
}
}

0 comments on commit f61215e

Please sign in to comment.