Skip to content

Commit c3f53b3

Browse files
authored
Deprecate ambiguous constructors (#2839)
Our constructors for `GenericContainer` and all the classes that inherit from it tend to be a bit inconsistent. In some places, a no-args constructor is favoured, in other places a String parameter is favoured - which can be either a full image name or a tag. This raises confusion, and in the case of an image tag being the parameter, it restricts which images people can easily use. Another problem this raises is when the container class specifies a default tag version: this frequently falls out of date. We're stuck for options: some users (and image maintainers) would like to see newer image versions being used, but this could be a breaking change for existing users. This is especially risky when a major version bump is proposed. This change aims to: - Deprecate 'ambiguous' constructors for containers - any no-args or String-arg constructor gets this treatment. - Introduce a new constructor which accepts a `DockerImageName` object. This class is preferred to a String because its meaning is unambiguous. - Enhance `DockerImageName` with a static factory method and a convenience `withTag` method so that users can easily create and derive new instances. - Update all modules to follow suit
1 parent fd823a1 commit c3f53b3

File tree

181 files changed

+1445
-565
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

181 files changed

+1445
-565
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public class DockerClientFactory {
6060
TESTCONTAINERS_SESSION_ID_LABEL, SESSION_ID
6161
);
6262

63-
private static final String TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyImage();
63+
private static final String TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString();
6464
private static DockerClientFactory instance;
6565

6666
// Cached client configuration

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCom
586586

587587
public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
588588

589-
super(TestcontainersConfiguration.getInstance().getDockerComposeContainerImage());
589+
super(TestcontainersConfiguration.getInstance().getDockerComposeDockerImageName());
590590
addEnv(ENV_PROJECT_NAME, identifier);
591591

592592
// Map the docker compose file into the container

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
* not available - which could manifest as flaky or unstable tests.</p>
1313
*/
1414
public class FixedHostPortGenericContainer<SELF extends FixedHostPortGenericContainer<SELF>> extends GenericContainer<SELF> {
15+
16+
/**
17+
* @deprecated it is highly recommended that {@link FixedHostPortGenericContainer} not be used, as it risks port conflicts.
18+
*/
19+
@Deprecated
1520
public FixedHostPortGenericContainer(@NotNull String dockerImageName) {
1621
super(dockerImageName);
1722
}

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import com.github.dockerjava.api.model.ContainerNetwork;
1313
import com.github.dockerjava.api.model.ExposedPort;
1414
import com.github.dockerjava.api.model.HostConfig;
15-
import com.github.dockerjava.api.model.Info;
1615
import com.github.dockerjava.api.model.Link;
1716
import com.github.dockerjava.api.model.PortBinding;
1817
import com.github.dockerjava.api.model.Volume;
@@ -55,6 +54,7 @@
5554
import org.testcontainers.lifecycle.TestDescription;
5655
import org.testcontainers.lifecycle.TestLifecycleAware;
5756
import org.testcontainers.utility.Base58;
57+
import org.testcontainers.utility.DockerImageName;
5858
import org.testcontainers.utility.DockerLoggerFactory;
5959
import org.testcontainers.utility.DockerMachineClient;
6060
import org.testcontainers.utility.MountableFile;
@@ -227,10 +227,27 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>
227227
@Setter(AccessLevel.NONE)
228228
private boolean shouldBeReused = false;
229229

230+
231+
public GenericContainer(@NonNull final DockerImageName dockerImageName) {
232+
this.image = new RemoteDockerImage(dockerImageName);
233+
}
234+
235+
public GenericContainer(@NonNull final RemoteDockerImage image) {
236+
this.image = image;
237+
}
238+
239+
/**
240+
* @deprecated use {@link GenericContainer(DockerImageName)} instead
241+
*/
242+
@Deprecated
230243
public GenericContainer() {
231244
this(TestcontainersConfiguration.getInstance().getTinyImage());
232245
}
233246

247+
/**
248+
* @deprecated use {@link GenericContainer(DockerImageName)} instead
249+
*/
250+
@Deprecated
234251
public GenericContainer(@NonNull final String dockerImageName) {
235252
this.setDockerImageName(dockerImageName);
236253
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
public enum PortForwardingContainer {
2020
INSTANCE;
2121

22-
private GenericContainer container;
22+
private GenericContainer<?> container;
2323

2424
private final Set<Entry<Integer, Integer>> exposedPorts = Collections.newSetFromMap(new ConcurrentHashMap<>());
2525

@@ -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().getSSHdImage())
32+
container = new GenericContainer<>(TestcontainersConfiguration.getInstance().getSSHdDockerImageName())
3333
.withExposedPorts(22)
3434
.withEnv("PASSWORD", password)
3535
.withCommand(

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.testcontainers.containers;
22

33
import org.testcontainers.utility.Base58;
4+
import org.testcontainers.utility.DockerImageName;
45
import org.testcontainers.utility.TestcontainersConfiguration;
56

67
import java.util.HashMap;
@@ -16,7 +17,11 @@ public class SocatContainer extends GenericContainer<SocatContainer> {
1617
private final Map<Integer, String> targets = new HashMap<>();
1718

1819
public SocatContainer() {
19-
super(TestcontainersConfiguration.getInstance().getSocatContainerImage());
20+
this(TestcontainersConfiguration.getInstance().getSocatDockerImageName());
21+
}
22+
23+
public SocatContainer(final DockerImageName dockerImageName) {
24+
super(dockerImageName);
2025
withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh"));
2126
withCreateContainerCmdModifier(it -> it.withName("testcontainers-socat-" + Base58.randomString(8)));
2227
}
@@ -39,4 +44,4 @@ protected void configure() {
3944
.collect(Collectors.joining(" & "))
4045
);
4146
}
42-
}
47+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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().getVncRecordedContainerImage());
55+
super(TestcontainersConfiguration.getInstance().getVncDockerImageName());
5656

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public AuthConfig effectiveAuthConfig(String imageName) {
4040
}
4141

4242
// try and obtain more accurate auth config using our resolution
43-
final DockerImageName parsed = new DockerImageName(imageName);
43+
final DockerImageName parsed = DockerImageName.parse(imageName);
4444
final AuthConfig effectiveAuthConfig = RegistryAuthLocator.instance()
4545
.lookupAuthConfig(parsed, fallbackAuthConfig);
4646

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public abstract class AbstractImagePullPolicy implements ImagePullPolicy {
1212

1313
@Override
1414
public boolean shouldPull(DockerImageName imageName) {
15-
Logger logger = DockerLoggerFactory.getLogger(imageName.toString());
15+
Logger logger = DockerLoggerFactory.getLogger(imageName.asCanonicalNameString());
1616

1717
// Does our cache already know the image?
1818
ImageData cachedImageData = LOCAL_IMAGES_CACHE.get(imageName);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public Optional<ImageData> refreshCache(DockerImageName imageName) {
3939

4040
InspectImageResponse response = null;
4141
try {
42-
response = dockerClient.inspectImageCmd(imageName.toString()).exec();
42+
response = dockerClient.inspectImageCmd(imageName.asCanonicalNameString()).exec();
4343
} catch (NotFoundException e) {
4444
log.trace("Image {} not found", imageName, e);
4545
}

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,18 @@ public class RemoteDockerImage extends LazyFuture<String> {
3838
@ToString.Exclude
3939
private DockerClient dockerClient = DockerClientFactory.lazyClient();
4040

41+
public RemoteDockerImage(DockerImageName dockerImageName) {
42+
this.imageNameFuture = CompletableFuture.completedFuture(dockerImageName);
43+
}
44+
45+
@Deprecated
4146
public RemoteDockerImage(String dockerImageName) {
42-
this.imageNameFuture = CompletableFuture.completedFuture(new DockerImageName(dockerImageName));
47+
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(dockerImageName));
4348
}
4449

50+
@Deprecated
4551
public RemoteDockerImage(@NonNull String repository, @NonNull String tag) {
46-
this.imageNameFuture = CompletableFuture.completedFuture(new DockerImageName(repository, tag));
52+
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(repository).withTag(tag));
4753
}
4854

4955
public RemoteDockerImage(@NonNull Future<String> imageFuture) {
@@ -57,7 +63,7 @@ protected final String resolve() {
5763
Logger logger = DockerLoggerFactory.getLogger(imageName.toString());
5864
try {
5965
if (!imagePullPolicy.shouldPull(imageName)) {
60-
return imageName.toString();
66+
return imageName.asCanonicalNameString();
6167
}
6268

6369
// The image is not available locally - pull it
@@ -76,7 +82,7 @@ protected final String resolve() {
7682

7783
LocalImagesCache.INSTANCE.refreshCache(imageName);
7884

79-
return imageName.toString();
85+
return imageName.asCanonicalNameString();
8086
} catch (InterruptedException | InternalServerErrorException e) {
8187
// these classes of exception often relate to timeout/connection errors so should be retried
8288
lastFailure = e;
@@ -104,7 +110,7 @@ private String imageNameToString() {
104110
}
105111

106112
try {
107-
return getImageName().toString();
113+
return getImageName().asCanonicalNameString();
108114
} catch (InterruptedException | ExecutionException e) {
109115
return e.getMessage();
110116
}

core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/FromStatementTrait.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
public interface FromStatementTrait<SELF extends FromStatementTrait<SELF> & DockerfileBuilderTrait<SELF>> {
77

88
default SELF from(String dockerImageName) {
9-
new DockerImageName(dockerImageName).assertValid();
9+
DockerImageName.parse(dockerImageName).assertValid();
1010

1111
return ((SELF) this).withStatement(new SingleArgumentStatement("FROM", dockerImageName));
1212
}

core/src/main/java/org/testcontainers/utility/DockerImageName.java

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33

44
import com.google.common.net.HostAndPort;
5+
import lombok.AccessLevel;
6+
import lombok.AllArgsConstructor;
57
import lombok.Data;
68
import lombok.EqualsAndHashCode;
9+
import org.jetbrains.annotations.NotNull;
710

811
import java.util.regex.Pattern;
912

1013
@EqualsAndHashCode(exclude = "rawName")
14+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
1115
public final class DockerImageName {
1216

1317
/* Regex patterns used for validation */
@@ -19,22 +23,42 @@ public final class DockerImageName {
1923
private final String rawName;
2024
private final String registry;
2125
private final String repo;
22-
private final Versioning versioning;
26+
@NotNull private final Versioning versioning;
2327

24-
public DockerImageName(String name) {
25-
this.rawName = name;
26-
final int slashIndex = name.indexOf('/');
28+
/**
29+
* Parses a docker image name from a provided string.
30+
*
31+
* @param fullImageName in standard Docker format, e.g. <code>name:tag</code>,
32+
* <code>some.registry/path/name:tag</code>,
33+
* <code>some.registry/path/name@sha256:abcdef...</code>, etc.
34+
*/
35+
public static DockerImageName parse(String fullImageName) {
36+
return new DockerImageName(fullImageName);
37+
}
38+
39+
/**
40+
* Parses a docker image name from a provided string.
41+
*
42+
* @param fullImageName in standard Docker format, e.g. <code>name:tag</code>,
43+
* <code>some.registry/path/name:tag</code>,
44+
* <code>some.registry/path/name@sha256:abcdef...</code>, etc.
45+
* @deprecated use {@link DockerImageName#parse(String)} instead
46+
*/
47+
@Deprecated
48+
public DockerImageName(String fullImageName) {
49+
this.rawName = fullImageName;
50+
final int slashIndex = fullImageName.indexOf('/');
2751

2852
String remoteName;
2953
if (slashIndex == -1 ||
30-
(!name.substring(0, slashIndex).contains(".") &&
31-
!name.substring(0, slashIndex).contains(":") &&
32-
!name.substring(0, slashIndex).equals("localhost"))) {
54+
(!fullImageName.substring(0, slashIndex).contains(".") &&
55+
!fullImageName.substring(0, slashIndex).contains(":") &&
56+
!fullImageName.substring(0, slashIndex).equals("localhost"))) {
3357
registry = "";
34-
remoteName = name;
58+
remoteName = fullImageName;
3559
} else {
36-
registry = name.substring(0, slashIndex);
37-
remoteName = name.substring(slashIndex + 1);
60+
registry = fullImageName.substring(0, slashIndex);
61+
remoteName = fullImageName.substring(slashIndex + 1);
3862
}
3963

4064
if (remoteName.contains("@sha256:")) {
@@ -49,28 +73,40 @@ public DockerImageName(String name) {
4973
}
5074
}
5175

52-
public DockerImageName(String name, String tag) {
53-
this.rawName = name;
54-
final int slashIndex = name.indexOf('/');
76+
/**
77+
* Parses a docker image name from a provided string, and uses a separate provided version.
78+
*
79+
* @param nameWithoutTag in standard Docker format, e.g. <code>name</code>,
80+
* <code>some.registry/path/name</code>,
81+
* <code>some.registry/path/name</code>, etc.
82+
* @param version a docker image version identifier, either as a tag or sha256 checksum, e.g.
83+
* <code>tag</code>,
84+
* <code>sha256:abcdef...</code>.
85+
* @deprecated use {@link DockerImageName#parse(String)}.{@link DockerImageName#withTag(String)} instead
86+
*/
87+
@Deprecated
88+
public DockerImageName(String nameWithoutTag, @NotNull String version) {
89+
this.rawName = nameWithoutTag;
90+
final int slashIndex = nameWithoutTag.indexOf('/');
5591

5692
String remoteName;
5793
if (slashIndex == -1 ||
58-
(!name.substring(0, slashIndex).contains(".") &&
59-
!name.substring(0, slashIndex).contains(":") &&
60-
!name.substring(0, slashIndex).equals("localhost"))) {
94+
(!nameWithoutTag.substring(0, slashIndex).contains(".") &&
95+
!nameWithoutTag.substring(0, slashIndex).contains(":") &&
96+
!nameWithoutTag.substring(0, slashIndex).equals("localhost"))) {
6197
registry = "";
62-
remoteName = name;
98+
remoteName = nameWithoutTag;
6399
} else {
64-
registry = name.substring(0, slashIndex);
65-
remoteName = name.substring(slashIndex + 1);
100+
registry = nameWithoutTag.substring(0, slashIndex);
101+
remoteName = nameWithoutTag.substring(slashIndex + 1);
66102
}
67103

68-
if (tag.startsWith("sha256:")) {
104+
if (version.startsWith("sha256:")) {
69105
repo = remoteName;
70-
versioning = new Sha256Versioning(tag.replace("sha256:", ""));
106+
versioning = new Sha256Versioning(version.replace("sha256:", ""));
71107
} else {
72108
repo = remoteName;
73-
versioning = new TagVersioning(tag);
109+
versioning = new TagVersioning(version);
74110
}
75111
}
76112

@@ -92,13 +128,16 @@ public String getVersionPart() {
92128
return versioning.toString();
93129
}
94130

131+
/**
132+
* @return canonical name for the image
133+
*/
134+
public String asCanonicalNameString() {
135+
return getUnversionedPart() + versioning.getSeparator() + versioning.toString();
136+
}
137+
95138
@Override
96139
public String toString() {
97-
if (versioning == null) {
98-
return getUnversionedPart();
99-
} else {
100-
return getUnversionedPart() + versioning.getSeparator() + versioning.toString();
101-
}
140+
return asCanonicalNameString();
102141
}
103142

104143
/**
@@ -111,10 +150,6 @@ public void assertValid() {
111150
if (!REPO_NAME.matcher(repo).matches()) {
112151
throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")");
113152
}
114-
if (versioning == null) {
115-
throw new IllegalArgumentException("No image tag was specified in docker image name " +
116-
"(" + rawName + "). Please provide a tag; this may be 'latest' or a specific version");
117-
}
118153
if (!versioning.isValid()) {
119154
throw new IllegalArgumentException(versioning + " is not a valid image versioning identifier (in " + rawName + ")");
120155
}
@@ -124,8 +159,13 @@ public String getRegistry() {
124159
return registry;
125160
}
126161

162+
public DockerImageName withTag(final String newTag) {
163+
return new DockerImageName(rawName, registry, repo, new TagVersioning(newTag));
164+
}
165+
127166
private interface Versioning {
128167
boolean isValid();
168+
129169
String getSeparator();
130170
}
131171

@@ -155,7 +195,7 @@ public String toString() {
155195
}
156196

157197
@Data
158-
private class Sha256Versioning implements Versioning {
198+
private static class Sha256Versioning implements Versioning {
159199
public static final String HASH_REGEX = "[0-9a-fA-F]{32,}";
160200
private final String hash;
161201

core/src/main/java/org/testcontainers/utility/DockerMachineClient.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.testcontainers.utility;
22

33
import lombok.NonNull;
4-
54
import org.apache.commons.lang.SystemUtils;
65
import org.slf4j.Logger;
76

0 commit comments

Comments
 (0)