rhel-system-roles/SOURCES/0111-fix-proper-cleanup-for-networks-ensure-cleanup-of-re.patch

616 lines
23 KiB
Diff

From e11e1ff198f0840fcef6cbe75c74ca69dd22f694 Mon Sep 17 00:00:00 2001
From: Rich Megginson <rmeggins@redhat.com>
Date: Mon, 8 Jul 2024 16:35:29 -0600
Subject: [PATCH 111/115] fix: proper cleanup for networks; ensure cleanup of
resources
Cause: The code was not managing network systemd quadlet units.
Consequence: Network systemd quadlet units were not being stopped and
disabled. Subsequent runs would fail due to the network units not
being cleaned up properly.
Fix: The role manages network systemd quadlet units, including stopping
and removing.
Result: Systemd quadlet network units are properly cleaned up.
In addition - improve the removal of all types of quadlet resources,
and include code which can be used to test and debug quadlet resource
removal.
(cherry picked from commit a85908ec7f6f8e19908f8d4d18d6d7b64ab1d31e)
---
README.md | 6 +
defaults/main.yml | 4 +
tasks/cancel_linger.yml | 2 +-
tasks/cleanup_quadlet_spec.yml | 188 +++++++++++++-----
tasks/handle_quadlet_spec.yml | 2 +
tasks/manage_linger.yml | 2 +-
tasks/parse_quadlet_file.yml | 57 ++++++
tests/files/quadlet-basic.network | 2 +-
.../templates/quadlet-demo-mysql.container.j2 | 2 +-
tests/tests_quadlet_basic.yml | 69 ++++++-
tests/tests_quadlet_demo.yml | 33 +++
11 files changed, 309 insertions(+), 58 deletions(-)
create mode 100644 tasks/parse_quadlet_file.yml
diff --git a/README.md b/README.md
index e5a7c12..8b6496e 100644
--- a/README.md
+++ b/README.md
@@ -388,6 +388,12 @@ a newer version. For example, if you attempt to manage quadlet or secrets with
podman 4.3 or earlier, the role will fail with an error. If you want the role to
be skipped instead, use `podman_fail_if_too_old: false`.
+### 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 92e4eb8..02453c9 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -109,3 +109,7 @@ podman_continue_if_pull_fails: false
# If true, if a pull attempt fails, it will be retried according
# to the default Ansible `until` behavior.
podman_pull_retry: false
+
+# 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 ede71fe..f233fc4 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 8ea069b..df69243 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 ce6ef67..851c8a3 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 b506b70..be69490 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 0000000..5f5297f
--- /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 7db6e0d..5b002ba 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/templates/quadlet-demo-mysql.container.j2 b/tests/templates/quadlet-demo-mysql.container.j2
index c84f0e8..92097d4 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 2891b1a..0fdced4 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 b6c27ef..1cc7e62 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
--
2.46.0