diff --git a/core/src/main/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategy.java index e0afba9af93..e9bdbd90bea 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategy.java @@ -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. + *

+ * Resolution order is: + *

    + *
  1. DOCKER_HOST env var
  2. + *
  3. docker.host in ~/.testcontainers.properties
  4. + *
* * @deprecated this class is used by the SPI and should not be used directly */ @@ -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 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 diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java index 3b4d9f8f70c..9b2ab707a2e 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -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() { @@ -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; } @@ -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 .testcontainers.properties file in the user's home directory or * a testcontainers.properties found on the classpath. + *

+ * 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 @@ -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 .testcontainers.properties file in the user's home directory. + *

+ * 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 @@ -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 .testcontainers.properties file in the user's home directory. + * Gets a configured setting from an environment variable. + *

+ * 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 diff --git a/core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java b/core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java new file mode 100644 index 00000000000..91f61848d69 --- /dev/null +++ b/core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java @@ -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()); + } +} diff --git a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java index e5300bce58d..0fca36f0f2f 100644 --- a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java +++ b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java @@ -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(); @@ -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()); } diff --git a/docs/features/configuration.md b/docs/features/configuration.md index da7bbd82d60..2336ec840c5 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -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) @@ -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 +```