diff --git a/plugins/module_utils/podman/podman_image_lib.py b/plugins/module_utils/podman/podman_image_lib.py index 1e07925..d149ca8 100644 --- a/plugins/module_utils/podman/podman_image_lib.py +++ b/plugins/module_utils/podman/podman_image_lib.py @@ -569,11 +569,27 @@ class PodmanImageManager: """Ensure image is present (pull or build if needed).""" image = self.find_image() + # Get digest before any operations + digest_before = None + if image: + inspect_data = self.inspector.inspect_image(self.repository.full_name) + if inspect_data and len(inspect_data) > 0: + digest_before = inspect_data[0].get("Digest") or inspect_data[0].get("digest") + # If Digest is not available, try to get from RepoDigests + if not digest_before: + repo_digests = inspect_data[0].get("RepoDigests", []) + if repo_digests: + # Extract digest from first RepoDigest (format: repo@sha256:...) + for repo_digest in repo_digests: + if "@" in repo_digest: + digest_before = repo_digest.split("@", 1)[1] + break + if not image or self._should_rebuild_image(image): if self.params.get("state") == "build" or self.params.get("path"): self._build_image() else: - self._pull_image() + self._pull_image(digest_before) if self.params.get("push"): self._push_image() @@ -611,7 +627,7 @@ class PodmanImageManager: self.results["changed"] = True self.results["actions"].append(f"Built image {self.repository.full_name} from {path or 'context'}") - def _pull_image(self): + def _pull_image(self, digest_before=None): """Pull an image.""" if not self.params.get("pull", True): self.module.fail_json(msg=f"Image {self.repository.full_name} not found locally and pull is disabled") @@ -623,8 +639,27 @@ class PodmanImageManager: self.results["image"] = self.inspector.inspect_image(self.repository.full_name) self.results["podman_actions"].append(podman_command) - self.results["changed"] = True - self.results["actions"].append(f"Pulled image {self.repository.full_name}") + # Check if digest actually changed + digest_after = None + if self.results["image"] and len(self.results["image"]) > 0: + digest_after = self.results["image"][0].get("Digest") or self.results["image"][0].get("digest") + # If Digest is not available, try to get from RepoDigests + if not digest_after: + repo_digests = self.results["image"][0].get("RepoDigests", []) + if repo_digests: + # Extract digest from first RepoDigest (format: repo@sha256:...) + for repo_digest in repo_digests: + if "@" in repo_digest: + digest_after = repo_digest.split("@", 1)[1] + break + + changed = digest_before != digest_after + self.results["changed"] = changed + if changed: + self.results["actions"].append(f"Pulled image {self.repository.full_name}") + else: + self.results["changed"] = True + self.results["actions"].append(f"Pulled image {self.repository.full_name}") def _push_image(self): """Push an image.""" diff --git a/tests/integration/targets/podman_image/tasks/main.yml b/tests/integration/targets/podman_image/tasks/main.yml index e142492..80668cb 100644 --- a/tests/integration/targets/podman_image/tasks/main.yml +++ b/tests/integration/targets/podman_image/tasks/main.yml @@ -589,6 +589,7 @@ - include_tasks: additional_tests.yml - include_tasks: test_issue_947.yml + - include_tasks: test_issue_981.yml always: - name: Cleanup images diff --git a/tests/integration/targets/podman_image/tasks/test_issue_981.yml b/tests/integration/targets/podman_image/tasks/test_issue_981.yml new file mode 100644 index 0000000..faa70ae --- /dev/null +++ b/tests/integration/targets/podman_image/tasks/test_issue_981.yml @@ -0,0 +1,54 @@ +--- +# Test for issue #981: podman_image with force=true should be idempotent +# https://github.com/containers/ansible-podman-collections/issues/981 + +- name: Test issue # 981 - Remove alpine image if exists + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: docker.io/library/alpine + tag: latest + state: absent + ignore_errors: true + +- name: Test issue # 981 - Pull alpine image first time + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: docker.io/library/alpine + tag: latest + register: issue_981_pull1 + +- name: Test issue # 981 - Pull alpine with force=true (same digest, should not change) + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: docker.io/library/alpine + tag: latest + force: true + register: issue_981_pull2 + +- name: Test issue # 981 - Pull alpine with force=true again (same digest, should not change) + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: docker.io/library/alpine + tag: latest + force: true + register: issue_981_pull3 + +- name: Test issue # 981 - Verify force=true idempotency + assert: + that: + - issue_981_pull1 is changed + - issue_981_pull1.actions | length > 0 + - "'Pulled image' in issue_981_pull1.actions[0]" + - issue_981_pull2 is not changed + - issue_981_pull2.actions | length == 0 + - issue_981_pull3 is not changed + - issue_981_pull3.actions | length == 0 + fail_msg: "Issue #981 not fixed: force=true is not idempotent when digest hasn't changed" + success_msg: "Issue #981 fixed: force=true is idempotent when digest hasn't changed" + +- name: Test issue # 981 - Cleanup + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: docker.io/library/alpine + tag: latest + state: absent