From 4d2c5e606516771dd3cdd44dec0669870b876c0d Mon Sep 17 00:00:00 2001 From: Timur Rakhmatullin <174210871+TimurRakhmatullin86@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:58:43 -0700 Subject: [PATCH 1/2] Fix false positive in DockerImageName.isCompatibleWith for library/-prefixed images --- .../utility/DockerImageName.java | 21 ++++---- .../DockerImageNameCompatibilityTest.java | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/testcontainers/utility/DockerImageName.java b/core/src/main/java/org/testcontainers/utility/DockerImageName.java index e3a35855bf8..7010e4f3704 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerImageName.java +++ b/core/src/main/java/org/testcontainers/utility/DockerImageName.java @@ -233,15 +233,13 @@ public DockerImageName asCompatibleSubstituteFor(DockerImageName otherImageName) * @return whether this image has declared compatibility. */ public boolean isCompatibleWith(DockerImageName other) { - // Make sure we always compare against a version of the image name containing the LIBRARY_PREFIX - String finalImageName; - if (this.repository.startsWith(LIBRARY_PREFIX)) { - finalImageName = this.repository; - } else { - finalImageName = LIBRARY_PREFIX + this.repository; + if (other.equals(this)) { + return true; } - DockerImageName imageWithLibraryPrefix = DockerImageName.parse(finalImageName); - if (other.equals(this) || imageWithLibraryPrefix.equals(this)) { + + // 'library/foo' and 'foo' refer to the same official Docker Hub image, so compare both + // names normalized to include the LIBRARY_PREFIX. + if (withLibraryPrefix(other).equals(withLibraryPrefix(this))) { return true; } @@ -252,6 +250,13 @@ public boolean isCompatibleWith(DockerImageName other) { return this.compatibleSubstituteFor.isCompatibleWith(other); } + private static DockerImageName withLibraryPrefix(DockerImageName image) { + if (!image.registry.isEmpty() || image.repository.startsWith(LIBRARY_PREFIX)) { + return image; + } + return image.withRepository(LIBRARY_PREFIX + image.repository); + } + /** * Behaves as {@link DockerImageName#isCompatibleWith(DockerImageName)} but throws an exception * rather than returning false if a mismatch is detected. diff --git a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java index dc7bf9ba1ab..1cac92b38af 100644 --- a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java +++ b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java @@ -97,6 +97,54 @@ void testAssertMethodAcceptsCompatibleLibraryPrefix() { subject.assertCompatibleWith(DockerImageName.parse("foo")); } + @Test + void testLibraryPrefixedImageIsNotCompatibleWithDifferentImage() { + DockerImageName subject = DockerImageName.parse("library/foo:1.2.3"); + + assertThat(subject.isCompatibleWith(DockerImageName.parse("bar"))).as("library/foo:1.2.3 != bar").isFalse(); + assertThat(subject.isCompatibleWith(DockerImageName.parse("repo/bar"))) + .as("library/foo:1.2.3 != repo/bar") + .isFalse(); + assertThat(subject.isCompatibleWith(DockerImageName.parse("library/bar"))) + .as("library/foo:1.2.3 != library/bar") + .isFalse(); + } + + @Test + void testLibraryPrefixIsInterchangeable() { + assertThat(DockerImageName.parse("library/foo").isCompatibleWith(DockerImageName.parse("foo"))) + .as("library/foo ~= foo") + .isTrue(); + assertThat(DockerImageName.parse("foo").isCompatibleWith(DockerImageName.parse("library/foo"))) + .as("foo ~= library/foo") + .isTrue(); + assertThat(DockerImageName.parse("library/foo:1.2.3").isCompatibleWith(DockerImageName.parse("foo"))) + .as("library/foo:1.2.3 ~= foo") + .isTrue(); + assertThat(DockerImageName.parse("foo:1.2.3").isCompatibleWith(DockerImageName.parse("library/foo"))) + .as("foo:1.2.3 ~= library/foo") + .isTrue(); + assertThat(DockerImageName.parse("library/foo:1.2.3").isCompatibleWith(DockerImageName.parse("foo:1.2.3"))) + .as("library/foo:1.2.3 ~= foo:1.2.3") + .isTrue(); + assertThat(DockerImageName.parse("library/foo:1.2.3").isCompatibleWith(DockerImageName.parse("foo:4.5.6"))) + .as("library/foo:1.2.3 != foo:4.5.6") + .isFalse(); + assertThat(DockerImageName.parse("some.registry/foo").isCompatibleWith(DockerImageName.parse("library/foo"))) + .as("some.registry/foo != library/foo") + .isFalse(); + } + + @Test + void testLibraryPrefixedImageWithClaimedCompatibility() { + DockerImageName subject = DockerImageName.parse("library/foo:1.2.3").asCompatibleSubstituteFor("bar"); + + assertThat(subject.isCompatibleWith(DockerImageName.parse("bar"))).as("library/foo:1.2.3(bar) ~= bar").isTrue(); + assertThat(subject.isCompatibleWith(DockerImageName.parse("fizz"))) + .as("library/foo:1.2.3(bar) != fizz") + .isFalse(); + } + @Test void testAssertMethodRejectsIncompatible() { DockerImageName subject = DockerImageName.parse("foo"); From cae01263bc4b3f0d8063642276e13b0dffef61a1 Mon Sep 17 00:00:00 2001 From: Timur Rakhmatullin <174210871+TimurRakhmatullin86@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:33:47 -0700 Subject: [PATCH 2/2] Restrict library/ normalization to single-segment image names Per review: withLibraryPrefix() prefixed any registry-less repository, including multi-segment user namespaces, so foo/bar was treated as compatible with library/foo/bar. The library/ namespace only applies to single-segment official images; skip normalization when the repository contains a slash. --- .../testcontainers/utility/DockerImageName.java | 4 ++++ .../DockerImageNameCompatibilityTest.java | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/src/main/java/org/testcontainers/utility/DockerImageName.java b/core/src/main/java/org/testcontainers/utility/DockerImageName.java index 7010e4f3704..a7447192d59 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerImageName.java +++ b/core/src/main/java/org/testcontainers/utility/DockerImageName.java @@ -254,6 +254,10 @@ private static DockerImageName withLibraryPrefix(DockerImageName image) { if (!image.registry.isEmpty() || image.repository.startsWith(LIBRARY_PREFIX)) { return image; } + if (image.repository.indexOf('/') >= 0) { + // Not a Docker Hub official image; the library/ namespace only applies to single-segment names. + return image; + } return image.withRepository(LIBRARY_PREFIX + image.repository); } diff --git a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java index 1cac92b38af..bdaf2a5f64e 100644 --- a/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java +++ b/core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java @@ -135,6 +135,23 @@ void testLibraryPrefixIsInterchangeable() { .isFalse(); } + @Test + void testLibraryPrefixOnlyAppliesToSingleSegmentNames() { + // 'library/' is the single-segment official-image namespace, so a multi-segment user namespace + // (e.g. 'foo/bar') must not be treated as interchangeable with a 'library/'-prefixed form. + assertThat(DockerImageName.parse("foo/bar").isCompatibleWith(DockerImageName.parse("library/foo/bar"))) + .as("foo/bar != library/foo/bar") + .isFalse(); + assertThat( + DockerImageName.parse("myuser/myimage:1").isCompatibleWith(DockerImageName.parse("library/myuser/myimage")) + ) + .as("myuser/myimage:1 != library/myuser/myimage") + .isFalse(); + assertThat(DockerImageName.parse("library/foo/bar").isCompatibleWith(DockerImageName.parse("foo/bar"))) + .as("library/foo/bar != foo/bar") + .isFalse(); + } + @Test void testLibraryPrefixedImageWithClaimedCompatibility() { DockerImageName subject = DockerImageName.parse("library/foo:1.2.3").asCompatibleSubstituteFor("bar");