Skip to content

Commit 1e597cc

Browse files
rnorthkiview
andauthored
Add image compatibility checks (#3021)
Co-authored-by: Kevin Wittek <[email protected]>
1 parent 63e2c49 commit 1e597cc

File tree

45 files changed

+692
-364
lines changed

Some content is hidden

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

45 files changed

+692
-364
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ subprojects {
2727
}
2828

2929
lombok {
30-
version = '1.18.8'
30+
version = '1.18.12'
3131
}
3232

3333
task delombok(type: io.franzbecker.gradle.lombok.task.DelombokTask) {

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

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

3+
import static com.google.common.collect.Lists.newArrayList;
4+
import static org.testcontainers.utility.CommandLine.runShellCommand;
5+
36
import com.fasterxml.jackson.core.JsonProcessingException;
47
import com.fasterxml.jackson.databind.MapperFeature;
5-
import com.fasterxml.jackson.databind.ObjectMapper;
68
import com.fasterxml.jackson.databind.SerializationFeature;
79
import com.github.dockerjava.api.DockerClient;
810
import com.github.dockerjava.api.command.CreateContainerCmd;
@@ -21,6 +23,39 @@
2123
import com.google.common.base.Strings;
2224
import com.google.common.collect.ImmutableMap;
2325
import com.google.common.hash.Hashing;
26+
import java.io.File;
27+
import java.io.IOException;
28+
import java.lang.reflect.InvocationTargetException;
29+
import java.lang.reflect.Method;
30+
import java.lang.reflect.UndeclaredThrowableException;
31+
import java.nio.charset.Charset;
32+
import java.nio.file.Files;
33+
import java.nio.file.Path;
34+
import java.nio.file.Paths;
35+
import java.time.Duration;
36+
import java.time.Instant;
37+
import java.util.ArrayList;
38+
import java.util.Arrays;
39+
import java.util.Collections;
40+
import java.util.HashMap;
41+
import java.util.HashSet;
42+
import java.util.Iterator;
43+
import java.util.LinkedHashMap;
44+
import java.util.LinkedHashSet;
45+
import java.util.List;
46+
import java.util.Map;
47+
import java.util.Map.Entry;
48+
import java.util.Optional;
49+
import java.util.Set;
50+
import java.util.concurrent.ExecutionException;
51+
import java.util.concurrent.Future;
52+
import java.util.concurrent.TimeUnit;
53+
import java.util.concurrent.atomic.AtomicInteger;
54+
import java.util.function.Consumer;
55+
import java.util.stream.Collectors;
56+
import java.util.stream.Stream;
57+
import java.util.zip.Adler32;
58+
import java.util.zip.Checksum;
2459
import lombok.AccessLevel;
2560
import lombok.Data;
2661
import lombok.NonNull;
@@ -62,43 +97,6 @@
6297
import org.testcontainers.utility.ResourceReaper;
6398
import org.testcontainers.utility.TestcontainersConfiguration;
6499

65-
import java.io.File;
66-
import java.io.IOException;
67-
import java.lang.reflect.InvocationTargetException;
68-
import java.lang.reflect.Method;
69-
import java.lang.reflect.UndeclaredThrowableException;
70-
import java.nio.charset.Charset;
71-
import java.nio.file.Files;
72-
import java.nio.file.Path;
73-
import java.nio.file.Paths;
74-
import java.time.Duration;
75-
import java.time.Instant;
76-
import java.util.ArrayList;
77-
import java.util.Arrays;
78-
import java.util.Collections;
79-
import java.util.HashMap;
80-
import java.util.HashSet;
81-
import java.util.Iterator;
82-
import java.util.LinkedHashMap;
83-
import java.util.LinkedHashSet;
84-
import java.util.List;
85-
import java.util.Map;
86-
import java.util.Map.Entry;
87-
import java.util.Optional;
88-
import java.util.Set;
89-
import java.util.concurrent.ExecutionException;
90-
import java.util.concurrent.Future;
91-
import java.util.concurrent.TimeUnit;
92-
import java.util.concurrent.atomic.AtomicInteger;
93-
import java.util.function.Consumer;
94-
import java.util.stream.Collectors;
95-
import java.util.stream.Stream;
96-
import java.util.zip.Adler32;
97-
import java.util.zip.Checksum;
98-
99-
import static com.google.common.collect.Lists.newArrayList;
100-
import static org.testcontainers.utility.CommandLine.runShellCommand;
101-
102100
/**
103101
* Base class for that allows a container to be launched and controlled.
104102
*/
@@ -241,7 +239,7 @@ public GenericContainer(@NonNull final RemoteDockerImage image) {
241239
*/
242240
@Deprecated
243241
public GenericContainer() {
244-
this(TestcontainersConfiguration.getInstance().getTinyImage());
242+
this(TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString());
245243
}
246244

247245
/**

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

Lines changed: 99 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,32 @@
44
import com.google.common.net.HostAndPort;
55
import lombok.AccessLevel;
66
import lombok.AllArgsConstructor;
7-
import lombok.Data;
87
import lombok.EqualsAndHashCode;
8+
import lombok.With;
99
import org.jetbrains.annotations.NotNull;
10+
import org.jetbrains.annotations.Nullable;
11+
import org.testcontainers.utility.Versioning.Sha256Versioning;
12+
import org.testcontainers.utility.Versioning.TagVersioning;
1013

1114
import java.util.regex.Pattern;
1215

13-
@EqualsAndHashCode(exclude = "rawName")
16+
@EqualsAndHashCode(exclude = { "rawName", "compatibleSubstituteFor" })
1417
@AllArgsConstructor(access = AccessLevel.PRIVATE)
1518
public final class DockerImageName {
1619

1720
/* Regex patterns used for validation */
1821
private static final String ALPHA_NUMERIC = "[a-z0-9]+";
19-
private static final String SEPARATOR = "([\\.]{1}|_{1,2}|-+)";
22+
private static final String SEPARATOR = "([.]|_{1,2}|-+)";
2023
private static final String REPO_NAME_PART = ALPHA_NUMERIC + "(" + SEPARATOR + ALPHA_NUMERIC + ")*";
2124
private static final Pattern REPO_NAME = Pattern.compile(REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*");
2225

2326
private final String rawName;
2427
private final String registry;
2528
private final String repo;
26-
@NotNull private final Versioning versioning;
29+
@NotNull @With(AccessLevel.PRIVATE)
30+
private final Versioning versioning;
31+
@Nullable @With(AccessLevel.PRIVATE)
32+
private final DockerImageName compatibleSubstituteFor;
2733

2834
/**
2935
* Parses a docker image name from a provided string.
@@ -52,8 +58,8 @@ public DockerImageName(String fullImageName) {
5258
String remoteName;
5359
if (slashIndex == -1 ||
5460
(!fullImageName.substring(0, slashIndex).contains(".") &&
55-
!fullImageName.substring(0, slashIndex).contains(":") &&
56-
!fullImageName.substring(0, slashIndex).equals("localhost"))) {
61+
!fullImageName.substring(0, slashIndex).contains(":") &&
62+
!fullImageName.substring(0, slashIndex).equals("localhost"))) {
5763
registry = "";
5864
remoteName = fullImageName;
5965
} else {
@@ -69,8 +75,10 @@ public DockerImageName(String fullImageName) {
6975
versioning = new TagVersioning(remoteName.split(":")[1]);
7076
} else {
7177
repo = remoteName;
72-
versioning = new TagVersioning("latest");
78+
versioning = Versioning.ANY;
7379
}
80+
81+
compatibleSubstituteFor = null;
7482
}
7583

7684
/**
@@ -92,8 +100,8 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
92100
String remoteName;
93101
if (slashIndex == -1 ||
94102
(!nameWithoutTag.substring(0, slashIndex).contains(".") &&
95-
!nameWithoutTag.substring(0, slashIndex).contains(":") &&
96-
!nameWithoutTag.substring(0, slashIndex).equals("localhost"))) {
103+
!nameWithoutTag.substring(0, slashIndex).contains(":") &&
104+
!nameWithoutTag.substring(0, slashIndex).equals("localhost"))) {
97105
registry = "";
98106
remoteName = nameWithoutTag;
99107
} else {
@@ -108,6 +116,8 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
108116
repo = remoteName;
109117
versioning = new TagVersioning(version);
110118
}
119+
120+
compatibleSubstituteFor = null;
111121
}
112122

113123
/**
@@ -132,7 +142,7 @@ public String getVersionPart() {
132142
* @return canonical name for the image
133143
*/
134144
public String asCanonicalNameString() {
135-
return getUnversionedPart() + versioning.getSeparator() + versioning.toString();
145+
return getUnversionedPart() + versioning.getSeparator() + getVersionPart();
136146
}
137147

138148
@Override
@@ -146,7 +156,8 @@ public String toString() {
146156
* @throws IllegalArgumentException if not valid
147157
*/
148158
public void assertValid() {
149-
HostAndPort.fromString(registry);
159+
//noinspection UnstableApiUsage
160+
HostAndPort.fromString(registry); // return value ignored - this throws if registry is not a valid host:port string
150161
if (!REPO_NAME.matcher(repo).matches()) {
151162
throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")");
152163
}
@@ -159,63 +170,98 @@ public String getRegistry() {
159170
return registry;
160171
}
161172

173+
/**
174+
* @param newTag version tag for the copy to use
175+
* @return an immutable copy of this {@link DockerImageName} with the new version tag
176+
*/
162177
public DockerImageName withTag(final String newTag) {
163-
return new DockerImageName(rawName, registry, repo, new TagVersioning(newTag));
178+
return withVersioning(new TagVersioning(newTag));
164179
}
165180

166-
private interface Versioning {
167-
boolean isValid();
168-
169-
String getSeparator();
181+
/**
182+
* Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image
183+
* behaves as the other does, and is compatible with Testcontainers' assumptions about the other image.
184+
*
185+
* @param otherImageName the image name of the other image
186+
* @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached.
187+
*/
188+
public DockerImageName asCompatibleSubstituteFor(String otherImageName) {
189+
return withCompatibleSubstituteFor(DockerImageName.parse(otherImageName));
170190
}
171191

172-
@Data
173-
private static class TagVersioning implements Versioning {
174-
public static final String TAG_REGEX = "[\\w][\\w\\.\\-]{0,127}";
175-
private final String tag;
176-
177-
TagVersioning(String tag) {
178-
this.tag = tag;
179-
}
192+
/**
193+
* Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image
194+
* behaves as the other does, and is compatible with Testcontainers' assumptions about the other image.
195+
*
196+
* @param otherImageName the image name of the other image
197+
* @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached.
198+
*/
199+
public DockerImageName asCompatibleSubstituteFor(DockerImageName otherImageName) {
200+
return withCompatibleSubstituteFor(otherImageName);
201+
}
180202

181-
@Override
182-
public boolean isValid() {
183-
return tag.matches(TAG_REGEX);
203+
/**
204+
* Test whether this {@link DockerImageName} has declared compatibility with another image (set using
205+
* {@link DockerImageName#asCompatibleSubstituteFor(String)} or
206+
* {@link DockerImageName#asCompatibleSubstituteFor(DockerImageName)}.
207+
* <p>
208+
* If a version tag part is present in the <code>other</code> image name, the tags must exactly match, unless it
209+
* is 'latest'. If a version part is not present in the <code>other</code> image name, the tag contents are ignored.
210+
*
211+
* @param other the other image that we are trying to test compatibility with
212+
* @return whether this image has declared compatibility.
213+
*/
214+
public boolean isCompatibleWith(DockerImageName other) {
215+
// is this image already the same or equivalent?
216+
if (other.equals(this)) {
217+
return true;
184218
}
185219

186-
@Override
187-
public String getSeparator() {
188-
return ":";
220+
if (this.compatibleSubstituteFor == null) {
221+
return false;
189222
}
190223

191-
@Override
192-
public String toString() {
193-
return tag;
194-
}
224+
return this.compatibleSubstituteFor.isCompatibleWith(other);
195225
}
196226

197-
@Data
198-
private static class Sha256Versioning implements Versioning {
199-
public static final String HASH_REGEX = "[0-9a-fA-F]{32,}";
200-
private final String hash;
201-
202-
Sha256Versioning(String hash) {
203-
this.hash = hash;
204-
}
205-
206-
@Override
207-
public boolean isValid() {
208-
return hash.matches(HASH_REGEX);
227+
/**
228+
* Behaves as {@link DockerImageName#isCompatibleWith(DockerImageName)} but throws an exception
229+
* rather than returning false if a mismatch is detected.
230+
*
231+
* @param anyOthers the other image(s) that we are trying to check compatibility with. If more
232+
* than one is provided, this method will check compatibility with at least one
233+
* of them.
234+
* @throws IllegalStateException if {@link DockerImageName#isCompatibleWith(DockerImageName)}
235+
* returns false
236+
*/
237+
public void assertCompatibleWith(DockerImageName... anyOthers) {
238+
if (anyOthers.length == 0) {
239+
throw new IllegalArgumentException("anyOthers parameter must be non-empty");
209240
}
210241

211-
@Override
212-
public String getSeparator() {
213-
return "@";
242+
for (DockerImageName anyOther : anyOthers) {
243+
if (this.isCompatibleWith(anyOther)) {
244+
return;
245+
}
214246
}
215247

216-
@Override
217-
public String toString() {
218-
return "sha256:" + hash;
219-
}
248+
final DockerImageName exampleOther = anyOthers[0];
249+
250+
throw new IllegalStateException(
251+
String.format(
252+
"Failed to verify that image '%s' is a compatible substitute for '%s'. This generally means that "
253+
+
254+
"you are trying to use an image that Testcontainers has not been designed to use. If this is "
255+
+
256+
"deliberate, and if you are confident that the image is compatible, you should declare "
257+
+
258+
"compatibility in code using the `asCompatibleSubstituteFor` method. For example:\n"
259+
+
260+
" DockerImageName myImage = DockerImageName.parse(\"%s\").asCompatibleSubstituteFor(\"%s\");\n"
261+
+
262+
"and then use `myImage` instead.",
263+
this.rawName, exampleOther.rawName, this.rawName, exampleOther.rawName
264+
)
265+
);
220266
}
221267
}

0 commit comments

Comments
 (0)