diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 9d669e46b07..fdd7279f86b 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -89,9 +89,10 @@ protected final String resolve() { final Instant startedAt = Instant.now(); final Instant lastRetryAllowed = Instant.now().plus(PULL_RETRY_TIME_LIMIT); final AtomicReference lastFailure = new AtomicReference<>(); + String pullTag = imageName.getDigest() != null ? imageName.getDigest() : imageName.getVersionPart(); final PullImageCmd pullImageCmd = dockerClient .pullImageCmd(imageName.getUnversionedPart()) - .withTag(imageName.getVersionPart()); + .withTag(pullTag); final AtomicReference dockerImageName = new AtomicReference<>(); // The following poll interval in ms: 50, 100, 200, 400, 800.... diff --git a/core/src/main/java/org/testcontainers/utility/DockerImageName.java b/core/src/main/java/org/testcontainers/utility/DockerImageName.java index e3a35855bf8..c9f1957d4d2 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerImageName.java +++ b/core/src/main/java/org/testcontainers/utility/DockerImageName.java @@ -87,8 +87,15 @@ public DockerImageName(String fullImageName) { } if (remoteName.contains("@sha256:")) { - repository = remoteName.split("@sha256:")[0]; - versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]); + String beforeDigest = remoteName.split("@sha256:")[0]; + if (beforeDigest.contains(":")) { + repository = beforeDigest.split(":")[0]; + String tag = beforeDigest.split(":")[1]; + versioning = new Sha256Versioning(remoteName.split("@sha256:")[1], tag); + } else { + repository = beforeDigest; + versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]); + } } else if (remoteName.contains(":")) { repository = remoteName.split(":")[0]; versioning = new TagVersioning(remoteName.split(":")[1]); @@ -155,16 +162,40 @@ public String getUnversionedPart() { } /** - * @return the versioned part of this name (tag or sha256) + * @return the versioned part of this name (tag or sha256). When both tag and digest are present, + * the tag is returned. */ public String getVersionPart() { + if (versioning instanceof Sha256Versioning) { + String tag = ((Sha256Versioning) versioning).getTag(); + if (tag != null) { + return tag; + } + } return versioning.toString(); } + /** + * @return the sha256 digest if present, or null + */ + @Nullable + public String getDigest() { + if (versioning instanceof Sha256Versioning) { + return versioning.toString(); + } + return null; + } + /** * @return canonical name for the image */ public String asCanonicalNameString() { + if (versioning instanceof Sha256Versioning) { + String tag = ((Sha256Versioning) versioning).getTag(); + if (tag != null) { + return getUnversionedPart() + ":" + tag + "@" + versioning; + } + } return getUnversionedPart() + versioning.getSeparator() + getVersionPart(); } diff --git a/core/src/main/java/org/testcontainers/utility/Versioning.java b/core/src/main/java/org/testcontainers/utility/Versioning.java index 372a105518f..bc38ec4ac18 100644 --- a/core/src/main/java/org/testcontainers/utility/Versioning.java +++ b/core/src/main/java/org/testcontainers/utility/Versioning.java @@ -79,8 +79,20 @@ class Sha256Versioning implements Versioning { private final String hash; + @EqualsAndHashCode.Exclude + private final String tag; + Sha256Versioning(String hash) { + this(hash, null); + } + + Sha256Versioning(String hash, String tag) { this.hash = hash; + this.tag = tag; + } + + String getTag() { + return tag; } @Override diff --git a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java index dc7bf9ba1ab..ce0759fec47 100644 --- a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java +++ b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java @@ -85,6 +85,35 @@ void testImageWithClaimedCompatibilityForVersion() { .isFalse(); } + @Test + void testDigestOnlyImageIsCompatible() { + DockerImageName subject = DockerImageName.parse("postgres@sha256:1234abcd1234abcd1234abcd1234abcd"); + + assertThat(subject.isCompatibleWith(DockerImageName.parse("postgres"))) + .as("postgres@sha256:... ~= postgres") + .isTrue(); + } + + @Test + void testTagAndDigestImageIsCompatible() { + DockerImageName subject = DockerImageName.parse("postgres:16.8@sha256:1234abcd1234abcd1234abcd1234abcd"); + + assertThat(subject.isCompatibleWith(DockerImageName.parse("postgres"))) + .as("postgres:16.8@sha256:... ~= postgres") + .isTrue(); + } + + @Test + void testTagAndDigestImageWithRegistryIsCompatible() { + DockerImageName subject = DockerImageName.parse( + "registry.foo.com/repo:tag@sha256:1234abcd1234abcd1234abcd1234abcd" + ); + + assertThat(subject.isCompatibleWith(DockerImageName.parse("registry.foo.com/repo"))) + .as("registry.foo.com/repo:tag@sha256:... ~= registry.foo.com/repo") + .isTrue(); + } + @Test void testAssertMethodAcceptsCompatible() { DockerImageName subject = DockerImageName.parse("foo").asCompatibleSubstituteFor("bar"); diff --git a/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java b/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java index 7d6f8cd2633..f57d7304cc9 100644 --- a/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java +++ b/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java @@ -1,6 +1,7 @@ package org.testcontainers.utility; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -24,6 +25,8 @@ public static String[] getNames() { "registry.foo.com:1234/repo_here/my-name:1.0", "registry.foo.com:1234/repo-here/my-name@sha256:1234abcd1234abcd1234abcd1234abcd", "registry.foo.com:1234/my-name@sha256:1234abcd1234abcd1234abcd1234abcd", + "myname:latest@sha256:1234abcd1234abcd1234abcd1234abcd", + "registry.foo.com:1234/repo-here/my-name:1.0@sha256:1234abcd1234abcd1234abcd1234abcd", "1.2.3.4/my-name:1.0", "1.2.3.4:1234/my-name:1.0", "1.2.3.4/repo-here/my-name:1.0", @@ -147,4 +150,56 @@ void testParsing( } } } + + @Nested + class TagAndDigestParsing { + + @Test + void testTagAndDigestStripsTagFromRepository() { + DockerImageName imageName = DockerImageName.parse("myname:latest@sha256:1234abcd1234abcd1234abcd1234abcd"); + + assertThat(imageName.getRegistry()).isEqualTo(""); + assertThat(imageName.getUnversionedPart()).isEqualTo("myname"); + assertThat(imageName.getVersionPart()).isEqualTo("latest"); + assertThat(imageName.getDigest()).isEqualTo("sha256:1234abcd1234abcd1234abcd1234abcd"); + assertThat(imageName.asCanonicalNameString()) + .isEqualTo("myname:latest@sha256:1234abcd1234abcd1234abcd1234abcd"); + } + + @Test + void testTagAndDigestWithRepoPath() { + DockerImageName imageName = DockerImageName.parse( + "repo/myname:1.0@sha256:1234abcd1234abcd1234abcd1234abcd" + ); + + assertThat(imageName.getRegistry()).isEqualTo(""); + assertThat(imageName.getUnversionedPart()).isEqualTo("repo/myname"); + assertThat(imageName.getVersionPart()).isEqualTo("1.0"); + assertThat(imageName.getDigest()).isEqualTo("sha256:1234abcd1234abcd1234abcd1234abcd"); + assertThat(imageName.asCanonicalNameString()) + .isEqualTo("repo/myname:1.0@sha256:1234abcd1234abcd1234abcd1234abcd"); + } + + @Test + void testTagAndDigestWithRegistry() { + DockerImageName imageName = DockerImageName.parse( + "registry.foo.com:1234/repo-here/my-name:1.0@sha256:1234abcd1234abcd1234abcd1234abcd" + ); + + assertThat(imageName.getRegistry()).isEqualTo("registry.foo.com:1234"); + assertThat(imageName.getUnversionedPart()).isEqualTo("registry.foo.com:1234/repo-here/my-name"); + assertThat(imageName.getVersionPart()).isEqualTo("1.0"); + assertThat(imageName.getDigest()).isEqualTo("sha256:1234abcd1234abcd1234abcd1234abcd"); + assertThat(imageName.asCanonicalNameString()) + .isEqualTo("registry.foo.com:1234/repo-here/my-name:1.0@sha256:1234abcd1234abcd1234abcd1234abcd"); + } + + @Test + void testDigestOnlyHasNoTag() { + DockerImageName imageName = DockerImageName.parse("myname@sha256:1234abcd1234abcd1234abcd1234abcd"); + + assertThat(imageName.getVersionPart()).isEqualTo("sha256:1234abcd1234abcd1234abcd1234abcd"); + assertThat(imageName.getDigest()).isEqualTo("sha256:1234abcd1234abcd1234abcd1234abcd"); + } + } }