diff --git a/core/build.gradle b/core/build.gradle index 723b14fd0e6..e043313ac38 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -5,7 +5,7 @@ buildscript { dependencies { // https://github.com/melix/japicmp-gradle-plugin/issues/36 - classpath 'com.google.guava:guava:30.0-jre' + classpath 'com.google.guava:guava:29.0-jre' } } 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..a0d01ff794d --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java @@ -0,0 +1,51 @@ +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} followed by {@link PrefixingImageNameSubstitutor}. + *

+ * 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; + private final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor; + + public DefaultImageNameSubstitutor() { + configurationFileImageNameSubstitutor = new ConfigurationFileImageNameSubstitutor(); + prefixingImageNameSubstitutor = new PrefixingImageNameSubstitutor(); + } + + @VisibleForTesting + DefaultImageNameSubstitutor( + final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor, + final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor + ) { + this.configurationFileImageNameSubstitutor = configurationFileImageNameSubstitutor; + this.prefixingImageNameSubstitutor = prefixingImageNameSubstitutor; + } + + @Override + public DockerImageName apply(final DockerImageName original) { + return prefixingImageNameSubstitutor.apply( + configurationFileImageNameSubstitutor.apply( + original + ) + ); + } + + @Override + protected int getPriority() { + return 0; + } + + @Override + protected String getDescription() { + return "DefaultImageNameSubstitutor (composite of '" + configurationFileImageNameSubstitutor.getDescription() + "' and '" + prefixingImageNameSubstitutor.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..6f9516f695f --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java @@ -0,0 +1,94 @@ +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(); + + 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/PrefixingImageNameSubstitutor.java b/core/src/main/java/org/testcontainers/utility/PrefixingImageNameSubstitutor.java new file mode 100644 index 00000000000..6253f478a1b --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/PrefixingImageNameSubstitutor.java @@ -0,0 +1,53 @@ +package org.testcontainers.utility; + +import com.google.common.annotations.VisibleForTesting; +import lombok.NoArgsConstructor; +import org.testcontainers.UnstableAPI; + +/** + * An {@link ImageNameSubstitutor} which applies a prefix to all image names, e.g. a private registry host and path. + * The prefix may be set via an environment variable (TESTCONTAINERS_IMAGE_NAME_PREFIX) or an equivalent + * configuration file entry (see {@link TestcontainersConfiguration}). + *

+ * 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. + */ +@UnstableAPI +@NoArgsConstructor +public final class PrefixingImageNameSubstitutor extends ImageNameSubstitutor { + + @VisibleForTesting + static final String PROPERTY_KEY = "testcontainers.image.name.prefix"; + + private TestcontainersConfiguration configuration = TestcontainersConfiguration.getInstance(); + + @VisibleForTesting + PrefixingImageNameSubstitutor(final TestcontainersConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public DockerImageName apply(DockerImageName original) { + final String prefix = configuration.getEnvVarOrProperty(PROPERTY_KEY, ""); + + if (prefix != null && !prefix.isEmpty()) { + if (!original.asCanonicalNameString().startsWith(prefix)) { + return DockerImageName.parse(prefix + original.asCanonicalNameString()); + } else { + return original; + } + } else { + return original; + } + } + + @Override + protected int getPriority() { + return -1; + } + + @Override + protected String getDescription() { + return getClass().getSimpleName(); + } +} 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 5b89140fd61..1bf766edb77 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -1,15 +1,17 @@ package org.testcontainers.utility; import com.google.common.annotations.VisibleForTesting; -import lombok.AccessLevel; +import com.google.common.collect.ImmutableMap; import lombok.Data; import lombok.Getter; import lombok.NonNull; -import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.exception.ExceptionUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.testcontainers.UnstableAPI; import java.io.File; @@ -20,22 +22,57 @@ import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; /** - * Provides a mechanism for fetching configuration/defaults from the classpath. + * Provides a mechanism for fetching configuration/default settings. + *

+ * Configuration may be provided in: + *

    + *
  • A file in the user's home directory named .testcontainers.properties
  • + *
  • A file in the classpath named testcontainers.properties
  • + *
  • Environment variables
  • + *
+ *

+ * Note that, if using environment variables, property names are in upper case separated by underscores, preceded by + * TESTCONTAINERS_. */ @Data @Slf4j -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class TestcontainersConfiguration { private static String PROPERTIES_FILE_NAME = "testcontainers.properties"; - private static File ENVIRONMENT_CONFIG_FILE = new File(System.getProperty("user.home"), "." + PROPERTIES_FILE_NAME); + private static File USER_CONFIG_FILE = new File(System.getProperty("user.home"), "." + PROPERTIES_FILE_NAME); + + private static final String AMBASSADOR_IMAGE = "richnorth/ambassador"; + private static final String SOCAT_IMAGE = "alpine/socat"; + private static final String VNC_RECORDER_IMAGE = "testcontainers/vnc-recorder"; + private static final String COMPOSE_IMAGE = "docker/compose"; + private static final String ALPINE_IMAGE = "alpine"; + private static final String RYUK_IMAGE = "testcontainers/ryuk"; + private static final String KAFKA_IMAGE = "confluentinc/cp-kafka"; + private static final String PULSAR_IMAGE = "apachepulsar/pulsar"; + private static final String LOCALSTACK_IMAGE = "localstack/localstack"; + private static final String SSHD_IMAGE = "testcontainers/sshd"; + + private static final ImmutableMap CONTAINER_MAPPING = ImmutableMap.builder() + .put(DockerImageName.parse(AMBASSADOR_IMAGE), "ambassador.container.image") + .put(DockerImageName.parse(SOCAT_IMAGE), "socat.container.image") + .put(DockerImageName.parse(VNC_RECORDER_IMAGE), "vncrecorder.container.image") + .put(DockerImageName.parse(COMPOSE_IMAGE), "compose.container.image") + .put(DockerImageName.parse(ALPINE_IMAGE), "tinyimage.container.image") + .put(DockerImageName.parse(RYUK_IMAGE), "ryuk.container.image") + .put(DockerImageName.parse(KAFKA_IMAGE), "kafka.container.image") + .put(DockerImageName.parse(PULSAR_IMAGE), "pulsar.container.image") + .put(DockerImageName.parse(LOCALSTACK_IMAGE), "localstack.container.image") + .put(DockerImageName.parse(SSHD_IMAGE), "sshd.container.image") + .build(); @Getter(lazy = true) private static final TestcontainersConfiguration instance = loadConfiguration(); @@ -47,163 +84,196 @@ 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(); - } - - public DockerImageName getSocatDockerImageName() { - return getImage("socat.container.image", "alpine/socat:latest"); + return getImage(SOCAT_IMAGE).asCanonicalNameString(); } @Deprecated public String getVncRecordedContainerImage() { - return getVncDockerImageName().asCanonicalNameString(); - } - - public DockerImageName getVncDockerImageName() { - return getImage("vncrecorder.container.image", "testcontainers/vnc-recorder:1.1.0"); + return getImage(VNC_RECORDER_IMAGE).asCanonicalNameString(); } @Deprecated public String getDockerComposeContainerImage() { - return getDockerComposeDockerImageName().asCanonicalNameString(); - } - - public DockerImageName getDockerComposeDockerImageName() { - return getImage("compose.container.image", "docker/compose:1.24.1"); + return getImage(COMPOSE_IMAGE).asCanonicalNameString(); } @Deprecated public String getTinyImage() { - return getTinyDockerImageName().asCanonicalNameString(); - } - - public DockerImageName getTinyDockerImageName() { - return getImage("tinyimage.container.image", "alpine:3.5"); + return getImage(ALPINE_IMAGE).asCanonicalNameString(); } 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(); - } - - public DockerImageName getRyukDockerImageName() { - return getImage("ryuk.container.image", "testcontainers/ryuk:0.3.0"); + return getImage(RYUK_IMAGE).asCanonicalNameString(); } @Deprecated public String getSSHdImage() { - return getSSHdDockerImageName().asCanonicalNameString(); - } - - public DockerImageName getSSHdDockerImageName() { - return getImage("sshd.container.image", "testcontainers/sshd:1.0.0"); + return getImage(SSHD_IMAGE).asCanonicalNameString(); } 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"); + @Deprecated + public String getOracleImage() { + return getEnvVarOrUserProperty("oracle.container.image", null); } @Deprecated public String getPulsarImage() { - return getPulsarDockerImageName().asCanonicalNameString(); - } - - public DockerImageName getPulsarDockerImageName() { - return getImage("pulsar.container.image", "apachepulsar/pulsar"); + return getImage(PULSAR_IMAGE).asCanonicalNameString(); } @Deprecated public String getLocalStackImage() { - return getLocalstackDockerImageName().asCanonicalNameString(); - } - - public DockerImageName getLocalstackDockerImageName() { - return getImage("localstack.container.image", "localstack/localstack"); + return getImage(LOCALSTACK_IMAGE).asCanonicalNameString(); } 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 +281,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 +293,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 +309,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/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/PrefixingImageNameSubstitutorTest.java b/core/src/test/java/org/testcontainers/utility/PrefixingImageNameSubstitutorTest.java new file mode 100644 index 00000000000..3e6675660cf --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/PrefixingImageNameSubstitutorTest.java @@ -0,0 +1,75 @@ +package org.testcontainers.utility; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +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; +import static org.testcontainers.utility.PrefixingImageNameSubstitutor.PROPERTY_KEY; + +public class PrefixingImageNameSubstitutorTest { + + private TestcontainersConfiguration mockConfiguration; + private PrefixingImageNameSubstitutor underTest; + + @Before + public void setUp() { + mockConfiguration = mock(TestcontainersConfiguration.class); + underTest = new PrefixingImageNameSubstitutor(mockConfiguration); + } + + @Test + public void testHappyPath() { + when(mockConfiguration.getEnvVarOrProperty(eq(PROPERTY_KEY), any())).thenReturn("someregistry.com/"); + + final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); + + assertEquals( + "The prefix is applied", + "someregistry.com/some/image:tag", + result.asCanonicalNameString() + ); + } + + @Test + public void testNoDoublePrefixing() { + when(mockConfiguration.getEnvVarOrProperty(eq(PROPERTY_KEY), any())).thenReturn("someregistry.com/"); + + final DockerImageName result = underTest.apply(DockerImageName.parse("someregistry.com/some/image:tag")); + + assertEquals( + "The prefix is not applied if already present", + "someregistry.com/some/image:tag", + result.asCanonicalNameString() + ); + } + + @Test + public void testHandlesNullValue() { + when(mockConfiguration.getEnvVarOrProperty(eq(PROPERTY_KEY), any())).thenReturn(null); + + final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); + + assertEquals( + "The prefix is not applied if the env var is not set", + "some/image:tag", + result.asCanonicalNameString() + ); + } + + @Test + public void testHandlesEmptyValue() { + when(mockConfiguration.getEnvVarOrProperty(eq(PROPERTY_KEY), any())).thenReturn(""); + + final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); + + assertEquals( + "The prefix is not applied if the env var is not set", + "some/image:tag", + result.asCanonicalNameString() + ); + } +} diff --git a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java index ccd68fbd984..147e5e60bd4 100644 --- a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java +++ b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java @@ -1,57 +1,137 @@ 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 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()); 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()); + } - environmentProperties.setProperty("docker.client.strategy", "foo"); - assertEquals("Docker client strategy is changed", "foo", newConfig().getDockerClientStrategyClassName()); + @Test + public void shouldReadDockerClientStrategyFromUserProperties() { + userProperties.setProperty("docker.client.strategy", "foo"); + assertEquals("Docker client strategy is changed by user property", "foo", newConfig().getDockerClientStrategyClassName()); } @Test - public void shouldReadReuseFromEnvironmentOnly() { + 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 shouldNotReadReuseFromClasspathProperties() { assertFalse("no reuse by default", newConfig().environmentSupportsReuse()); classpathProperties.setProperty("testcontainers.reuse.enable", "true"); assertFalse("reuse is not affected by classpath properties", newConfig().environmentSupportsReuse()); + } + + @Test + public void shouldReadReuseFromUserProperties() { + assertFalse("no reuse by default", newConfig().environmentSupportsReuse()); - environmentProperties.setProperty("testcontainers.reuse.enable", "true"); - assertTrue("reuse enabled", 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); } } 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..9270660704a --- /dev/null +++ b/docs/features/image_name_substitution.md @@ -0,0 +1,137 @@ +# 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: + * Recommended: [Adding a registry URL prefix to image names automatically](#adding-a-registry-url-prefix-to-image-names-automatically) + * [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 + + + + + + +## Adding a registry URL prefix to image names automatically + +Consider this if: + +* Developers and CI machines need to use different image names. For example, developers are able to pull images from Docker Hub, but CI machines need to pull from a private registry +* Your private registry has copies of images from Docker Hub where the names are predictable, and just adding a prefix is enough. + For example, `registry.mycompany.com/mirror/mysql:8.0.22` can be derived from the original Docker Hub image name (`mysql:8.0.22`) with a consistent prefix string: `registry.mycompany.com/mirror/` + +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 then configure Testcontainers to apply the prefix `registry.mycompany.com/mirror/` to every image that it tries to pull. +This can be done in one of two ways: + +* Setting an environment variable, `TESTCONTAINERS_IMAGE_NAME_PREFIX=registry.mycompany.com/mirror/` +* Via config file, setting `testcontainers.image.name.prefix=registry.mycompany.com/mirror/` in either: + * the `~/.testcontainers.properties` file in your user home directory, or + * a file named `testcontainers.properties` on the classpath + +Testcontainers will automatically apply this prefix to every image that it pulls - please verify that all [the required images](./pull_rate_limiting.md#which-images-are-used-by-testcontainers) exist in your registry. + +Note that the prefix-based substitution will skip applying a prefix if it is already set. +This is intended to help avoid obvious mistakes if image names have been partially migrated to a private image registry via changes to code. + + + + +## 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/docs/modules/elasticsearch.md b/docs/modules/elasticsearch.md index cd47c9c4ed3..9959d0b7926 100644 --- a/docs/modules/elasticsearch.md +++ b/docs/modules/elasticsearch.md @@ -3,7 +3,7 @@ This module helps running [elasticsearch](https://www.elastic.co/products/elasticsearch) using Testcontainers. -Note that it's based on the [official Docker image](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html) provided by elastic. +Note that it's based on the [official Docker image](https://www.elastic.co/guide/en/elasticsearch/reference/6.3/docker.html) provided by elastic. ## Usage example @@ -15,26 +15,17 @@ You can start an elasticsearch container instance from any Java application by u -Note that if you are still using the [TransportClient](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html) +Note that if you are still using the [TransportClient](https://www.elastic.co/guide/en/elasticsearch/client/java-api/6.3/transport-client.html) (not recommended as it is deprecated), the default cluster name is set to `docker-cluster` so you need to change `cluster.name` setting or set `client.transport.ignore_cluster_name` to `true`. -## Secure your Elasticsearch cluster - -The default distribution of Elasticsearch comes with the basic license which contains security feature. -You can turn on security by providing a password: - - -[HttpClient](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:httpClientSecuredContainer - - ## Choose your Elasticsearch license If you prefer to start a Docker image with the pure OSS version (which means with no security in older versions or other new and advanced features), you can use this instead: -[Elasticsearch OSS](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:ossContainer +[Elasticsearch OSS](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:oosContainer ## Adding this module to your project dependencies diff --git a/docs/modules/localstack.md b/docs/modules/localstack.md index 41fd2a50a52..4ff46233b05 100644 --- a/docs/modules/localstack.md +++ b/docs/modules/localstack.md @@ -7,10 +7,8 @@ Testcontainers module for the Atlassian's [LocalStack](https://github.com/locals Running LocalStack as a stand-in for AWS S3 during a test: ```java -DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:0.11.3"); - @Rule -public LocalStackContainer localstack = new LocalStackContainer(localstackImage) +public LocalStackContainer localstack = new LocalStackContainer() .withServices(S3); @Test diff --git a/examples/disque-job-queue/build.gradle b/examples/disque-job-queue/build.gradle index c31cb39b7fe..c5e191f4f94 100644 --- a/examples/disque-job-queue/build.gradle +++ b/examples/disque-job-queue/build.gradle @@ -7,7 +7,7 @@ repositories { } dependencies { - compileOnly "org.projectlombok:lombok:1.18.16" + compileOnly "org.projectlombok:lombok:1.18.14" annotationProcessor "org.projectlombok:lombok:1.18.14" implementation 'biz.paluch.redis:spinach:0.3' implementation 'com.google.code.gson:gson:2.8.6' diff --git a/examples/linked-container/build.gradle b/examples/linked-container/build.gradle index f5f21a57d21..5c4c473becb 100644 --- a/examples/linked-container/build.gradle +++ b/examples/linked-container/build.gradle @@ -9,7 +9,7 @@ dependencies { compileOnly 'org.slf4j:slf4j-api:1.7.30' implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'org.json:json:20180813' - testImplementation 'org.postgresql:postgresql:42.2.18' + testImplementation 'org.postgresql:postgresql:42.2.17' testImplementation 'ch.qos.logback:logback-classic:1.2.3' testImplementation 'org.testcontainers:postgresql' } diff --git a/examples/solr-container/build.gradle b/examples/solr-container/build.gradle index 75524364100..5a8973cab27 100644 --- a/examples/solr-container/build.gradle +++ b/examples/solr-container/build.gradle @@ -7,8 +7,8 @@ repositories { } dependencies { - compileOnly "org.projectlombok:lombok:1.18.16" - annotationProcessor "org.projectlombok:lombok:1.18.16" + compileOnly "org.projectlombok:lombok:1.18.14" + annotationProcessor "org.projectlombok:lombok:1.18.10" implementation 'org.apache.solr:solr-solrj:8.6.3' diff --git a/mkdocs.yml b/mkdocs.yml index 0fa9b2b4c91..dfff99bb302 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,4 +92,4 @@ nav: - contributing_docs.md - bounty.md extra: - latest_version: 1.15.0-rc2 + latest_version: 1.14.3 diff --git a/modules/dynalite/build.gradle b/modules/dynalite/build.gradle index 8aa9959ea13..fe8fb883839 100644 --- a/modules/dynalite/build.gradle +++ b/modules/dynalite/build.gradle @@ -3,6 +3,6 @@ description = "Testcontainers :: Dynalite" dependencies { compile project(':testcontainers') - compileOnly 'com.amazonaws:aws-java-sdk-dynamodb:1.11.882' - testCompile 'com.amazonaws:aws-java-sdk-dynamodb:1.11.882' + compileOnly 'com.amazonaws:aws-java-sdk-dynamodb:1.11.880' + testCompile 'com.amazonaws:aws-java-sdk-dynamodb:1.11.865' } diff --git a/modules/elasticsearch/build.gradle b/modules/elasticsearch/build.gradle index 126722759ec..e6de1daa7ab 100644 --- a/modules/elasticsearch/build.gradle +++ b/modules/elasticsearch/build.gradle @@ -3,5 +3,5 @@ description = "TestContainers :: elasticsearch" dependencies { compile project(':testcontainers') testCompile "org.elasticsearch.client:elasticsearch-rest-client:7.9.2" - testCompile "org.elasticsearch.client:transport:7.9.2" + testCompile "org.elasticsearch.client:transport:6.7.1" } diff --git a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java index acbca15aaf5..7ed660d38c6 100644 --- a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java +++ b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java @@ -23,9 +23,7 @@ public class ElasticsearchContainer extends GenericContainer basic */ private static final String ELASTICSEARCH_USERNAME = "elastic"; /** - * From 6.8, we can optionally activate security with a default password. + * Elasticsearch 5.x default password. In 6.x images, there's no security by default as shipped with a basic license. */ private static final String ELASTICSEARCH_PASSWORD = "changeme"; private RestClient client = null; - private RestClient anonymousClient = null; @After public void stopRestClient() throws IOException { @@ -55,40 +56,13 @@ public void stopRestClient() throws IOException { client.close(); client = null; } - if (anonymousClient != null) { - anonymousClient.close(); - anonymousClient = null; - } } @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility - @Test - @Deprecated // We will remove this test in the future - public void elasticsearchDeprecatedCtorTest() throws IOException { - // Create the elasticsearch container. - try (ElasticsearchContainer container = new ElasticsearchContainer() - .withEnv("foo", "bar") // dummy env for compiler checking correct generics usage - ) { - // Start the container. This step might take some time... - container.start(); - - // Do whatever you want with the rest client ... - Response response = getClient(container).performRequest(new Request("GET", "/")); - assertThat(response.getStatusLine().getStatusCode(), is(200)); - assertThat(EntityUtils.toString(response.getEntity()), containsString(ELASTICSEARCH_VERSION)); - - // The default image is running with the features under Elastic License - response = getClient(container).performRequest(new Request("GET", "/_xpack/")); - assertThat(response.getStatusLine().getStatusCode(), is(200)); - // For now we test that we have the monitoring feature available - assertThat(EntityUtils.toString(response.getEntity()), containsString("monitoring")); - } - } - @Test public void elasticsearchDefaultTest() throws IOException { // Create the elasticsearch container. - try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) + try (ElasticsearchContainer container = new ElasticsearchContainer() .withEnv("foo", "bar") // dummy env for compiler checking correct generics usage ) { // Start the container. This step might take some time... @@ -97,7 +71,7 @@ public void elasticsearchDefaultTest() throws IOException { // Do whatever you want with the rest client ... Response response = getClient(container).performRequest(new Request("GET", "/")); assertThat(response.getStatusLine().getStatusCode(), is(200)); - assertThat(EntityUtils.toString(response.getEntity()), containsString(ELASTICSEARCH_VERSION)); + assertThat(EntityUtils.toString(response.getEntity()), containsString(DEFAULT_TAG)); // The default image is running with the features under Elastic License response = getClient(container).performRequest(new Request("GET", "/_xpack/")); @@ -107,24 +81,6 @@ public void elasticsearchDefaultTest() throws IOException { } } - @Test - public void elasticsearchSecuredTest() throws IOException { - try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) - .withPassword(ELASTICSEARCH_PASSWORD)) { - container.start(); - - // The cluster should be secured so it must fail when we try to access / without credentials - assertThrows("We should not be able to access / URI with an anonymous client.", - ResponseException.class, - () -> getAnonymousClient(container).performRequest(new Request("GET", "/"))); - - // But it should work when we try to access / with the proper login and password - Response response = getClient(container).performRequest(new Request("GET", "/")); - assertThat(response.getStatusLine().getStatusCode(), is(200)); - assertThat(EntityUtils.toString(response.getEntity()), containsString(ELASTICSEARCH_VERSION)); - } - } - @Test public void elasticsearchVersion() throws IOException { try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { @@ -139,7 +95,7 @@ public void elasticsearchVersion() throws IOException { @Test public void elasticsearchOssImage() throws IOException { try (ElasticsearchContainer container = - // ossContainer { + // oosContainer { new ElasticsearchContainer( DockerImageName .parse("docker.elastic.co/elasticsearch/elasticsearch-oss") @@ -157,11 +113,12 @@ public void elasticsearchOssImage() throws IOException { } } + @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility @Test public void restClientClusterHealth() throws IOException { // httpClientContainer { // Create the elasticsearch container. - try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { + try (ElasticsearchContainer container = new ElasticsearchContainer()) { // Start the container. This step might take some time... container.start(); @@ -183,40 +140,15 @@ public void restClientClusterHealth() throws IOException { // } } - @Test - public void restClientSecuredClusterHealth() throws IOException { - // httpClientSecuredContainer { - // Create the elasticsearch container. - try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) - // With a password - .withPassword(ELASTICSEARCH_PASSWORD)) { - // Start the container. This step might take some time... - container.start(); - - // Create the secured client. - final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(AuthScope.ANY, - new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)); - - client = RestClient.builder(HttpHost.create(container.getHttpHostAddress())) - .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)) - .build(); - - Response response = client.performRequest(new Request("GET", "/_cluster/health")); - // }} - assertThat(response.getStatusLine().getStatusCode(), is(200)); - assertThat(EntityUtils.toString(response.getEntity()), containsString("cluster_name")); - // httpClientSecuredContainer {{ - } - // } - } - - @SuppressWarnings("deprecation") // The TransportClient will be removed in Elasticsearch 8. @Test public void transportClientClusterHealth() { // transportClientContainer { // Create the elasticsearch container. - try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)){ + try (ElasticsearchContainer container = new ElasticsearchContainer( + DockerImageName + .parse("docker.elastic.co/elasticsearch/elasticsearch") + .withTag("6.4.1") + )){ // Start the container. This step might take some time... container.start(); @@ -236,19 +168,6 @@ public void transportClientClusterHealth() { // } } - @Test - public void incompatibleSettingsTest() { - // The OSS image can not use security feature - assertThrows("We should not be able to activate security with an OSS License", - IllegalArgumentException.class, - () -> new ElasticsearchContainer( - DockerImageName - .parse("docker.elastic.co/elasticsearch/elasticsearch-oss") - .withTag(ELASTICSEARCH_VERSION)) - .withPassword("foo") - ); - } - private RestClient getClient(ElasticsearchContainer container) { if (client == null) { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); @@ -263,11 +182,4 @@ private RestClient getClient(ElasticsearchContainer container) { return client; } - private RestClient getAnonymousClient(ElasticsearchContainer container) { - if (anonymousClient == null) { - anonymousClient = RestClient.builder(HttpHost.create(container.getHttpHostAddress())).build(); - } - - return anonymousClient; - } } diff --git a/modules/jdbc-test/build.gradle b/modules/jdbc-test/build.gradle index 99841c1d62a..ec57ddcea41 100644 --- a/modules/jdbc-test/build.gradle +++ b/modules/jdbc-test/build.gradle @@ -2,7 +2,7 @@ dependencies { compile project(':jdbc') compile project(':test-support') - compile 'com.google.guava:guava:30.0-jre' + compile 'com.google.guava:guava:29.0-jre' compile 'org.apache.commons:commons-lang3:3.11' compile 'com.zaxxer:HikariCP-java6:2.3.13' compile 'commons-dbutils:commons-dbutils:1.7' @@ -17,5 +17,5 @@ dependencies { compile 'commons-dbutils:commons-dbutils:1.7' compile 'org.apache.tomcat:tomcat-jdbc:9.0.39' compile 'org.vibur:vibur-dbcp:25.0' - compile 'mysql:mysql-connector-java:8.0.22' + compile 'mysql:mysql-connector-java:8.0.21' } diff --git a/modules/junit-jupiter/build.gradle b/modules/junit-jupiter/build.gradle index 61594d9842b..28f6049faf4 100644 --- a/modules/junit-jupiter/build.gradle +++ b/modules/junit-jupiter/build.gradle @@ -15,8 +15,8 @@ dependencies { testCompile 'org.assertj:assertj-core:3.17.2' testCompile 'org.junit.jupiter:junit-jupiter-params:5.7.0' - testRuntime 'org.postgresql:postgresql:42.2.18' - testRuntime 'mysql:mysql-connector-java:8.0.22' + testRuntime 'org.postgresql:postgresql:42.2.17' + testRuntime 'mysql:mysql-connector-java:8.0.21' testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.7.0' } 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/build.gradle b/modules/localstack/build.gradle index 418344b82f2..29abca90878 100644 --- a/modules/localstack/build.gradle +++ b/modules/localstack/build.gradle @@ -3,9 +3,9 @@ description = "Testcontainers :: Localstack" dependencies { compile project(':testcontainers') - compileOnly 'com.amazonaws:aws-java-sdk-s3:1.11.882' + compileOnly 'com.amazonaws:aws-java-sdk-s3:1.11.880' testCompile 'com.amazonaws:aws-java-sdk-s3:1.11.880' - testCompile 'com.amazonaws:aws-java-sdk-sqs:1.11.884' + testCompile 'com.amazonaws:aws-java-sdk-sqs:1.11.880' testCompile 'com.amazonaws:aws-java-sdk-logs:1.11.807' - testCompile 'software.amazon.awssdk:s3:2.15.9' + testCompile 'software.amazon.awssdk:s3:2.15.7' } 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/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java index af4d0694ab6..fad4060341c 100644 --- a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java +++ b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java @@ -10,7 +10,6 @@ import static org.testcontainers.containers.localstack.LocalStackContainer.Service.KMS; import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3; import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS; -import static org.testcontainers.containers.localstack.LocalstackTestImages.LOCALSTACK_IMAGE; import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClientBuilder; @@ -65,7 +64,9 @@ public static class WithoutNetwork { // without_network { @ClassRule - public static LocalStackContainer localstack = new LocalStackContainer(LOCALSTACK_IMAGE) + public static LocalStackContainer localstack = new LocalStackContainer( + DockerImageName.parse("localstack/localstack:0.11.0") + ) .withServices(S3, SQS, CLOUDWATCHLOGS, KMS); // } @@ -178,7 +179,9 @@ public static class WithNetwork { private static Network network = Network.newNetwork(); @ClassRule - public static LocalStackContainer localstackInDockerNetwork = new LocalStackContainer(LOCALSTACK_IMAGE) + public static LocalStackContainer localstackInDockerNetwork = new LocalStackContainer( + DockerImageName.parse("localstack/localstack:0.11.0") + ) .withNetwork(network) .withNetworkAliases("notthis", "localstack") // the last alias is used for HOSTNAME_EXTERNAL .withServices(S3, SQS, CLOUDWATCHLOGS); diff --git a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java index 0dd0ca9b9ac..60e5498daf3 100644 --- a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java +++ b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java @@ -3,6 +3,6 @@ import org.testcontainers.utility.DockerImageName; public interface LocalstackTestImages { - DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:0.11.3"); + DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:0.11.0"); DockerImageName AWS_CLI_IMAGE = DockerImageName.parse("atlassian/pipelines-awscli:1.16.302"); } diff --git a/modules/mssqlserver/build.gradle b/modules/mssqlserver/build.gradle index 9aab7844148..f0170500e7d 100644 --- a/modules/mssqlserver/build.gradle +++ b/modules/mssqlserver/build.gradle @@ -10,12 +10,12 @@ dependencies { compileOnly 'io.r2dbc:r2dbc-mssql:0.8.4.RELEASE' testCompile project(':jdbc-test') - testCompile 'com.microsoft.sqlserver:mssql-jdbc:9.1.0.jre8-preview' + testCompile 'com.microsoft.sqlserver:mssql-jdbc:8.4.1.jre8' testCompile project(':r2dbc') testCompile 'io.r2dbc:r2dbc-mssql:0.8.3.RELEASE' // MSSQL's wait strategy requires the JDBC driver testCompile testFixtures(project(':r2dbc')) - testCompile 'com.microsoft.sqlserver:mssql-jdbc:9.1.0.jre8-preview' + testCompile 'com.microsoft.sqlserver:mssql-jdbc:8.4.1.jre8' } diff --git a/modules/mysql/build.gradle b/modules/mysql/build.gradle index da3dad9f2b3..9df2e177d3f 100644 --- a/modules/mysql/build.gradle +++ b/modules/mysql/build.gradle @@ -10,7 +10,7 @@ dependencies { compileOnly 'dev.miku:r2dbc-mysql:0.8.2.RELEASE' testCompile project(':jdbc-test') - testCompile 'mysql:mysql-connector-java:8.0.22' + testCompile 'mysql:mysql-connector-java:8.0.21' testCompile testFixtures(project(':r2dbc')) testCompile 'dev.miku:r2dbc-mysql:0.8.1.RELEASE' 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/postgresql/build.gradle b/modules/postgresql/build.gradle index b16ceaea2a8..aa5db7028b5 100644 --- a/modules/postgresql/build.gradle +++ b/modules/postgresql/build.gradle @@ -11,7 +11,7 @@ dependencies { testCompile project(':jdbc-test') testCompile project(':test-support') - testCompile 'org.postgresql:postgresql:42.2.18' + testCompile 'org.postgresql:postgresql:42.2.17' testCompile testFixtures(project(':r2dbc')) testCompile 'io.r2dbc:r2dbc-postgresql:0.8.5.RELEASE' 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/build.gradle b/modules/spock/build.gradle index a095bcb8a80..86724262a29 100644 --- a/modules/spock/build.gradle +++ b/modules/spock/build.gradle @@ -15,8 +15,8 @@ dependencies { testCompile 'com.zaxxer:HikariCP:3.4.5' testCompile 'org.apache.httpcomponents:httpclient:4.5.13' - testRuntime 'org.postgresql:postgresql:42.2.18' - testRuntime 'mysql:mysql-connector-java:8.0.22' + testRuntime 'org.postgresql:postgresql:42.2.17' + testRuntime 'mysql:mysql-connector-java:8.0.21' testCompileClasspath 'org.jetbrains:annotations:20.1.0' } 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") } diff --git a/requirements.txt b/requirements.txt index f962b36a44f..227c646dd6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ mkdocs==1.0.4 -mkdocs-codeinclude-plugin==0.1.0 +mkdocs-codeinclude-plugin==0.0.1 mkdocs-material==4.6.0 mkdocs-markdownextradata-plugin==0.1.1 markdown>=3.1,<3.2