--- - name: Set effective k3s data-dir ansible.builtin.set_fact: k3s_effective_data_dir: "{{ k3s_data_dir | default('/var/lib/rancher/k3s', true) }}" # k3s reset: cluster-side node removal (best-effort) # # Why this exists: # - Prevents stale `NotReady` node objects after a node reset. # - Prevents DaemonSet pods stuck `Terminating` (the scheduler still thinks the node exists). # - Prevents lingering `kube-node-lease` entries which can cause subsequent install/join hangs. # # Important safety rules: # - We run these `kubectl` operations on a healthy control-plane host via `delegate_to`. # - We skip if there is no reachable control-plane host or if its kubeconfig is missing. # - We do not attempt this when resetting the *last* control-plane node (there is no other # healthy server to delegate to, and we don't want the reset to break the cluster). - name: Build node-name candidates for cluster-side cleanup (prefer inventory_hostname) ansible.builtin.set_fact: k3s_reset_node_name_candidates: >- {{ [inventory_hostname, ansible_fqdn | default(''), ansible_hostname | default('')] | reject('equalto', '') | unique | list }} k3s_reset_node_name: >- {{ ([inventory_hostname, ansible_fqdn | default(''), ansible_hostname | default('')] | reject('equalto', '') | unique | list)[0] }} when: k3s_reset_force_cluster_node_delete | bool - name: Pick control-plane delegate host for cluster-side cleanup (first available k3s server) ansible.builtin.set_fact: k3s_reset_control_plane_hosts: "{{ groups['k3s_servers'] | default([]) }}" k3s_reset_is_last_control_plane: >- {{ (inventory_hostname in (groups['k3s_servers'] | default([]))) and ((groups['k3s_servers'] | default([])) | length == 1) }} k3s_reset_delegate_host: >- {{ ((groups['k3s_servers'] | default([])) | difference([inventory_hostname]) | first) | default('') }} when: k3s_reset_force_cluster_node_delete | bool - name: Log why cluster-side node removal is skipped (last control-plane) ansible.builtin.debug: msg: >- Skipping cluster-side node deletion for {{ inventory_hostname }} because it is the last control-plane host in inventory (no other server to delegate kubectl to). when: - k3s_reset_force_cluster_node_delete | bool - k3s_reset_is_last_control_plane | bool - name: Log why cluster-side node removal is skipped (no control-plane delegate host) ansible.builtin.debug: msg: >- Skipping cluster-side node deletion for {{ inventory_hostname }} because no control-plane host is available to delegate to (inventory group `k3s_servers` is empty, or only contains this host). when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (k3s_reset_delegate_host | default('') | length) == 0 - name: Check whether delegated control-plane host is reachable ansible.builtin.ping: delegate_to: "{{ (k3s_reset_delegate_host | default('') | trim | length > 0) | ternary(k3s_reset_delegate_host, omit) }}" register: _k3s_reset_delegate_ping failed_when: false changed_when: false when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (k3s_reset_delegate_host | default('') | length) > 0 - name: Log why cluster-side node removal is skipped (control-plane host unreachable) ansible.builtin.debug: msg: >- Skipping cluster-side node deletion for {{ inventory_hostname }} because delegated control-plane host '{{ k3s_reset_delegate_host }}' is unreachable. when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (k3s_reset_delegate_host | default('') | length) > 0 - _k3s_reset_delegate_ping is not defined or (_k3s_reset_delegate_ping.ping | default('')) != 'pong' - name: Check for kubeconfig on delegated control-plane host ansible.builtin.stat: path: "{{ k3s_reset_kubeconfig_path }}" delegate_to: "{{ (k3s_reset_delegate_host | default('') | trim | length > 0) | ternary(k3s_reset_delegate_host, omit) }}" register: _k3s_reset_kubeconfig_stat failed_when: false changed_when: false when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - name: Log why cluster-side node removal is skipped (kubeconfig missing on delegate) ansible.builtin.debug: msg: >- Skipping cluster-side node deletion for {{ inventory_hostname }} because kubeconfig '{{ k3s_reset_kubeconfig_path }}' is missing on delegated control-plane host '{{ k3s_reset_delegate_host }}'. when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - _k3s_reset_kubeconfig_stat is defined - not (_k3s_reset_kubeconfig_stat.stat.exists | default(false) | bool) - name: Fetch cluster node list (best-effort) to resolve the exact Kubernetes node name ansible.builtin.command: >- timeout 15s kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' --request-timeout=5s delegate_to: "{{ (k3s_reset_delegate_host | default('') | trim | length > 0) | ternary(k3s_reset_delegate_host, omit) }}" environment: KUBECONFIG: "{{ k3s_reset_kubeconfig_path }}" register: _k3s_reset_nodes_list failed_when: false changed_when: false when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - (_k3s_reset_kubeconfig_stat.stat.exists | default(false)) | bool - name: Resolve Kubernetes node name from cluster (fallback to inventory_hostname) ansible.builtin.set_fact: k3s_reset_node_name: >- {{ (k3s_reset_node_name_candidates | select('in', _k3s_reset_nodes_list.stdout_lines | default([])) | list | first) | default(k3s_reset_node_name) }} when: - k3s_reset_force_cluster_node_delete | bool - _k3s_reset_nodes_list is defined - name: Log planned cluster-side cleanup target ansible.builtin.debug: msg: >- Cluster-side cleanup enabled. Will attempt to delete node/lease '{{ k3s_reset_node_name }}' via delegated control-plane host '{{ k3s_reset_delegate_host }}' (kubeconfig: {{ k3s_reset_kubeconfig_path }}). when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - (_k3s_reset_kubeconfig_stat.stat.exists | default(false)) | bool - name: Cluster-side cleanup - delete node object (best-effort) ansible.builtin.command: >- timeout 15s kubectl delete node {{ k3s_reset_node_name }} --ignore-not-found=true --wait=false --request-timeout=5s delegate_to: "{{ (k3s_reset_delegate_host | default('') | trim | length > 0) | ternary(k3s_reset_delegate_host, omit) }}" environment: KUBECONFIG: "{{ k3s_reset_kubeconfig_path }}" register: _k3s_reset_delete_node_1 failed_when: false changed_when: "'deleted' in (_k3s_reset_delete_node_1.stdout | default('') | lower)" when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - (_k3s_reset_kubeconfig_stat.stat.exists | default(false)) | bool - name: Cluster-side cleanup - patch away node finalizers if deletion is stuck (best-effort) ansible.builtin.command: >- timeout 15s kubectl patch node {{ k3s_reset_node_name }} --type=merge -p '{"metadata":{"finalizers":[]}}' --request-timeout=5s delegate_to: "{{ (k3s_reset_delegate_host | default('') | trim | length > 0) | ternary(k3s_reset_delegate_host, omit) }}" environment: KUBECONFIG: "{{ k3s_reset_kubeconfig_path }}" register: _k3s_reset_patch_node_finalizers failed_when: false changed_when: "'patched' in (_k3s_reset_patch_node_finalizers.stdout | default('') | lower)" when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - (_k3s_reset_kubeconfig_stat.stat.exists | default(false)) | bool - name: Cluster-side cleanup - delete node object again after patch (best-effort) ansible.builtin.command: >- timeout 15s kubectl delete node {{ k3s_reset_node_name }} --ignore-not-found=true --wait=false --request-timeout=5s delegate_to: "{{ (k3s_reset_delegate_host | default('') | trim | length > 0) | ternary(k3s_reset_delegate_host, omit) }}" environment: KUBECONFIG: "{{ k3s_reset_kubeconfig_path }}" register: _k3s_reset_delete_node_2 failed_when: false changed_when: "'deleted' in (_k3s_reset_delete_node_2.stdout | default('') | lower)" when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - (_k3s_reset_kubeconfig_stat.stat.exists | default(false)) | bool - name: Cluster-side cleanup - delete kube-node-lease for the node (best-effort) ansible.builtin.command: >- timeout 15s kubectl delete lease {{ k3s_reset_node_name }} -n kube-node-lease --ignore-not-found=true --wait=false --request-timeout=5s delegate_to: "{{ (k3s_reset_delegate_host | default('') | trim | length > 0) | ternary(k3s_reset_delegate_host, omit) }}" environment: KUBECONFIG: "{{ k3s_reset_kubeconfig_path }}" register: _k3s_reset_delete_lease failed_when: false changed_when: "'deleted' in (_k3s_reset_delete_lease.stdout | default('') | lower)" when: - k3s_reset_force_cluster_node_delete | bool - not (k3s_reset_is_last_control_plane | default(false) | bool) - (_k3s_reset_delegate_ping.ping | default('')) == 'pong' - (_k3s_reset_kubeconfig_stat.stat.exists | default(false)) | bool - name: Log cluster-side cleanup results (best-effort) ansible.builtin.debug: msg: - "kubectl delete node (1): rc={{ _k3s_reset_delete_node_1.rc | default('n/a') }} stdout={{ _k3s_reset_delete_node_1.stdout | default('') | trim }} stderr={{ _k3s_reset_delete_node_1.stderr | default('') | trim }}" - "kubectl patch node finalizers: rc={{ _k3s_reset_patch_node_finalizers.rc | default('n/a') }} stdout={{ _k3s_reset_patch_node_finalizers.stdout | default('') | trim }} stderr={{ _k3s_reset_patch_node_finalizers.stderr | default('') | trim }}" - "kubectl delete node (2): rc={{ _k3s_reset_delete_node_2.rc | default('n/a') }} stdout={{ _k3s_reset_delete_node_2.stdout | default('') | trim }} stderr={{ _k3s_reset_delete_node_2.stderr | default('') | trim }}" - "kubectl delete lease: rc={{ _k3s_reset_delete_lease.rc | default('n/a') }} stdout={{ _k3s_reset_delete_lease.stdout | default('') | trim }} stderr={{ _k3s_reset_delete_lease.stderr | default('') | trim }}" when: - k3s_reset_force_cluster_node_delete | bool - _k3s_reset_delete_node_1 is defined or _k3s_reset_delete_lease is defined - name: Best-effort purge kube-system workloads before uninstall ansible.builtin.command: >- timeout 15s kubectl -n kube-system delete all --all --ignore-not-found --wait=false --request-timeout=5s failed_when: false changed_when: false - name: Best-effort purge kube-system namespaces ansible.builtin.command: >- timeout 15s kubectl delete namespace kube-system --ignore-not-found --wait=false --request-timeout=5s failed_when: false changed_when: false - name: Force-remove kube-system finalizers if namespace is stuck ansible.builtin.command: >- timeout 15s kubectl patch namespace kube-system -p '{"spec":{"finalizers":[]}}' --type=merge --request-timeout=5s failed_when: false changed_when: false - name: Stop k3s services if present ansible.builtin.service: name: "{{ item }}" state: stopped enabled: false loop: - k3s - k3s-agent failed_when: false - name: Check for k3s uninstall scripts ansible.builtin.stat: path: "{{ item }}" loop: - /usr/local/bin/k3s-uninstall.sh - /usr/local/bin/k3s-agent-uninstall.sh register: k3s_uninstall_scripts - name: Run k3s uninstall scripts if present ansible.builtin.command: "{{ item.stat.path }}" loop: "{{ k3s_uninstall_scripts.results }}" when: item.stat.exists - name: Remove k3s systemd units and directories ansible.builtin.file: path: "{{ item }}" state: absent loop: - /etc/rancher - /etc/systemd/system/k3s.service - /etc/systemd/system/k3s-agent.service - /etc/systemd/system/k3s.service.d - /etc/systemd/system/k3s-agent.service.d - /etc/rancher/node - /etc/rancher/k3s - /etc/cni - /opt/cni - "{{ k3s_effective_data_dir }}" - "{{ k3s_effective_data_dir }}/server/tls" - "{{ k3s_effective_data_dir }}/server/cred" - "{{ k3s_effective_data_dir }}/server/token" - "{{ k3s_effective_data_dir }}/server/node-token" - "{{ k3s_effective_data_dir }}/server/agent-token" - "{{ k3s_effective_data_dir }}/server/db" - "{{ k3s_effective_data_dir }}/agent" - "{{ k3s_effective_data_dir }}/data" - /var/lib/kubelet - /var/lib/etcd - /var/lib/cni - /var/lib/containerd - /run/k3s - /run/flannel - /var/log/k3s - /usr/local/bin/k3s - /usr/local/bin/k3s-killall.sh - /usr/local/bin/k3s-uninstall.sh - /usr/local/bin/k3s-agent-uninstall.sh - /usr/local/bin/kubectl - /usr/local/bin/crictl - /usr/local/bin/ctr - name: Reload systemd after k3s cleanup ansible.builtin.systemd: daemon_reload: true