From 9e666af1ab26d9455fd9cfd5016936cd8cc842d2 Mon Sep 17 00:00:00 2001 From: Paul Berruti Date: Sat, 22 Nov 2025 16:58:54 -0800 Subject: [PATCH] Fix parse_repository_tag to handle images with both tag and digest The parse_repository_tag() function was incorrectly parsing Docker image references that contained both a tag and a digest (e.g., nginx:1.0@sha256:abc). Previously, when splitting by '@' first, the tag would be included in the repository name, resulting in incorrect parsing: - Input: "nginx:1.0@sha256:abc123" - Old output: ("nginx:1.0", "sha256:abc123") - Expected: ("nginx", "1.0@sha256:abc123") The fix now: 1. Checks for digest (@) separator first 2. Examines the part before the digest for a tag (:) separator 3. Combines tag and digest as "tag@digest" when both are present Added test cases: - test_index_image_tag_and_sha - test_index_user_image_tag_and_sha - test_private_reg_image_tag_and_sha --- plugins/module_utils/_api/utils/utils.py | 16 +++++++++++++++- .../module_utils/_api/utils/test_utils.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/_api/utils/utils.py b/plugins/module_utils/_api/utils/utils.py index 8e4cb47e..30a2bf5a 100644 --- a/plugins/module_utils/_api/utils/utils.py +++ b/plugins/module_utils/_api/utils/utils.py @@ -240,9 +240,23 @@ def convert_service_networks( def parse_repository_tag(repo_name: str) -> tuple[str, str | None]: + # Check for digest (@ separator) first parts = repo_name.rsplit("@", 1) if len(parts) == 2: - return tuple(parts) # type: ignore + # We have a digest, but there might also be a tag before it + repo_and_tag = parts[0] + digest = parts[1] + + # Check if there's a tag in the part before the digest + tag_parts = repo_and_tag.rsplit(":", 1) + if len(tag_parts) == 2 and "/" not in tag_parts[1]: + # We have both tag and digest: return repo and "tag@digest" + return tag_parts[0], f"{tag_parts[1]}@{digest}" + else: + # Only digest, no tag: return repo and digest + return repo_and_tag, digest + + # No digest, check for tag only parts = repo_name.rsplit(":", 1) if len(parts) == 2 and "/" not in parts[1]: return tuple(parts) # type: ignore diff --git a/tests/unit/plugins/module_utils/_api/utils/test_utils.py b/tests/unit/plugins/module_utils/_api/utils/test_utils.py index 5c91196b..5070b7b0 100644 --- a/tests/unit/plugins/module_utils/_api/utils/test_utils.py +++ b/tests/unit/plugins/module_utils/_api/utils/test_utils.py @@ -359,6 +359,24 @@ class ParseRepositoryTagTest(unittest.TestCase): f"sha256:{self.sha}", ) + def test_index_image_tag_and_sha(self) -> None: + assert parse_repository_tag(f"root:tag@sha256:{self.sha}") == ( + "root", + f"tag@sha256:{self.sha}", + ) + + def test_index_user_image_tag_and_sha(self) -> None: + assert parse_repository_tag(f"user/repo:tag@sha256:{self.sha}") == ( + "user/repo", + f"tag@sha256:{self.sha}", + ) + + def test_private_reg_image_tag_and_sha(self) -> None: + assert parse_repository_tag(f"url:5000/repo:tag@sha256:{self.sha}") == ( + "url:5000/repo", + f"tag@sha256:{self.sha}", + ) + class ParseDeviceTest(unittest.TestCase): def test_dict(self) -> None: