diff --git a/.github/workflows/podman_prune.yml b/.github/workflows/podman_prune.yml new file mode 100644 index 00000000..8797d844 --- /dev/null +++ b/.github/workflows/podman_prune.yml @@ -0,0 +1,107 @@ +name: Podman prune + +on: + push: + paths: + - '.github/workflows/podman_prune.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_prune.yml' + - 'plugins/modules/podman_prune.py' + - 'tests/integration/targets/podman_prune/**' + branches: + - master + pull_request: + paths: + - '.github/workflows/podman_prune.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_prune.yml' + - 'plugins/modules/podman_prune.py' + - 'tests/integration/targets/podman_prune/**' + schedule: + - cron: 4 0 * * * # Run daily at 0:03 UTC + +jobs: + + test_podman_prune: + name: Podman prune ${{ matrix.ansible-version }}-${{ matrix.os || 'ubuntu-22.04' }} + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + 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-22.04 + python-version: + - 3.9 + + steps: + + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + 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@v3 + 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 prune + run: | + export PATH=~/.local/bin:$PATH + + echo "Run ansible version" + command -v ansible + ansible --version + + export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg + if [[ '${{ 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_prune ./ci/run_containers_tests.sh + shell: bash diff --git a/ci/playbooks/containers/podman_prune.yml b/ci/playbooks/containers/podman_prune.yml new file mode 100644 index 00000000..cffc19f8 --- /dev/null +++ b/ci/playbooks/containers/podman_prune.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + gather_facts: true + tasks: + - include_role: + name: podman_prune + vars: + ansible_python_interpreter: "/usr/bin/python" diff --git a/plugins/modules/podman_prune.py b/plugins/modules/podman_prune.py new file mode 100644 index 00000000..ee4c68a9 --- /dev/null +++ b/plugins/modules/podman_prune.py @@ -0,0 +1,252 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Copyright (c) 2023, Roberto Alfieri + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +module: podman_prune +author: + - 'Roberto Alfieri (@rebtoor)' +version_added: '1.10.0' +short_description: Allows to prune various podman objects +notes: [] +description: + - Allows to run C(podman container prune), C(podman image prune), C(podman network prune), + C(podman volume prune) and C(podman system prune) +requirements: + - 'Podman installed on host' +options: + executable: + description: + - Podman binary. + type: str + default: podman + container: + description: + - Whether to prune containers. + type: bool + default: false + container_filters: + description: + - A dictionary of filter values used for selecting containers to delete. + - 'For example, C(until: 24h).' + - See L(the podman documentation, + https://docs.podman.io/en/latest/markdown/podman-container-prune.1.html#filter-filters) + for more information on possible filters. + type: dict + image: + description: + - Whether to prune images. + type: bool + default: false + image_filters: + description: + - A dictionary of filter values used for selecting images to delete. + - 'You can also use C(dangling_only: false) to delete dangling and non-dangling images or C(external: true) + to delete images even when they are used by external containers.' + - See L(the podman documentation, + https://docs.podman.io/en/latest/markdown/podman-image-prune.1.html#filter-filters) + for more information on possible filters. + type: dict + network: + description: + - Whether to prune networks. + type: bool + default: false + network_filters: + description: + - A dictionary of filter values used for selecting networks to delete. + - See L(the podman documentation, + https://docs.podman.io/en/latest/markdown/podman-network-prune.1.html#filter) + for more information on possible filters. + type: dict + system: + description: + - Wheter to prune unused pods, containers, image, networks and volume data + type: bool + default: false + system_all: + description: + - Wheter to prune all unused images, not only dangling images. + type: bool + default: false + system_volumes: + description: + - Wheter to prune volumes currently unused by any container. + type: bool + default: false + volume: + description: + - Whether to prune volumes. + type: bool + default: false + volume_filters: + description: + - A dictionary of filter values used for selecting volumes to delete. + - See L(the podman documentation, + https://docs.podman.io/en/latest/markdown/podman-volume-prune.1.html#filter) + for more information on possible filters. + type: dict +''' + +EXAMPLES = r''' +- name: Prune containers older than 24h + containers.podman.podman_prune: + containers: true + containers_filters: + # only consider containers created more than 24 hours ago + until: 24h + +- name: Prune everything + containers.podman.podman_prune: + system: true + +- name: Prune everything (including non-dangling images) + containers.podman.podman_prune: + system: true + system_all: true + system_volumes: true +''' + +RETURN = r''' +# containers +containers: + description: + - List of IDs of deleted containers. + returned: I(containers) is C(true) + type: list + elements: str + sample: [] + +# images +images: + description: + - List of IDs of deleted images. + returned: I(images) is C(true) + type: list + elements: str + sample: [] + +# networks +networks: + description: + - List of IDs of deleted networks. + returned: I(networks) is C(true) + type: list + elements: str + sample: [] + +# volumes +volumes: + description: + - List of IDs of deleted volumes. + returned: I(volumes) is C(true) + type: list + elements: str + sample: [] + +# system +system: + description: + - List of ID of deleted containers, volumes, images, network and total reclaimed space + returned: I(system) is C(true) + type: list + elements: str + sample: [] +''' + + +from ansible.module_utils.basic import AnsibleModule + + +def filtersPrepare(target, filters): + filter_out = [] + if target == 'system': + for system_filter in filters: + filter_out.append(filters[system_filter]) + else: + for common_filter in filters: + if isinstance(filters[common_filter], dict): + dict_filters = filters[common_filter] + for single_filter in dict_filters: + filter_out.append('--filter={label}={key}={value}'.format(label=common_filter, key=single_filter, + value=dict_filters[single_filter])) + else: + if target == 'image' and (common_filter in ('dangling_only', 'external')): + if common_filter == 'dangling_only' and not filters['dangling_only']: + filter_out.append('-a') + if common_filter == 'external' and filters['external']: + filter_out.append('--external') + else: + filter_out.append('--filter={label}={value}'.format(label=common_filter, + value=filters[common_filter])) + + return filter_out + + +def podmanExec(module, target, filters, executable): + command = [executable, target, 'prune', '--force'] + if filters is not None: + command.extend(filtersPrepare(target, filters)) + rc, out, err = module.run_command(command) + changed = bool(out) + + if rc != 0: + module.fail_json( + msg='Error executing prune on {target}: {err}'.format(target=target, err=err)) + + return { + "changed": changed, + target: list(filter(None, out.split('\n'))), + "errors": err + } + + +def main(): + results = dict() + module_args = dict( + container=dict(type='bool', default=False), + container_filters=dict(type='dict'), + image=dict(type='bool', default=False), + image_filters=dict(type='dict'), + network=dict(type='bool', default=False), + network_filters=dict(type='dict'), + volume=dict(type='bool', default=False), + volume_filters=dict(type='dict'), + system=dict(type='bool', default=False), + system_all=dict(type='bool', default=False), + system_volumes=dict(type='bool', default=False), + executable=dict(type='str', default='podman') + ) + + module = AnsibleModule( + argument_spec=module_args + ) + + executable = module.get_bin_path( + module.params['executable'], required=True) + + for target, filters in ( + ('container', 'container_filters'), ('image', 'image_filters'), ('network', 'network_filters'), + ('volume', 'volume_filters')): + if module.params[target]: + results[target] = podmanExec(module, target, module.params[filters], executable) + + if module.params['system']: + target = 'system' + system_filters = {} + if module.params['system_all']: + system_filters['system_all'] = '--all' + if module.params['system_volumes']: + system_filters['system_volumes'] = '--volumes' + results[target] = podmanExec(module, target, system_filters, executable) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/podman_prune/tasks/main.yml b/tests/integration/targets/podman_prune/tasks/main.yml new file mode 100644 index 00000000..804543fe --- /dev/null +++ b/tests/integration/targets/podman_prune/tasks/main.yml @@ -0,0 +1,131 @@ +- name: Create random names + ansible.builtin.set_fact: + cname: "{{ 'ansible-container-%0x' % ((2**32) | random) }}" + nname: "{{ 'ansible-network-%0x' % ((2**32) | random) }}" + vname: "{{ 'ansible-volume-%0x' % ((2**32) | random) }}" + +- name: Test podman_prune module + block: + # Create objects to be pruned + - name: Create container + containers.podman.podman_container: + name: "{{ cname }}" + image: quay.io/podman/hello:latest + state: present + register: container + + - name: Create network + containers.podman.podman_network: + name: "{{ nname }}" + state: present + register: network + + - name: Create volume + containers.podman.podman_volume: + name: "{{ vname }}" + state: present + register: volume + + # Prune objects + - name: Prune objects + containers.podman.podman_prune: + container: true + network: true + volume: true + + - name: Check if container exists + containers.podman.podman_container_info: + register: container_exists + + - name: Check if podman network exists + containers.podman.podman_network_info: + register: network_exists + + - name: Check if podman volume exists + containers.podman.podman_volume_info: + register: volume_exists + + - name: Verify assertions for network, container and volume + ansible.builtin.assert: + that: + # containers + - container.container.Id not in container_exists.containers | map(attribute='Name') | list | flatten + # networks + - network.network.name not in network_exists.networks | map(attribute='id') | list | flatten + # volumes + - volume.volume.Name not in volume_exists.volumes | map(attribute='Name') | list | flatten + + # Test with filters + - name: Prune objects with filters + containers.podman.podman_prune: + image: true + image_filters: + dangling_only: false + external: true + + - name: Check if image exists + containers.podman.podman_image_info: + register: image_exists + + - name: Verify assertions for image (with filters) + ansible.builtin.assert: + that: + - image_exists.images | length == 0 + + - name: Create container + containers.podman.podman_container: + name: "{{ cname }}" + image: quay.io/podman/hello:latest + state: present + register: container_system + + - name: Create network + containers.podman.podman_network: + name: "{{ nname }}" + state: present + register: network_system + + - name: Create volume + containers.podman.podman_volume: + name: "{{ vname }}" + state: present + register: volume_system + + - name: System prune + containers.podman.podman_prune: + system: true + system_all: true + system_volumes: true + + - name: Check if container exists + containers.podman.podman_container_info: + register: container_system_exists + + - name: Check if podman network exists + containers.podman.podman_network_info: + register: network_system_exists + + - name: Check if podman volume exists + containers.podman.podman_volume_info: + register: volume_system_exists + + - name: Check if image exists + containers.podman.podman_image_info: + register: image_system_exists + + - name: Verify assertions for system + ansible.builtin.assert: + that: + # container + - container_system.container.Id not in container_system_exists.containers | map(attribute='Name') | list | flatten + # networks + - network_system.network.name not in network_system_exists.networks | map(attribute='id') | list | flatten + # volumes + - volume_system.volume.Name not in volume_system_exists.volumes | map(attribute='Name') | list | flatten + # images + - image_system_exists.images | length == 0 + + always: + - name: Cleanup + ansible.builtin.command: podman system prune -a -f --volumes + ignore_errors: true