Files
k3s-ansible/.github/scripts/cleanup-runner-resources.sh
T
Timothy Stewart bb3843dbb1 fix(ci): skip removed stale Molecule state
- Avoid rereading state files after removing a stale scenario directory.\n- Extend cleanup fixture coverage to multiple stale machine states.
2026-08-01 12:27:12 -05:00

250 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
printf '%s\n' \
'Usage: cleanup-runner-resources.sh [--snapshot|--dry-run|--apply]' \
'' \
'Discover and, with --apply, remove only VirtualBox resources referenced by' \
'repository-owned Molecule Vagrant state. The default is --dry-run.'
}
mode="dry-run"
case "${1:-}" in
"") ;;
--snapshot) mode="snapshot" ;;
--dry-run) mode="dry-run" ;;
--apply) mode="apply" ;;
--help|-h) usage; exit 0 ;;
*) usage >&2; exit 2 ;;
esac
home_dir="${HOME:?HOME must be set}"
molecule_root="${K3S_CI_MOLECULE_ROOT:-${home_dir}/.cache/molecule}"
repository_name="${K3S_CI_MOLECULE_PROJECT:-k3s-ansible}"
virtualbox_root="${K3S_CI_VIRTUALBOX_ROOT:-${home_dir}/VirtualBox VMs}"
hostonly_marker="${K3S_CI_HOSTONLY_MARKER:-${home_dir}/.cache/k3s-ci/hostonly-interfaces}"
record_hostonly() {
local marker_dir="${hostonly_marker%/*}"
local marker_tmp="${hostonly_marker}.tmp"
local hostonly_inventory
if ! hostonly_inventory="$(VBoxManage list hostonlyifs)"; then
fail_closed 'unable to inventory VirtualBox host-only interfaces'
fi
mkdir -p -- "$marker_dir"
awk -F': ' '
/^Name:/ { name=$2 }
/^IPAddress:/ { print name "|" $2 }
' <<< "$hostonly_inventory" > "$marker_tmp"
mv -- "$marker_tmp" "$hostonly_marker"
chmod 600 "$hostonly_marker"
printf 'Recorded host-only interface baseline: %s\n' "$hostonly_marker"
}
cleanup_hostonly() {
local hostonly_inventory
if [[ ! -f "$hostonly_marker" ]]; then
printf 'No host-only interface baseline found; leaving interfaces unchanged.\n'
return 0
fi
if ! hostonly_inventory="$(VBoxManage list hostonlyifs)"; then
fail_closed 'unable to inventory VirtualBox host-only interfaces'
fi
while IFS='|' read -r interface_name interface_ip; do
[[ "$interface_name" == vboxnet* ]] || continue
[[ "$interface_ip" == 192.168.30.* || "$interface_ip" == fdad:bad:ba55:* ]] || continue
if grep -Fqx "${interface_name}|${interface_ip}" "$hostonly_marker"; then
continue
fi
if [[ "$mode" == apply ]]; then
VBoxManage hostonlyif remove "$interface_name"
printf 'Removed host-only interface %s (%s)\n' "$interface_name" "$interface_ip"
else
printf 'Would remove host-only interface %s (%s)\n' "$interface_name" "$interface_ip"
fi
done < <(awk -F': ' '
/^Name:/ { name=$2 }
/^IPAddress:/ { print name "|" $2 }
' <<< "$hostonly_inventory")
}
if [[ "$mode" == snapshot ]]; then
record_hostonly
exit 0
fi
resolve_existing_dir() {
local candidate="$1"
if [[ ! -d "$candidate" ]]; then
return 1
fi
readlink -f -- "$candidate"
}
root_contains() {
local root="$1"
local path="$2"
[[ "$path" == "$root"/* ]]
}
is_supported_scenario() {
case "$1" in
default|single_node|calico|cilium|kube-vip|ipv6) return 0 ;;
*) return 1 ;;
esac
}
is_unregistered_vm_error() {
grep -Eq 'Could not find a registered machine|VBOX_E_OBJECT_NOT_FOUND'
}
fail_closed() {
printf 'cleanup refused: %s\n' "$1" >&2
exit 3
}
if [[ ! "$repository_name" =~ ^[A-Za-z0-9._-]+$ ]]; then
fail_closed 'invalid Molecule repository name'
fi
print_inventory() {
local phase="$1"
printf '%s VirtualBox inventory:\n' "$phase"
VBoxManage list vms || true
VBoxManage list hdds || true
VBoxManage list hostonlyifs || true
}
molecule_root_real="$(resolve_existing_dir "$molecule_root" || true)"
if [[ -z "$molecule_root_real" ]]; then
printf 'No Molecule root exists: %s\n' "$molecule_root"
cleanup_hostonly
exit 0
fi
print_inventory before
repository_root_real="$(resolve_existing_dir "$molecule_root_real/$repository_name" || true)"
if [[ -z "$repository_root_real" ]]; then
printf 'No repository Molecule state root exists: %s\n' "$molecule_root_real/$repository_name"
cleanup_hostonly
print_inventory after
exit 0
fi
if ! root_contains "$molecule_root_real" "$repository_root_real"; then
fail_closed "repository Molecule state root is outside Molecule root: $repository_root_real"
fi
declare -a state_files=()
while IFS= read -r -d '' state_file; do
state_files+=("$state_file")
done < <(find "$repository_root_real" -mindepth 6 -maxdepth 6 -type f \
-path '*/.vagrant/machines/*/virtualbox/id' -print0 2>/dev/null)
if ((${#state_files[@]} == 0)); then
printf 'No repository-owned Molecule Vagrant state found under %s\n' "$repository_root_real"
cleanup_hostonly
print_inventory after
exit 0
fi
virtualbox_root_real="$(resolve_existing_dir "$virtualbox_root" || true)"
declare -a vm_records=()
for state_file in "${state_files[@]}"; do
if [[ ! -f "$state_file" ]]; then
printf 'Skipping Vagrant state removed with its stale scenario directory: %s\n' "$state_file"
continue
fi
state_file_real="$(readlink -f -- "$state_file")"
state_dir="${state_file_real%/.vagrant/machines/*/virtualbox/id}"
machine_dir="${state_file_real%/virtualbox/id}"
machine_name="${machine_dir##*/}"
scenario_name="${state_dir##*/}"
if ! root_contains "$repository_root_real" "$state_dir"; then
fail_closed "state path is outside the repository Molecule root: $state_file_real"
fi
if ! is_supported_scenario "$scenario_name"; then
fail_closed "unexpected Molecule scenario: $scenario_name"
fi
if [[ "$machine_name" != control* && "$machine_name" != node* ]]; then
fail_closed "unexpected Molecule machine name: $machine_name"
fi
vm_uuid="$(tr -d '[:space:]' < "$state_file_real")"
if [[ ! "$vm_uuid" =~ ^[0-9a-fA-F-]{36}$ ]]; then
fail_closed "invalid VirtualBox UUID in $state_file_real"
fi
if ! vm_info="$(VBoxManage showvminfo "$vm_uuid" --machinereadable 2>&1)"; then
if is_unregistered_vm_error <<< "$vm_info"; then
printf 'Stale Vagrant state without a registered VM: %s (%s)\n' "$machine_name" "$vm_uuid"
if [[ "$mode" == apply ]]; then
rm -rf -- "${state_dir}/.vagrant"
printf 'Removed stale Vagrant state: %s\n' "${state_dir}/.vagrant"
fi
continue
fi
fail_closed "unable to inspect VirtualBox VM $vm_uuid: $vm_info"
fi
if [[ -z "$virtualbox_root_real" ]]; then
fail_closed "VirtualBox VM root does not exist: $virtualbox_root"
fi
cfg_file="$(awk -F= '$1 == "CfgFile" {gsub(/"/, "", $2); print $2; exit}' <<< "$vm_info")"
if [[ -z "$cfg_file" ]]; then
fail_closed "VirtualBox configuration path missing for $vm_uuid"
fi
cfg_file_real="$(readlink -f -- "$cfg_file")"
if ! root_contains "$virtualbox_root_real" "$cfg_file_real"; then
fail_closed "VM configuration is outside VirtualBox root: $cfg_file_real"
fi
while IFS= read -r disk_path; do
[[ -z "$disk_path" ]] && continue
disk_path_real="$(readlink -f -- "$disk_path" 2>/dev/null || true)"
if [[ -z "$disk_path_real" ]] || ! root_contains "$virtualbox_root_real" "$disk_path_real"; then
fail_closed "attached disk is outside VirtualBox root: $disk_path"
fi
done < <(awk -F= '$1 ~ /^(SATA|IDE|SCSI|SAS|VirtioSCSI|NVMe)-[0-9]+-[0-9]+$/ {gsub(/"/, "", $2); print $2}' <<< "$vm_info")
vm_records+=("$vm_uuid|$machine_name|$cfg_file_real")
done
if ((${#vm_records[@]} == 0)); then
printf 'No live repository-owned VirtualBox resources found\n'
cleanup_hostonly
print_inventory after
exit 0
fi
for record in "${vm_records[@]}"; do
IFS='|' read -r vm_uuid machine_name cfg_file_real <<< "$record"
if [[ "$mode" == dry-run ]]; then
printf 'Would remove VM %s (%s) config=%s\n' "$machine_name" "$vm_uuid" "$cfg_file_real"
continue
fi
vm_state="$(VBoxManage showvminfo "$vm_uuid" --machinereadable | awk -F= '$1 == "VMState" {gsub(/"/, "", $2); print $2; exit}')"
if [[ "$vm_state" != poweroff && "$vm_state" != saved ]]; then
VBoxManage controlvm "$vm_uuid" poweroff
fi
VBoxManage unregistervm "$vm_uuid" --delete
printf 'Removed VM %s (%s)\n' "$machine_name" "$vm_uuid"
done
cleanup_hostonly
print_inventory after
if [[ "$mode" == apply ]]; then
printf 'Repository-owned VM and host-only interface cleanup complete.\n'
else
printf 'Dry run complete. No resources were modified.\n'
fi