From ee52d9de7878a06bb8a0222c5a618d54b5359bec Mon Sep 17 00:00:00 2001 From: Sergey <6213510+sshnaidm@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:30:35 +0300 Subject: [PATCH] Add podman image scp option (#970) * Add podman image scp option Fix #536 --------- Signed-off-by: Sagi Shnaidman --- .github/workflows/podman_image.yml | 1 + .github/workflows/reusable-module-test.yml | 11 ++ .../module_utils/podman/podman_image_lib.py | 38 +++++ plugins/modules/podman_image.py | 18 +- .../podman_image/tasks/additional_tests.yml | 4 + .../targets/podman_image/tasks/scp.yml | 155 ++++++++++++++++++ 6 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 tests/integration/targets/podman_image/tasks/scp.yml diff --git a/.github/workflows/podman_image.yml b/.github/workflows/podman_image.yml index a21a03a..07e39d3 100644 --- a/.github/workflows/podman_image.yml +++ b/.github/workflows/podman_image.yml @@ -36,3 +36,4 @@ jobs: with: module_name: 'podman_image' display_name: 'Podman image' + extra_collections: 'ansible.posix' diff --git a/.github/workflows/reusable-module-test.yml b/.github/workflows/reusable-module-test.yml index 3deec99..dd8de10 100644 --- a/.github/workflows/reusable-module-test.yml +++ b/.github/workflows/reusable-module-test.yml @@ -27,6 +27,11 @@ on: required: false type: string default: '["git+https://github.com/ansible/ansible.git@stable-2.18", "git+https://github.com/ansible/ansible.git@devel"]' + extra_collections: + description: 'Space-separated list of extra Ansible collections to install before running tests (e.g., "ansible.posix community.general")' + required: false + type: string + default: '' jobs: test_module: @@ -78,6 +83,12 @@ jobs: ~/.local/bin/ansible-galaxy collection build --output-path /tmp/just_new_collection --force ~/.local/bin/ansible-galaxy collection install -vvv --force /tmp/just_new_collection/*.tar.gz + - name: Install extra Ansible collections (optional) + if: ${{ inputs.extra_collections != '' }} + run: | + echo "Installing extra collections: ${{ inputs.extra_collections }}" + ~/.local/bin/ansible-galaxy collection install -vvv --force ${{ inputs.extra_collections }} + - name: Run collection tests for ${{ inputs.module_name }} run: | export PATH=~/.local/bin:$PATH diff --git a/plugins/module_utils/podman/podman_image_lib.py b/plugins/module_utils/podman/podman_image_lib.py index ddb9db9..1e07925 100644 --- a/plugins/module_utils/podman/podman_image_lib.py +++ b/plugins/module_utils/podman/podman_image_lib.py @@ -318,6 +318,44 @@ class PodmanImagePusher: def push_image(self, image_name, push_config): """Push an image to a registry.""" + transport = (push_config or {}).get("transport") + + # Special handling for scp transport which uses 'podman image scp' + if transport == "scp": + args = [] + + # Allow passing global --ssh options to podman + ssh_opts = push_config.get("ssh") if push_config else None + if ssh_opts: + args.extend(["--ssh", ssh_opts]) + + args.extend(["image", "scp"]) + + # Extra args (e.g., --quiet) if provided + if push_config.get("extra_args"): + args.extend(shlex.split(push_config["extra_args"])) + + # Source image (local) + args.append(image_name) + + # Destination host spec + dest = push_config.get("dest") + if not dest: + self.module.fail_json(msg="When using transport 'scp', push_args.dest must be provided") + + # If user did not include '::' in dest, append it to copy into remote storage with same name + dest_spec = dest if "::" in dest else f"{dest}::" + args.append(dest_spec) + + action = " ".join(args) + rc, out, err = run_podman_command(self.module, self.executable, args, ignore_errors=True) + if rc != 0: + self.module.fail_json( + msg=f"Failed to scp image {image_name} to {dest}", stdout=out, stderr=err, actions=[action] + ) + return out + err, action + + # Default push behavior for all other transports args = ["push"] self._add_auth_args(args) diff --git a/plugins/modules/podman_image.py b/plugins/modules/podman_image.py index df2b1da..e7dfe4d 100644 --- a/plugins/modules/podman_image.py +++ b/plugins/modules/podman_image.py @@ -154,6 +154,10 @@ DOCUMENTATION = r""" type: dict default: {} suboptions: + ssh: + description: + - SSH options to use when pushing images with SCP transport. + type: str compress: description: - Compress tarball image layers when pushing to a directory using the 'dir' transport. @@ -184,11 +188,12 @@ DOCUMENTATION = r""" type: str choices: - dir - - docker - docker-archive - docker-daemon - oci-archive - ostree + - docker + - scp extra_args: description: - Extra args to pass to push, if executed. Does not idempotently check for new push args. @@ -329,6 +334,15 @@ EXAMPLES = r""" tag: 3 dest: docker.io/acme +- name: Push image to a remote host via scp transport + containers.podman.podman_image: + name: testimage + pull: false + push: true + push_args: + dest: user@server + transport: scp + - name: Pull an image for a specific CPU architecture containers.podman.podman_image: name: nginx @@ -484,6 +498,7 @@ def main(): type="dict", default={}, options=dict( + ssh=dict(type="str"), compress=dict(type="bool"), format=dict(type="str", choices=["oci", "v2s1", "v2s2"]), remove_signatures=dict(type="bool"), @@ -502,6 +517,7 @@ def main(): "oci-archive", "ostree", "docker", + "scp", ], ), ), diff --git a/tests/integration/targets/podman_image/tasks/additional_tests.yml b/tests/integration/targets/podman_image/tasks/additional_tests.yml index 642afa0..15ec408 100644 --- a/tests/integration/targets/podman_image/tasks/additional_tests.yml +++ b/tests/integration/targets/podman_image/tasks/additional_tests.yml @@ -186,6 +186,10 @@ - extra_args_build is changed - "'Built image test-extra-args:latest from' in extra_args_build.actions[0]" + # SCP transport tests + - name: Include scp transport tests + include_tasks: scp.yml + # Test push functionality with different transports - name: Test push to directory transport containers.podman.podman_image: diff --git a/tests/integration/targets/podman_image/tasks/scp.yml b/tests/integration/targets/podman_image/tasks/scp.yml new file mode 100644 index 0000000..da855d7 --- /dev/null +++ b/tests/integration/targets/podman_image/tasks/scp.yml @@ -0,0 +1,155 @@ +- name: Validate scp transport behavior in podman_image + block: + - name: Fail when scp transport is used without destination + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: testimage + pull: false + push: true + push_args: + transport: scp + register: scp_missing_dest + ignore_errors: true + + - name: Ensure scp without dest fails with clear message + assert: + that: + - scp_missing_dest is failed + - "'push_args.dest must be provided' in scp_missing_dest.msg" + + +- name: Build a local image to test scp transport idempotence + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: testimage_scp + path: /var/tmp/build + register: built_local + +- name: Try to scp push to a fake remote (should fail on CI env without remote) + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: testimage_scp + pull: false + push: true + push_args: + dest: user@server + transport: scp + register: scp_push_fake + ignore_errors: true + +- name: Ensure scp push to fake remote fails but reports action + assert: + that: + - built_local is changed + - scp_push_fake is failed + - scp_push_fake.actions is defined + +- name: Prepare SSH access to localhost for scp tests + block: + + - name: Ensure SSH keys exist + ansible.builtin.shell: >- + ssh-keygen -b 2048 -t rsa -f {{ lookup('env','HOME') }}/.ssh/id_rsa -N "" || true + args: + creates: "{{ lookup('env','HOME') }}/.ssh/id_rsa" + + - name: Get public key for user + ansible.builtin.command: >- + cat {{ lookup('env','HOME') }}/.ssh/id_rsa.pub + register: public_key + + - name: Authorize our public key for localhost for user + ansible.posix.authorized_key: + user: "{{ lookup('env','USER') }}" + state: present + key: "{{ public_key.stdout }}" + + - name: Authorize our public key for localhost for root user + become: true + ansible.posix.authorized_key: + user: root + state: present + key: "{{ public_key.stdout }}" + + - name: Start SSH service (Ubuntu uses 'ssh') + ansible.builtin.systemd_service: + name: ssh + state: started + become: true + ignore_errors: true + + - name: Start SSH service (fallback to 'sshd') + ansible.builtin.systemd_service: + name: sshd + state: started + become: true + ignore_errors: true + + - name: Verify we can SSH to localhost non-interactively + ansible.builtin.command: >- + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {{ lookup('env','USER') }}@localhost true + +- name: Build a local image for scp to localhost + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: testimage_scp_local + path: /var/tmp/build + register: built_localhost + +- name: Add system connection for Podman < 5 + ansible.builtin.command: podman system connection add local --identity {{ lookup('env','HOME') }}/.ssh/id_rsa {{ lookup('env','USER') }}@127.0.0.1 + +- name: Add system connection for root user for Podman < 5 + ansible.builtin.command: podman system connection add rootlocal --identity {{ lookup('env','HOME') }}/.ssh/id_rsa root@127.0.0.1 + +- name: Push image to localhost via scp transport + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: testimage_scp_local + pull: false + push: true + push_args: + dest: "local::newimage" + transport: scp + register: scp_localhost_push + +- name: Validate scp localhost push executed + assert: + that: + - built_localhost is changed + - scp_localhost_push is changed + - scp_localhost_push.actions is defined + - scp_localhost_push.podman_actions is defined + - scp_localhost_push.actions | select('search', 'image scp') | list | length > 0 + +- name: Push image to localhost via scp transport root user + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: testimage_scp_local + pull: false + push: true + push_args: + dest: "rootlocal" + transport: scp + register: scp_localhost_push + +- name: Validate scp localhost push executed + assert: + that: + - built_localhost is changed + - scp_localhost_push is changed + - scp_localhost_push.actions is defined + - scp_localhost_push.podman_actions is defined + - scp_localhost_push.actions | select('search', 'image scp') | list | length > 0 + +- name: Ensure image is available for root user + become: true + ansible.builtin.command: >- + podman images --format '{{ '{{.Repository}}:{{.Tag}}' }}' testimage_scp_local + register: scp_localhost_root_check + +- name: Validate image is available for root user + assert: + that: + - "'testimage_scp_local:latest' in scp_localhost_root_check.stdout" + - scp_localhost_root_check.stderr == ''