diff --git a/.github/workflows/podman_save.yml b/.github/workflows/podman_save.yml new file mode 100644 index 0000000..c5df0c7 --- /dev/null +++ b/.github/workflows/podman_save.yml @@ -0,0 +1,108 @@ +name: Podman save + +on: + push: + paths: + - '.github/workflows/podman_save.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_save.yml' + - 'plugins/modules/podman_save.py' + - 'tests/integration/targets/podman_save/**' + branches: + - master + pull_request: + paths: + - '.github/workflows/podman_save.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_save.yml' + - 'plugins/modules/podman_save.py' + - 'tests/integration/targets/podman_save/**' + schedule: + - cron: 4 0 * * * # Run daily at 0:03 UTC + +jobs: + + test_podman_save: + name: Podman save ${{ matrix.ansible-version }}-${{ matrix.os || 'ubuntu-latest' }} + runs-on: ${{ matrix.os || 'ubuntu-latest' }} + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + ansible-version: + - ansible<2.10 + # - git+https://github.com/ansible/ansible.git@stable-2.11 + - git+https://github.com/ansible/ansible.git@devel + os: + - ubuntu-20.04 + python-version: + - 3.7 + + steps: + + - name: Check out repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip and display Python and PIP versions + run: | + sudo apt-get update + sudo apt-get install -y python*-wheel python*-yaml + python -m pip install --upgrade pip + python -V + pip --version + + - name: Set up pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.ref }}-units-VMs + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install Ansible ${{ matrix.ansible-version }} + run: python3 -m pip install --user --force-reinstall --upgrade '${{ matrix.ansible-version }}' + + - name: Build and install the collection tarball + run: | + rm -rf /tmp/just_new_collection + ~/.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: Run collection tests for podman save + run: | + export PATH=~/.local/bin:$PATH + + echo "Run ansible version" + command -v ansible + ansible --version + + if [[ '${{ matrix.ansible-version }}' == 'git+https://github.com/ansible/ansible.git@devel' ]]; then + export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg + elif [[ '${{ matrix.ansible-version }}' == 'ansible<2.10' ]]; then + export ANSIBLE_CONFIG=$(pwd)/ci/ansible-2.9.cfg + fi + + echo $ANSIBLE_CONFIG + command -v ansible-playbook + pip --version + python --version + ansible-playbook --version + + ansible-playbook -vv ci/playbooks/pre.yml \ + -e host=localhost \ + -i localhost, \ + -e ansible_connection=local \ + -e setup_python=false + + TEST2RUN=podman_save ./ci/run_containers_tests.sh + shell: bash diff --git a/ci/playbooks/containers/podman_save.yml b/ci/playbooks/containers/podman_save.yml new file mode 100644 index 0000000..8bf25a4 --- /dev/null +++ b/ci/playbooks/containers/podman_save.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + gather_facts: true + tasks: + - include_role: + name: podman_save + vars: + ansible_python_interpreter: "{{ _ansible_python_interpreter }}" diff --git a/plugins/modules/podman_save.py b/plugins/modules/podman_save.py new file mode 100644 index 0000000..54354d7 --- /dev/null +++ b/plugins/modules/podman_save.py @@ -0,0 +1,151 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, Sagi Shnaidman +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +module: podman_save +short_description: Saves podman image to tar file +author: Sagi Shnaidman (@sshnaidm) +description: + - podman save saves an image to either docker-archive, oci-archive, oci-dir + (directory with oci manifest type), or docker-dir (directory with v2s2 manifest type) + on the local machine, default is docker-archive. + +options: + image: + description: + - Image to save. + type: str + required: true + compress: + description: + - Compress tarball image layers when pushing to a directory using the 'dir' transport. + (default is same compression type, compressed or uncompressed, as source) + type: bool + dest: + description: + - Destination file to write image to. + type: str + required: true + format: + description: + - Save image to docker-archive, oci-archive (see containers-transports(5)), oci-dir + (oci transport), or docker-dir (dir transport with v2s2 manifest type). + type: str + choices: + - docker-archive + - oci-archive + - oci-dir + - docker-dir + multi_image_archive: + description: + - Allow for creating archives with more than one image. Additional names will be + interpreted as images instead of tags. Only supported for docker-archive. + type: bool + force: + description: + - Force saving to file even if it exists. + type: bool + default: False + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the + machine running C(podman) + default: 'podman' + type: str +requirements: + - "Podman installed on host" +''' + +RETURN = ''' +''' + +EXAMPLES = ''' +# What modules does for example +- containers.podman.podman_save: + dest: /path/to/tar/file + compress: true + format: oci-dir +''' + +import os # noqa: E402 +import shutil # noqa: E402 +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + + +def remove_file_or_dir(path): + if os.path.isfile(path): + os.unlink(path) + elif os.path.isdir(path): + shutil.rmtree(path) + else: + raise ValueError("file %s is not a file or dir." % path) + + +def save(module, executable): + changed = False + command = [executable, 'save'] + cmd_args = { + 'compress': ['--compress'], + 'dest': ['-o=%s' % module.params['dest']], + 'format': ['--format=%s' % module.params['format']], + 'multi_image_archive': ['--multi-image-archive'], + } + for param in module.params: + if module.params[param] is not None and param in cmd_args: + command += cmd_args[param] + command.append(module.params['image']) + if module.params['force']: + dest = module.params['dest'] + if os.path.exists(dest): + changed = True + if module.check_mode: + return changed, '', '' + try: + remove_file_or_dir(dest) + except Exception as e: + module.fail_json(msg="Error deleting %s path: %s" % (dest, e)) + changed = True + if module.check_mode: + return changed, '', '' + rc, out, err = module.run_command(command) + if rc != 0: + module.fail_json(msg="Error: %s" % (err)) + return changed, out, err + + +def main(): + module = AnsibleModule( + argument_spec=dict( + image=dict(type='str', required=True), + compress=dict(type='bool'), + dest=dict(type='str', required=True), + format=dict(type='str', choices=['docker-archive', 'oci-archive', 'oci-dir', 'docker-dir']), + multi_image_archive=dict(type='bool'), + force=dict(type='bool', default=False), + executable=dict(type='str', default='podman') + ), + supports_check_mode=True, + ) + if module.params['compress'] and module.params['format'] not in ['oci-dir', 'docker-dir']: + module.fail_json(msg="Compression is only supported for oci-dir and docker-dir format") + + executable = module.get_bin_path(module.params['executable'], required=True) + changed, out, err = save(module, executable) + + results = { + "changed": changed, + "stdout": out, + "stderr": err, + } + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/podman_save/tasks/main.yml b/tests/integration/targets/podman_save/tasks/main.yml new file mode 100644 index 0000000..cb4d314 --- /dev/null +++ b/tests/integration/targets/podman_save/tasks/main.yml @@ -0,0 +1,85 @@ +--- +- name: Pull image + containers.podman.podman_image: + name: k8s.gcr.io/pause + +- name: Save image + containers.podman.podman_save: + image: k8s.gcr.io/pause + dest: /tmp/image.tar + +- name: Check file + stat: + path: /tmp/image.tar + register: img + +- name: Check it's saved + assert: + that: + - img.stat.exists + +- name: Save image + containers.podman.podman_save: + image: k8s.gcr.io/pause + dest: /tmp/image.tar + force: true + +- name: Check file + stat: + path: /tmp/image.tar + register: img + +- name: Check it's saved + assert: + that: + - img.stat.exists + +- name: Save image + containers.podman.podman_save: + image: k8s.gcr.io/pause + dest: /tmp/imagedir + format: oci-dir + +- name: Check file + stat: + path: /tmp/imagedir + register: img + +- name: Check it's saved + assert: + that: + - img.stat.exists + +- name: Save image + containers.podman.podman_save: + image: k8s.gcr.io/pause + dest: /tmp/imagedir + force: true + format: oci-dir + compress: true + +- name: Check file + stat: + path: /tmp/imagedir + register: img + +- name: Check it's saved + assert: + that: + - img.stat.exists + +- name: Save image + containers.podman.podman_save: + image: k8s.gcr.io/pause + dest: /tmp/image2.tar + multi_image_archive: true + +- name: Check file + stat: + path: /tmp/image2.tar + register: img + +- name: Check it's saved + assert: + that: + - img.stat.exists