diff --git a/.github/workflows/podman_export.yml b/.github/workflows/podman_export.yml new file mode 100644 index 0000000..18c52e9 --- /dev/null +++ b/.github/workflows/podman_export.yml @@ -0,0 +1,108 @@ +name: Podman export + +on: + push: + paths: + - '.github/workflows/podman_export.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_export.yml' + - 'plugins/modules/podman_export.py' + - 'tests/integration/targets/podman_export/**' + branches: + - master + pull_request: + paths: + - '.github/workflows/podman_export.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_export.yml' + - 'plugins/modules/podman_export.py' + - 'tests/integration/targets/podman_export/**' + schedule: + - cron: 4 0 * * * # Run daily at 0:03 UTC + +jobs: + + test_podman_export: + name: Podman export ${{ 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 export + 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_export ./ci/run_containers_tests.sh + shell: bash diff --git a/.github/workflows/podman_import.yml b/.github/workflows/podman_import.yml new file mode 100644 index 0000000..33d1e61 --- /dev/null +++ b/.github/workflows/podman_import.yml @@ -0,0 +1,108 @@ +name: Podman import + +on: + push: + paths: + - '.github/workflows/podman_import.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_import.yml' + - 'plugins/modules/podman_import.py' + - 'tests/integration/targets/podman_import/**' + branches: + - master + pull_request: + paths: + - '.github/workflows/podman_import.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_import.yml' + - 'plugins/modules/podman_import.py' + - 'tests/integration/targets/podman_import/**' + schedule: + - cron: 4 0 * * * # Run daily at 0:03 UTC + +jobs: + + test_podman_import: + name: Podman import ${{ 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 import + 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_import ./ci/run_containers_tests.sh + shell: bash diff --git a/ci/playbooks/containers/podman_export.yml b/ci/playbooks/containers/podman_export.yml new file mode 100644 index 0000000..b3d5d47 --- /dev/null +++ b/ci/playbooks/containers/podman_export.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + gather_facts: true + tasks: + - include_role: + name: podman_export + vars: + ansible_python_interpreter: "{{ _ansible_python_interpreter }}" diff --git a/ci/playbooks/containers/podman_import.yml b/ci/playbooks/containers/podman_import.yml new file mode 100644 index 0000000..2d6fac1 --- /dev/null +++ b/ci/playbooks/containers/podman_import.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + gather_facts: true + tasks: + - include_role: + name: podman_import + vars: + ansible_python_interpreter: "{{ _ansible_python_interpreter }}" diff --git a/plugins/module_utils/podman/common.py b/plugins/module_utils/podman/common.py index e0de789..379151e 100644 --- a/plugins/module_utils/podman/common.py +++ b/plugins/module_utils/podman/common.py @@ -4,6 +4,9 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +import os +import shutil + def run_podman_command(module, executable='podman', args=None, expected_rc=0, ignore_errors=False): if not isinstance(executable, list): @@ -25,3 +28,12 @@ def lower_keys(x): return dict((k.lower(), lower_keys(v)) for k, v in x.items()) else: return x + + +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) diff --git a/plugins/modules/podman_export.py b/plugins/modules/podman_export.py new file mode 100644 index 0000000..e2bb196 --- /dev/null +++ b/plugins/modules/podman_export.py @@ -0,0 +1,106 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2021, 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_export +short_description: Export a podman container +author: Sagi Shnaidman (@sshnaidm) +description: + - podman export exports the filesystem of a container and saves it as a + tarball on the local machine +options: + dest: + description: + - Path to export container to. + type: str + required: true + container: + description: + - Container to export. + type: str + required: true + force: + description: + - Force saving to file even if it exists. + type: bool + default: True + 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_export: + dest: /path/to/tar/file + container: container-name +''' + +import os # noqa: E402 +from ansible.module_utils.basic import AnsibleModule # noqa: E402 +from ..module_utils.podman.common import remove_file_or_dir # noqa: E402 + + +def export(module, executable): + changed = False + command = [executable, 'export'] + command += ['-o=%s' % module.params['dest'], module.params['container']] + 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)) + else: + changed = not os.path.exists(module.params['dest']) + if module.check_mode: + return changed, '', '' + rc, out, err = module.run_command(command) + if rc != 0: + module.fail_json(msg="Error exporting container %s: %s" % ( + module.params['container'], err)) + return changed, out, err + + +def main(): + module = AnsibleModule( + argument_spec=dict( + dest=dict(type='str', required=True), + container=dict(type='str', required=True), + force=dict(type='bool', default=True), + executable=dict(type='str', default='podman') + ), + supports_check_mode=True, + ) + + executable = module.get_bin_path(module.params['executable'], required=True) + changed, out, err = export(module, executable) + + results = { + "changed": changed, + "stdout": out, + "stderr": err, + } + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_import.py b/plugins/modules/podman_import.py new file mode 100644 index 0000000..5090b17 --- /dev/null +++ b/plugins/modules/podman_import.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2021, 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_import +short_description: Import Podman container from a tar file. +author: Sagi Shnaidman (@sshnaidm) +description: + - podman import imports a tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) + and saves it as a filesystem image. +options: + src: + description: + - Path to image file to load. + type: str + required: true + commit_message: + description: + - Set commit message for imported image + type: str + change: + description: + - Set changes as list of key-value pairs, see example. + type: list + elements: dict + 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 = ''' +image: + description: info from loaded image + returned: always + type: dict + sample: { + "Id": "cbc6d73c4d232db6e8441df96af81855f62c74157b5db80a1d5...", + "Digest": "sha256:8730c75be86a718929a658db4663d487e562d66762....", + "RepoTags": [], + "RepoDigests": [], + "Parent": "", + "Comment": "imported from tarball", + "Created": "2021-09-07T04:45:38.749977105+03:00", + "Config": {}, + "Version": "", + "Author": "", + "Architecture": "amd64", + "Os": "linux", + "Size": 5882449, + "VirtualSize": 5882449, + "GraphDriver": { + "Name": "overlay", + "Data": { + "UpperDir": "/home/...34/diff", + "WorkDir": "/home/.../work" + } + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:...." + ] + }, + "Labels": null, + "Annotations": {}, + "ManifestType": "application/vnd.oci.image.manifest.v1+json", + "User": "", + "History": [ + { + "created": "2021-09-07T04:45:38.749977105+03:00", + "created_by": "/bin/sh -c #(nop) ADD file:091... in /", + "comment": "imported from tarball" + } + ], + "NamesHistory": null + } +''' + +EXAMPLES = ''' +# What modules does for example +- containers.podman.podman_import: + src: /path/to/tar/file + change: + - "CMD": /bin/bash + - "User": root + commit_message: "Importing image" +''' + +import json # noqa: E402 +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + + +def load(module, executable): + changed = False + command = [executable, 'import'] + if module.params['commit_message']: + command.extend(['--message', module.params['commit_message']]) + if module.params['change']: + for change in module.params['change']: + command += ['--change', "=".join(list(change.items())[0])] + command += [module.params['src']] + changed = True + if module.check_mode: + return changed, '', '', '', command + rc, out, err = module.run_command(command) + if rc != 0: + module.fail_json(msg="Image loading failed: %s" % (err)) + image_name_line = [i for i in out.splitlines() if 'sha256' in i][0] + image_name = image_name_line.split(":", maxsplit=1)[1].strip() + rc, out2, err2 = module.run_command([executable, 'image', 'inspect', image_name]) + if rc != 0: + module.fail_json(msg="Image %s inspection failed: %s" % (image_name, err2)) + try: + info = json.loads(out2)[0] + except Exception as e: + module.fail_json(msg="Could not parse JSON from image %s: %s" % (image_name, e)) + return changed, out, err, info, command + + +def main(): + module = AnsibleModule( + argument_spec=dict( + src=dict(type='str', required=True), + commit_message=dict(type='str'), + change=dict(type='list', elements='dict'), + executable=dict(type='str', default='podman') + ), + supports_check_mode=True, + ) + + executable = module.get_bin_path(module.params['executable'], required=True) + changed, out, err, image_info, command = load(module, executable) + + results = { + "changed": changed, + "stdout": out, + "stderr": err, + "image": image_info, + "podman_command": " ".join(command) + } + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_save.py b/plugins/modules/podman_save.py index 54354d7..b8011b2 100644 --- a/plugins/modules/podman_save.py +++ b/plugins/modules/podman_save.py @@ -51,7 +51,7 @@ options: description: - Force saving to file even if it exists. type: bool - default: False + default: True executable: description: - Path to C(podman) executable if it is not in the C($PATH) on the @@ -74,17 +74,8 @@ EXAMPLES = ''' ''' 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) +from ..module_utils.podman.common import remove_file_or_dir # noqa: E402 def save(module, executable): @@ -110,7 +101,8 @@ def save(module, executable): remove_file_or_dir(dest) except Exception as e: module.fail_json(msg="Error deleting %s path: %s" % (dest, e)) - changed = True + else: + changed = not os.path.exists(module.params['dest']) if module.check_mode: return changed, '', '' rc, out, err = module.run_command(command) @@ -127,7 +119,7 @@ def main(): 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), + force=dict(type='bool', default=True), executable=dict(type='str', default='podman') ), supports_check_mode=True, diff --git a/tests/integration/targets/podman_export/tasks/main.yml b/tests/integration/targets/podman_export/tasks/main.yml new file mode 100644 index 0000000..7dc8791 --- /dev/null +++ b/tests/integration/targets/podman_export/tasks/main.yml @@ -0,0 +1,65 @@ +--- +- name: Test podman export + block: + - name: Start container + containers.podman.podman_container: + name: container + image: alpine:3.7 + state: started + command: sleep 1d + + - name: Export container + containers.podman.podman_export: + container: container + dest: /tmp/container + + - name: Check file + stat: + path: /tmp/container + register: img + + - name: Check it's exported + assert: + that: + - img.stat.exists + + - name: Import container + containers.podman.podman_import: + src: /tmp/container + register: image + + - name: Check it's imported + assert: + that: + - image is success + + - name: Export container without force + containers.podman.podman_export: + container: container + dest: /tmp/container + force: false + register: image2 + + - name: Check it's exported + assert: + that: + - image2 is success + - image2 is not changed + + - name: Export container with force + containers.podman.podman_export: + container: container + dest: /tmp/container + force: true + register: image3 + + - name: Check it's not exported + assert: + that: + - image3 is changed + + always: + - name: Remove container + containers.podman.podman_container: + name: container + state: absent diff --git a/tests/integration/targets/podman_import/tasks/main.yml b/tests/integration/targets/podman_import/tasks/main.yml new file mode 100644 index 0000000..e890826 --- /dev/null +++ b/tests/integration/targets/podman_import/tasks/main.yml @@ -0,0 +1,66 @@ +--- +- name: Test podman import + block: + - name: Start container + containers.podman.podman_container: + name: container + image: alpine:3.7 + state: started + command: sleep 1d + + - name: Export container + containers.podman.podman_export: + container: container + dest: /tmp/container + + - name: Check file + stat: + path: /tmp/container + register: img + + - name: Check it's saved + assert: + that: + - img.stat.exists + + - name: Import container + containers.podman.podman_import: + src: /tmp/container + register: test + + - name: Check it's imported + assert: + that: + - test is success + - test.image["Id"] != '' + + - name: Import container with commit message + containers.podman.podman_import: + src: /tmp/container + commit_message: 'Test in CI' + register: test1 + + - name: Check it's imported with commit message + assert: + that: + - test1.image.Comment == "Test in CI" + + - name: Import container with changes + containers.podman.podman_import: + src: /tmp/container + change: + - "User": "someuser" + - "CMD": "/bin/nonsh" + register: test2 + + - name: Check it's imported with changes + assert: + that: + - test2.image.User == 'someuser' + - test2.image["Config"]["Cmd"][2] == "/bin/nonsh" + + always: + - name: Remove container + containers.podman.podman_container: + name: container + state: absent