Skip to content

Commit 83df05a

Browse files
committed
Image substitution
Builds upon #3021: * 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`), but also... * For many orgs, sticking a prefix on the front of image names might be enough to use a private registry. I've added a default behaviour whereby, if a particular environment variable is present, image names are automatically substituted. e.g. `TESTCONTAINERS_IMAGE_NAME_PREFIX=my.registry.com/` would transform `redis` to `my.registry.com/redis` etc. Notes: * behaviour is similar but not quite identical to `TestcontainersConfiguration`: use of a configured custom image for, e.g. Kafka/Pulsar that does not have a tag specified causes the substitution to take effect for all usages. It seems very unlikely that people would be using a mix of the config file image overrides in some places _and_ specific images specified in code in others. * Duplication of default image names in modules vs `TestcontainersConfiguration` class is intentional: specifying image overrides in `testcontainers.properties` should be removed in the future. * ~Add log deprecation warnings when `testcontainers.properties` image overrides are used.~ Defer to a future release?
1 parent 1b4a2a6 commit 83df05a

File tree

36 files changed

+981
-181
lines changed

36 files changed

+981
-181
lines changed

core/src/main/java/org/testcontainers/DockerClientFactory.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.testcontainers.dockerclient.TransportConfig;
2626
import org.testcontainers.images.TimeLimitedLoggedPullImageResultCallback;
2727
import org.testcontainers.utility.ComparableVersion;
28+
import org.testcontainers.utility.DockerImageName;
29+
import org.testcontainers.utility.ImageNameSubstitutor;
2830
import org.testcontainers.utility.MountableFile;
2931
import org.testcontainers.utility.ResourceReaper;
3032
import org.testcontainers.utility.TestcontainersConfiguration;
@@ -61,7 +63,7 @@ public class DockerClientFactory {
6163
TESTCONTAINERS_SESSION_ID_LABEL, SESSION_ID
6264
);
6365

64-
private static final String TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString();
66+
private static final DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.5");
6567
private static DockerClientFactory instance;
6668

6769
// Cached client configuration
@@ -343,8 +345,11 @@ public <T> T runInsideDocker(Consumer<CreateContainerCmd> createContainerCmdCons
343345
}
344346

345347
private <T> T runInsideDocker(DockerClient client, Consumer<CreateContainerCmd> createContainerCmdConsumer, BiFunction<DockerClient, String, T> block) {
346-
checkAndPullImage(client, TINY_IMAGE);
347-
CreateContainerCmd createContainerCmd = client.createContainerCmd(TINY_IMAGE)
348+
349+
final String tinyImage = ImageNameSubstitutor.instance().apply(TINY_IMAGE).asCanonicalNameString();
350+
351+
checkAndPullImage(client, tinyImage);
352+
CreateContainerCmd createContainerCmd = client.createContainerCmd(tinyImage)
348353
.withLabels(DEFAULT_LABELS);
349354
createContainerCmdConsumer.accept(createContainerCmd);
350355
String id = createContainerCmd.exec().getId();

core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@
2929
import org.testcontainers.utility.AuditLogger;
3030
import org.testcontainers.utility.Base58;
3131
import org.testcontainers.utility.CommandLine;
32+
import org.testcontainers.utility.DockerImageName;
3233
import org.testcontainers.utility.DockerLoggerFactory;
3334
import org.testcontainers.utility.LogUtils;
3435
import org.testcontainers.utility.MountableFile;
3536
import org.testcontainers.utility.ResourceReaper;
36-
import org.testcontainers.utility.TestcontainersConfiguration;
3737
import org.zeroturnaround.exec.InvalidExitValueException;
3838
import org.zeroturnaround.exec.ProcessExecutor;
3939
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
@@ -608,10 +608,11 @@ interface DockerCompose {
608608
class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCompose> implements DockerCompose {
609609

610610
public static final char UNIX_PATH_SEPERATOR = ':';
611+
public static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker/compose:1.24.1");
611612

612613
public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
613614

614-
super(TestcontainersConfiguration.getInstance().getDockerComposeDockerImageName());
615+
super(DEFAULT_IMAGE_NAME);
615616
addEnv(ENV_PROJECT_NAME, identifier);
616617

617618
// Map the docker compose file into the container

core/src/main/java/org/testcontainers/containers/GenericContainer.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,9 @@ public GenericContainer(@NonNull final RemoteDockerImage image) {
239239
*/
240240
@Deprecated
241241
public GenericContainer() {
242-
this(TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString());
242+
this(TestcontainersConfiguration.getInstance().getTinyImage());
243243
}
244244

245-
/**
246-
* @deprecated use {@link GenericContainer(DockerImageName)} instead
247-
*/
248-
@Deprecated
249245
public GenericContainer(@NonNull final String dockerImageName) {
250246
this.setDockerImageName(dockerImageName);
251247
}

core/src/main/java/org/testcontainers/containers/PortForwardingContainer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
import lombok.AccessLevel;
66
import lombok.Getter;
77
import lombok.SneakyThrows;
8-
import org.testcontainers.utility.TestcontainersConfiguration;
8+
import org.testcontainers.utility.DockerImageName;
99

1010
import java.time.Duration;
1111
import java.util.AbstractMap;
1212
import java.util.Collections;
13+
import java.util.Map.Entry;
1314
import java.util.Optional;
1415
import java.util.Set;
1516
import java.util.UUID;
16-
import java.util.Map.Entry;
1717
import java.util.concurrent.ConcurrentHashMap;
1818

1919
public enum PortForwardingContainer {
@@ -29,7 +29,7 @@ public enum PortForwardingContainer {
2929
@SneakyThrows
3030
private Connection createSSHSession() {
3131
String password = UUID.randomUUID().toString();
32-
container = new GenericContainer<>(TestcontainersConfiguration.getInstance().getSSHdDockerImageName())
32+
container = new GenericContainer<>(DockerImageName.parse("testcontainers/sshd:1.0.0"))
3333
.withExposedPorts(22)
3434
.withEnv("PASSWORD", password)
3535
.withCommand(

core/src/main/java/org/testcontainers/containers/SocatContainer.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package org.testcontainers.containers;
22

3-
import org.testcontainers.utility.Base58;
4-
import org.testcontainers.utility.DockerImageName;
5-
import org.testcontainers.utility.TestcontainersConfiguration;
6-
73
import java.util.HashMap;
84
import java.util.Map;
95
import java.util.stream.Collectors;
6+
import org.testcontainers.utility.Base58;
7+
import org.testcontainers.utility.DockerImageName;
108

119
/**
1210
* 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<SocatContainer> {
1715
private final Map<Integer, String> targets = new HashMap<>();
1816

1917
public SocatContainer() {
20-
this(TestcontainersConfiguration.getInstance().getSocatDockerImageName());
18+
this(DockerImageName.parse("alpine/socat:1.7.3.4-r0"));
2119
}
2220

2321
public SocatContainer(final DockerImageName dockerImageName) {

core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import lombok.ToString;
77
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
88
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
9-
import org.testcontainers.utility.TestcontainersConfiguration;
9+
import org.testcontainers.utility.DockerImageName;
1010

1111
import java.io.File;
1212
import java.io.InputStream;
@@ -52,7 +52,7 @@ public VncRecordingContainer(@NonNull GenericContainer<?> targetContainer) {
5252
* Create a sidekick container and attach it to another container. The VNC output of that container will be recorded.
5353
*/
5454
public VncRecordingContainer(@NonNull Network network, @NonNull String targetNetworkAlias) throws IllegalStateException {
55-
super(TestcontainersConfiguration.getInstance().getVncDockerImageName());
55+
super(DockerImageName.parse("testcontainers/vnc-recorder:1.1.0"));
5656

5757
this.targetNetworkAlias = targetNetworkAlias;
5858
withNetwork(network);

core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public static DockerClientProviderStrategy getFirstValidStrategy(List<DockerClie
171171
}
172172

173173
if (strategy.isPersistable()) {
174-
TestcontainersConfiguration.getInstance().updateGlobalConfig("docker.client.strategy", strategy.getClass().getName());
174+
TestcontainersConfiguration.getInstance().updateUserConfig("docker.client.strategy", strategy.getClass().getName());
175175
}
176176

177177
return Stream.of(strategy);

core/src/main/java/org/testcontainers/images/RemoteDockerImage.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.testcontainers.containers.ContainerFetchException;
1616
import org.testcontainers.utility.DockerImageName;
1717
import org.testcontainers.utility.DockerLoggerFactory;
18+
import org.testcontainers.utility.ImageNameSubstitutor;
1819
import org.testcontainers.utility.LazyFuture;
1920

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

4546
@Deprecated
4647
public RemoteDockerImage(String dockerImageName) {
47-
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(dockerImageName));
48+
this(DockerImageName.parse(dockerImageName));
4849
}
4950

5051
@Deprecated
5152
public RemoteDockerImage(@NonNull String repository, @NonNull String tag) {
52-
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(repository).withTag(tag));
53+
this(DockerImageName.parse(repository).withTag(tag));
5354
}
5455

5556
public RemoteDockerImage(@NonNull Future<String> imageFuture) {
@@ -100,7 +101,10 @@ protected final String resolve() {
100101
}
101102

102103
private DockerImageName getImageName() throws InterruptedException, ExecutionException {
103-
return imageNameFuture.get();
104+
final DockerImageName specifiedImageName = imageNameFuture.get();
105+
106+
// Allow the image name to be substituted
107+
return ImageNameSubstitutor.instance().apply(specifiedImageName);
104108
}
105109

106110
@ToString.Include(name = "imageName", rank = 1)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.testcontainers.utility;
2+
3+
import com.google.common.annotations.VisibleForTesting;
4+
import lombok.extern.slf4j.Slf4j;
5+
6+
/**
7+
* {@link ImageNameSubstitutor} which takes replacement image names from configuration.
8+
* See {@link TestcontainersConfiguration} for the subset of image names which can be substituted using this mechanism.
9+
* <p>
10+
* WARNING: this class is not intended to be public, but {@link java.util.ServiceLoader}
11+
* requires it to be so. Public visibility DOES NOT make it part of the public API.
12+
*/
13+
@Slf4j
14+
public class ConfigurationFileImageNameSubstitutor extends ImageNameSubstitutor {
15+
16+
private final TestcontainersConfiguration configuration;
17+
18+
public ConfigurationFileImageNameSubstitutor() {
19+
this(TestcontainersConfiguration.getInstance());
20+
}
21+
22+
@VisibleForTesting
23+
ConfigurationFileImageNameSubstitutor(TestcontainersConfiguration configuration) {
24+
this.configuration = configuration;
25+
}
26+
27+
@Override
28+
public DockerImageName apply(final DockerImageName original) {
29+
final DockerImageName result = configuration
30+
.getConfiguredSubstituteImage(original)
31+
.asCompatibleSubstituteFor(original);
32+
33+
if (!result.equals(original)) {
34+
log.warn("Image name {} was substituted by configuration to {}. This approach is deprecated and will be removed in the future",
35+
original,
36+
result
37+
);
38+
}
39+
40+
return result;
41+
}
42+
43+
@Override
44+
protected int getPriority() {
45+
return -2;
46+
}
47+
48+
@Override
49+
protected String getDescription() {
50+
return getClass().getSimpleName();
51+
}
52+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.testcontainers.utility;
2+
3+
import com.google.common.annotations.VisibleForTesting;
4+
import lombok.extern.slf4j.Slf4j;
5+
6+
/**
7+
* Testcontainers' default implementation of {@link ImageNameSubstitutor}.
8+
* Delegates to {@link ConfigurationFileImageNameSubstitutor} followed by {@link PrefixingImageNameSubstitutor}.
9+
* <p>
10+
* WARNING: this class is not intended to be public, but {@link java.util.ServiceLoader}
11+
* requires it to be so. Public visibility DOES NOT make it part of the public API.
12+
*/
13+
@Slf4j
14+
public class DefaultImageNameSubstitutor extends ImageNameSubstitutor {
15+
16+
private final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor;
17+
private final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor;
18+
19+
public DefaultImageNameSubstitutor() {
20+
configurationFileImageNameSubstitutor = new ConfigurationFileImageNameSubstitutor();
21+
prefixingImageNameSubstitutor = new PrefixingImageNameSubstitutor();
22+
}
23+
24+
@VisibleForTesting
25+
DefaultImageNameSubstitutor(
26+
final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor,
27+
final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor
28+
) {
29+
this.configurationFileImageNameSubstitutor = configurationFileImageNameSubstitutor;
30+
this.prefixingImageNameSubstitutor = prefixingImageNameSubstitutor;
31+
}
32+
33+
@Override
34+
public DockerImageName apply(final DockerImageName original) {
35+
return prefixingImageNameSubstitutor.apply(
36+
configurationFileImageNameSubstitutor.apply(
37+
original
38+
)
39+
);
40+
}
41+
42+
@Override
43+
protected int getPriority() {
44+
return 0;
45+
}
46+
47+
@Override
48+
protected String getDescription() {
49+
return "DefaultImageNameSubstitutor (composite of '" + configurationFileImageNameSubstitutor.getDescription() + "' and '" + prefixingImageNameSubstitutor.getDescription() + "')";
50+
}
51+
}

0 commit comments

Comments
 (0)