#!/bin/sh set -e # Put the standard system directories on PATH. semodule, umount and the other # tools this script calls live in the sbin ones, which a root shell started # with su rather than su - leaves off PATH. for _dir in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin; do case ":$PATH:" in *":$_dir:"*) ;; *) PATH="${PATH:+$PATH:}$_dir" ;; esac done export PATH # Function to handle errors handle_error() { echo "❌ Error: $1" exit 1 } # Check if running as root check_root() { # Check if EUID is 0 (root) or if id -u returns 0 if [ "$(id -u)" -ne 0 ]; then echo "❌ This script must be run as root (use sudo)" echo "" echo "Please run: sudo $0 $*" exit 1 fi } # Function to detect init system detect_init_system() { if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then echo "systemd" elif [ -f /sbin/init ] && /sbin/init --version 2>/dev/null | grep -q upstart; then echo "upstart" elif command -v openrc >/dev/null 2>&1 || [ -f /sbin/openrc ]; then echo "openrc" elif [ -d /etc/s6 ] || command -v s6-svc >/dev/null 2>&1; then echo "s6" elif command -v runit >/dev/null 2>&1 || [ -d /etc/runit ]; then echo "runit" elif [ -d /etc/init.d ]; then echo "sysvinit" else echo "unknown" fi } # Default configuration APP_NAME="kubesolo" INSTALL_PATH="/usr/local/bin/$APP_NAME" CONFIG_PATH="${KUBESOLO_PATH:-/var/lib/kubesolo}" # Parse command line arguments REMOVE_DATA=false REMOVE_KUBECONFIG=false KEEP_CONFIG=false CONFIG_FILE="/etc/kubesolo/config.yaml" for arg in "$@"; do case $arg in --path=*) CONFIG_PATH="${arg#*=}" ;; --remove-data) REMOVE_DATA=true ;; --remove-kubeconfig) REMOVE_KUBECONFIG=true ;; --keep-config) KEEP_CONFIG=true ;; --help) echo "Usage: $0 [options]" echo "Options:" echo " --path=PATH Set configuration path (default: $CONFIG_PATH)" echo " --remove-data Remove configuration and data directory" echo " --remove-kubeconfig Remove kubeconfig from ~/.kube/config" echo " --keep-config Leave $CONFIG_FILE in place for a later reinstall" echo " --help Show this help message" exit 0 ;; esac done # Detect init system INIT_SYSTEM=$(detect_init_system) echo "🔍 Detected init system: $INIT_SYSTEM" # Function to stop and disable systemd service stop_systemd_service() { if systemctl is-active --quiet "$APP_NAME" 2>/dev/null; then echo "🛑 Stopping $APP_NAME service..." systemctl stop "$APP_NAME" || echo "âš ī¸ Failed to stop service (may already be stopped)" fi if systemctl is-enabled --quiet "$APP_NAME" 2>/dev/null; then echo "🔌 Disabling $APP_NAME service..." systemctl disable "$APP_NAME" || echo "âš ī¸ Failed to disable service" fi if [ -f "/etc/systemd/system/$APP_NAME.service" ] || [ -f "/etc/systemd/system/$APP_NAME.service.bak" ] \ || [ -d "/etc/systemd/system/$APP_NAME.service.d" ]; then echo "đŸ—‘ī¸ Removing systemd service file..." # The .d directory holds the upgrade's boot-guard drop-in; .bak is the # unit the flags-to-configuration-file migration replaced. rm -rf "/etc/systemd/system/$APP_NAME.service" "/etc/systemd/system/$APP_NAME.service.bak" \ "/etc/systemd/system/$APP_NAME.service.d" systemctl daemon-reload || echo "âš ī¸ Failed to reload systemd daemon" fi } # Function to stop and disable SysV init service stop_sysvinit_service() { if [ -f "/etc/init.d/$APP_NAME" ]; then echo "🛑 Stopping $APP_NAME service..." if command -v service >/dev/null 2>&1; then service "$APP_NAME" stop 2>/dev/null || echo "âš ī¸ Service may already be stopped" else "/etc/init.d/$APP_NAME" stop 2>/dev/null || echo "âš ī¸ Service may already be stopped" fi echo "🔌 Disabling $APP_NAME service..." if command -v update-rc.d >/dev/null 2>&1; then update-rc.d -f "$APP_NAME" remove 2>/dev/null || echo "âš ī¸ Failed to remove service" elif command -v chkconfig >/dev/null 2>&1; then chkconfig --del "$APP_NAME" 2>/dev/null || echo "âš ī¸ Failed to remove service" fi echo "đŸ—‘ī¸ Removing SysV init script..." rm -f "/etc/init.d/$APP_NAME" fi } # Function to stop and disable OpenRC service stop_openrc_service() { if [ -f "/etc/init.d/$APP_NAME" ]; then echo "🛑 Stopping $APP_NAME service..." rc-service "$APP_NAME" stop 2>/dev/null || echo "âš ī¸ Service may already be stopped" echo "🔌 Disabling $APP_NAME service..." rc-update del "$APP_NAME" default 2>/dev/null || echo "âš ī¸ Failed to remove service" echo "đŸ—‘ī¸ Removing OpenRC service script..." rm -f "/etc/init.d/$APP_NAME" fi } # Function to stop and disable s6 service stop_s6_service() { S6_SERVICE_DIR="/etc/s6/sv/$APP_NAME" if [ -d "$S6_SERVICE_DIR" ]; then echo "🛑 Stopping $APP_NAME service..." if command -v s6-svc >/dev/null 2>&1; then s6-svc -d "$S6_SERVICE_DIR" 2>/dev/null || echo "âš ī¸ Service may already be stopped" fi # Remove symlink if it exists if [ -L "/etc/s6/adminsv/default/$APP_NAME" ]; then rm -f "/etc/s6/adminsv/default/$APP_NAME" fi echo "đŸ—‘ī¸ Removing s6 service directory..." rm -rf "$S6_SERVICE_DIR" fi } # Function to stop and disable runit service stop_runit_service() { RUNIT_SERVICE_DIR="/etc/runit/sv/$APP_NAME" # Stop service if symlink exists if [ -L "/var/service/$APP_NAME" ]; then echo "🛑 Stopping $APP_NAME service..." sv stop "$APP_NAME" 2>/dev/null || echo "âš ī¸ Service may already be stopped" rm -f "/var/service/$APP_NAME" elif [ -L "/etc/runit/runsvdir/default/$APP_NAME" ]; then echo "🛑 Stopping $APP_NAME service..." sv stop "$APP_NAME" 2>/dev/null || echo "âš ī¸ Service may already be stopped" rm -f "/etc/runit/runsvdir/default/$APP_NAME" fi if [ -d "$RUNIT_SERVICE_DIR" ]; then echo "đŸ—‘ī¸ Removing runit service directory..." rm -rf "$RUNIT_SERVICE_DIR" fi } # Function to stop and disable upstart service stop_upstart_service() { if [ -f "/etc/init/$APP_NAME.conf" ]; then echo "🛑 Stopping $APP_NAME service..." initctl stop "$APP_NAME" 2>/dev/null || echo "âš ī¸ Service may already be stopped" echo "đŸ—‘ī¸ Removing upstart service file..." rm -f "/etc/init/$APP_NAME.conf" initctl reload-configuration 2>/dev/null || echo "âš ī¸ Failed to reload upstart configuration" fi } # Function to stop daemon mode stop_daemon() { PIDFILE="/var/run/$APP_NAME.pid" if [ -f "$PIDFILE" ]; then PID=$(cat "$PIDFILE" 2>/dev/null || echo "") if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then echo "🛑 Stopping $APP_NAME daemon (PID: $PID)..." kill "$PID" 2>/dev/null || echo "âš ī¸ Failed to stop daemon" sleep 1 fi rm -f "$PIDFILE" fi # Try to kill any remaining processes if pgrep -x "$APP_NAME" >/dev/null 2>&1; then echo "🛑 Stopping remaining $APP_NAME processes..." pkill -x "$APP_NAME" 2>/dev/null || echo "âš ī¸ Failed to stop processes" fi } # Function to kill processes holding KubeSolo ports kill_port_processes() { echo "🔍 Checking for processes holding KubeSolo ports..." # KubeSolo ports: 2379 (Kine), 6443 (API Server), 10443 (Webhook), 6060 (pprof) local ports="2379 6443 10443 6060" local found_processes=false for port in $ports; do # Find processes using the port (works with both lsof and ss/netstat) local pids="" # Try lsof first (more reliable) if command -v lsof >/dev/null 2>&1; then pids=$(lsof -ti ":$port" 2>/dev/null || true) # Fallback to ss elif command -v ss >/dev/null 2>&1; then # ss format: "LISTEN 0 128 *:2379 *:* users:(("kubesolo",pid=12345,fd=3))" pids=$(ss -lptn "sport = :$port" 2>/dev/null | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u || true) # Fallback to netstat elif command -v netstat >/dev/null 2>&1; then pids=$(netstat -tlnp 2>/dev/null | grep ":$port " | awk '{print $7}' | cut -d'/' -f1 | grep -E '^[0-9]+$' | sort -u || true) fi if [ -n "$pids" ]; then found_processes=true for pid in $pids; do # Check if it's actually a kubesolo-related process local cmdline="" local procname="" local is_kubesolo=false if [ -f "/proc/$pid/cmdline" ]; then cmdline=$(cat "/proc/$pid/cmdline" 2>/dev/null | tr '\0' ' ' || echo "") fi if [ -f "/proc/$pid/comm" ]; then procname=$(cat "/proc/$pid/comm" 2>/dev/null || echo "") fi # Check if it's a kubesolo process if echo "$cmdline" | grep -q "kubesolo" || echo "$procname" | grep -qi "kubesolo"; then is_kubesolo=true fi # For KubeSolo-specific ports, be more aggressive if we can't determine the process # Port 2379 is Kine (KubeSolo-specific), so if something is holding it, it's likely leftover if [ "$is_kubesolo" = "true" ] || ([ "$port" = "2379" ] && [ -z "$cmdline" ]); then echo "🛑 Killing process $pid holding port $port" kill -TERM "$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true else echo "âš ī¸ Process $pid is holding port $port but doesn't appear to be KubeSolo-related (skipping)" fi done fi done if [ "$found_processes" = "true" ]; then echo "âŗ Waiting for ports to be released..." sleep 2 else echo "â„šī¸ No processes found holding KubeSolo ports" fi } # Function to kill all remaining kubesolo processes # # Processes are matched by their executable — the KubeSolo binary, or anything # run from its data directory (containerd, its shims, crun) — not by the word # "kubesolo" in their command line. That matched far more: an editor open on # /etc/kubesolo/config.yaml, a `tail -f /var/log/kubesolo.log`, or the shell # that ran this script, which was killed partway through the uninstall. kubesolo_pids() { for d in /proc/[0-9]*; do pid=${d#/proc/} [ "$pid" = "$$" ] && continue exe=$(readlink "$d/exe" 2>/dev/null) || continue exe=${exe% (deleted)} case "$exe" in "$INSTALL_PATH"|"$CONFIG_PATH"/*) echo "$pid" ;; esac done } kill_all_kubesolo_processes() { echo "🔍 Checking for any remaining KubeSolo processes..." pids=$(kubesolo_pids) if [ -z "$pids" ]; then echo "â„šī¸ No remaining KubeSolo processes found" return 0 fi echo "🛑 Killing remaining KubeSolo processes..." for pid in $pids; do echo " Killing PID $pid ($(readlink "/proc/$pid/exe" 2>/dev/null || echo exited))" kill -TERM "$pid" 2>/dev/null || true done # Wait a bit for processes to terminate sleep 2 # Force kill any that are still running pids=$(kubesolo_pids) if [ -n "$pids" ]; then echo "🛑 Force killing remaining processes..." for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true done sleep 1 fi } # Function to unmount container runtime mounts unmount_runtime_mounts() { local config_path="$1" if [ ! -d "$config_path" ]; then return 0 fi echo "🔍 Checking for container runtime mounts under $config_path..." # Find all mounts under the config path and unmount them in reverse order # This handles nested mounts correctly (e.g., overlay mounts) local mounts mounts=$(mount | awk -v path="$config_path" '$3 ~ "^" path {print $3}' | sort -r 2>/dev/null || true) if [ -z "$mounts" ]; then echo "â„šī¸ No mounts found under $config_path" return 0 fi echo "🔓 Unmounting container runtime mounts..." local unmount_failed=false while IFS= read -r mount_point; do if [ -n "$mount_point" ]; then echo " Unmounting: $mount_point" # Use lazy unmount (-l) to handle busy mounts if ! umount -l "$mount_point" 2>/dev/null; then echo "âš ī¸ Failed to unmount $mount_point (may already be unmounted)" unmount_failed=true fi fi done </dev/null 2>&1; then for unit in $(systemctl list-units --plain --no-legend 'kubesolo-upgrade-*' 2>/dev/null | awk '{print $1}'); do echo "🛑 Stopping upgrade in progress ($unit)..." systemctl stop "$unit" 2>/dev/null || true done fi for f in /proc/[0-9]*/cmdline; do if { tr '\0' ' ' < "$f"; } 2>/dev/null | grep -q -- '--upgrade-executor='; then pid=${f#/proc/}; pid=${pid%/cmdline} echo "🛑 Stopping upgrade in progress (PID $pid)..." kill -KILL "$pid" 2>/dev/null || true fi done } # Function to stop pod processes. The service runs with KillMode=process so that # a restart reattaches to running containers; stopping it for good leaves them # running, so they are stopped here. stop_workloads() { pids="" for procs in $(find /sys/fs/cgroup -path '*kubepods*' -name cgroup.procs 2>/dev/null); do pids="$pids $(cat "$procs" 2>/dev/null || true)" done pids=$(echo "$pids" | tr ' ' '\n' | grep -v '^$' | sort -u) [ -z "$pids" ] && return 0 echo "🛑 Stopping $(echo "$pids" | wc -l | tr -d ' ') pod process(es)..." for pid in $pids; do kill -TERM "$pid" 2>/dev/null || true; done i=0 while [ $i -lt 10 ]; do alive="" for pid in $pids; do kill -0 "$pid" 2>/dev/null && alive="$alive $pid"; done [ -z "$alive" ] && return 0 sleep 1 i=$((i + 1)) done for pid in $alive; do kill -KILL "$pid" 2>/dev/null || true; done } # Function to remove kubeconfig remove_kubeconfig() { # Detect real user when running under sudo REAL_USER="$USER" REAL_HOME="$HOME" if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then REAL_USER="$SUDO_USER" REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6) if [ -z "$REAL_HOME" ]; then REAL_HOME="/home/$SUDO_USER" fi fi KUBECONFIG_FILE="$REAL_HOME/.kube/config" if [ -f "$KUBECONFIG_FILE" ]; then echo "🔍 Checking for KubeSolo context in kubeconfig..." # Check if kubectl is available to manipulate kubeconfig if command -v kubectl >/dev/null 2>&1; then # Try to remove kubesolo context if kubectl config get-contexts 2>/dev/null | grep -q "kubesolo"; then echo "đŸ—‘ī¸ Removing KubeSolo context from kubeconfig..." kubectl config delete-context kubesolo 2>/dev/null || true kubectl config unset clusters.kubesolo 2>/dev/null || true kubectl config unset users.kubesolo-admin 2>/dev/null || true fi # Check if there's a backup and restore it LATEST_BACKUP=$(ls -t "$REAL_HOME/.kube/config.backup-"* 2>/dev/null | head -n1) if [ -n "$LATEST_BACKUP" ] && [ -f "$LATEST_BACKUP" ]; then echo "📋 Found backup kubeconfig: $LATEST_BACKUP" printf "Restore backup kubeconfig? [y/N] " read -r REPLY if echo "$REPLY" | grep -qi "^y"; then cp "$LATEST_BACKUP" "$KUBECONFIG_FILE" echo "✅ Restored backup kubeconfig" fi fi else echo "âš ī¸ kubectl not found, cannot automatically remove KubeSolo context" echo "💡 You may need to manually edit $KUBECONFIG_FILE to remove KubeSolo entries" fi fi } # Check for root privileges check_root "$@" # Main uninstall process echo "đŸ—‘ī¸ Uninstalling $APP_NAME..." stop_upgrade_executor # Stop and remove service based on init system case "$INIT_SYSTEM" in "systemd") stop_systemd_service ;; "sysvinit") stop_sysvinit_service ;; "openrc") stop_openrc_service ;; "s6") stop_s6_service ;; "runit") stop_runit_service ;; "upstart") stop_upstart_service ;; *) echo "âš ī¸ Unknown init system, trying to stop daemon mode..." stop_daemon ;; esac # Wait a bit for graceful shutdown echo "âŗ Waiting for graceful shutdown..." sleep 2 # Kill processes holding KubeSolo ports (ensures ports are released) kill_port_processes # Kill any remaining KubeSolo processes kill_all_kubesolo_processes # Stop the pods' own processes, which outlive the service stop_workloads # Service definitions the flags-to-configuration-file migration replaced for def in "/etc/init.d/$APP_NAME" "/etc/init/$APP_NAME.conf"; do rm -f "$def.bak" done # Remove binary if [ -f "$INSTALL_PATH" ]; then echo "đŸ—‘ī¸ Removing binary from $INSTALL_PATH..." rm -f "$INSTALL_PATH" else echo "â„šī¸ Binary not found at $INSTALL_PATH (may have been removed already)" fi # Remove control script (from minimal install) if [ -f "/usr/local/bin/kubesolo-ctl" ]; then echo "đŸ—‘ī¸ Removing control script..." rm -f "/usr/local/bin/kubesolo-ctl" fi # Remove PID file PIDFILE="/var/run/$APP_NAME.pid" if [ -f "$PIDFILE" ]; then echo "đŸ—‘ī¸ Removing PID file..." rm -f "$PIDFILE" fi # Remove log file LOGFILE="/var/log/$APP_NAME.log" if [ -f "$LOGFILE" ]; then echo "đŸ—‘ī¸ Removing log file..." rm -f "$LOGFILE" fi # Remove the CNI configuration KubeSolo writes into the standard CNI directory. # Left behind, a container runtime managed by the host keeps loading it after # KubeSolo is gone. Not cluster state, so it goes regardless of --remove-data. # -e is false for a dangling symlink, so test for a symlink as well. CNI_CONFIG_FILE="/etc/cni/net.d/10-bridge.conflist" if [ -e "$CNI_CONFIG_FILE" ] || [ -L "$CNI_CONFIG_FILE" ]; then echo "đŸ—‘ī¸ Removing CNI configuration..." rm -f "$CNI_CONFIG_FILE" fi # Remove the SELinux policy module KubeSolo installs on start where SELinux is # enabled. It only lets systemd run the container device filters KubeSolo's # crun creates, so it goes regardless of --remove-data. if command -v semodule >/dev/null 2>&1 && semodule -l 2>/dev/null | awk '{print $1}' | grep -qx kubesolo; then echo "đŸ—‘ī¸ Removing SELinux policy module kubesolo..." semodule -r kubesolo || echo "âš ī¸ Could not remove the SELinux policy module kubesolo" fi # Remove the configuration file. # # Removed by default, unlike the data directory: it holds settings rather than # cluster state, and leaving a stale one behind would silently configure a later # reinstall with the settings of the install just removed. if [ "$KEEP_CONFIG" = "true" ]; then echo "â„šī¸ Configuration file preserved: $CONFIG_FILE" elif [ -f "$CONFIG_FILE" ]; then echo "đŸ—‘ī¸ Removing configuration file: $CONFIG_FILE" rm -f "$CONFIG_FILE" "$CONFIG_FILE.bak" # Only if nothing else was put alongside it. rmdir "$(dirname "$CONFIG_FILE")" 2>/dev/null || true echo "✅ Configuration file removed" fi # Remove configuration and data directory if requested if [ "$REMOVE_DATA" = "true" ]; then if [ -d "$CONFIG_PATH" ]; then echo "đŸ—‘ī¸ Removing configuration and data directory: $CONFIG_PATH" printf "âš ī¸ This will delete all KubeSolo data. Continue? [y/N] " read -r REPLY if echo "$REPLY" | grep -qi "^y"; then # Unmount any container runtime mounts before removing directory unmount_runtime_mounts "$CONFIG_PATH" rm -rf "$CONFIG_PATH" echo "✅ Configuration directory removed" else echo "â„šī¸ Skipping data directory removal" fi fi else echo "â„šī¸ Configuration directory preserved: $CONFIG_PATH" echo "💡 Use --remove-data to delete it" fi # Remove kubeconfig if requested if [ "$REMOVE_KUBECONFIG" = "true" ]; then remove_kubeconfig else echo "â„šī¸ Kubeconfig preserved in ~/.kube/config" echo "💡 Use --remove-kubeconfig to remove KubeSolo context" fi echo "✅ $APP_NAME uninstallation completed!" echo "" echo "📋 Summary:" echo " - Service stopped and removed" echo " - Binary removed: $INSTALL_PATH" if [ "$REMOVE_DATA" = "true" ]; then echo " - Configuration directory removed: $CONFIG_PATH" else echo " - Configuration directory preserved: $CONFIG_PATH" fi if [ "$REMOVE_KUBECONFIG" = "true" ]; then echo " - Kubeconfig entries removed" else echo " - Kubeconfig entries preserved" fi exit 0