Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
484f3cd
Implement image compatibility checks
rnorth Aug 14, 2020
49c2360
Revert default image name selection in Kafka/Pulsar containers
rnorth Aug 14, 2020
a0ef1fe
Undeprecate remaining String constructors
rnorth Aug 15, 2020
5be1a79
Add comment re validation of HostAndPort
rnorth Aug 15, 2020
ddf8a28
Add missing annotation
rnorth Aug 15, 2020
c5c1e74
Fix Kafka version string constructor
rnorth Aug 15, 2020
7d82db9
Use @EqualsAndHashCode for Versioning
rnorth Aug 15, 2020
de6dec6
Clarify and expand test
rnorth Aug 15, 2020
d87bcee
Remove duplicate test
rnorth Aug 15, 2020
6b63e88
Rename test
rnorth Aug 15, 2020
202a163
Add continue-on-error for cache step, and upgrade GH cache action
rnorth Aug 15, 2020
b6e5191
Update core/src/test/java/org/testcontainers/utility/DockerImageNameC…
rnorth Aug 20, 2020
33c7ae5
Merge remote-tracking branch 'origin/master' into image-overrides
rnorth Aug 20, 2020
ee5eae3
Merge from origin/master and incorporate trim() to fix accidental whi…
rnorth Aug 20, 2020
502c647
Merge remote-tracking branch 'origin/image-overrides' into image-over…
rnorth Aug 20, 2020
7a8cd7c
Resolve some review comments
rnorth Aug 26, 2020
ad797a5
Avoid dirty state in Kafka test
rnorth Aug 26, 2020
1ee4b78
Tidy up some nits
rnorth Aug 26, 2020
0b0212d
Use Lombok @With and restore generated constructor
rnorth Aug 26, 2020
e22e639
Remove now-unused `TestcontainersConfiguration` accessors
rnorth Aug 26, 2020
9068833
Fix test compilation issue
rnorth Aug 26, 2020
64108f1
Revert "Remove now-unused `TestcontainersConfiguration` accessors"
rnorth Aug 27, 2020
b068af5
Merge branch 'master' into image-overrides
rnorth Aug 27, 2020
3624787
Merge branch 'master' into image-overrides
rnorth Sep 1, 2020
89a6b73
Merge remote-tracking branch 'origin/master' into image-overrides
rnorth Sep 6, 2020
dfa1e63
Add an 'AnyVersion' for images
rnorth Sep 6, 2020
61c5fbc
Merge remote-tracking branch 'origin/image-overrides' into image-over…
rnorth Sep 6, 2020
a5b63ff
Add import
rnorth Sep 6, 2020
3d43019
Adapt test for mocking compatibility (avoid testing latest vs any for…
rnorth Sep 6, 2020
022c493
Restore previous behaviour using a fixed version of Neo4j Enterprise …
rnorth Sep 24, 2020
6cc5c5f
Reinstate standard image check
rnorth Sep 24, 2020
ef9633f
Merge branch 'master' into image-overrides
rnorth Sep 24, 2020
db4dabf
Merge branch 'master' into image-overrides
rnorth Sep 25, 2020
80e6971
Merge branch 'master' into image-overrides
rnorth Sep 28, 2020
7794be6
Merge branch 'master' into image-overrides
rnorth Sep 28, 2020
a1b1f48
Merge branch 'master' into image-overrides
rnorth Sep 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ subprojects {
}

lombok {
version = '1.18.8'
version = '1.18.12'
}

task delombok(type: io.franzbecker.gradle.lombok.task.DelombokTask) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.testcontainers.containers;

import static com.google.common.collect.Lists.newArrayList;
import static org.testcontainers.utility.CommandLine.runShellCommand;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
Expand All @@ -21,6 +23,39 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Adler32;
import java.util.zip.Checksum;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NonNull;
Expand Down Expand Up @@ -62,43 +97,6 @@
import org.testcontainers.utility.ResourceReaper;
import org.testcontainers.utility.TestcontainersConfiguration;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Adler32;
import java.util.zip.Checksum;

import static com.google.common.collect.Lists.newArrayList;
import static org.testcontainers.utility.CommandLine.runShellCommand;

/**
* Base class for that allows a container to be launched and controlled.
*/
Expand Down Expand Up @@ -241,7 +239,7 @@ public GenericContainer(@NonNull final RemoteDockerImage image) {
*/
@Deprecated
public GenericContainer() {
this(TestcontainersConfiguration.getInstance().getTinyImage());
this(TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString());
}

/**
Expand Down
152 changes: 99 additions & 53 deletions core/src/main/java/org/testcontainers/utility/DockerImageName.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,32 @@
import com.google.common.net.HostAndPort;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
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")
@EqualsAndHashCode(exclude = { "rawName", "compatibleSubstituteFor" })
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class DockerImageName {

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

private final String rawName;
private final String registry;
private final String repo;
@NotNull private final Versioning versioning;
@NotNull @With(AccessLevel.PRIVATE)
private final Versioning versioning;
@Nullable @With(AccessLevel.PRIVATE)
private final DockerImageName compatibleSubstituteFor;

/**
* Parses a docker image name from a provided string.
Expand Down Expand Up @@ -52,8 +58,8 @@ public DockerImageName(String fullImageName) {
String remoteName;
if (slashIndex == -1 ||
(!fullImageName.substring(0, slashIndex).contains(".") &&
!fullImageName.substring(0, slashIndex).contains(":") &&
!fullImageName.substring(0, slashIndex).equals("localhost"))) {
!fullImageName.substring(0, slashIndex).contains(":") &&
!fullImageName.substring(0, slashIndex).equals("localhost"))) {
registry = "";
remoteName = fullImageName;
} else {
Expand All @@ -69,8 +75,10 @@ public DockerImageName(String fullImageName) {
versioning = new TagVersioning(remoteName.split(":")[1]);
} else {
repo = remoteName;
versioning = new TagVersioning("latest");
versioning = Versioning.ANY;
}

compatibleSubstituteFor = null;
}

/**
Expand All @@ -92,8 +100,8 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
String remoteName;
if (slashIndex == -1 ||
(!nameWithoutTag.substring(0, slashIndex).contains(".") &&
!nameWithoutTag.substring(0, slashIndex).contains(":") &&
!nameWithoutTag.substring(0, slashIndex).equals("localhost"))) {
!nameWithoutTag.substring(0, slashIndex).contains(":") &&
!nameWithoutTag.substring(0, slashIndex).equals("localhost"))) {
registry = "";
remoteName = nameWithoutTag;
} else {
Expand All @@ -108,6 +116,8 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
repo = remoteName;
versioning = new TagVersioning(version);
}

compatibleSubstituteFor = null;
}

/**
Expand All @@ -132,7 +142,7 @@ public String getVersionPart() {
* @return canonical name for the image
*/
public String asCanonicalNameString() {
return getUnversionedPart() + versioning.getSeparator() + versioning.toString();
return getUnversionedPart() + versioning.getSeparator() + getVersionPart();
}

@Override
Expand All @@ -146,7 +156,8 @@ public String toString() {
* @throws IllegalArgumentException if not valid
*/
public void assertValid() {
HostAndPort.fromString(registry);
//noinspection UnstableApiUsage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add a comment that we just use this function to throw the exception in case of invalid input? I was wondering at first, why the return value is not used.

HostAndPort.fromString(registry); // return value ignored - this throws if registry is not a valid host:port string
if (!REPO_NAME.matcher(repo).matches()) {
throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")");
}
Expand All @@ -159,63 +170,98 @@ public String getRegistry() {
return registry;
}

/**
* @param newTag version tag for the copy to use
* @return an immutable copy of this {@link DockerImageName} with the new version tag
*/
public DockerImageName withTag(final String newTag) {
return new DockerImageName(rawName, registry, repo, new TagVersioning(newTag));
return withVersioning(new TagVersioning(newTag));
}

private interface Versioning {
boolean isValid();

String getSeparator();
/**
* Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image
* behaves as the other does, and is compatible with Testcontainers' assumptions about the other image.
*
* @param otherImageName the image name of the other image
* @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached.
*/
public DockerImageName asCompatibleSubstituteFor(String otherImageName) {
return withCompatibleSubstituteFor(DockerImageName.parse(otherImageName));
}

@Data
private static class TagVersioning implements Versioning {
public static final String TAG_REGEX = "[\\w][\\w\\.\\-]{0,127}";
private final String tag;

TagVersioning(String tag) {
this.tag = tag;
}
/**
* Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image
* behaves as the other does, and is compatible with Testcontainers' assumptions about the other image.
*
* @param otherImageName the image name of the other image
* @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached.
*/
public DockerImageName asCompatibleSubstituteFor(DockerImageName otherImageName) {
return withCompatibleSubstituteFor(otherImageName);
}

@Override
public boolean isValid() {
return tag.matches(TAG_REGEX);
/**
* Test whether this {@link DockerImageName} has declared compatibility with another image (set using
* {@link DockerImageName#asCompatibleSubstituteFor(String)} or
* {@link DockerImageName#asCompatibleSubstituteFor(DockerImageName)}.
* <p>
* If a version tag part is present in the <code>other</code> image name, the tags must exactly match, unless it
* is 'latest'. If a version part is not present in the <code>other</code> image name, the tag contents are ignored.
*
* @param other the other image that we are trying to test compatibility with
* @return whether this image has declared compatibility.
*/
public boolean isCompatibleWith(DockerImageName other) {
// is this image already the same or equivalent?
if (other.equals(this)) {
return true;
}

@Override
public String getSeparator() {
return ":";
if (this.compatibleSubstituteFor == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idea: make compatibleSubstituteFor @NonNull, use SelfCompatible by default

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't really manage to make this work, or at least elegantly.

return false;
}

@Override
public String toString() {
return tag;
}
return this.compatibleSubstituteFor.isCompatibleWith(other);
}

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

Sha256Versioning(String hash) {
this.hash = hash;
}

@Override
public boolean isValid() {
return hash.matches(HASH_REGEX);
/**
* Behaves as {@link DockerImageName#isCompatibleWith(DockerImageName)} but throws an exception
* rather than returning false if a mismatch is detected.
*
* @param anyOthers the other image(s) that we are trying to check compatibility with. If more
* than one is provided, this method will check compatibility with at least one
* of them.
* @throws IllegalStateException if {@link DockerImageName#isCompatibleWith(DockerImageName)}
* returns false
*/
public void assertCompatibleWith(DockerImageName... anyOthers) {
if (anyOthers.length == 0) {
throw new IllegalArgumentException("anyOthers parameter must be non-empty");
}

@Override
public String getSeparator() {
return "@";
for (DockerImageName anyOther : anyOthers) {
if (this.isCompatibleWith(anyOther)) {
return;
}
}

@Override
public String toString() {
return "sha256:" + hash;
}
final DockerImageName exampleOther = anyOthers[0];

throw new IllegalStateException(
String.format(
"Failed to verify that image '%s' is a compatible substitute for '%s'. This generally means that "
+
"you are trying to use an image that Testcontainers has not been designed to use. If this is "
+
"deliberate, and if you are confident that the image is compatible, you should declare "
+
"compatibility in code using the `asCompatibleSubstituteFor` method. For example:\n"
+
" DockerImageName myImage = DockerImageName.parse(\"%s\").asCompatibleSubstituteFor(\"%s\");\n"
+
"and then use `myImage` instead.",
this.rawName, exampleOther.rawName, this.rawName, exampleOther.rawName
)
);
}
}
Loading