-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add image compatibility checks #3021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
484f3cd
49c2360
a0ef1fe
5be1a79
ddf8a28
c5c1e74
7d82db9
de6dec6
d87bcee
6b63e88
202a163
b6e5191
33c7ae5
ee5eae3
502c647
7a8cd7c
ad797a5
1ee4b78
0b0212d
e22e639
9068833
64108f1
b068af5
3624787
89a6b73
dfa1e63
61c5fbc
a5b63ff
3d43019
022c493
6cc5c5f
ef9633f
db4dabf
80e6971
7794be6
a1b1f48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -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 { | ||
|
|
@@ -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; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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 { | ||
|
|
@@ -108,6 +116,8 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) { | |
| repo = remoteName; | ||
| versioning = new TagVersioning(version); | ||
| } | ||
|
|
||
| compatibleSubstituteFor = null; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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 | ||
|
|
@@ -146,7 +156,8 @@ public String toString() { | |
| * @throws IllegalArgumentException if not valid | ||
| */ | ||
| public void assertValid() { | ||
| HostAndPort.fromString(registry); | ||
| //noinspection UnstableApiUsage | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 + ")"); | ||
| } | ||
|
|
@@ -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) { | ||
bsideup marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // is this image already the same or equivalent? | ||
| if (other.equals(this)) { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public String getSeparator() { | ||
| return ":"; | ||
| if (this.compatibleSubstituteFor == null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idea: make
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.