Skip to content

Fix DockerImageName compatibility check when digest is present#11629

Open
konstantinosGkilas wants to merge 3 commits intotestcontainers:mainfrom
konstantinosGkilas:feature/10527-dockerimagename-digest-compatibility
Open

Fix DockerImageName compatibility check when digest is present#11629
konstantinosGkilas wants to merge 3 commits intotestcontainers:mainfrom
konstantinosGkilas:feature/10527-dockerimagename-digest-compatibility

Conversation

@konstantinosGkilas
Copy link
Copy Markdown

Fixes #10527

Summary

  • Fix parsing bug in DockerImageName where images containing both a tag and a digest (e.g., postgres:16.8@sha256:...) had the tag leak into the repository name, causing isCompatibleWith() to incorrectly fail
  • Strip the tag portion from the repository when splitting on @sha256: so that the repository is correctly extracted
  • Add test coverage for digest-only, tag+digest, and tag+digest with registry image name variants

Context

The bug originates from DockerImageName's constructor parsing logic at line 90, where images containing both a tag and a digest (e.g., postgres:16.8@sha256:301bcb...) have the remoteName split only on @sha256:, leaving the tag embedded in the repository (postgres:16.8 instead of postgres). This causes isCompatibleWith() to fail because the repository comparison becomes "postgres:16.8".equals("postgres"). While downstream projects like kroxylicious have worked around this by explicitly declaring compatibility via asCompatibleSubstituteFor(), the fix should reside in testcontainers-java itself so that digest-pinned images are parsed correctly without requiring manual workarounds.

Test plan

  • Existing DockerImageNameTest and DockerImageNameCompatibilityTest pass
  • New parsing tests verify correct repository/versioning extraction for tag+digest images
  • New compatibility tests verify isCompatibleWith() succeeds for digest-only, tag+digest, and registry/tag+digest images

…ontainers#10527)

Strip tag from repository when parsing images with both tag and digest
(e.g., postgres:16.8@sha256:...) to prevent tag leaking into repository name.
@konstantinosGkilas konstantinosGkilas requested a review from a team as a code owner April 2, 2026 07:07
@kiview
Copy link
Copy Markdown
Member

kiview commented Apr 3, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kiview
Copy link
Copy Markdown
Member

kiview commented Apr 3, 2026

Thanks for your contribution @konstantinosGkilas.

It seems, we would still have issues with that rely on getVersionPart() part, such as in that example:

DockerImageName image = DockerImageName.parse(
      "confluentinc/cp-kafka:7.3.0@sha256:1234abcd1234abcd1234abcd1234abcd"
  );

KafkaContainer container = new KafkaContainer(image).withKraft();

But I don't think this needs to block this PR, since it is already an improvement over the status quo.

Note that CI is failing for linting issues, so unfortunately you have to run ./gradlew :testcontainers:spotlessApply first to fix those.

@kiview kiview self-assigned this Apr 3, 2026
When parsing image names like "image:7.3.0@sha256:abcd", the tag was
previously discarded. This caused getVersionPart() to return the sha256
digest instead of the tag, breaking version-based feature checks in
modules like Kafka, Neo4j, Elasticsearch, etc.

Now getVersionPart() returns the tag when both are present, and a new
getDigest() method provides access to the sha256 digest. RemoteDockerImage
uses the digest for pulling when available. asCanonicalNameString()
correctly outputs "image:tag@sha256:hash" format.
@konstantinosGkilas
Copy link
Copy Markdown
Author

Thanks for the review @kiview ! I've addressed the linting issues and also took a stab at the getVersionPart() limitation you mentioned.

When both tag and digest are present (e.g. confluentinc/cp-kafka:7.3.0@sha256:…), getVersionPart() now returns the tag (7.3.0) instead of the digest, so version-based checks in modules like Kafka, Elasticsearch, Neo4j, etc. should work correctly. A new getDigest() method provides access to the sha256 when needed, and RemoteDockerImage uses it for pulling.asCanonicalNameString() also outputs the full image:tag@sha256:hash format.

I could not locate any other open issue, also affected by this change positively. If you have something in mind please feel free to reference it as well.

@kiview
Copy link
Copy Markdown
Member

kiview commented Apr 4, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6da85e7f45

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

String beforeDigest = remoteName.split("@sha256:")[0];
if (beforeDigest.contains(":")) {
repository = beforeDigest.split(":")[0];
String tag = beforeDigest.split(":")[1];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle empty tag before digest parsing

This branch assumes beforeDigest.split(":")[1] always exists, but Java drops trailing empty segments, so an input like repo:@sha256:... throws ArrayIndexOutOfBoundsException during parse instead of being rejected cleanly by assertValid(). That turns a user-facing validation error into an unexpected runtime crash path for malformed image names.

Useful? React with 👍 / 👎.

Comment on lines +89 to +92
Sha256Versioning(String hash, String tag) {
this.hash = hash;
this.tag = tag;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate tag syntax for tag+digest inputs

After introducing the optional tag field on Sha256Versioning, DockerImageName.parse("name:tag@sha256:...") now stores the tag there, but Sha256Versioning.isValid() still validates only the hash. This means malformed tags (for example name:-bad@sha256:...) can pass assertValid(), which is a regression in image-name validation and can defer failures to later pull operations.

Useful? React with 👍 / 👎.

@kiview
Copy link
Copy Markdown
Member

kiview commented Apr 4, 2026

Thanks for looking at the versioning part @konstantinosGkilas.
Related to the Codex findings above, I also checked locally, that the following examples would now behave wrongly.

Malformed tags with sha present will now be considered valid and normalized:

DockerImageName
  .parse("repo/image:tag:extra@sha256:1234abcd1234abcd1234abcd1234abcd")
  .assertValid();

Normalized to:

repo/image:tag@sha256:1234abcd1234abcd1234abcd1234abcd

Also, I don't think we should change the hashCode/equals contract here, might create some confusion to users.
So images with different tags should not be equal:

DockerImageName a = DockerImageName.parse(
    "repo/image:1.0@sha256:1234abcd1234abcd1234abcd1234abcd"
);
DockerImageName b = DockerImageName.parse(
    "repo/image:latest@sha256:1234abcd1234abcd1234abcd1234abcd"
);

@konstantinosGkilas
Copy link
Copy Markdown
Author

Thanks for looking at the versioning part @konstantinosGkilas. Related to the Codex findings above, I also checked locally, that the following examples would now behave wrongly.

Malformed tags with sha present will now be considered valid and normalized:

DockerImageName
  .parse("repo/image:tag:extra@sha256:1234abcd1234abcd1234abcd1234abcd")
  .assertValid();

Normalized to:

repo/image:tag@sha256:1234abcd1234abcd1234abcd1234abcd

Also, I don't think we should change the hashCode/equals contract here, might create some confusion to users. So images with different tags should not be equal:

DockerImageName a = DockerImageName.parse(
    "repo/image:1.0@sha256:1234abcd1234abcd1234abcd1234abcd"
);
DockerImageName b = DockerImageName.parse(
    "repo/image:latest@sha256:1234abcd1234abcd1234abcd1234abcd"
);

I will have another look probably with a small change the image parsing/evaluation should work.
With the codex input I believe that a regex should be enough in order to resolve the issue.

Also https://github.com/testcontainers/testcontainers-java/actions/runs/23963765698/job/69959157481?pr=11629#logs seems to have issues besides my PR.

@kiview
Copy link
Copy Markdown
Member

kiview commented Apr 4, 2026

Yep, that run looked like some networking glitched, triggered a re-run.

- Use split(":", 2) to preserve full tag content instead of silently
  truncating at extra colons
- Remove @EqualsAndHashCode.Exclude from tag field so images with
  different tags are not incorrectly equal
- Validate tag against TAG_REGEX in Sha256Versioning.isValid()
- Add test cases for malformed tag rejection and tag-based equality
@konstantinosGkilas
Copy link
Copy Markdown
Author

@kiview
I started by writing test cases for the two scenarios you identified:

  • A malformed tag with extra colons (repo/image:tag:extra@sha256:...) being silently normalized
    instead of rejected
  • Images with different tags but the same digest incorrectly being treated as equal

Then addressed them with three targeted fixes:

  1. DockerImageName.java: Changed split(":")[1] → split(":", 2)[1] so the full tag (including any
    malformed parts) is preserved rather than silently truncated
  2. Versioning.java: Removed @EqualsAndHashCode.Exclude from the tag field in Sha256Versioning so
    that images with different tags are no longer incorrectly equal
  3. Versioning.java: Added tag validation against TAG_REGEX in Sha256Versioning.isValid(), so
    malformed tags are properly rejected by assertValid()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: DockerImageName fails to check compatibility if digest is present

2 participants