Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow DOCKER_HOST and related settings to be set in ~/.testcontainers.properties #4118

Merged
merged 10 commits into from
May 20, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import org.testcontainers.utility.TestcontainersConfiguration;

import java.util.Optional;

/**
* Use environment variables and system properties (as supported by the underlying DockerClient DefaultConfigBuilder)
* to try and locate a docker environment.
* <p>
* Resolution order is:
* <ol>
* <li>DOCKER_HOST env var</li>
* <li>docker.host in ~/.testcontainers.properties</li>
* </ol>
*
* @deprecated this class is used by the SPI and should not be used directly
*/
Expand All @@ -14,12 +23,26 @@ public final class EnvironmentAndSystemPropertyClientProviderStrategy extends Do

public static final int PRIORITY = 100;

// Try using environment variables
private final DockerClientConfig dockerClientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
private final DockerClientConfig dockerClientConfig;

public EnvironmentAndSystemPropertyClientProviderStrategy() {
// use docker-java defaults if present, overridden if our own configuration is set
DefaultDockerClientConfig.Builder configBuilder = DefaultDockerClientConfig.createDefaultConfigBuilder();

getSetting("docker.host").ifPresent(configBuilder::withDockerHost);
getSetting("docker.tls.verify").ifPresent(configBuilder::withDockerTlsVerify);
getSetting("docker.cert.path").ifPresent(configBuilder::withDockerCertPath);

dockerClientConfig = configBuilder.build();
}

private Optional<String> getSetting(final String name) {
return Optional.ofNullable(TestcontainersConfiguration.getInstance().getEnvVarOrUserProperty(name, null));
}

@Override
protected boolean isApplicable() {
return System.getenv("DOCKER_HOST") != null;
return getSetting("docker.host").isPresent();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,11 @@ public boolean environmentSupportsReuse() {
}

public String getDockerClientStrategyClassName() {
return getEnvVarOrUserProperty("docker.client.strategy", null);
// getConfigurable won't apply the TESTCONTAINERS_ prefix when looking for env vars if DOCKER_ appears at the beginning.
// Because of this overlap, and the desire to not change this specific TESTCONTAINERS_DOCKER_CLIENT_STRATEGY setting,
// we special-case the logic here so that docker.client.strategy is used when reading properties files and
// TESTCONTAINERS_DOCKER_CLIENT_STRATEGY is used when searching environment variables.
return getEnvVarOrUserProperty("docker.client.strategy", environment.get("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY"));
}

public String getTransportType() {
Expand All @@ -187,7 +191,7 @@ public String getImageSubstitutorClassName() {
@Contract("_, !null, _ -> !null")
private String getConfigurable(@NotNull final String propertyName, @Nullable final String defaultValue, Properties... propertiesSources) {
String envVarName = propertyName.replaceAll("\\.", "_").toUpperCase();
if (!envVarName.startsWith("TESTCONTAINERS_")) {
if (!envVarName.startsWith("TESTCONTAINERS_") && !envVarName.startsWith("DOCKER_")) {
envVarName = "TESTCONTAINERS_" + envVarName;
}

Expand All @@ -208,6 +212,10 @@ private String getConfigurable(@NotNull final String propertyName, @Nullable fin
* Gets a configured setting from an environment variable (if present) or a configuration file property otherwise.
* The configuration file will be the <code>.testcontainers.properties</code> file in the user's home directory or
* a <code>testcontainers.properties</code> found on the classpath.
* <p>
* Note that when searching environment variables, the prefix `TESTCONTAINERS_` will usually be applied to the
* property name, which will be converted to upper-case with underscore separators. This prefix will not be added
* if the property name begins `docker.`.
*
* @param propertyName name of configuration file property (dot-separated lower case)
* @return the found value, or null if not set
Expand All @@ -220,6 +228,10 @@ public String getEnvVarOrProperty(@NotNull final String propertyName, @Nullable
/**
* Gets a configured setting from an environment variable (if present) or a configuration file property otherwise.
* The configuration file will be the <code>.testcontainers.properties</code> file in the user's home directory.
* <p>
* Note that when searching environment variables, the prefix `TESTCONTAINERS_` will usually be applied to the
* property name, which will be converted to upper-case with underscore separators. This prefix will not be added
* if the property name begins `docker.`.
*
* @param propertyName name of configuration file property (dot-separated lower case)
* @return the found value, or null if not set
Expand All @@ -230,8 +242,11 @@ public String getEnvVarOrUserProperty(@NotNull final String propertyName, @Nulla
}

/**
* Gets a configured setting from a the user's configuration file.
* The configuration file will be the <code>.testcontainers.properties</code> file in the user's home directory.
* Gets a configured setting from an environment variable.
* <p>
* Note that when searching environment variables, the prefix `TESTCONTAINERS_` will usually be applied to the
* property name, which will be converted to upper-case with underscore separators. This prefix will not be added
* if the property name begins `docker.`.
*
* @param propertyName name of configuration file property (dot-separated lower case)
* @return the found value, or null if not set
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.testcontainers.dockerclient;

import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.LocalDirectorySSLConfig;
import com.github.dockerjava.transport.SSLConfig;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.testcontainers.utility.MockTestcontainersConfigurationRule;
import org.testcontainers.utility.TestcontainersConfiguration;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;

/**
* Test that we can use Testcontainers configuration file to override settings. We assume that docker-java has test
* coverage for detection of environment variables (e.g. DOCKER_HOST) and its own properties config file.
*/
@RunWith(MockitoJUnitRunner.class)
public class EnvironmentAndSystemPropertyClientProviderStrategyTest {

@Rule
public MockTestcontainersConfigurationRule mockConfig = new MockTestcontainersConfigurationRule();
private URI defaultDockerHost;
private com.github.dockerjava.core.SSLConfig defaultSSLConfig;

@Before
public void checkEnvironmentClear() {
// If docker-java picks up non-default settings from the environment, our test needs to know to expect those
DefaultDockerClientConfig defaultConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
defaultDockerHost = defaultConfig.getDockerHost();
defaultSSLConfig = defaultConfig.getSSLConfig();
}

@Test
public void testWhenConfigAbsent() {
Mockito.doReturn(null).when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.host"), isNull());
Mockito.doReturn(null).when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.tls.verify"), isNull());
Mockito.doReturn(null).when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.cert.path"), isNull());

EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy();

TransportConfig transportConfig = strategy.getTransportConfig();
assertEquals(defaultDockerHost, transportConfig.getDockerHost());
assertEquals(defaultSSLConfig, transportConfig.getSslConfig());
}

@Test
public void testWhenDockerHostPresent() {
Mockito.doReturn("tcp://1.2.3.4:2375").when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.host"), isNull());
Mockito.doReturn(null).when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.tls.verify"), isNull());
Mockito.doReturn(null).when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.cert.path"), isNull());

EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy();

TransportConfig transportConfig = strategy.getTransportConfig();
assertEquals("tcp://1.2.3.4:2375", transportConfig.getDockerHost().toString());
assertEquals(defaultSSLConfig, transportConfig.getSslConfig());
}

@Test
public void testWhenDockerHostAndSSLConfigPresent() throws IOException {
Path tempDir = Files.createTempDirectory("testcontainers-test");
String tempDirPath = tempDir.toAbsolutePath().toString();

Mockito.doReturn("tcp://1.2.3.4:2375").when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.host"), isNull());
Mockito.doReturn("1").when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.tls.verify"), isNull());
Mockito.doReturn(tempDirPath).when(TestcontainersConfiguration.getInstance()).getEnvVarOrUserProperty(eq("docker.cert.path"), isNull());

EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy();

TransportConfig transportConfig = strategy.getTransportConfig();
assertEquals("tcp://1.2.3.4:2375", transportConfig.getDockerHost().toString());

SSLConfig sslConfig = transportConfig.getSslConfig();
assertNotNull(sslConfig);
assertTrue(sslConfig instanceof LocalDirectorySSLConfig);
assertEquals(tempDirPath, ((LocalDirectorySSLConfig) sslConfig).getDockerCertPath());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ public void shouldReadChecksFromEnvironment() {
assertTrue("checks disabled via env var", newConfig().isDisableChecks());
}

@Test
public void shouldReadDockerSettingsFromEnvironmentWithoutTestcontainersPrefix() {
userProperties.remove("docker.foo");
environment.put("DOCKER_FOO", "some value");
assertEquals("reads unprefixed env vars for docker. settings", "some value", newConfig().getEnvVarOrUserProperty("docker.foo", "default"));
}

@Test
public void shouldNotReadDockerSettingsFromEnvironmentWithTestcontainersPrefix() {
userProperties.remove("docker.foo");
environment.put("TESTCONTAINERS_DOCKER_FOO", "some value");
assertEquals("reads unprefixed env vars for docker. settings", "default", newConfig().getEnvVarOrUserProperty("docker.foo", "default"));
}

@Test
public void shouldReadDockerSettingsFromUserProperties() {
environment.remove("DOCKER_FOO");
userProperties.put("docker.foo", "some value");
assertEquals("reads unprefixed user properties for docker. settings", "some value", newConfig().getEnvVarOrUserProperty("docker.foo", "default"));
}

@Test
public void shouldNotReadDockerClientStrategyFromClasspathProperties() {
String currentValue = newConfig().getDockerClientStrategyClassName();
Expand All @@ -129,7 +150,7 @@ public void shouldReadDockerClientStrategyFromUserProperties() {

@Test
public void shouldReadDockerClientStrategyFromEnvironment() {
userProperties.remove("docker.client.strategy");
userProperties.remove("docker.client.strategy");
environment.put("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY", "foo");
assertEquals("Docker client strategy is changed by env var", "foo", newConfig().getDockerClientStrategyClassName());
}
Expand Down
15 changes: 13 additions & 2 deletions docs/features/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ but does not allow starting privileged containers, you can turn off the Ryuk con

## Customizing Docker host detection

Testcontainers will attempt to detect the Docker environment and configure everything.
Testcontainers will attempt to detect the Docker environment and configure everything to work automatically.

However, sometimes a customization is required. For that, you can provide the following environment variables:
However, sometimes customization is required. Testcontainers will respect the following **environment variables**:

> **DOCKER_HOST** = unix:///var/run/docker.sock
> See [Docker environment variables](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
Expand All @@ -110,3 +110,14 @@ However, sometimes a customization is required. For that, you can provide the fo
> **TESTCONTAINERS_HOST_OVERRIDE**
> Docker's host on which ports are exposed.
> Example: `docker.svc.local`

For advanced users, the Docker host connection can be configured **via configuration** in `~/.testcontainers.properties`.
Note that these settings require use of the `EnvironmentAndSystemPropertyClientProviderStrategy`. The example below
illustrates usage:

```properties
docker.client.strategy=org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy
docker.host=tcp\://my.docker.host\:1234 # Equivalent to the DOCKER_HOST environment variable. Colons should be escaped.
docker.tls.verify=1 # Equivalent to the DOCKER_TLS_VERIFY environment variable
docker.cert.path=/some/path # Equivalent to the DOCKER_CERT_PATH environment variable
```