From 0f51dfa6206162baa610463ce5d56358b5812d60 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 29 Oct 2020 11:02:48 +0000 Subject: [PATCH 01/16] Refactor Testcontainers configuration to allow config by env var --- .../utility/TestcontainersConfiguration.java | 252 +++++++++++++----- .../TestcontainersConfigurationTest.java | 86 ++++-- 2 files changed, 258 insertions(+), 80 deletions(-) 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: + *

+ *

+ * 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); } } From 950af34178f3a28a3747b0b446738e83c7b62bbf Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 29 Oct 2020 11:53:17 +0000 Subject: [PATCH 02/16] Add Image substitution mechanism Builds upon #3021 and #3411: * adds a pluggable image substitution mechanism using ServiceLoader, enabling users to perform custom substitution/auditing of images being used by their tests * provides a default implementation that behaves similarly to legacy `TestcontainersConfiguration` approach (`testcontainers.properties`) Notes: * behaviour is similar but not quite identical to `TestcontainersConfiguration`: use of a configured custom image for, e.g. Kafka/Pulsar that does not have a tag specified causes the substitution to take effect for all usages. It seems very unlikely that people would be using a mix of the config file image overrides in some places _and_ specific images specified in code in others. * Duplication of default image names in modules vs `TestcontainersConfiguration` class is intentional: specifying image overrides in `testcontainers.properties` should be removed in the future. * ~Add log deprecation warnings when `testcontainers.properties` image overrides are used.~ Defer to a future release? --- .../testcontainers/DockerClientFactory.java | 11 +- .../containers/DockerComposeContainer.java | 5 +- .../containers/GenericContainer.java | 6 +- .../containers/PortForwardingContainer.java | 6 +- .../containers/SocatContainer.java | 8 +- .../containers/VncRecordingContainer.java | 4 +- .../DockerClientProviderStrategy.java | 2 +- .../images/RemoteDockerImage.java | 10 +- ...ConfigurationFileImageNameSubstitutor.java | 52 +++++++++ .../utility/DefaultImageNameSubstitutor.java | 43 +++++++ .../utility/ImageNameSubstitutor.java | 97 ++++++++++++++++ .../utility/ResourceReaper.java | 4 +- .../utility/TestcontainersConfiguration.java | 37 ------ .../testcontainers/utility/Versioning.java | 1 + ...estcontainers.utility.ImageNameSubstitutor | 1 + .../DockerClientFactoryTest.java | 9 +- .../java/org/testcontainers/TestImages.java | 3 +- .../utility/AuthenticatedImagePullTest.java | 26 ++--- .../DefaultImageNameSubstitutorTest.java | 38 ++++++ .../DockerImageNameCompatibilityTest.java | 11 +- .../utility/ImageNameSubstitutorTest.java | 17 +++ .../TestcontainersConfigurationTest.java | 30 +++++ docs/examples/junit4/generic/build.gradle | 3 + .../generic/ExampleImageNameSubstitutor.java | 32 +++++ .../generic/ImageNameSubstitutionTest.java | 43 +++++++ docs/features/configuration.md | 36 ++++-- docs/features/image_name_substitution.md | 109 ++++++++++++++++++ docs/features/pull_rate_limiting.md | 19 +++ .../containers/KafkaContainer.java | 5 +- .../containers/KafkaContainerTest.java | 2 +- .../localstack/LocalStackContainer.java | 5 +- .../containers/OracleContainer.java | 3 +- .../containers/PulsarContainer.java | 5 +- .../spock/SpockTestImages.groovy | 5 +- 34 files changed, 575 insertions(+), 113 deletions(-) create mode 100644 core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java create mode 100644 core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java create mode 100644 core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java create mode 100644 core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor create mode 100644 core/src/test/java/org/testcontainers/utility/DefaultImageNameSubstitutorTest.java create mode 100644 core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java create mode 100644 docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java create mode 100644 docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java create mode 100644 docs/features/image_name_substitution.md create mode 100644 docs/features/pull_rate_limiting.md diff --git a/core/src/main/java/org/testcontainers/DockerClientFactory.java b/core/src/main/java/org/testcontainers/DockerClientFactory.java index 571b18fbcce..d44cd3b5b6f 100644 --- a/core/src/main/java/org/testcontainers/DockerClientFactory.java +++ b/core/src/main/java/org/testcontainers/DockerClientFactory.java @@ -25,6 +25,8 @@ import org.testcontainers.dockerclient.TransportConfig; import org.testcontainers.images.TimeLimitedLoggedPullImageResultCallback; import org.testcontainers.utility.ComparableVersion; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.ImageNameSubstitutor; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.ResourceReaper; import org.testcontainers.utility.TestcontainersConfiguration; @@ -61,7 +63,7 @@ public class DockerClientFactory { TESTCONTAINERS_SESSION_ID_LABEL, SESSION_ID ); - private static final String TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString(); + private static final DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.5"); private static DockerClientFactory instance; // Cached client configuration @@ -343,8 +345,11 @@ public T runInsideDocker(Consumer createContainerCmdCons } private T runInsideDocker(DockerClient client, Consumer createContainerCmdConsumer, BiFunction block) { - checkAndPullImage(client, TINY_IMAGE); - CreateContainerCmd createContainerCmd = client.createContainerCmd(TINY_IMAGE) + + final String tinyImage = ImageNameSubstitutor.instance().apply(TINY_IMAGE).asCanonicalNameString(); + + checkAndPullImage(client, tinyImage); + CreateContainerCmd createContainerCmd = client.createContainerCmd(tinyImage) .withLabels(DEFAULT_LABELS); createContainerCmdConsumer.accept(createContainerCmd); String id = createContainerCmd.exec().getId(); diff --git a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java index bd06083644d..7a67073e383 100644 --- a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java +++ b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java @@ -29,11 +29,11 @@ import org.testcontainers.utility.AuditLogger; import org.testcontainers.utility.Base58; import org.testcontainers.utility.CommandLine; +import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.LogUtils; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.ResourceReaper; -import org.testcontainers.utility.TestcontainersConfiguration; import org.zeroturnaround.exec.InvalidExitValueException; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; @@ -608,10 +608,11 @@ interface DockerCompose { class ContainerisedDockerCompose extends GenericContainer implements DockerCompose { public static final char UNIX_PATH_SEPERATOR = ':'; + public static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker/compose:1.24.1"); public ContainerisedDockerCompose(List composeFiles, String identifier) { - super(TestcontainersConfiguration.getInstance().getDockerComposeDockerImageName()); + super(DEFAULT_IMAGE_NAME); addEnv(ENV_PROJECT_NAME, identifier); // Map the docker compose file into the container diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 95050d8f8ee..641096a0779 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -239,13 +239,9 @@ public GenericContainer(@NonNull final RemoteDockerImage image) { */ @Deprecated public GenericContainer() { - this(TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString()); + this(TestcontainersConfiguration.getInstance().getTinyImage()); } - /** - * @deprecated use {@link GenericContainer(DockerImageName)} instead - */ - @Deprecated public GenericContainer(@NonNull final String dockerImageName) { this.setDockerImageName(dockerImageName); } diff --git a/core/src/main/java/org/testcontainers/containers/PortForwardingContainer.java b/core/src/main/java/org/testcontainers/containers/PortForwardingContainer.java index e42f2681675..b3f020500a1 100644 --- a/core/src/main/java/org/testcontainers/containers/PortForwardingContainer.java +++ b/core/src/main/java/org/testcontainers/containers/PortForwardingContainer.java @@ -5,15 +5,15 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.SneakyThrows; -import org.testcontainers.utility.TestcontainersConfiguration; +import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.AbstractMap; import java.util.Collections; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; public enum PortForwardingContainer { @@ -29,7 +29,7 @@ public enum PortForwardingContainer { @SneakyThrows private Connection createSSHSession() { String password = UUID.randomUUID().toString(); - container = new GenericContainer<>(TestcontainersConfiguration.getInstance().getSSHdDockerImageName()) + container = new GenericContainer<>(DockerImageName.parse("testcontainers/sshd:1.0.0")) .withExposedPorts(22) .withEnv("PASSWORD", password) .withCommand( diff --git a/core/src/main/java/org/testcontainers/containers/SocatContainer.java b/core/src/main/java/org/testcontainers/containers/SocatContainer.java index fbca73eb29d..7592d9f4d1f 100644 --- a/core/src/main/java/org/testcontainers/containers/SocatContainer.java +++ b/core/src/main/java/org/testcontainers/containers/SocatContainer.java @@ -1,12 +1,10 @@ package org.testcontainers.containers; -import org.testcontainers.utility.Base58; -import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.TestcontainersConfiguration; - import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import org.testcontainers.utility.Base58; +import org.testcontainers.utility.DockerImageName; /** * A socat container is used as a TCP proxy, enabling any TCP port of another container to be exposed @@ -17,7 +15,7 @@ public class SocatContainer extends GenericContainer { private final Map targets = new HashMap<>(); public SocatContainer() { - this(TestcontainersConfiguration.getInstance().getSocatDockerImageName()); + this(DockerImageName.parse("alpine/socat:1.7.3.4-r0")); } public SocatContainer(final DockerImageName dockerImageName) { diff --git a/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java b/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java index 8ac060c2af6..de720110a51 100644 --- a/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java +++ b/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java @@ -6,7 +6,7 @@ import lombok.ToString; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; -import org.testcontainers.utility.TestcontainersConfiguration; +import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.InputStream; @@ -52,7 +52,7 @@ public VncRecordingContainer(@NonNull GenericContainer targetContainer) { * Create a sidekick container and attach it to another container. The VNC output of that container will be recorded. */ public VncRecordingContainer(@NonNull Network network, @NonNull String targetNetworkAlias) throws IllegalStateException { - super(TestcontainersConfiguration.getInstance().getVncDockerImageName()); + super(DockerImageName.parse("testcontainers/vnc-recorder:1.1.0")); this.targetNetworkAlias = targetNetworkAlias; withNetwork(network); diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java index e21c75a581d..1bd51da1263 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java @@ -171,7 +171,7 @@ public static DockerClientProviderStrategy getFirstValidStrategy(List imageFuture) { @@ -100,7 +101,10 @@ protected final String resolve() { } private DockerImageName getImageName() throws InterruptedException, ExecutionException { - return imageNameFuture.get(); + final DockerImageName specifiedImageName = imageNameFuture.get(); + + // Allow the image name to be substituted + return ImageNameSubstitutor.instance().apply(specifiedImageName); } @ToString.Include(name = "imageName", rank = 1) diff --git a/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java new file mode 100644 index 00000000000..e163f8ffff4 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java @@ -0,0 +1,52 @@ +package org.testcontainers.utility; + +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +/** + * {@link ImageNameSubstitutor} which takes replacement image names from configuration. + * See {@link TestcontainersConfiguration} for the subset of image names which can be substituted using this mechanism. + *

+ * WARNING: this class is not intended to be public, but {@link java.util.ServiceLoader} + * requires it to be so. Public visibility DOES NOT make it part of the public API. + */ +@Slf4j +public class ConfigurationFileImageNameSubstitutor extends ImageNameSubstitutor { + + private final TestcontainersConfiguration configuration; + + public ConfigurationFileImageNameSubstitutor() { + this(TestcontainersConfiguration.getInstance()); + } + + @VisibleForTesting + ConfigurationFileImageNameSubstitutor(TestcontainersConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public DockerImageName apply(final DockerImageName original) { + final DockerImageName result = configuration + .getConfiguredSubstituteImage(original) + .asCompatibleSubstituteFor(original); + + if (!result.equals(original)) { + log.warn("Image name {} was substituted by configuration to {}. This approach is deprecated and will be removed in the future", + original, + result + ); + } + + return result; + } + + @Override + protected int getPriority() { + return -2; + } + + @Override + protected String getDescription() { + return getClass().getSimpleName(); + } +} diff --git a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java new file mode 100644 index 00000000000..5657a9051aa --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java @@ -0,0 +1,43 @@ +package org.testcontainers.utility; + +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +/** + * Testcontainers' default implementation of {@link ImageNameSubstitutor}. + * Delegates to {@link ConfigurationFileImageNameSubstitutor}. + *

+ * WARNING: this class is not intended to be public, but {@link java.util.ServiceLoader} + * requires it to be so. Public visibility DOES NOT make it part of the public API. + */ +@Slf4j +public class DefaultImageNameSubstitutor extends ImageNameSubstitutor { + + private final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor; + + public DefaultImageNameSubstitutor() { + configurationFileImageNameSubstitutor = new ConfigurationFileImageNameSubstitutor(); + } + + @VisibleForTesting + DefaultImageNameSubstitutor( + final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor + ) { + this.configurationFileImageNameSubstitutor = configurationFileImageNameSubstitutor; + } + + @Override + public DockerImageName apply(final DockerImageName original) { + return configurationFileImageNameSubstitutor.apply(original); + } + + @Override + protected int getPriority() { + return 0; + } + + @Override + protected String getDescription() { + return "DefaultImageNameSubstitutor (delegates to '" + configurationFileImageNameSubstitutor.getDescription() + "')"; + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java new file mode 100644 index 00000000000..4179d2b5f52 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java @@ -0,0 +1,97 @@ +package org.testcontainers.utility; + +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.util.ServiceLoader; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +import static java.util.Comparator.comparingInt; + +/** + * An image name substitutor converts a Docker image name, as may be specified in code, to an alternative name. + * This is intended to provide a way to override image names, for example to enforce pulling of images from a private + * registry. + */ +@Slf4j +public abstract class ImageNameSubstitutor implements Function { + + @VisibleForTesting + static ImageNameSubstitutor instance; + + public synchronized static ImageNameSubstitutor instance() { + if (instance == null) { + final ServiceLoader serviceLoader = ServiceLoader.load(ImageNameSubstitutor.class); + + instance = StreamSupport.stream(serviceLoader.spliterator(), false) + .peek(it -> log.debug("Found ImageNameSubstitutor using ServiceLoader: {} (priority {}) ", it, it.getPriority())) + .max(comparingInt(ImageNameSubstitutor::getPriority)) + .map(ImageNameSubstitutor::wrapWithLogging) + .orElseThrow(() -> new RuntimeException("Unable to find any ImageNameSubstitutor using ServiceLoader")); + + log.info("Using ImageNameSubstitutor: {}", instance); + } + + return instance; + } + + private static ImageNameSubstitutor wrapWithLogging(final ImageNameSubstitutor wrappedInstance) { + return new LogWrappedImageNameSubstitutor(wrappedInstance); + } + + /** + * Substitute a {@link DockerImageName} for another, for example to replace a generic Docker Hub image name with a + * private registry copy of the image. + * + * @param original original name to be replaced + * @return a replacement name, or the original, as appropriate + */ + public abstract DockerImageName apply(DockerImageName original); + + /** + * Priority of this {@link ImageNameSubstitutor} compared to other instances that may be found by the service + * loader. The highest priority instance found will always be used. + * + * @return a priority + */ + protected abstract int getPriority(); + + protected abstract String getDescription(); + + /** + * Wrapper substitutor which logs which substitutions have been performed. + */ + static class LogWrappedImageNameSubstitutor extends ImageNameSubstitutor { + @VisibleForTesting + final ImageNameSubstitutor wrappedInstance; + + public LogWrappedImageNameSubstitutor(final ImageNameSubstitutor wrappedInstance) { + this.wrappedInstance = wrappedInstance; + } + + @Override + public DockerImageName apply(final DockerImageName original) { + final String className = wrappedInstance.getClass().getName(); + final DockerImageName replacementImage = wrappedInstance.apply(original); + + if (!replacementImage.equals(original)) { + log.info("Using {} as a substitute image for {} (using image substitutor: {})", replacementImage.asCanonicalNameString(), original.asCanonicalNameString(), className); + return replacementImage; + } else { + log.debug("Did not find a substitute image for {} (using image substitutor: {})", original.asCanonicalNameString(), className); + return original; + } + } + + @Override + protected int getPriority() { + return wrappedInstance.getPriority(); + } + + @Override + protected String getDescription() { + return wrappedInstance.getDescription(); + } + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java index c37e6660654..3b0cfa1e289 100644 --- a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java +++ b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java @@ -72,7 +72,9 @@ private ResourceReaper() { @SneakyThrows(InterruptedException.class) public static String start(String hostIpAddress, DockerClient client) { - String ryukImage = TestcontainersConfiguration.getInstance().getRyukDockerImageName().asCanonicalNameString(); + String ryukImage = ImageNameSubstitutor.instance() + .apply(DockerImageName.parse("testcontainers/ryuk:0.3.0")) + .asCanonicalNameString(); DockerClientFactory.instance().checkAndPullImage(client, ryukImage); List binds = new ArrayList<>(); diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java index b3c8e88a328..b685eb011bc 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -104,37 +104,21 @@ public String getSocatContainerImage() { return getImage(SOCAT_IMAGE).asCanonicalNameString(); } - public DockerImageName getSocatDockerImageName() { - return getImage(SOCAT_IMAGE); - } - @Deprecated public String getVncRecordedContainerImage() { return getImage(VNC_RECORDER_IMAGE).asCanonicalNameString(); } - public DockerImageName getVncDockerImageName() { - return getImage(VNC_RECORDER_IMAGE); - } - @Deprecated public String getDockerComposeContainerImage() { return getImage(COMPOSE_IMAGE).asCanonicalNameString(); } - public DockerImageName getDockerComposeDockerImageName() { - return getImage(COMPOSE_IMAGE); - } - @Deprecated public String getTinyImage() { return getImage(ALPINE_IMAGE).asCanonicalNameString(); } - public DockerImageName getTinyDockerImageName() { - return getImage(ALPINE_IMAGE); - } - public boolean isRyukPrivileged() { return Boolean .parseBoolean(getEnvVarOrProperty("ryuk.container.privileged", "false")); @@ -145,19 +129,11 @@ public String getRyukImage() { return getImage(RYUK_IMAGE).asCanonicalNameString(); } - public DockerImageName getRyukDockerImageName() { - return getImage(RYUK_IMAGE); - } - @Deprecated public String getSSHdImage() { return getImage(SSHD_IMAGE).asCanonicalNameString(); } - public DockerImageName getSSHdDockerImageName() { - return getImage(SSHD_IMAGE); - } - public Integer getRyukTimeout() { return Integer.parseInt(getEnvVarOrProperty("ryuk.container.timeout", "30")); } @@ -167,11 +143,6 @@ public String getKafkaImage() { return getImage(KAFKA_IMAGE).asCanonicalNameString(); } - public DockerImageName getKafkaDockerImageName() { - return getImage(KAFKA_IMAGE); - } - - @Deprecated public String getOracleImage() { return getEnvVarOrUserProperty("oracle.container.image", null); @@ -182,19 +153,11 @@ public String getPulsarImage() { return getImage(PULSAR_IMAGE).asCanonicalNameString(); } - public DockerImageName getPulsarDockerImageName() { - return getImage(PULSAR_IMAGE); - } - @Deprecated public String getLocalStackImage() { return getImage(LOCALSTACK_IMAGE).asCanonicalNameString(); } - public DockerImageName getLocalstackDockerImageName() { - return getImage(LOCALSTACK_IMAGE); - } - public boolean isDisableChecks() { return Boolean.parseBoolean(getEnvVarOrUserProperty("checks.disable", "false")); diff --git a/core/src/main/java/org/testcontainers/utility/Versioning.java b/core/src/main/java/org/testcontainers/utility/Versioning.java index 84dcf46274e..4b1da407440 100644 --- a/core/src/main/java/org/testcontainers/utility/Versioning.java +++ b/core/src/main/java/org/testcontainers/utility/Versioning.java @@ -46,6 +46,7 @@ public int hashCode() { @EqualsAndHashCode class TagVersioning implements Versioning { public static final String TAG_REGEX = "[\\w][\\w.\\-]{0,127}"; + static final TagVersioning LATEST = new TagVersioning("latest"); private final String tag; TagVersioning(String tag) { diff --git a/core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor b/core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor new file mode 100644 index 00000000000..069dc7ee710 --- /dev/null +++ b/core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor @@ -0,0 +1 @@ +org.testcontainers.utility.DefaultImageNameSubstitutor diff --git a/core/src/test/java/org/testcontainers/DockerClientFactoryTest.java b/core/src/test/java/org/testcontainers/DockerClientFactoryTest.java index 030e4164f00..d484cc92eaf 100644 --- a/core/src/test/java/org/testcontainers/DockerClientFactoryTest.java +++ b/core/src/test/java/org/testcontainers/DockerClientFactoryTest.java @@ -1,5 +1,9 @@ package org.testcontainers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.testcontainers.TestImages.TINY_IMAGE; + import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.exception.DockerException; import com.github.dockerjava.api.exception.NotFoundException; @@ -12,9 +16,6 @@ import org.testcontainers.utility.MockTestcontainersConfigurationRule; import org.testcontainers.utility.TestcontainersConfiguration; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - /** * Test for {@link DockerClientFactory}. */ @@ -30,7 +31,7 @@ public void runCommandInsideDockerShouldNotFailIfImageDoesNotExistsLocally() { try { //remove tiny image, so it will be pulled during next command run dockFactory.client() - .removeImageCmd(TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString()) + .removeImageCmd(TINY_IMAGE.asCanonicalNameString()) .withForce(true).exec(); } catch (NotFoundException ignored) { // Do not fail if it's not pulled yet diff --git a/core/src/test/java/org/testcontainers/TestImages.java b/core/src/test/java/org/testcontainers/TestImages.java index 3f16e88d882..7052f7e8f9d 100644 --- a/core/src/test/java/org/testcontainers/TestImages.java +++ b/core/src/test/java/org/testcontainers/TestImages.java @@ -1,7 +1,6 @@ package org.testcontainers; import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.TestcontainersConfiguration; public interface TestImages { DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:3.0.2"); @@ -9,5 +8,5 @@ public interface TestImages { DockerImageName MONGODB_IMAGE = DockerImageName.parse("mongo:3.1.5"); DockerImageName ALPINE_IMAGE = DockerImageName.parse("alpine:3.2"); DockerImageName DOCKER_REGISTRY_IMAGE = DockerImageName.parse("registry:2.7.0"); - DockerImageName TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyDockerImageName(); + DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.5"); } diff --git a/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java b/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java index e49ff2880df..c99770c4fbf 100644 --- a/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java +++ b/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java @@ -1,9 +1,21 @@ package org.testcontainers.utility; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; +import static org.testcontainers.TestImages.DOCKER_REGISTRY_IMAGE; +import static org.testcontainers.TestImages.TINY_IMAGE; + import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.PullImageResultCallback; import com.github.dockerjava.api.model.AuthConfig; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; import org.intellij.lang.annotations.Language; import org.junit.AfterClass; import org.junit.Before; @@ -18,18 +30,6 @@ import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; -import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; -import static org.testcontainers.TestImages.DOCKER_REGISTRY_IMAGE; - /** * This test checks the integration between Testcontainers and an authenticated registry, but uses * a mock instance of {@link RegistryAuthLocator} - the purpose of the test is solely to ensure that @@ -165,7 +165,7 @@ private Path getLocalTempFile(String s) throws IOException { private static void putImageInRegistry() throws InterruptedException { // It doesn't matter which image we use for this test, but use one that's likely to have been pulled already - final String dummySourceImage = TestcontainersConfiguration.getInstance().getRyukDockerImageName().asCanonicalNameString(); + final String dummySourceImage = TINY_IMAGE.asCanonicalNameString(); client.pullImageCmd(dummySourceImage) .exec(new PullImageResultCallback()) diff --git a/core/src/test/java/org/testcontainers/utility/DefaultImageNameSubstitutorTest.java b/core/src/test/java/org/testcontainers/utility/DefaultImageNameSubstitutorTest.java new file mode 100644 index 00000000000..a6f7813c3d3 --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/DefaultImageNameSubstitutorTest.java @@ -0,0 +1,38 @@ +package org.testcontainers.utility; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; + +public class DefaultImageNameSubstitutorTest { + + public static final DockerImageName ORIGINAL_IMAGE = DockerImageName.parse("foo"); + public static final DockerImageName SUBSTITUTE_IMAGE = DockerImageName.parse("bar"); + private ConfigurationFileImageNameSubstitutor underTest; + + @Rule + public MockTestcontainersConfigurationRule config = new MockTestcontainersConfigurationRule(); + + @Before + public void setUp() { + underTest = new ConfigurationFileImageNameSubstitutor(TestcontainersConfiguration.getInstance()); + } + + @Test + public void testConfigurationLookup() { + Mockito + .doReturn(SUBSTITUTE_IMAGE) + .when(TestcontainersConfiguration.getInstance()) + .getConfiguredSubstituteImage(eq(ORIGINAL_IMAGE)); + + final DockerImageName substitute = underTest.apply(ORIGINAL_IMAGE); + + assertEquals("match is found", SUBSTITUTE_IMAGE, substitute); + assertTrue("compatibility is automatically set", substitute.isCompatibleWith(ORIGINAL_IMAGE)); + } +} diff --git a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java index 9e7136ea6d3..22d67de5785 100644 --- a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java +++ b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java @@ -1,13 +1,13 @@ package org.testcontainers.utility; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - import static org.hamcrest.core.StringContains.containsString; import static org.rnorth.visibleassertions.VisibleAssertions.assertFalse; import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + public class DockerImageNameCompatibilityTest { @@ -26,9 +26,8 @@ public void testNoTagTreatedAsWildcard() { /* foo:1.2.3 != foo:4.5.6 foo:1.2.3 ~= foo - foo:1.2.3 ~= foo:latest - The test is effectively making sure that no tag and `latest` tag are equivalent + The test is effectively making sure that 'no tag' is treated as a wildcard */ assertFalse("foo:4.5.6 != foo:1.2.3", subject.isCompatibleWith(DockerImageName.parse("foo:1.2.3"))); assertTrue("foo:4.5.6 ~= foo", subject.isCompatibleWith(DockerImageName.parse("foo"))); diff --git a/core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java b/core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java new file mode 100644 index 00000000000..d39e83391f9 --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java @@ -0,0 +1,17 @@ +package org.testcontainers.utility; + +import org.junit.Test; +import org.testcontainers.utility.ImageNameSubstitutor.LogWrappedImageNameSubstitutor; + +import static org.junit.Assert.assertTrue; + +public class ImageNameSubstitutorTest { + + @Test + public void simpleServiceLoadingTest() { + final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); + + assertTrue(imageNameSubstitutor instanceof LogWrappedImageNameSubstitutor); + assertTrue(((LogWrappedImageNameSubstitutor) imageNameSubstitutor).wrappedInstance instanceof DefaultImageNameSubstitutor); + } +} diff --git a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java index e48a2a6fdf9..147e5e60bd4 100644 --- a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java +++ b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java @@ -25,6 +25,36 @@ public void setUp() { environment = new HashMap<>(); } + @Test + public void shouldSubstituteImageNamesFromClasspathProperties() { + classpathProperties.setProperty("ryuk.container.image", "foo:version"); + assertEquals( + "an image name can be pulled from classpath properties", + DockerImageName.parse("foo:version"), + newConfig().getConfiguredSubstituteImage(DockerImageName.parse("testcontainers/ryuk:any")) + ); + } + + @Test + public void shouldSubstituteImageNamesFromUserProperties() { + userProperties.setProperty("ryuk.container.image", "foo:version"); + assertEquals( + "an image name can be pulled from user properties", + DockerImageName.parse("foo:version"), + newConfig().getConfiguredSubstituteImage(DockerImageName.parse("testcontainers/ryuk:any")) + ); + } + + @Test + public void shouldSubstituteImageNamesFromEnvironmentVariables() { + environment.put("TESTCONTAINERS_RYUK_CONTAINER_IMAGE", "foo:version"); + assertEquals( + "an image name can be pulled from an environment variable", + DockerImageName.parse("foo:version"), + newConfig().getConfiguredSubstituteImage(DockerImageName.parse("testcontainers/ryuk:any")) + ); + } + @Test public void shouldNotReadChecksFromClasspathProperties() { assertFalse("checks enabled by default", newConfig().isDisableChecks()); diff --git a/docs/examples/junit4/generic/build.gradle b/docs/examples/junit4/generic/build.gradle index 5ce878c7ba2..ff1b11f142e 100644 --- a/docs/examples/junit4/generic/build.gradle +++ b/docs/examples/junit4/generic/build.gradle @@ -4,6 +4,9 @@ dependencies { testCompile "junit:junit:4.12" testCompile project(":testcontainers") testCompile project(":selenium") + testCompile project(":mysql") + + testCompile 'mysql:mysql-connector-java:8.0.21' testCompile "org.seleniumhq.selenium:selenium-api:3.141.59" testCompile 'org.assertj:assertj-core:3.15.0' } diff --git a/docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java b/docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java new file mode 100644 index 00000000000..dfb5351ecfd --- /dev/null +++ b/docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java @@ -0,0 +1,32 @@ +package generic; + +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.ImageNameSubstitutor; + +public class ExampleImageNameSubstitutor extends ImageNameSubstitutor { + + @Override + public DockerImageName apply(DockerImageName original) { + // convert the original name to something appropriate for + // our build environment + return DockerImageName.parse( + // your code goes here - silly example of capitalising + // the original name is shown + original.asCanonicalNameString().toUpperCase() + ); + } + + @Override + protected int getPriority() { + // the highest priority substitutor is used. + // Use something higher than 0, which is the priority + // of the default implementation + return 1; + } + + @Override + protected String getDescription() { + // used in logs + return "example image name substitutor"; + } +} diff --git a/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java new file mode 100644 index 00000000000..dcb421a7cbc --- /dev/null +++ b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java @@ -0,0 +1,43 @@ +package generic; + + +import org.junit.Ignore; +import org.junit.Test; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.utility.DockerImageName; + +public class ImageNameSubstitutionTest { + + @Test + public void simpleExample() { + try ( + // directDockerHubReference { + // Referring directly to an image on Docker Hub (mysql:8.0.22) + final MySQLContainer mysql = new MySQLContainer<>( + DockerImageName.parse("mysql:8.0.22") + ) + + // start the container and use it for testing + // } + ) { + mysql.start(); + } + } + + @Test @Ignore + public void substitutedExample() { + try ( + // hardcodedMirror { + // Referring directly to an image on a private registry - image name will vary + final MySQLContainer mysql = new MySQLContainer<>( + DockerImageName.parse("registry.mycompany.com/mirror/mysql:8.0.22") + .asCompatibleSubstituteFor("mysql") + ) + + // start the container and use it for testing + // } + ) { + mysql.start(); + } + } +} diff --git a/docs/features/configuration.md b/docs/features/configuration.md index 8b12f8e8d54..0ae05e7d989 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -2,14 +2,19 @@ You can override some default properties if your environment requires that. -## Configuration file location +## Configuration locations The configuration will be loaded from multiple locations. Properties are considered in the following order: -1. `.testcontainers.properties` in user's home folder. Example locations: +1. Environment variables +2. `.testcontainers.properties` in user's home folder. Example locations: **Linux:** `/home/myuser/.testcontainers.properties` **Windows:** `C:/Users/myuser/.testcontainers.properties` **macOS:** `/Users/myuser/.testcontainers.properties` -2. `testcontainers.properties` on classpath +3. `testcontainers.properties` on classpath + +Note that when using environment variables, configuration property names should be set in upper +case with underscore separators, preceded by `TESTCONTAINERS_` - e.g. `checks.disable` becomes +`TESTCONTAINERS_CHECKS_DISABLE`. ## Disabling the startup checks > **checks.disable = [true|false]** @@ -26,18 +31,28 @@ It takes a couple of seconds, but if you want to speed up your tests, you can di ## Customizing images +!!! note + This approach is discouraged and deprecated, but is documented for completeness. + Overriding individual image names via configuration may be removed in 2021. + Testcontainers uses public Docker images to perform different actions like startup checks, VNC recording and others. Some companies disallow the usage of Docker Hub, but you can override `*.image` properties with your own images from your private registry to workaround that. +> **ryuk.container.image = testcontainersofficial/ryuk:0.3.0** +> Performs fail-safe cleanup of containers, and always required (unless [Ryuk is disabled](#disabling-ryuk)) + > **tinyimage.container.image = alpine:3.5** -> Used by Testcontainers' core +> Used to check whether images can be pulled at startup, and always required (unless [startup checks are disabled](#disabling-the-startup-checks)) + +> **sshd.container.image = testcontainers/sshd:1.0.0** +> Required if [exposing host ports to containers](./networking.md#exposing-host-ports-to-the-container) > **vncrecorder.container.image = testcontainersofficial/vnc-recorder:1.1.0** -> Used by VNC recorder in Testcontainers' Seleniun integration +> Used by VNC recorder in Testcontainers' Selenium integration -> **ambassador.container.image = richnorth/ambassador:latest** +> **socat.container.image = alpine/socat** > **compose.container.image = docker/compose:1.8.0** -> Used by Docker Compose integration +> Required if using [Docker Compose](../modules/docker_compose.md) > **kafka.container.image = confluentinc/cp-kafka** > Used by KafkaContainer @@ -45,11 +60,10 @@ Some companies disallow the usage of Docker Hub, but you can override `*.image` > **localstack.container.image = localstack/localstack** > Used by LocalStack -Another possibility is to set up a registry mirror in your environment so that all images are pulled from there and not directly from Docker Hub. -For more information, see the [official Docker documentation about "Registry as a pull through cache"](https://docs.docker.com/registry/recipes/mirror/). +> **pulsar.container.image = apachepulsar/pulsar:2.2.0** +> Used by Apache Pulsar -!!!tip - Registry mirror currently only works for Docker images with image name that has no registry specified (for example, for Docker image `mariadb:10.3.6`, it works, for Docker image `quay.io/something/else`, not). +See [Image Name Substitution](./image_name_substitution.md) for other strategies for substituting image names to pull from other registries. ## Customizing Ryuk resource reaper diff --git a/docs/features/image_name_substitution.md b/docs/features/image_name_substitution.md new file mode 100644 index 00000000000..ee24e790bb7 --- /dev/null +++ b/docs/features/image_name_substitution.md @@ -0,0 +1,109 @@ +# Image name substitution + +Testcontainers supports automatic substitution of Docker image names. + +This allows replacement of an image name specified in test code with an alternative name - for example, to replace the +name of a Docker Hub image dependency with an alternative hosted on a private image registry. + +This is advisable to avoid [Docker Hub rate limiting](./pull_rate_limiting.md), and some companies will prefer this for policy reasons. + +This page describes four approaches for image name substitution: + +* [Manual substitution](#manual-substitution) - not relying upon an automated approach +* Using an Image Name Substitutor: + * [Developing a custom function for transforming image names on the fly](#developing-a-custom-function-for-transforming-image-names-on-the-fly) + * [Overriding image names individually in configuration](#overriding-image-names-individually-in-configuration) + +It is assumed that you have already set up a private registry hosting [all the Docker images your build requires](./pull_rate_limiting.md#which-images-are-used-by-testcontainers). + + + + +## Manual substitution + +Consider this if: + +* You use only a few images and updating code is not a chore +* All developers and CI machines in your organisation have access to a common registry server +* You also use one of the automated mechanisms to substitute [the images that Testcontainers itself requires](./pull_rate_limiting.md#which-images-are-used-by-testcontainers) + +This approach simply entails modifying test code manually, e.g. changing: + +For example, you may have a test that uses the `mysql` container image from Docker Hub: + + +[Direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference + + +to: + + +[Private registry image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:hardcodedMirror + + + + + + + + + + +## Developing a custom function for transforming image names on the fly + +Consider this if: + +* You have complex rules about which private registry images should be used as substitutes, e.g.: + * non-deterministic mapping of names meaning that a [name prefix](#adding-a-registry-url-prefix-to-image-names-automatically) cannot be used + * rules depending upon developer identity or location +* or you wish to add audit logging of images used in the build +* or you wish to prevent accidental usage of images that are not on an approved list + +In this case, image name references in code are **unchanged**. +i.e. you would leave as-is: + + +[Unchanged direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference + + +You can implement a custom image name substitutor by: + +* subclassing `org.testcontainers.utility.ImageNameSubstitutor` +* making sure that Testcontainers can find your custom implementation by creating a service loader file. **Do not miss this step!** + +The following is an example image substitutor implementation: + + +[Example Image Substitutor](../examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java) block:ExampleImageNameSubstitutor + + +Testcontainers can be configured to find it at runtime using the Service Loader mechanism. +To do this, create a file on the classpath at `META-INF/services/org.testcontainers.utility.ImageNameSubstitutor` +containing the full name of your custom image substitutor. + +For example: + +```text tab="src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor" +com.mycompany.testcontainers.ExampleImageNameSubstitutor +``` + + +## Overriding image names individually in configuration + +!!! note + This approach is discouraged and deprecated, but is documented for completeness. + Overriding individual image names via configuration may be removed in 2021. + +Consider this if: + +* You have many references to image names in code and changing them is impractical, and +* None of the other options are practical for you + +In this case, image name references in code are left **unchanged**. +i.e. you would leave as-is: + + +[Unchanged direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference + + +You can force Testcontainers to substitute in a different image [using a configuration file](./configuration.md), which allows some (but not all) container names to be substituted. diff --git a/docs/features/pull_rate_limiting.md b/docs/features/pull_rate_limiting.md new file mode 100644 index 00000000000..be266545afb --- /dev/null +++ b/docs/features/pull_rate_limiting.md @@ -0,0 +1,19 @@ +# Image Registry rate limiting + +As of November 2020 Docker Hub pulls are rate limited. +As Testcontainers uses Docker Hub for standard images, some users may hit these rate limits and should mitigate accordingly. + +Suggested mitigations are noted in [this issue](https://github.com/testcontainers/testcontainers-java/issues/3099) at present. + +## Which images are used by Testcontainers? + +As of the current version of Testcontainers ({{latest_version}}): + +* every image directly used by your tests +* images pulled by Testcontainers itself to support functionality: + * [`testcontainers/ryuk`](https://hub.docker.com/r/testcontainers/ryuk) - performs fail-safe cleanup of containers, and always required (unless [Ryuk is disabled](./configuration.md#disabling-ryuk)) + * [`alpine`](https://hub.docker.com/r/_/alpine) - used to check whether images can be pulled at startup, and always required (unless [startup checks are disabled](./configuration.md#disabling-the-startup-checks)) + * [`testcontainers/sshd`](https://hub.docker.com/r/testcontainers/sshd) - required if [exposing host ports to containers](./networking.md#exposing-host-ports-to-the-container) + * [`testcontainers/vnc-recorder`](https://hub.docker.com/r/testcontainers/vnc-recorder) - required if using [Webdriver containers](../modules/webdriver_containers.md) and using the screen recording feature + * [`docker/compose`](https://hub.docker.com/r/docker/compose) - required if using [Docker Compose](../modules/docker_compose.md) + * [`alpine/socat`](https://hub.docker.com/r/alpine/socat) - required if using [Docker Compose](../modules/docker_compose.md) diff --git a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java index ae44e4803a9..e195d72f64a 100644 --- a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java +++ b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java @@ -4,7 +4,6 @@ import lombok.SneakyThrows; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.TestcontainersConfiguration; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; @@ -36,7 +35,7 @@ public class KafkaContainer extends GenericContainer { */ @Deprecated public KafkaContainer() { - this(TestcontainersConfiguration.getInstance().getKafkaDockerImageName().withTag(DEFAULT_TAG)); + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** @@ -44,7 +43,7 @@ public KafkaContainer() { */ @Deprecated public KafkaContainer(String confluentPlatformVersion) { - this(TestcontainersConfiguration.getInstance().getKafkaDockerImageName().withTag(confluentPlatformVersion)); + this(DEFAULT_IMAGE_NAME.withTag(confluentPlatformVersion)); } public KafkaContainer(final DockerImageName dockerImageName) { diff --git a/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java b/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java index 08b9fb6f856..cfc2f8178bd 100644 --- a/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java +++ b/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java @@ -61,7 +61,7 @@ public void testUsageWithSpecificImage() throws Exception { @Test public void testUsageWithVersion() throws Exception { try ( - KafkaContainer kafka = new KafkaContainer("5.2.1") + KafkaContainer kafka = new KafkaContainer("5.5.1") ) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); diff --git a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java index 7532ebe0b3d..b8416afb1ed 100644 --- a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java +++ b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java @@ -14,7 +14,6 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.TestcontainersConfiguration; import java.net.InetAddress; import java.net.URI; @@ -65,7 +64,7 @@ public class LocalStackContainer extends GenericContainer { */ @Deprecated public LocalStackContainer() { - this(TestcontainersConfiguration.getInstance().getLocalstackDockerImageName().withTag(DEFAULT_TAG)); + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** @@ -73,7 +72,7 @@ public LocalStackContainer() { */ @Deprecated public LocalStackContainer(String version) { - this(TestcontainersConfiguration.getInstance().getLocalstackDockerImageName().withTag(version)); + this(DEFAULT_IMAGE_NAME.withTag(version)); } /** diff --git a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java index b5700c1e84e..0a66ad6fd27 100644 --- a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java +++ b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java @@ -22,8 +22,7 @@ public class OracleContainer extends JdbcDatabaseContainer { private String password = "oracle"; private static String resolveImageName() { - String image = TestcontainersConfiguration.getInstance() - .getProperties().getProperty("oracle.container.image"); + String image = TestcontainersConfiguration.getInstance().getOracleImage(); if (image == null) { throw new IllegalStateException("An image to use for Oracle containers must be configured. " + diff --git a/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java b/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java index bceff8927d7..b3c836b56a5 100644 --- a/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java +++ b/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java @@ -3,7 +3,6 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.TestcontainersConfiguration; /** * This container wraps Apache Pulsar running in standalone mode @@ -25,7 +24,7 @@ public class PulsarContainer extends GenericContainer { */ @Deprecated public PulsarContainer() { - this(TestcontainersConfiguration.getInstance().getPulsarDockerImageName().withTag(DEFAULT_TAG)); + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** @@ -33,7 +32,7 @@ public PulsarContainer() { */ @Deprecated public PulsarContainer(String pulsarVersion) { - this(TestcontainersConfiguration.getInstance().getPulsarDockerImageName().withTag(pulsarVersion)); + this(DEFAULT_IMAGE_NAME.withTag(pulsarVersion)); } public PulsarContainer(final DockerImageName dockerImageName) { diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy index 526c1fe5c42..9698e7d433a 100644 --- a/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy @@ -1,11 +1,10 @@ -package org.testcontainers.spock; +package org.testcontainers.spock import org.testcontainers.utility.DockerImageName -import org.testcontainers.utility.TestcontainersConfiguration; interface SpockTestImages { DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:5.7.22") DockerImageName POSTGRES_TEST_IMAGE = DockerImageName.parse("postgres:9.6.12") DockerImageName HTTPD_IMAGE = DockerImageName.parse("httpd:2.4-alpine") - DockerImageName TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyDockerImageName() + DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.5") } From b02c734d12858a416a11f49a1cd978a350a54977 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 29 Oct 2020 13:20:17 +0000 Subject: [PATCH 03/16] Remove extraneous change --- core/src/main/java/org/testcontainers/utility/Versioning.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/utility/Versioning.java b/core/src/main/java/org/testcontainers/utility/Versioning.java index 4b1da407440..84dcf46274e 100644 --- a/core/src/main/java/org/testcontainers/utility/Versioning.java +++ b/core/src/main/java/org/testcontainers/utility/Versioning.java @@ -46,7 +46,6 @@ public int hashCode() { @EqualsAndHashCode class TagVersioning implements Versioning { public static final String TAG_REGEX = "[\\w][\\w.\\-]{0,127}"; - static final TagVersioning LATEST = new TagVersioning("latest"); private final String tag; TagVersioning(String tag) { From 192475997afd7868e3b2674066c4e4acd8c6aeb1 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 29 Oct 2020 13:31:38 +0000 Subject: [PATCH 04/16] Un-ignore docs example test by implementing a 'reversing' image name substitutor --- .../generic/ImageNameSubstitutionTest.java | 8 +++-- .../TestSpecificImageNameSubstitutor.java | 34 +++++++++++++++++++ ...estcontainers.utility.ImageNameSubstitutor | 1 + .../src/test/resources/logback-test.xml | 14 ++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java create mode 100644 docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor create mode 100644 docs/examples/junit4/generic/src/test/resources/logback-test.xml diff --git a/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java index dcb421a7cbc..b9e67437b5d 100644 --- a/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java +++ b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java @@ -1,7 +1,6 @@ package generic; -import org.junit.Ignore; import org.junit.Test; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.utility.DockerImageName; @@ -24,7 +23,12 @@ public void simpleExample() { } } - @Test @Ignore + /** + * Note that this test uses a fake image name, which will only work because + * {@link generic.support.TestSpecificImageNameSubstitutor} steps in to override the substitution for this exact + * image name. + */ + @Test public void substitutedExample() { try ( // hardcodedMirror { diff --git a/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java b/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java new file mode 100644 index 00000000000..aa5e6b30f4b --- /dev/null +++ b/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java @@ -0,0 +1,34 @@ +package generic.support; + +import org.testcontainers.utility.DefaultImageNameSubstitutor; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.ImageNameSubstitutor; + +/** + * An {@link ImageNameSubstitutor} which makes it possible to use fake image names in + * {@link generic.ImageNameSubstitutionTest}. This implementation simply reverses a fake image name when presented, and + * is hardcoded to act upon the specific fake name in that test. + */ +public class TestSpecificImageNameSubstitutor extends ImageNameSubstitutor { + + private final DefaultImageNameSubstitutor defaultImageNameSubstitutor = new DefaultImageNameSubstitutor(); + + @Override + public DockerImageName apply(final DockerImageName original) { + if (original.equals(DockerImageName.parse("registry.mycompany.com/mirror/mysql:8.0.22"))) { + return defaultImageNameSubstitutor.apply(DockerImageName.parse("mysql")); + } else { + return defaultImageNameSubstitutor.apply(original); + } + } + + @Override + protected int getPriority() { + return 1; + } + + @Override + protected String getDescription() { + return TestSpecificImageNameSubstitutor.class.getSimpleName(); + } +} diff --git a/docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor b/docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor new file mode 100644 index 00000000000..93a4769fe46 --- /dev/null +++ b/docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor @@ -0,0 +1 @@ +generic.support.TestSpecificImageNameSubstitutor diff --git a/docs/examples/junit4/generic/src/test/resources/logback-test.xml b/docs/examples/junit4/generic/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..1378a823a63 --- /dev/null +++ b/docs/examples/junit4/generic/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + From f94089bb8689914e92cb208ecbdfc28bf2d31fc5 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 29 Oct 2020 20:37:51 +0000 Subject: [PATCH 05/16] Use configuration, not service loader, to select an ImageNameSubstitutor --- ...ConfigurationFileImageNameSubstitutor.java | 5 --- .../utility/DefaultImageNameSubstitutor.java | 5 --- .../utility/ImageNameSubstitutor.java | 34 +++++-------------- .../utility/TestcontainersConfiguration.java | 4 +++ ...estcontainers.utility.ImageNameSubstitutor | 1 - .../utility/FakeImageSubstitutor.java | 13 +++++++ .../utility/ImageNameSubstitutorTest.java | 25 +++++++++++--- .../generic/ExampleImageNameSubstitutor.java | 8 ----- .../TestSpecificImageNameSubstitutor.java | 5 --- ...estcontainers.utility.ImageNameSubstitutor | 1 - .../test/resources/testcontainers.properties | 1 + docs/features/image_name_substitution.md | 18 ++++++---- 12 files changed, 60 insertions(+), 60 deletions(-) delete mode 100644 core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor create mode 100644 core/src/test/java/org/testcontainers/utility/FakeImageSubstitutor.java delete mode 100644 docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor create mode 100644 docs/examples/junit4/generic/src/test/resources/testcontainers.properties diff --git a/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java index e163f8ffff4..29314668cd7 100644 --- a/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java @@ -40,11 +40,6 @@ public DockerImageName apply(final DockerImageName original) { return result; } - @Override - protected int getPriority() { - return -2; - } - @Override protected String getDescription() { return getClass().getSimpleName(); diff --git a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java index 5657a9051aa..75e0ae6e066 100644 --- a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java @@ -31,11 +31,6 @@ public DockerImageName apply(final DockerImageName original) { return configurationFileImageNameSubstitutor.apply(original); } - @Override - protected int getPriority() { - return 0; - } - @Override protected String getDescription() { return "DefaultImageNameSubstitutor (delegates to '" + configurationFileImageNameSubstitutor.getDescription() + "')"; diff --git a/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java index 4179d2b5f52..3016cc66f44 100644 --- a/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java @@ -3,11 +3,7 @@ import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; -import java.util.ServiceLoader; import java.util.function.Function; -import java.util.stream.StreamSupport; - -import static java.util.Comparator.comparingInt; /** * An image name substitutor converts a Docker image name, as may be specified in code, to an alternative name. @@ -22,15 +18,16 @@ public abstract class ImageNameSubstitutor implements Function serviceLoader = ServiceLoader.load(ImageNameSubstitutor.class); - - instance = StreamSupport.stream(serviceLoader.spliterator(), false) - .peek(it -> log.debug("Found ImageNameSubstitutor using ServiceLoader: {} (priority {}) ", it, it.getPriority())) - .max(comparingInt(ImageNameSubstitutor::getPriority)) - .map(ImageNameSubstitutor::wrapWithLogging) - .orElseThrow(() -> new RuntimeException("Unable to find any ImageNameSubstitutor using ServiceLoader")); + final String configuredClassName = TestcontainersConfiguration.getInstance().getImageSubstitutorClassName(); + log.debug("Attempting to instantiate an ImageNameSubstitutor with class: {}", configuredClassName); + try { + ImageNameSubstitutor configuredSubstitutor = (ImageNameSubstitutor) Class.forName(configuredClassName).getConstructor().newInstance(); + instance = wrapWithLogging(configuredSubstitutor); + } catch (Exception e) { + throw new IllegalArgumentException("Configured Image Substitutor could not be loaded: " + configuredClassName, e); + } - log.info("Using ImageNameSubstitutor: {}", instance); + log.info("Using ImageNameSubstitutor: {}", instance.getDescription()); } return instance; @@ -49,14 +46,6 @@ private static ImageNameSubstitutor wrapWithLogging(final ImageNameSubstitutor w */ public abstract DockerImageName apply(DockerImageName original); - /** - * Priority of this {@link ImageNameSubstitutor} compared to other instances that may be found by the service - * loader. The highest priority instance found will always be used. - * - * @return a priority - */ - protected abstract int getPriority(); - protected abstract String getDescription(); /** @@ -84,11 +73,6 @@ public DockerImageName apply(final DockerImageName original) { } } - @Override - protected int getPriority() { - return wrappedInstance.getPriority(); - } - @Override protected String getDescription() { return wrappedInstance.getDescription(); diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java index 1bf766edb77..a3d86fb56f2 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -180,6 +180,10 @@ public Integer getImagePullPauseTimeout() { return Integer.parseInt(getEnvVarOrProperty("pull.pause.timeout", "30")); } + public String getImageSubstitutorClassName() { + return getEnvVarOrProperty("image.substitutor", DefaultImageNameSubstitutor.class.getCanonicalName()); + } + @Nullable @Contract("_, !null, _ -> !null") private String getConfigurable(@NotNull final String propertyName, @Nullable final String defaultValue, Properties... propertiesSources) { diff --git a/core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor b/core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor deleted file mode 100644 index 069dc7ee710..00000000000 --- a/core/src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor +++ /dev/null @@ -1 +0,0 @@ -org.testcontainers.utility.DefaultImageNameSubstitutor diff --git a/core/src/test/java/org/testcontainers/utility/FakeImageSubstitutor.java b/core/src/test/java/org/testcontainers/utility/FakeImageSubstitutor.java new file mode 100644 index 00000000000..076bd540c16 --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/FakeImageSubstitutor.java @@ -0,0 +1,13 @@ +package org.testcontainers.utility; + +public class FakeImageSubstitutor extends ImageNameSubstitutor { + @Override + public DockerImageName apply(final DockerImageName original) { + return null; + } + + @Override + protected String getDescription() { + return null; + } +} diff --git a/core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java b/core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java index d39e83391f9..3186809aa56 100644 --- a/core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java +++ b/core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java @@ -1,17 +1,34 @@ package org.testcontainers.utility; +import org.junit.Rule; import org.junit.Test; +import org.mockito.Mockito; import org.testcontainers.utility.ImageNameSubstitutor.LogWrappedImageNameSubstitutor; import static org.junit.Assert.assertTrue; public class ImageNameSubstitutorTest { + @Rule + public MockTestcontainersConfigurationRule config = new MockTestcontainersConfigurationRule(); + @Test - public void simpleServiceLoadingTest() { - final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); + public void simpleConfigrationTest() { + final ImageNameSubstitutor original = ImageNameSubstitutor.instance; + ImageNameSubstitutor.instance = null; + try { + Mockito + .doReturn(FakeImageSubstitutor.class.getCanonicalName()) + .when(TestcontainersConfiguration.getInstance()) + .getImageSubstitutorClassName(); + + final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); - assertTrue(imageNameSubstitutor instanceof LogWrappedImageNameSubstitutor); - assertTrue(((LogWrappedImageNameSubstitutor) imageNameSubstitutor).wrappedInstance instanceof DefaultImageNameSubstitutor); + assertTrue(imageNameSubstitutor instanceof LogWrappedImageNameSubstitutor); + assertTrue(((LogWrappedImageNameSubstitutor) imageNameSubstitutor).wrappedInstance instanceof FakeImageSubstitutor); + } finally { + ImageNameSubstitutor.instance = original; + } } + } diff --git a/docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java b/docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java index dfb5351ecfd..f6355312b3f 100644 --- a/docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java +++ b/docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java @@ -16,14 +16,6 @@ public DockerImageName apply(DockerImageName original) { ); } - @Override - protected int getPriority() { - // the highest priority substitutor is used. - // Use something higher than 0, which is the priority - // of the default implementation - return 1; - } - @Override protected String getDescription() { // used in logs diff --git a/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java b/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java index aa5e6b30f4b..2f83cf20b86 100644 --- a/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java +++ b/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java @@ -22,11 +22,6 @@ public DockerImageName apply(final DockerImageName original) { } } - @Override - protected int getPriority() { - return 1; - } - @Override protected String getDescription() { return TestSpecificImageNameSubstitutor.class.getSimpleName(); diff --git a/docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor b/docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor deleted file mode 100644 index 93a4769fe46..00000000000 --- a/docs/examples/junit4/generic/src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor +++ /dev/null @@ -1 +0,0 @@ -generic.support.TestSpecificImageNameSubstitutor diff --git a/docs/examples/junit4/generic/src/test/resources/testcontainers.properties b/docs/examples/junit4/generic/src/test/resources/testcontainers.properties new file mode 100644 index 00000000000..19bf8288d33 --- /dev/null +++ b/docs/examples/junit4/generic/src/test/resources/testcontainers.properties @@ -0,0 +1 @@ +image.substitutor=generic.support.TestSpecificImageNameSubstitutor diff --git a/docs/features/image_name_substitution.md b/docs/features/image_name_substitution.md index ee24e790bb7..4aab4f255da 100644 --- a/docs/features/image_name_substitution.md +++ b/docs/features/image_name_substitution.md @@ -69,7 +69,7 @@ i.e. you would leave as-is: You can implement a custom image name substitutor by: * subclassing `org.testcontainers.utility.ImageNameSubstitutor` -* making sure that Testcontainers can find your custom implementation by creating a service loader file. **Do not miss this step!** +* configuring Testcontainers to use your custom implementation The following is an example image substitutor implementation: @@ -77,16 +77,22 @@ The following is an example image substitutor implementation: [Example Image Substitutor](../examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java) block:ExampleImageNameSubstitutor -Testcontainers can be configured to find it at runtime using the Service Loader mechanism. -To do this, create a file on the classpath at `META-INF/services/org.testcontainers.utility.ImageNameSubstitutor` -containing the full name of your custom image substitutor. +Testcontainers can be configured to find it at runtime via configuration. +To do this, create or modify a file on the classpath named `testcontainers.properties`. For example: -```text tab="src/main/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor" -com.mycompany.testcontainers.ExampleImageNameSubstitutor +```text tab="src/test/resources/testcontainers.properties" +image.substitutor=com.mycompany.testcontainers.ExampleImageNameSubstitutor ``` +Note that it is also possible to provide this same configuration property: + +* in a properties file in the user's home directory (`~/.testcontainers.properties`, note the leading `.`) +* or as an environment variable (e.g. `TESTCONTAINERS_IMAGE_SUBSTITUTOR=com.mycompany.testcontainers.ExampleImageNameSubstitutor`). + +Please see [the documentation on configuration mechanisms](./configuration.md) for more information. + ## Overriding image names individually in configuration From 3d0866949abfe9738fab6bef06342c664398d111 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 29 Oct 2020 20:51:26 +0000 Subject: [PATCH 06/16] Add check for order of config setting precedence --- .../TestcontainersConfigurationTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java index 147e5e60bd4..ae07e2d9131 100644 --- a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java +++ b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java @@ -55,6 +55,39 @@ public void shouldSubstituteImageNamesFromEnvironmentVariables() { ); } + @Test + public void shouldApplySettingsInOrder() { + assertEquals( + "precedence order for multiple sources of the same value is correct", + "default", + newConfig().getEnvVarOrProperty("key", "default") + ); + + classpathProperties.setProperty("key", "foo"); + + assertEquals( + "precedence order for multiple sources of the same value is correct", + "foo", + newConfig().getEnvVarOrProperty("key", "default") + ); + + userProperties.setProperty("key", "bar"); + + assertEquals( + "precedence order for multiple sources of the same value is correct", + "bar", + newConfig().getEnvVarOrProperty("key", "default") + ); + + environment.put("TESTCONTAINERS_KEY", "baz"); + + assertEquals( + "precedence order for multiple sources of the same value is correct", + "baz", + newConfig().getEnvVarOrProperty("key", "default") + ); + } + @Test public void shouldNotReadChecksFromClasspathProperties() { assertFalse("checks enabled by default", newConfig().isDisableChecks()); From f8dabd4b940ca692a97cd9142c57ae9cbe79ea4b Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 29 Oct 2020 21:28:00 +0000 Subject: [PATCH 07/16] Extract classpath scanner and support finding of multiple resources --- .../utility/ClasspathScanner.java | 48 ++++++++++++ .../utility/TestcontainersConfiguration.java | 13 +--- .../utility/ClasspathScannerTest.java | 76 +++++++++++++++++++ .../test/resources/expectedClasspathFile.txt | 1 + 4 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 core/src/main/java/org/testcontainers/utility/ClasspathScanner.java create mode 100644 core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java create mode 100644 core/src/test/resources/expectedClasspathFile.txt diff --git a/core/src/main/java/org/testcontainers/utility/ClasspathScanner.java b/core/src/main/java/org/testcontainers/utility/ClasspathScanner.java new file mode 100644 index 00000000000..7356a245e05 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ClasspathScanner.java @@ -0,0 +1,48 @@ +package org.testcontainers.utility; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +import java.net.URL; +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * Utility for identifying resource files on classloaders. + */ +@Slf4j +class ClasspathScanner { + + @VisibleForTesting + static Stream scanFor(final String name, ClassLoader... classLoaders) { + return Stream + .of(classLoaders) + .flatMap(classLoader -> getAllPropertyFilesOnClassloader(classLoader, name)) + .distinct() + .filter(Objects::nonNull); + } + + /** + * @param name the resource name to search for + * @return distinct, ordered stream of resources found by searching this class' classloader and then the current thread's context classloader + */ + static Stream scanFor(final String name) { + return scanFor( + name, + ClasspathScanner.class.getClassLoader(), + Thread.currentThread().getContextClassLoader() + ); + } + + @Nullable + private static Stream getAllPropertyFilesOnClassloader(final ClassLoader it, final String s) { + try { + return Collections.list(it.getResources(s)).stream(); + } catch (Exception e) { + log.error("Unable to read configuration from classloader {} - this is probably a bug", it, e); + return Stream.empty(); + } + } +} diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java index a3d86fb56f2..b9780421857 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -23,7 +23,6 @@ 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; @@ -286,13 +285,7 @@ public boolean updateUserConfig(@NonNull String prop, @NonNull String value) { private static TestcontainersConfiguration loadConfiguration() { return new TestcontainersConfiguration( readProperties(USER_CONFIG_FILE.toURI().toURL()), - Stream - .of( - TestcontainersConfiguration.class.getClassLoader(), - Thread.currentThread().getContextClassLoader() - ) - .map(it -> it.getResource(PROPERTIES_FILE_NAME)) - .filter(Objects::nonNull) + ClasspathScanner.scanFor(PROPERTIES_FILE_NAME) .map(TestcontainersConfiguration::readProperties) .reduce(new Properties(), (a, b) -> { a.putAll(b); @@ -307,9 +300,9 @@ private static Properties readProperties(URL url) { try (InputStream inputStream = url.openStream()) { properties.load(inputStream); } catch (FileNotFoundException e) { - log.warn("Testcontainers config override was found on {} but the file was not found. Exception message: {}", url, ExceptionUtils.getRootCauseMessage(e)); + log.warn("Attempted to read Testcontainers configuration file at {} but the file was not found. Exception message: {}", url, ExceptionUtils.getRootCauseMessage(e)); } catch (IOException e) { - log.warn("Testcontainers config override was found on {} but could not be loaded. Exception message: {}", url, ExceptionUtils.getRootCauseMessage(e)); + log.warn("Attempted to read Testcontainers configuration file at {} but could it not be loaded. Exception message: {}", url, ExceptionUtils.getRootCauseMessage(e)); } return properties; } diff --git a/core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java b/core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java new file mode 100644 index 00000000000..95572fe5c14 --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java @@ -0,0 +1,76 @@ +package org.testcontainers.utility; + +import org.junit.Test; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; + +public class ClasspathScannerTest { + + @Test + public void realClassLoaderLookupOccurs() { + // look for a resource that we know exists only once + final List foundURLs = ClasspathScanner.scanFor("expectedClasspathFile.txt").collect(toList()); + + assertEquals("Exactly one resource was found", 1, foundURLs.size()); + } + + @Test + public void multipleResultsOnOneClassLoaderAreFound() throws IOException { + final ClassLoader firstMockClassLoader = mock(ClassLoader.class); + when(firstMockClassLoader.getResources(eq("someName"))).thenReturn( + Collections.enumeration( + asList( + new URL("file:///a/someName"), + new URL("file:///b/someName") + ) + ) + ); + + final List foundURLs = ClasspathScanner.scanFor("someName", firstMockClassLoader).collect(toList()); + assertEquals( + "The expected URLs are found", + asList(new URL("file:///a/someName"), new URL("file:///b/someName")), + foundURLs + ); + } + + @Test + public void multipleClassLoadersAreQueried() throws IOException { + final ClassLoader firstMockClassLoader = mock(ClassLoader.class); + when(firstMockClassLoader.getResources(eq("someName"))).thenReturn( + Collections.enumeration( + asList( + new URL("file:///a/someName"), + new URL("file:///b/someName") + ) + ) + ); + final ClassLoader secondMockClassLoader = mock(ClassLoader.class); + when(secondMockClassLoader.getResources(eq("someName"))).thenReturn( + Collections.enumeration( + asList( + new URL("file:///b/someName"), // duplicate + new URL("file:///c/someName") + ) + ) + ); + + final List foundURLs = ClasspathScanner.scanFor("someName", firstMockClassLoader, secondMockClassLoader).collect(toList()); + + assertEquals( + "The expected URLs are found", + asList(new URL("file:///a/someName"), new URL("file:///b/someName"), new URL("file:///c/someName")), + foundURLs + ); + } +} diff --git a/core/src/test/resources/expectedClasspathFile.txt b/core/src/test/resources/expectedClasspathFile.txt new file mode 100644 index 00000000000..e13c06f078f --- /dev/null +++ b/core/src/test/resources/expectedClasspathFile.txt @@ -0,0 +1 @@ +This file exists for org.testcontainers.utility.ClasspathScannerTest From 23ac3941d886e0b9a47e8df43314b2d2fa506ee0 Mon Sep 17 00:00:00 2001 From: Richard North Date: Fri, 30 Oct 2020 14:03:36 +0000 Subject: [PATCH 08/16] Introduce deterministic merging of classpath properties files --- .../utility/ClasspathScanner.java | 17 ++++-- .../utility/TestcontainersConfiguration.java | 13 ++++- .../utility/ClasspathScannerTest.java | 54 ++++++++++++++++--- docs/features/configuration.md | 9 +++- 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/org/testcontainers/utility/ClasspathScanner.java b/core/src/main/java/org/testcontainers/utility/ClasspathScanner.java index 7356a245e05..76716cfdb7c 100644 --- a/core/src/main/java/org/testcontainers/utility/ClasspathScanner.java +++ b/core/src/main/java/org/testcontainers/utility/ClasspathScanner.java @@ -6,6 +6,7 @@ import java.net.URL; import java.util.Collections; +import java.util.Comparator; import java.util.Objects; import java.util.stream.Stream; @@ -20,13 +21,23 @@ static Stream scanFor(final String name, ClassLoader... classLoaders) { return Stream .of(classLoaders) .flatMap(classLoader -> getAllPropertyFilesOnClassloader(classLoader, name)) - .distinct() - .filter(Objects::nonNull); + .filter(Objects::nonNull) + .sorted( + Comparator + .comparing(ClasspathScanner::filesFileSchemeFirst) // resolve 'local' files first + .thenComparing(URL::toString) // sort alphabetically for the sake of determinism + ) + .distinct(); + } + + private static Integer filesFileSchemeFirst(final URL t) { + return t.getProtocol().equals("file") ? 0 : 1; } /** * @param name the resource name to search for - * @return distinct, ordered stream of resources found by searching this class' classloader and then the current thread's context classloader + * @return distinct, ordered stream of resources found by searching this class' classloader and the current thread's + * context classloader. Results are currently alphabetically sorted. */ static Stream scanFor(final String name) { return scanFor( diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java index b9780421857..9b1ca1ba9b0 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -244,6 +244,13 @@ public String getUserProperty(@NotNull final String propertyName, @Nullable fina return getConfigurable(propertyName, defaultValue); } + /** + * @return properties values available from user properties and classpath properties. Values set by environment + * variable are NOT included. + * @deprecated usages should be removed ASAP. See {@link TestcontainersConfiguration#getEnvVarOrProperty(String, String)}, + * {@link TestcontainersConfiguration#getEnvVarOrUserProperty(String, String)} or {@link TestcontainersConfiguration#getUserProperty(String, String)} + * for suitable replacements. + */ @Deprecated public Properties getProperties() { return Stream.of(userProperties, classpathProperties) @@ -288,8 +295,10 @@ private static TestcontainersConfiguration loadConfiguration() { ClasspathScanner.scanFor(PROPERTIES_FILE_NAME) .map(TestcontainersConfiguration::readProperties) .reduce(new Properties(), (a, b) -> { - a.putAll(b); - return a; + // first-write-wins merging - URLs appearing first on the classpath alphabetically will take priority. + // Note that this means that file: URLs will always take priority over jar: URLs. + b.putAll(a); + return b; }), System.getenv()); } diff --git a/core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java b/core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java index 95572fe5c14..bdd71b0db9f 100644 --- a/core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java +++ b/core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java @@ -1,5 +1,6 @@ package org.testcontainers.utility; +import org.junit.BeforeClass; import org.junit.Test; import java.io.IOException; @@ -16,6 +17,21 @@ public class ClasspathScannerTest { + private static URL FILE_A; + private static URL FILE_B; + private static URL JAR_A; + private static URL JAR_B; + private static URL FILE_C; + + @BeforeClass + public static void setUp() throws Exception { + FILE_A = new URL("file:///a/someName"); + FILE_B = new URL("file:///b/someName"); + FILE_C = new URL("file:///c/someName"); + JAR_A = new URL("jar:file:a!/someName"); + JAR_B = new URL("jar:file:b!/someName"); + } + @Test public void realClassLoaderLookupOccurs() { // look for a resource that we know exists only once @@ -30,8 +46,8 @@ public void multipleResultsOnOneClassLoaderAreFound() throws IOException { when(firstMockClassLoader.getResources(eq("someName"))).thenReturn( Collections.enumeration( asList( - new URL("file:///a/someName"), - new URL("file:///b/someName") + FILE_A, + FILE_B ) ) ); @@ -39,7 +55,29 @@ public void multipleResultsOnOneClassLoaderAreFound() throws IOException { final List foundURLs = ClasspathScanner.scanFor("someName", firstMockClassLoader).collect(toList()); assertEquals( "The expected URLs are found", - asList(new URL("file:///a/someName"), new URL("file:///b/someName")), + asList(FILE_A, FILE_B), + foundURLs + ); + } + + @Test + public void orderIsAlphabeticalForDeterminism() throws IOException { + final ClassLoader firstMockClassLoader = mock(ClassLoader.class); + when(firstMockClassLoader.getResources(eq("someName"))).thenReturn( + Collections.enumeration( + asList( + FILE_B, + JAR_A, + JAR_B, + FILE_A + ) + ) + ); + + final List foundURLs = ClasspathScanner.scanFor("someName", firstMockClassLoader).collect(toList()); + assertEquals( + "The expected URLs are found in the expected order", + asList(FILE_A, FILE_B, JAR_A, JAR_B), foundURLs ); } @@ -50,8 +88,8 @@ public void multipleClassLoadersAreQueried() throws IOException { when(firstMockClassLoader.getResources(eq("someName"))).thenReturn( Collections.enumeration( asList( - new URL("file:///a/someName"), - new URL("file:///b/someName") + FILE_A, + FILE_B ) ) ); @@ -59,8 +97,8 @@ public void multipleClassLoadersAreQueried() throws IOException { when(secondMockClassLoader.getResources(eq("someName"))).thenReturn( Collections.enumeration( asList( - new URL("file:///b/someName"), // duplicate - new URL("file:///c/someName") + FILE_B, // duplicate + FILE_C ) ) ); @@ -69,7 +107,7 @@ public void multipleClassLoadersAreQueried() throws IOException { assertEquals( "The expected URLs are found", - asList(new URL("file:///a/someName"), new URL("file:///b/someName"), new URL("file:///c/someName")), + asList(FILE_A, FILE_B, FILE_C), foundURLs ); } diff --git a/docs/features/configuration.md b/docs/features/configuration.md index 959f40e7976..02b79ae77fb 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -10,12 +10,19 @@ The configuration will be loaded from multiple locations. Properties are conside **Linux:** `/home/myuser/.testcontainers.properties` **Windows:** `C:/Users/myuser/.testcontainers.properties` **macOS:** `/Users/myuser/.testcontainers.properties` -3. `testcontainers.properties` on classpath +3. `testcontainers.properties` on the classpath. Note that when using environment variables, configuration property names should be set in upper case with underscore separators, preceded by `TESTCONTAINERS_` - e.g. `checks.disable` becomes `TESTCONTAINERS_CHECKS_DISABLE`. +The classpath `testcontainers.properties` file may exist within the local codebase (e.g. within the `src/test/resources` directory) or within library dependencies that you may have. +Any such configuration files will have their contents merged. +If any keys conflict, the value will be taken on the basis of the first value found in: + +* 'local' classpath (i.e. where the URL of the file on the classpath begins with `file:`), then +* other classpath locations (i.e. JAR files) - considered in _alphabetical order of path_ to provide deterministic ordering. + ## Disabling the startup checks > **checks.disable = [true|false]** From 09e4e7caeb2c4b8ca1fc1de9e9b909ab02ef72c3 Mon Sep 17 00:00:00 2001 From: Richard North Date: Fri, 30 Oct 2020 14:03:53 +0000 Subject: [PATCH 09/16] Update docs --- docs/features/image_name_substitution.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/features/image_name_substitution.md b/docs/features/image_name_substitution.md index 4aab4f255da..699cffc95a7 100644 --- a/docs/features/image_name_substitution.md +++ b/docs/features/image_name_substitution.md @@ -88,7 +88,8 @@ image.substitutor=com.mycompany.testcontainers.ExampleImageNameSubstitutor Note that it is also possible to provide this same configuration property: -* in a properties file in the user's home directory (`~/.testcontainers.properties`, note the leading `.`) +* in a `testcontainers.properties` file at the root of a library JAR file (useful if you wish to distribute a drop-in image substitutor JAR within an organization) +* in a properties file in the user's home directory (`~/.testcontainers.properties`; note the leading `.`) * or as an environment variable (e.g. `TESTCONTAINERS_IMAGE_SUBSTITUTOR=com.mycompany.testcontainers.ExampleImageNameSubstitutor`). Please see [the documentation on configuration mechanisms](./configuration.md) for more information. From d1974059f102ce333b990d1d12ab60396d9f379b Mon Sep 17 00:00:00 2001 From: Richard North Date: Fri, 30 Oct 2020 14:05:08 +0000 Subject: [PATCH 10/16] Update docs --- docs/features/image_name_substitution.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/features/image_name_substitution.md b/docs/features/image_name_substitution.md index 699cffc95a7..0cf2d4cf8f0 100644 --- a/docs/features/image_name_substitution.md +++ b/docs/features/image_name_substitution.md @@ -99,6 +99,7 @@ Please see [the documentation on configuration mechanisms](./configuration.md) f !!! note This approach is discouraged and deprecated, but is documented for completeness. + Please consider one of the other approaches outlined in this page instead. Overriding individual image names via configuration may be removed in 2021. Consider this if: From 3cf653ba197e8de7a6559d0caa96a2ad5ffc3da7 Mon Sep 17 00:00:00 2001 From: Richard North Date: Fri, 30 Oct 2020 15:20:02 +0000 Subject: [PATCH 11/16] Remove service loader reference --- .../testcontainers/utility/DefaultImageNameSubstitutor.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java index 75e0ae6e066..9a0e3dab9df 100644 --- a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java @@ -6,9 +6,6 @@ /** * Testcontainers' default implementation of {@link ImageNameSubstitutor}. * Delegates to {@link ConfigurationFileImageNameSubstitutor}. - *

- * WARNING: this class is not intended to be public, but {@link java.util.ServiceLoader} - * requires it to be so. Public visibility DOES NOT make it part of the public API. */ @Slf4j public class DefaultImageNameSubstitutor extends ImageNameSubstitutor { From 9651adedac0933ce5ed94cccfaab6f7e40cc19b7 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 1 Nov 2020 12:50:42 +0000 Subject: [PATCH 12/16] Chain substitution through default and configured implementations --- .../utility/DefaultImageNameSubstitutor.java | 2 +- .../utility/ImageNameSubstitutor.java | 64 ++++++++++++--- .../utility/TestcontainersConfiguration.java | 2 +- .../DockerImageNameCompatibilityTest.java | 8 +- .../utility/FakeImageSubstitutor.java | 4 +- .../utility/ImageNameSubstitutorTest.java | 77 ++++++++++++++----- 6 files changed, 121 insertions(+), 36 deletions(-) diff --git a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java index 9a0e3dab9df..f928b920668 100644 --- a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java @@ -30,6 +30,6 @@ public DockerImageName apply(final DockerImageName original) { @Override protected String getDescription() { - return "DefaultImageNameSubstitutor (delegates to '" + configurationFileImageNameSubstitutor.getDescription() + "')"; + return "DefaultImageNameSubstitutor (" + configurationFileImageNameSubstitutor.getDescription() + ")"; } } diff --git a/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java index 3016cc66f44..7dc41d4c623 100644 --- a/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java @@ -16,18 +16,33 @@ public abstract class ImageNameSubstitutor implements Function Date: Wed, 4 Nov 2020 09:12:03 +0000 Subject: [PATCH 13/16] Small tweaks following review --- .../utility/ConfigurationFileImageNameSubstitutor.java | 5 +---- .../testcontainers/utility/DefaultImageNameSubstitutor.java | 2 +- docs/features/configuration.md | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java index 29314668cd7..530d5a76118 100644 --- a/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java @@ -6,12 +6,9 @@ /** * {@link ImageNameSubstitutor} which takes replacement image names from configuration. * See {@link TestcontainersConfiguration} for the subset of image names which can be substituted using this mechanism. - *

- * WARNING: this class is not intended to be public, but {@link java.util.ServiceLoader} - * requires it to be so. Public visibility DOES NOT make it part of the public API. */ @Slf4j -public class ConfigurationFileImageNameSubstitutor extends ImageNameSubstitutor { +final class ConfigurationFileImageNameSubstitutor extends ImageNameSubstitutor { private final TestcontainersConfiguration configuration; diff --git a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java index f928b920668..bcc7c96ba18 100644 --- a/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java @@ -8,7 +8,7 @@ * Delegates to {@link ConfigurationFileImageNameSubstitutor}. */ @Slf4j -public class DefaultImageNameSubstitutor extends ImageNameSubstitutor { +final class DefaultImageNameSubstitutor extends ImageNameSubstitutor { private final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor; diff --git a/docs/features/configuration.md b/docs/features/configuration.md index 02b79ae77fb..b567a8f564f 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -41,6 +41,8 @@ It takes a couple of seconds, but if you want to speed up your tests, you can di !!! note This approach is discouraged and deprecated, but is documented for completeness. Overriding individual image names via configuration may be removed in 2021. + See [Image Name Substitution](./image_name_substitution.md) for other strategies for substituting image names to pull from other registries. + Testcontainers uses public Docker images to perform different actions like startup checks, VNC recording and others. Some companies disallow the usage of Docker Hub, but you can override `*.image` properties with your own images from your private registry to workaround that. @@ -70,8 +72,6 @@ Some companies disallow the usage of Docker Hub, but you can override `*.image` > **pulsar.container.image = apachepulsar/pulsar:2.2.0** > Used by Apache Pulsar -See [Image Name Substitution](./image_name_substitution.md) for other strategies for substituting image names to pull from other registries. - ## Customizing Ryuk resource reaper > **ryuk.container.image = testcontainers/ryuk:0.3.0** From 008b0365f17d87594abb4ab4e140011204dfa4af Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 4 Nov 2020 10:34:15 +0000 Subject: [PATCH 14/16] Fix test compile error --- .../src/test/java/generic/ImageNameSubstitutionTest.java | 2 +- .../utility}/TestSpecificImageNameSubstitutor.java | 6 +----- .../generic/src/test/resources/testcontainers.properties | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) rename docs/examples/junit4/generic/src/test/java/{generic/support => org/testcontainers/utility}/TestSpecificImageNameSubstitutor.java (83%) diff --git a/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java index b9e67437b5d..397ed04fb2a 100644 --- a/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java +++ b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java @@ -25,7 +25,7 @@ public void simpleExample() { /** * Note that this test uses a fake image name, which will only work because - * {@link generic.support.TestSpecificImageNameSubstitutor} steps in to override the substitution for this exact + * {@link org.testcontainers.utility.TestSpecificImageNameSubstitutor} steps in to override the substitution for this exact * image name. */ @Test diff --git a/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java b/docs/examples/junit4/generic/src/test/java/org/testcontainers/utility/TestSpecificImageNameSubstitutor.java similarity index 83% rename from docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java rename to docs/examples/junit4/generic/src/test/java/org/testcontainers/utility/TestSpecificImageNameSubstitutor.java index 2f83cf20b86..ff9508bbcba 100644 --- a/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java +++ b/docs/examples/junit4/generic/src/test/java/org/testcontainers/utility/TestSpecificImageNameSubstitutor.java @@ -1,8 +1,4 @@ -package generic.support; - -import org.testcontainers.utility.DefaultImageNameSubstitutor; -import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.ImageNameSubstitutor; +package org.testcontainers.utility; /** * An {@link ImageNameSubstitutor} which makes it possible to use fake image names in diff --git a/docs/examples/junit4/generic/src/test/resources/testcontainers.properties b/docs/examples/junit4/generic/src/test/resources/testcontainers.properties index 19bf8288d33..f089d3c1ca6 100644 --- a/docs/examples/junit4/generic/src/test/resources/testcontainers.properties +++ b/docs/examples/junit4/generic/src/test/resources/testcontainers.properties @@ -1 +1 @@ -image.substitutor=generic.support.TestSpecificImageNameSubstitutor +image.substitutor=org.testcontainers.utility.TestSpecificImageNameSubstitutor From 6587210a35416d59cc33dda1e6be70f3bdbf239e Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 5 Nov 2020 09:47:25 +0000 Subject: [PATCH 15/16] Add UnstableAPI annotation --- .../org/testcontainers/utility/ImageNameSubstitutor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java index 7dc41d4c623..645b717a562 100644 --- a/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java +++ b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java @@ -2,6 +2,7 @@ import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; +import org.testcontainers.UnstableAPI; import java.util.function.Function; @@ -9,8 +10,12 @@ * An image name substitutor converts a Docker image name, as may be specified in code, to an alternative name. * This is intended to provide a way to override image names, for example to enforce pulling of images from a private * registry. + *

+ * This is marked as @{@link UnstableAPI} as this API is new. While we do not think major changes will be required, we + * will react to feedback if necessary. */ @Slf4j +@UnstableAPI public abstract class ImageNameSubstitutor implements Function { @VisibleForTesting From 16ef03d2da8a67362497544d2ab4a9efa93174d7 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 5 Nov 2020 12:07:41 +0000 Subject: [PATCH 16/16] Move TestSpecificImageNameSubstitutor back to original package and remove duplicate use of default substitutor --- .../test/java/generic/ImageNameSubstitutionTest.java | 3 ++- .../support}/TestSpecificImageNameSubstitutor.java | 11 ++++++----- .../src/test/resources/testcontainers.properties | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) rename docs/examples/junit4/generic/src/test/java/{org/testcontainers/utility => generic/support}/TestSpecificImageNameSubstitutor.java (70%) diff --git a/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java index 397ed04fb2a..edc622a4104 100644 --- a/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java +++ b/docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java @@ -1,6 +1,7 @@ package generic; +import generic.support.TestSpecificImageNameSubstitutor; import org.junit.Test; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.utility.DockerImageName; @@ -25,7 +26,7 @@ public void simpleExample() { /** * Note that this test uses a fake image name, which will only work because - * {@link org.testcontainers.utility.TestSpecificImageNameSubstitutor} steps in to override the substitution for this exact + * {@link TestSpecificImageNameSubstitutor} steps in to override the substitution for this exact * image name. */ @Test diff --git a/docs/examples/junit4/generic/src/test/java/org/testcontainers/utility/TestSpecificImageNameSubstitutor.java b/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java similarity index 70% rename from docs/examples/junit4/generic/src/test/java/org/testcontainers/utility/TestSpecificImageNameSubstitutor.java rename to docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java index ff9508bbcba..5f9e92c0add 100644 --- a/docs/examples/junit4/generic/src/test/java/org/testcontainers/utility/TestSpecificImageNameSubstitutor.java +++ b/docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java @@ -1,4 +1,7 @@ -package org.testcontainers.utility; +package generic.support; + +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.ImageNameSubstitutor; /** * An {@link ImageNameSubstitutor} which makes it possible to use fake image names in @@ -7,14 +10,12 @@ */ public class TestSpecificImageNameSubstitutor extends ImageNameSubstitutor { - private final DefaultImageNameSubstitutor defaultImageNameSubstitutor = new DefaultImageNameSubstitutor(); - @Override public DockerImageName apply(final DockerImageName original) { if (original.equals(DockerImageName.parse("registry.mycompany.com/mirror/mysql:8.0.22"))) { - return defaultImageNameSubstitutor.apply(DockerImageName.parse("mysql")); + return DockerImageName.parse("mysql"); } else { - return defaultImageNameSubstitutor.apply(original); + return original; } } diff --git a/docs/examples/junit4/generic/src/test/resources/testcontainers.properties b/docs/examples/junit4/generic/src/test/resources/testcontainers.properties index f089d3c1ca6..19bf8288d33 100644 --- a/docs/examples/junit4/generic/src/test/resources/testcontainers.properties +++ b/docs/examples/junit4/generic/src/test/resources/testcontainers.properties @@ -1 +1 @@ -image.substitutor=org.testcontainers.utility.TestSpecificImageNameSubstitutor +image.substitutor=generic.support.TestSpecificImageNameSubstitutor