diff --git a/README.md b/README.md index c2a96236..b3184319 100644 --- a/README.md +++ b/README.md @@ -529,6 +529,12 @@ registries will validate TLS certs or not. The default `null` means to use whatever is the default used by the `containers.podman.podman_image` module. You can override this on a per-spec basis using `validate_certs`. +### podman_prune_images + +Boolean - default is `false` - by default, the role will not prune unused images +when removing quadlets and other resources. Set this to `true` to tell the role +to remove unused images when cleaning up. + ## Variables Exported by the Role ### podman_version diff --git a/defaults/main.yml b/defaults/main.yml index 64d7e7c2..e667f35d 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -135,3 +135,7 @@ podman_registry_certificates: [] # by the containers.podman.podman_image module # You can override this on a per-spec basis using validate_certs podman_validate_certs: null + +# Prune images when removing quadlets/kube specs - +# this will remove all unused/unreferenced images +podman_prune_images: false diff --git a/tasks/cancel_linger.yml b/tasks/cancel_linger.yml index ede71fea..f233fc46 100644 --- a/tasks/cancel_linger.yml +++ b/tasks/cancel_linger.yml @@ -49,7 +49,7 @@ when: __podman_xdg_stat.stat.exists - name: Cancel linger if no more resources are in use - command: loginctl disable-linger {{ __podman_linger_user }} + command: loginctl disable-linger {{ __podman_linger_user | quote }} when: - __podman_xdg_stat.stat.exists - __podman_container_info.containers | length == 0 diff --git a/tasks/cleanup_quadlet_spec.yml b/tasks/cleanup_quadlet_spec.yml index 8ea069b3..df692435 100644 --- a/tasks/cleanup_quadlet_spec.yml +++ b/tasks/cleanup_quadlet_spec.yml @@ -33,39 +33,11 @@ - name: See if quadlet file exists stat: path: "{{ __podman_quadlet_file }}" - register: __podman_network_stat - when: __podman_quadlet_type == "network" + register: __podman_quadlet_stat -- name: Get network quadlet network name - when: - - __podman_quadlet_type == "network" - - __podman_network_stat.stat.exists - block: - - name: Create tempdir - tempfile: - prefix: podman_ - suffix: _lsr.ini - state: directory - register: __podman_network_tmpdir - delegate_to: localhost - - - name: Fetch the network quadlet - fetch: - dest: "{{ __podman_network_tmpdir.path }}/network.ini" - src: "{{ __podman_quadlet_file }}" - flat: true - - - name: Get the network name - set_fact: - __podman_network_name: "{{ - lookup('ini', 'NetworkName section=Network file=' ~ - __podman_network_tmpdir.path ~ '/network.ini') }}" - always: - - name: Remove tempdir - file: - path: "{{ __podman_network_tmpdir.path }}" - state: absent - delegate_to: localhost +- name: Parse quadlet file + include_tasks: parse_quadlet_file.yml + when: __podman_quadlet_stat.stat.exists - name: Remove quadlet file file: @@ -73,40 +45,158 @@ state: absent register: __podman_file_removed +- name: Refresh systemd # noqa no-handler + systemd: + daemon_reload: true + scope: "{{ __podman_systemd_scope }}" + become: "{{ __podman_rootless | ternary(true, omit) }}" + become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" + environment: + XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" + when: __podman_file_removed is changed # noqa no-handler + +- name: Remove managed resource + command: >- + podman {{ 'rm' if __podman_quadlet_type == 'container' + else 'network rm' if __podman_quadlet_type == 'network' + else 'volume rm' if __podman_quadlet_type == 'volume' }} + {{ __podman_quadlet_resource_name | quote }} + register: __podman_rm + failed_when: + - __podman_rm is failed + - not __podman_rm.stderr is search(__str) + changed_when: __podman_rm.rc == 0 + become: "{{ __podman_rootless | ternary(true, omit) }}" + become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" + environment: + XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" + vars: + __str: " found: no such " + __type_to_name: # map quadlet type to quadlet property name + container: + section: Container + name: ContainerName + network: + section: Network + name: NetworkName + volume: + section: Volume + name: VolumeName + __section: "{{ __type_to_name[__podman_quadlet_type]['section'] }}" + __name: "{{ __type_to_name[__podman_quadlet_type]['name'] }}" + __podman_quadlet_resource_name: "{{ + __podman_quadlet_parsed[__section][__name] + if __section in __podman_quadlet_parsed + and __name in __podman_quadlet_parsed[__section] + else 'systemd-' ~ __podman_quadlet_name }}" + when: + - __podman_file_removed is changed # noqa no-handler + - __podman_quadlet_type in __type_to_name + - not __podman_rootless or __podman_xdg_stat.stat.exists + - __podman_service_name | length > 0 + no_log: true + +- name: Remove volumes + command: podman volume rm {{ item | quote }} + loop: "{{ __volume_names }}" + when: + - __podman_file_removed is changed # noqa no-handler + - not __podman_rootless or __podman_xdg_stat.stat.exists + - __podman_service_name | length == 0 + - __podman_quadlet_file.endswith(".yml") or + __podman_quadlet_file.endswith(".yaml") + changed_when: true + vars: + __volumes: "{{ __podman_quadlet_parsed | + selectattr('apiVersion', 'defined') | selectattr('spec', 'defined') | + map(attribute='spec') | selectattr('volumes', 'defined') | + map(attribute='volumes') | flatten }}" + __config_maps: "{{ __volumes | selectattr('configMap', 'defined') | + map(attribute='configMap') | selectattr('name', 'defined') | + map(attribute='name') | list }}" + __secrets: "{{ __volumes | selectattr('secret', 'defined') | + map(attribute='secret') | selectattr('secretName', 'defined') | + map(attribute='secretName') | list }}" + __pvcs: "{{ __volumes | selectattr('persistentVolumeClaim', 'defined') | + map(attribute='persistentVolumeClaim') | selectattr('claimName', 'defined') | + map(attribute='claimName') | list }}" + __volume_names: "{{ __config_maps + __secrets + __pvcs }}" + no_log: true + +- name: Clear parsed podman variable + set_fact: + __podman_quadlet_parsed: null + +- name: Prune images no longer in use + command: podman image prune --all -f + when: + - podman_prune_images | bool + - not __podman_rootless or __podman_xdg_stat.stat.exists + changed_when: true + become: "{{ __podman_rootless | ternary(true, omit) }}" + become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" + environment: + XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" + - name: Manage linger include_tasks: manage_linger.yml vars: __podman_item_state: absent -- name: Cleanup container resources - when: __podman_file_removed is changed # noqa no-handler +- name: Collect information for testing/debugging + when: + - __podman_test_debug | d(false) + - not __podman_rootless or __podman_xdg_stat.stat.exists block: - - name: Reload systemctl # noqa no-handler - systemd: - daemon_reload: true - scope: "{{ __podman_systemd_scope }}" + - name: For testing and debugging - images + command: podman images -n + register: __podman_test_debug_images + changed_when: false become: "{{ __podman_rootless | ternary(true, omit) }}" become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" environment: XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" - - name: Prune images no longer in use - command: podman image prune -f + - name: For testing and debugging - volumes + command: podman volume ls -n + register: __podman_test_debug_volumes + changed_when: false + become: "{{ __podman_rootless | ternary(true, omit) }}" + become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" environment: XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" + + - name: For testing and debugging - containers + command: podman ps --noheading + register: __podman_test_debug_containers + changed_when: false become: "{{ __podman_rootless | ternary(true, omit) }}" become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" - changed_when: true + environment: + XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" + + - name: For testing and debugging - networks + command: podman network ls -n -q + register: __podman_test_debug_networks + changed_when: false + become: "{{ __podman_rootless | ternary(true, omit) }}" + become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" + environment: + XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" - - name: Remove network - command: podman network rm {{ __name | quote }} - changed_when: true - when: __podman_quadlet_type == "network" + - name: For testing and debugging - secrets + command: podman secret ls -n -q + register: __podman_test_debug_secrets + changed_when: false + no_log: true + become: "{{ __podman_rootless | ternary(true, omit) }}" + become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" environment: XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" + + - name: For testing and debugging - services + service_facts: become: "{{ __podman_rootless | ternary(true, omit) }}" become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}" - vars: - __name: "{{ __podman_network_name if - __podman_network_name | d('') | length > 0 - else 'systemd-' ~ __podman_quadlet_name }}" + environment: + XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}" diff --git a/tasks/handle_quadlet_spec.yml b/tasks/handle_quadlet_spec.yml index 23f44223..82f69e89 100644 --- a/tasks/handle_quadlet_spec.yml +++ b/tasks/handle_quadlet_spec.yml @@ -129,6 +129,8 @@ if __podman_quadlet_type in ['container', 'kube'] else __podman_quadlet_name ~ '-volume.service' if __podman_quadlet_type in ['volume'] + else __podman_quadlet_name ~ '-network.service' + if __podman_quadlet_type in ['network'] else none }}" - name: Set per-container variables part 4 diff --git a/tasks/manage_linger.yml b/tasks/manage_linger.yml index b506b703..be69490b 100644 --- a/tasks/manage_linger.yml +++ b/tasks/manage_linger.yml @@ -10,7 +10,7 @@ - __podman_item_state | d('present') != 'absent' block: - name: Enable linger if needed - command: loginctl enable-linger {{ __podman_user }} + command: loginctl enable-linger {{ __podman_user | quote }} when: __podman_rootless | bool args: creates: /var/lib/systemd/linger/{{ __podman_user }} diff --git a/tasks/parse_quadlet_file.yml b/tasks/parse_quadlet_file.yml new file mode 100644 index 00000000..5f5297f4 --- /dev/null +++ b/tasks/parse_quadlet_file.yml @@ -0,0 +1,57 @@ +--- +# Input: +# * __podman_quadlet_file - path to quadlet file to parse +# Output: +# * __podman_quadlet_parsed - dict +- name: Slurp quadlet file + slurp: + path: "{{ __podman_quadlet_file }}" + register: __podman_quadlet_raw + no_log: true + +- name: Parse quadlet file + set_fact: + __podman_quadlet_parsed: |- + {% set rv = {} %} + {% set section = ["DEFAULT"] %} + {% for line in __val %} + {% if line.startswith("[") %} + {% set val = line.replace("[", "").replace("]", "") %} + {% set _ = section.__setitem__(0, val) %} + {% else %} + {% set ary = line.split("=", 1) %} + {% set key = ary[0] %} + {% set val = ary[1] %} + {% if key in rv.get(section[0], {}) %} + {% set curval = rv[section[0]][key] %} + {% if curval is string %} + {% set newary = [curval, val] %} + {% set _ = rv[section[0]].__setitem__(key, newary) %} + {% else %} + {% set _ = rv[section[0]][key].append(val) %} + {% endif %} + {% else %} + {% set _ = rv.setdefault(section[0], {}).__setitem__(key, val) %} + {% endif %} + {% endif %} + {% endfor %} + {{ rv }} + vars: + __val: "{{ (__podman_quadlet_raw.content | b64decode).split('\n') | + select | reject('match', '#') | list }}" + when: __podman_service_name | length > 0 + no_log: true + +- name: Parse quadlet yaml file + set_fact: + __podman_quadlet_parsed: "{{ __podman_quadlet_raw.content | b64decode | + from_yaml_all }}" + when: + - __podman_service_name | length == 0 + - __podman_quadlet_file.endswith(".yml") or + __podman_quadlet_file.endswith(".yaml") + no_log: true + +- name: Reset raw variable + set_fact: + __podman_quadlet_raw: null diff --git a/tests/files/quadlet-basic.network b/tests/files/quadlet-basic.network index 7db6e0d6..5b002ba9 100644 --- a/tests/files/quadlet-basic.network +++ b/tests/files/quadlet-basic.network @@ -2,4 +2,4 @@ Subnet=192.168.29.0/24 Gateway=192.168.29.1 Label=app=wordpress -NetworkName=quadlet-basic +NetworkName=quadlet-basic-name diff --git a/tests/tasks/cleanup_registry.yml b/tests/tasks/cleanup_registry.yml index f449c4ad..759e1981 100644 --- a/tests/tasks/cleanup_registry.yml +++ b/tests/tasks/cleanup_registry.yml @@ -1,9 +1,21 @@ # SPDX-License-Identifier: MIT --- +- name: Get volume for cleanup + command: >- + podman inspect podman_registry --format + '{% raw %}{{range .}}{{range .Mounts}}{{if eq .Type + "volume"}}{{.Name}}{{end}}{{end}}{{end}}{% endraw %}' + changed_when: false + register: __registry_volume + - name: Destroy registry container command: podman rm -f podman_registry changed_when: true +- name: Destroy volume + command: podman volume rm {{ __registry_volume.stdout | quote }} + changed_when: true + - name: Cleanup paths file: path: "{{ item }}" diff --git a/tests/templates/quadlet-demo-mysql.container.j2 b/tests/templates/quadlet-demo-mysql.container.j2 index c84f0e8a..92097d4d 100644 --- a/tests/templates/quadlet-demo-mysql.container.j2 +++ b/tests/templates/quadlet-demo-mysql.container.j2 @@ -9,7 +9,7 @@ Volume=/tmp/quadlet_demo:/var/lib/quadlet_demo:Z Network=quadlet-demo.network {% if podman_version is version("4.5", ">=") %} Secret=mysql-root-password-container,type=env,target=MYSQL_ROOT_PASSWORD -HealthCmd=/usr/bin/true +HealthCmd=/bin/true HealthOnFailure=kill {% else %} PodmanArgs=--secret=mysql-root-password-container,type=env,target=MYSQL_ROOT_PASSWORD diff --git a/tests/tests_quadlet_basic.yml b/tests/tests_quadlet_basic.yml index 2891b1a4..0fdced45 100644 --- a/tests/tests_quadlet_basic.yml +++ b/tests/tests_quadlet_basic.yml @@ -21,7 +21,14 @@ __podman_quadlet_specs: - file_src: files/quadlet-basic.network state: started + - name: quadlet-basic-unused-network + type: network + Network: {} - name: quadlet-basic-mysql + type: volume + Volume: + VolumeName: quadlet-basic-mysql-name + - name: quadlet-basic-unused-volume type: volume Volume: {} - name: quadlet-basic-mysql @@ -30,7 +37,7 @@ WantedBy: default.target Container: Image: "{{ mysql_image }}" - ContainerName: quadlet-basic-mysql + ContainerName: quadlet-basic-mysql-name Volume: quadlet-basic-mysql.volume:/var/lib/mysql Network: quadlet-basic.network # Once 4.5 is released change this line to use the quadlet Secret key @@ -192,13 +199,14 @@ register: __stat failed_when: not __stat.stat.exists - # must clean up networks last - cannot remove a network - # in use by a container - using reverse assumes the network - # is defined first in the list + # must clean up in the reverse order of creating - and + # ensure networks are removed last - name: Cleanup user include_role: name: linux-system-roles.podman vars: + podman_prune_images: true + __podman_test_debug: true podman_run_as_user: user_quadlet_basic __absent: {"state":"absent"} podman_secrets: "{{ __podman_secrets | map('combine', __absent) | @@ -206,6 +214,22 @@ podman_quadlet_specs: "{{ __podman_quadlet_specs | reverse | map('combine', __absent) | list }}" + - name: Ensure no resources + assert: + that: + - __podman_test_debug_images.stdout == "" + - __podman_test_debug_networks.stdout_lines | + reject("match", "^podman$") | + reject("match", "^podman-default-kube-network$") | + list | length == 0 + - __podman_test_debug_volumes.stdout == "" + - __podman_test_debug_containers.stdout == "" + - __podman_test_debug_secrets.stdout == "" + - ansible_facts["services"] | dict2items | + rejectattr("value.status", "match", "not-found") | + selectattr("key", "match", "quadlet-demo") | + list | length == 0 + - name: Ensure no linger stat: path: /var/lib/systemd/linger/user_quadlet_basic @@ -230,12 +254,28 @@ - quadlet-basic-mysql.volume - name: Check JSON - command: podman exec quadlet-basic-mysql cat /tmp/test.json + command: podman exec quadlet-basic-mysql-name cat /tmp/test.json register: __result failed_when: __result.stdout != __json_secret_data changed_when: false rescue: + - name: Debug3 + shell: | + set -x + set -o pipefail + exec 1>&2 + #podman volume rm --all + #podman network prune -f + podman volume ls + podman network ls + podman secret ls + podman container ls + podman pod ls + podman images + systemctl list-units | grep quadlet + changed_when: false + - name: Check AVCs command: grep type=AVC /var/log/audit/audit.log changed_when: false @@ -253,6 +293,7 @@ include_role: name: linux-system-roles.podman vars: + podman_prune_images: true podman_run_as_user: user_quadlet_basic __absent: {"state":"absent"} podman_secrets: "{{ __podman_secrets | @@ -270,12 +311,30 @@ include_role: name: linux-system-roles.podman vars: + podman_prune_images: true + __podman_test_debug: true __absent: {"state":"absent"} podman_secrets: "{{ __podman_secrets | map('combine', __absent) | list }}" podman_quadlet_specs: "{{ __podman_quadlet_specs | reverse | map('combine', __absent) | list }}" + - name: Ensure no resources + assert: + that: + - __podman_test_debug_images.stdout == "" + - __podman_test_debug_networks.stdout_lines | + reject("match", "^podman$") | + reject("match", "^podman-default-kube-network$") | + list | length == 0 + - __podman_test_debug_volumes.stdout == "" + - __podman_test_debug_containers.stdout == "" + - __podman_test_debug_secrets.stdout == "" + - ansible_facts["services"] | dict2items | + rejectattr("value.status", "match", "not-found") | + selectattr("key", "match", "quadlet-demo") | + list | length == 0 + rescue: - name: Dump journal command: journalctl -ex diff --git a/tests/tests_quadlet_demo.yml b/tests/tests_quadlet_demo.yml index b6c27ef0..1cc7e62b 100644 --- a/tests/tests_quadlet_demo.yml +++ b/tests/tests_quadlet_demo.yml @@ -84,6 +84,11 @@ changed_when: false failed_when: false + - name: Check volumes + command: podman volume ls + changed_when: false + failed_when: false + - name: Check pods command: podman pod ps --ctr-ids --ctr-names --ctr-status changed_when: false @@ -150,6 +155,8 @@ include_role: name: linux-system-roles.podman vars: + podman_prune_images: true + __podman_test_debug: true __absent: {"state":"absent"} podman_quadlet_specs: "{{ __podman_quadlet_specs | reverse | map('combine', __absent) | list }}" @@ -161,7 +168,33 @@ - name: envoy-certificates state: absent + - name: Ensure no resources + assert: + that: + - __podman_test_debug_images.stdout == "" + - __podman_test_debug_networks.stdout_lines | + reject("match", "^podman$") | + reject("match", "^podman-default-kube-network$") | + list | length == 0 + - __podman_test_debug_volumes.stdout == "" + - __podman_test_debug_containers.stdout == "" + - __podman_test_debug_secrets.stdout == "" + - ansible_facts["services"] | dict2items | + rejectattr("value.status", "match", "not-found") | + selectattr("key", "match", "quadlet-demo") | + list | length == 0 + rescue: + - name: Debug + shell: | + exec 1>&2 + set -x + set -o pipefail + systemctl list-units --plain -l --all | grep quadlet || : + systemctl list-unit-files --all | grep quadlet || : + systemctl list-units --plain --failed -l --all | grep quadlet || : + changed_when: false + - name: Get journald command: journalctl -ex changed_when: false