diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java
index 5b89140fd61..b3c8e88a328 100644
--- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java
+++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java
@@ -1,15 +1,17 @@
package org.testcontainers.utility;
import com.google.common.annotations.VisibleForTesting;
-import lombok.AccessLevel;
+import com.google.common.collect.ImmutableMap;
import lombok.Data;
import lombok.Getter;
import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.exception.ExceptionUtils;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.testcontainers.UnstableAPI;
import java.io.File;
@@ -20,22 +22,57 @@
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
/**
- * Provides a mechanism for fetching configuration/defaults from the classpath.
+ * Provides a mechanism for fetching configuration/default settings.
+ *
+ * Configuration may be provided in:
+ *
+ * - A file in the user's home directory named
.testcontainers.properties
+ * - A file in the classpath named
testcontainers.properties
+ * - Environment variables
+ *
+ *
+ * Note that, if using environment variables, property names are in upper case separated by underscores, preceded by
+ * TESTCONTAINERS_.
*/
@Data
@Slf4j
-@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class TestcontainersConfiguration {
private static String PROPERTIES_FILE_NAME = "testcontainers.properties";
- private static File ENVIRONMENT_CONFIG_FILE = new File(System.getProperty("user.home"), "." + PROPERTIES_FILE_NAME);
+ private static File USER_CONFIG_FILE = new File(System.getProperty("user.home"), "." + PROPERTIES_FILE_NAME);
+
+ private static final String AMBASSADOR_IMAGE = "richnorth/ambassador";
+ private static final String SOCAT_IMAGE = "alpine/socat";
+ private static final String VNC_RECORDER_IMAGE = "testcontainers/vnc-recorder";
+ private static final String COMPOSE_IMAGE = "docker/compose";
+ private static final String ALPINE_IMAGE = "alpine";
+ private static final String RYUK_IMAGE = "testcontainers/ryuk";
+ private static final String KAFKA_IMAGE = "confluentinc/cp-kafka";
+ private static final String PULSAR_IMAGE = "apachepulsar/pulsar";
+ private static final String LOCALSTACK_IMAGE = "localstack/localstack";
+ private static final String SSHD_IMAGE = "testcontainers/sshd";
+
+ private static final ImmutableMap CONTAINER_MAPPING = ImmutableMap.builder()
+ .put(DockerImageName.parse(AMBASSADOR_IMAGE), "ambassador.container.image")
+ .put(DockerImageName.parse(SOCAT_IMAGE), "socat.container.image")
+ .put(DockerImageName.parse(VNC_RECORDER_IMAGE), "vncrecorder.container.image")
+ .put(DockerImageName.parse(COMPOSE_IMAGE), "compose.container.image")
+ .put(DockerImageName.parse(ALPINE_IMAGE), "tinyimage.container.image")
+ .put(DockerImageName.parse(RYUK_IMAGE), "ryuk.container.image")
+ .put(DockerImageName.parse(KAFKA_IMAGE), "kafka.container.image")
+ .put(DockerImageName.parse(PULSAR_IMAGE), "pulsar.container.image")
+ .put(DockerImageName.parse(LOCALSTACK_IMAGE), "localstack.container.image")
+ .put(DockerImageName.parse(SSHD_IMAGE), "sshd.container.image")
+ .build();
@Getter(lazy = true)
private static final TestcontainersConfiguration instance = loadConfiguration();
@@ -47,163 +84,234 @@ static AtomicReference getInstanceField() {
return (AtomicReference) (Object) instance;
}
- @Getter(AccessLevel.NONE)
- private final Properties environmentProperties;
+ private final Properties userProperties;
+ private final Properties classpathProperties;
+ private final Map environment;
- private final Properties properties = new Properties();
-
- TestcontainersConfiguration(Properties environmentProperties, Properties classpathProperties) {
- this.environmentProperties = environmentProperties;
-
- this.properties.putAll(classpathProperties);
- this.properties.putAll(environmentProperties);
- }
-
- private DockerImageName getImage(final String key, final String defaultValue) {
- return DockerImageName
- .parse(properties.getProperty(key, defaultValue).trim())
- .asCompatibleSubstituteFor(defaultValue);
+ TestcontainersConfiguration(Properties userProperties, Properties classpathProperties, final Map environment) {
+ this.userProperties = userProperties;
+ this.classpathProperties = classpathProperties;
+ this.environment = environment;
}
@Deprecated
public String getAmbassadorContainerImage() {
- return getAmbassadorContainerDockerImageName().asCanonicalNameString();
- }
-
- @Deprecated
- public DockerImageName getAmbassadorContainerDockerImageName() {
- return getImage("ambassador.container.image", "richnorth/ambassador:latest");
+ return getImage(AMBASSADOR_IMAGE).asCanonicalNameString();
}
@Deprecated
public String getSocatContainerImage() {
- return getSocatDockerImageName().asCanonicalNameString();
+ return getImage(SOCAT_IMAGE).asCanonicalNameString();
}
public DockerImageName getSocatDockerImageName() {
- return getImage("socat.container.image", "alpine/socat:latest");
+ return getImage(SOCAT_IMAGE);
}
@Deprecated
public String getVncRecordedContainerImage() {
- return getVncDockerImageName().asCanonicalNameString();
+ return getImage(VNC_RECORDER_IMAGE).asCanonicalNameString();
}
public DockerImageName getVncDockerImageName() {
- return getImage("vncrecorder.container.image", "testcontainers/vnc-recorder:1.1.0");
+ return getImage(VNC_RECORDER_IMAGE);
}
@Deprecated
public String getDockerComposeContainerImage() {
- return getDockerComposeDockerImageName().asCanonicalNameString();
+ return getImage(COMPOSE_IMAGE).asCanonicalNameString();
}
public DockerImageName getDockerComposeDockerImageName() {
- return getImage("compose.container.image", "docker/compose:1.24.1");
+ return getImage(COMPOSE_IMAGE);
}
@Deprecated
public String getTinyImage() {
- return getTinyDockerImageName().asCanonicalNameString();
+ return getImage(ALPINE_IMAGE).asCanonicalNameString();
}
public DockerImageName getTinyDockerImageName() {
- return getImage("tinyimage.container.image", "alpine:3.5");
+ return getImage(ALPINE_IMAGE);
}
public boolean isRyukPrivileged() {
- return Boolean.parseBoolean((String) properties.getOrDefault("ryuk.container.privileged", "false"));
+ return Boolean
+ .parseBoolean(getEnvVarOrProperty("ryuk.container.privileged", "false"));
}
@Deprecated
public String getRyukImage() {
- return getRyukDockerImageName().asCanonicalNameString();
+ return getImage(RYUK_IMAGE).asCanonicalNameString();
}
public DockerImageName getRyukDockerImageName() {
- return getImage("ryuk.container.image", "testcontainers/ryuk:0.3.0");
+ return getImage(RYUK_IMAGE);
}
@Deprecated
public String getSSHdImage() {
- return getSSHdDockerImageName().asCanonicalNameString();
+ return getImage(SSHD_IMAGE).asCanonicalNameString();
}
public DockerImageName getSSHdDockerImageName() {
- return getImage("sshd.container.image", "testcontainers/sshd:1.0.0");
+ return getImage(SSHD_IMAGE);
}
public Integer getRyukTimeout() {
- return Integer.parseInt((String) properties.getOrDefault("ryuk.container.timeout", "30"));
+ return Integer.parseInt(getEnvVarOrProperty("ryuk.container.timeout", "30"));
}
@Deprecated
public String getKafkaImage() {
- return getKafkaDockerImageName().asCanonicalNameString();
+ return getImage(KAFKA_IMAGE).asCanonicalNameString();
}
public DockerImageName getKafkaDockerImageName() {
- return getImage("kafka.container.image", "confluentinc/cp-kafka");
+ return getImage(KAFKA_IMAGE);
+ }
+
+
+ @Deprecated
+ public String getOracleImage() {
+ return getEnvVarOrUserProperty("oracle.container.image", null);
}
@Deprecated
public String getPulsarImage() {
- return getPulsarDockerImageName().asCanonicalNameString();
+ return getImage(PULSAR_IMAGE).asCanonicalNameString();
}
public DockerImageName getPulsarDockerImageName() {
- return getImage("pulsar.container.image", "apachepulsar/pulsar");
+ return getImage(PULSAR_IMAGE);
}
@Deprecated
public String getLocalStackImage() {
- return getLocalstackDockerImageName().asCanonicalNameString();
+ return getImage(LOCALSTACK_IMAGE).asCanonicalNameString();
}
public DockerImageName getLocalstackDockerImageName() {
- return getImage("localstack.container.image", "localstack/localstack");
+ return getImage(LOCALSTACK_IMAGE);
}
+
public boolean isDisableChecks() {
- return Boolean.parseBoolean((String) environmentProperties.getOrDefault("checks.disable", "false"));
+ return Boolean.parseBoolean(getEnvVarOrUserProperty("checks.disable", "false"));
}
@UnstableAPI
public boolean environmentSupportsReuse() {
- return Boolean.parseBoolean((String) environmentProperties.getOrDefault("testcontainers.reuse.enable", "false"));
+ // specifically not supported as an environment variable or classpath property
+ return Boolean.parseBoolean(getEnvVarOrUserProperty("testcontainers.reuse.enable", "false"));
}
public String getDockerClientStrategyClassName() {
- return (String) environmentProperties.get("docker.client.strategy");
+ return getEnvVarOrUserProperty("docker.client.strategy", null);
}
public String getTransportType() {
- return properties.getProperty("transport.type", "okhttp");
+ return getEnvVarOrProperty("transport.type", "okhttp");
}
public Integer getImagePullPauseTimeout() {
- return Integer.parseInt((String) properties.getOrDefault("pull.pause.timeout", "30"));
+ return Integer.parseInt(getEnvVarOrProperty("pull.pause.timeout", "30"));
}
- @Synchronized
+ @Nullable
+ @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_")) {
+ envVarName = "TESTCONTAINERS_" + envVarName;
+ }
+
+ if (environment.containsKey(envVarName)) {
+ return environment.get(envVarName);
+ }
+
+ for (final Properties properties : propertiesSources) {
+ if (properties.get(propertyName) != null) {
+ return (String) properties.get(propertyName);
+ }
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * 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.
+ *
+ * @param propertyName name of configuration file property (dot-separated lower case)
+ * @return the found value, or null if not set
+ */
+ @Nullable
+ @Contract("_, !null -> !null")
+ public String getEnvVarOrProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
+ return getConfigurable(propertyName, defaultValue, userProperties, classpathProperties);
+ }
+
+ /**
+ * 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.
+ *
+ * @param propertyName name of configuration file property (dot-separated lower case)
+ * @return the found value, or null if not set
+ */
+ @Nullable
+ @Contract("_, !null -> !null")
+ public String getEnvVarOrUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
+ return getConfigurable(propertyName, defaultValue, userProperties);
+ }
+
+ /**
+ * 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.
+ *
+ * @param propertyName name of configuration file property (dot-separated lower case)
+ * @return the found value, or null if not set
+ */
+ @Nullable
+ @Contract("_, !null -> !null")
+ public String getUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
+ return getConfigurable(propertyName, defaultValue);
+ }
+
+ @Deprecated
+ public Properties getProperties() {
+ return Stream.of(userProperties, classpathProperties)
+ .reduce(new Properties(), (a, b) -> {
+ a.putAll(b);
+ return a;
+ });
+ }
+
+ @Deprecated
public boolean updateGlobalConfig(@NonNull String prop, @NonNull String value) {
+ return updateUserConfig(prop, value);
+ }
+
+ @Synchronized
+ public boolean updateUserConfig(@NonNull String prop, @NonNull String value) {
try {
- if (value.equals(environmentProperties.get(prop))) {
+ if (value.equals(userProperties.get(prop))) {
return false;
}
- environmentProperties.setProperty(prop, value);
+ userProperties.setProperty(prop, value);
- ENVIRONMENT_CONFIG_FILE.createNewFile();
- try (OutputStream outputStream = new FileOutputStream(ENVIRONMENT_CONFIG_FILE)) {
- environmentProperties.store(outputStream, "Modified by Testcontainers");
+ USER_CONFIG_FILE.createNewFile();
+ try (OutputStream outputStream = new FileOutputStream(USER_CONFIG_FILE)) {
+ userProperties.store(outputStream, "Modified by Testcontainers");
}
// Update internal state only if environment config was successfully updated
- properties.setProperty(prop, value);
+ userProperties.setProperty(prop, value);
return true;
} catch (Exception e) {
- log.debug("Can't store environment property {} in {}", prop, ENVIRONMENT_CONFIG_FILE);
+ log.debug("Can't store environment property {} in {}", prop, USER_CONFIG_FILE);
return false;
}
}
@@ -211,7 +319,7 @@ public boolean updateGlobalConfig(@NonNull String prop, @NonNull String value) {
@SneakyThrows(MalformedURLException.class)
private static TestcontainersConfiguration loadConfiguration() {
return new TestcontainersConfiguration(
- readProperties(ENVIRONMENT_CONFIG_FILE.toURI().toURL()),
+ readProperties(USER_CONFIG_FILE.toURI().toURL()),
Stream
.of(
TestcontainersConfiguration.class.getClassLoader(),
@@ -223,8 +331,8 @@ private static TestcontainersConfiguration loadConfiguration() {
.reduce(new Properties(), (a, b) -> {
a.putAll(b);
return a;
- })
- );
+ }),
+ System.getenv());
}
private static Properties readProperties(URL url) {
@@ -239,4 +347,24 @@ private static Properties readProperties(URL url) {
}
return properties;
}
+
+ private DockerImageName getImage(final String defaultValue) {
+ return getConfiguredSubstituteImage(DockerImageName.parse(defaultValue));
+ }
+
+ DockerImageName getConfiguredSubstituteImage(DockerImageName original) {
+ for (final Map.Entry entry : CONTAINER_MAPPING.entrySet()) {
+ if (original.isCompatibleWith(entry.getKey())) {
+ return
+ Optional.ofNullable(entry.getValue())
+ .map(propertyName -> getEnvVarOrProperty(propertyName, null))
+ .map(String::valueOf)
+ .map(String::trim)
+ .map(DockerImageName::parse)
+ .orElse(original)
+ .asCompatibleSubstituteFor(original);
+ }
+ }
+ return original;
+ }
}
diff --git a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java
index ccd68fbd984..e48a2a6fdf9 100644
--- a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java
+++ b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java
@@ -1,57 +1,107 @@
package org.testcontainers.utility;
-import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
-import static org.rnorth.visibleassertions.VisibleAssertions.assertFalse;
-import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Properties;
import java.util.UUID;
-import org.junit.Test;
+
+import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
+import static org.rnorth.visibleassertions.VisibleAssertions.assertFalse;
+import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
public class TestcontainersConfigurationTest {
- final Properties environmentProperties = new Properties();
+ private Properties userProperties;
+ private Properties classpathProperties;
+ private Map environment;
- final Properties classpathProperties = new Properties();
+ @Before
+ public void setUp() {
+ userProperties = new Properties();
+ classpathProperties = new Properties();
+ environment = new HashMap<>();
+ }
@Test
- public void shouldReadChecksFromEnvironmentOnly() {
+ public void shouldNotReadChecksFromClasspathProperties() {
assertFalse("checks enabled by default", newConfig().isDisableChecks());
classpathProperties.setProperty("checks.disable", "true");
assertFalse("checks are not affected by classpath properties", newConfig().isDisableChecks());
+ }
+
+ @Test
+ public void shouldReadChecksFromUserProperties() {
+ assertFalse("checks enabled by default", newConfig().isDisableChecks());
- environmentProperties.setProperty("checks.disable", "true");
- assertTrue("checks disabled", newConfig().isDisableChecks());
+ userProperties.setProperty("checks.disable", "true");
+ assertTrue("checks disabled via user properties", newConfig().isDisableChecks());
}
@Test
- public void shouldReadDockerClientStrategyFromEnvironmentOnly() {
+ public void shouldReadChecksFromEnvironment() {
+ assertFalse("checks enabled by default", newConfig().isDisableChecks());
+
+ userProperties.remove("checks.disable");
+ environment.put("TESTCONTAINERS_CHECKS_DISABLE", "true");
+ assertTrue("checks disabled via env var", newConfig().isDisableChecks());
+ }
+
+ @Test
+ public void shouldNotReadDockerClientStrategyFromClasspathProperties() {
String currentValue = newConfig().getDockerClientStrategyClassName();
classpathProperties.setProperty("docker.client.strategy", UUID.randomUUID().toString());
assertEquals("Docker client strategy is not affected by classpath properties", currentValue, newConfig().getDockerClientStrategyClassName());
+ }
+
+ @Test
+ public void shouldReadDockerClientStrategyFromUserProperties() {
+ userProperties.setProperty("docker.client.strategy", "foo");
+ assertEquals("Docker client strategy is changed by user property", "foo", newConfig().getDockerClientStrategyClassName());
+ }
- environmentProperties.setProperty("docker.client.strategy", "foo");
- assertEquals("Docker client strategy is changed", "foo", newConfig().getDockerClientStrategyClassName());
+ @Test
+ public void shouldReadDockerClientStrategyFromEnvironment() {
+ userProperties.remove("docker.client.strategy");
+ environment.put("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY", "foo");
+ assertEquals("Docker client strategy is changed by env var", "foo", newConfig().getDockerClientStrategyClassName());
}
@Test
- public void shouldReadReuseFromEnvironmentOnly() {
+ public void shouldNotReadReuseFromClasspathProperties() {
assertFalse("no reuse by default", newConfig().environmentSupportsReuse());
classpathProperties.setProperty("testcontainers.reuse.enable", "true");
assertFalse("reuse is not affected by classpath properties", newConfig().environmentSupportsReuse());
+ }
- environmentProperties.setProperty("testcontainers.reuse.enable", "true");
- assertTrue("reuse enabled", newConfig().environmentSupportsReuse());
+ @Test
+ public void shouldReadReuseFromUserProperties() {
+ assertFalse("no reuse by default", newConfig().environmentSupportsReuse());
+
+ userProperties.setProperty("testcontainers.reuse.enable", "true");
+ assertTrue("reuse enabled via user property", newConfig().environmentSupportsReuse());
+ }
+ @Test
+ public void shouldReadReuseFromEnvironment() {
+ assertFalse("no reuse by default", newConfig().environmentSupportsReuse());
- environmentProperties.setProperty("ryuk.container.image", " testcontainersofficial/ryuk:0.3.0 ");
- assertEquals("trailing whitespace was not removed from image name property", "testcontainersofficial/ryuk:0.3.0",newConfig().getRyukDockerImageName().asCanonicalNameString());
+ userProperties.remove("testcontainers.reuse.enable");
+ environment.put("TESTCONTAINERS_REUSE_ENABLE", "true");
+ assertTrue("reuse enabled via env var", newConfig().environmentSupportsReuse());
+ }
+ @Test
+ public void shouldTrimImageNames() {
+ userProperties.setProperty("ryuk.container.image", " testcontainersofficial/ryuk:0.3.0 ");
+ assertEquals("trailing whitespace was not removed from image name property", "testcontainersofficial/ryuk:0.3.0",newConfig().getRyukImage());
}
private TestcontainersConfiguration newConfig() {
- return new TestcontainersConfiguration(environmentProperties, classpathProperties);
+ return new TestcontainersConfiguration(userProperties, classpathProperties, environment);
}
}