Skip to content

Commit

Permalink
Image Substitution
Browse files Browse the repository at this point in the history
* adds a pluggable image substitution mechanism using ServiceLoader, enabling users to perform custom substitution/auditing of images being used by their tests
* provides a default implementation that behaves similarly to legacy `TestcontainersConfiguration` approach (`testcontainers.properties`)
  • Loading branch information
rnorth committed Sep 30, 2020
1 parent 1e597cc commit c776755
Show file tree
Hide file tree
Showing 27 changed files with 349 additions and 123 deletions.
11 changes: 8 additions & 3 deletions core/src/main/java/org/testcontainers/DockerClientFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -343,8 +345,11 @@ public <T> T runInsideDocker(Consumer<CreateContainerCmd> createContainerCmdCons
}

private <T> T runInsideDocker(DockerClient client, Consumer<CreateContainerCmd> createContainerCmdConsumer, BiFunction<DockerClient, String, T> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -608,10 +608,11 @@ interface DockerCompose {
class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCompose> 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<File> composeFiles, String identifier) {

super(TestcontainersConfiguration.getInstance().getDockerComposeDockerImageName());
super(DEFAULT_IMAGE_NAME);
addEnv(ENV_PROJECT_NAME, identifier);

// Map the docker compose file into the container
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class SocatContainer extends GenericContainer<SocatContainer> {
private final Map<Integer, String> targets = new HashMap<>();

public SocatContainer() {
this(TestcontainersConfiguration.getInstance().getSocatDockerImageName());
this(DockerImageName.parse("alpine/socat:latest"));
}

public SocatContainer(final DockerImageName dockerImageName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.testcontainers.containers.ContainerFetchException;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.DockerLoggerFactory;
import org.testcontainers.utility.ImageNameSubstitutor;
import org.testcontainers.utility.LazyFuture;

import java.time.Duration;
Expand Down Expand Up @@ -44,12 +45,12 @@ public RemoteDockerImage(DockerImageName dockerImageName) {

@Deprecated
public RemoteDockerImage(String dockerImageName) {
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(dockerImageName));
this(DockerImageName.parse(dockerImageName));
}

@Deprecated
public RemoteDockerImage(@NonNull String repository, @NonNull String tag) {
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(repository).withTag(tag));
this(DockerImageName.parse(repository).withTag(tag));
}

public RemoteDockerImage(@NonNull Future<String> imageFuture) {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.testcontainers.utility;

import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;

/**
* TODO: Javadocs
*/
@Slf4j
public class DefaultImageNameSubstitutor extends ImageNameSubstitutor {

private final TestcontainersConfiguration configuration;

public DefaultImageNameSubstitutor() {
this(TestcontainersConfiguration.getInstance());
}

@VisibleForTesting
DefaultImageNameSubstitutor(TestcontainersConfiguration configuration) {
this.configuration = configuration;
}

@Override
public DockerImageName apply(final DockerImageName original) {
return configuration
.getConfiguredSubstituteImage(original)
.asCompatibleSubstituteFor(original);
}

@Override
protected int getPriority() {
return 0;
}
}
21 changes: 9 additions & 12 deletions core/src/main/java/org/testcontainers/utility/DockerImageName.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@


import com.google.common.net.HostAndPort;
import java.util.regex.Pattern;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.With;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.testcontainers.utility.Versioning.Sha256Versioning;
import org.testcontainers.utility.Versioning.TagVersioning;

import java.util.regex.Pattern;

@EqualsAndHashCode(exclude = { "rawName", "compatibleSubstituteFor" })
@AllArgsConstructor(access = AccessLevel.PRIVATE)
Expand All @@ -26,7 +23,7 @@ public final class DockerImageName {
private final String rawName;
private final String registry;
private final String repo;
@NotNull @With(AccessLevel.PRIVATE)
@Nullable @With(AccessLevel.PRIVATE)
private final Versioning versioning;
@Nullable @With(AccessLevel.PRIVATE)
private final DockerImageName compatibleSubstituteFor;
Expand Down Expand Up @@ -69,10 +66,10 @@ public DockerImageName(String fullImageName) {

if (remoteName.contains("@sha256:")) {
repo = remoteName.split("@sha256:")[0];
versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]);
versioning = new Versioning.Sha256Versioning(remoteName.split("@sha256:")[1]);
} else if (remoteName.contains(":")) {
repo = remoteName.split(":")[0];
versioning = new TagVersioning(remoteName.split(":")[1]);
versioning = new Versioning.TagVersioning(remoteName.split(":")[1]);
} else {
repo = remoteName;
versioning = Versioning.ANY;
Expand Down Expand Up @@ -111,10 +108,10 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {

if (version.startsWith("sha256:")) {
repo = remoteName;
versioning = new Sha256Versioning(version.replace("sha256:", ""));
versioning = new Versioning.Sha256Versioning(version.replace("sha256:", ""));
} else {
repo = remoteName;
versioning = new TagVersioning(version);
versioning = new Versioning.TagVersioning(version);
}

compatibleSubstituteFor = null;
Expand All @@ -135,7 +132,7 @@ public String getUnversionedPart() {
* @return the versioned part of this name (tag or sha256)
*/
public String getVersionPart() {
return versioning.toString();
return versioning == null ? "latest" : versioning.toString();
}

/**
Expand All @@ -161,7 +158,7 @@ public void assertValid() {
if (!REPO_NAME.matcher(repo).matches()) {
throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")");
}
if (!versioning.isValid()) {
if (versioning != null && !versioning.isValid()) {
throw new IllegalArgumentException(versioning + " is not a valid image versioning identifier (in " + rawName + ")");
}
}
Expand All @@ -175,7 +172,7 @@ public String getRegistry() {
* @return an immutable copy of this {@link DockerImageName} with the new version tag
*/
public DockerImageName withTag(final String newTag) {
return withVersioning(new TagVersioning(newTag));
return withVersioning(new Versioning.TagVersioning(newTag));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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;

/**
* TODO: Javadocs
*/
@Slf4j
public abstract class ImageNameSubstitutor implements Function<DockerImageName, DockerImageName> {

@VisibleForTesting
static ImageNameSubstitutor instance;

public synchronized static ImageNameSubstitutor instance() {
if (instance == null) {
final ServiceLoader<ImageNameSubstitutor> 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();

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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bind> binds = new ArrayList<>();
Expand Down
Loading

0 comments on commit c776755

Please sign in to comment.