mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 00:27:11 +09:00
Critical install/update/uninstall hotfixes from the hardening plan (docs/plans/INSTALLER_UPDATER_UNINSTALLER_HARDENING_PLAN.md §4): - Firewall automation (G1): new sourceable scripts/lib/firewall.sh detects firewalld/ufw/nftables/iptables and opens 80/443; install.sh runs configure_firewall, update.sh re-ensures it idempotently. Exactly what we open is recorded in /etc/serverkit/install-state.json (scripts/lib/state.sh) so uninstall undoes only our rules. Fixes the #1 "installed but unreachable" failure on fresh RHEL-family boxes. - Accept Python 3.11 OR 3.12 (G2): locate_python now iterates python3.12/3.11/3 and provision_python installs the distro package (python3.11 on Debian 12, 3.12 on Ubuntu/Fedora, 3.11/3.12 on RHEL) before ever falling back to a source compile. - Use $PYTHON_BIN for secret generation instead of a bare python3. - Distro-first Node 20 (replacing unconditional NodeSource): install the distro nodejs+npm, fall back to NodeSource only when it's missing or < 18, and validate node>=18 before continuing. - Unify uninstall (G4): new scripts/lib/uninstall.sh is the single canonical teardown routine shared by uninstall.sh and `serverkit uninstall` (they had drifted — one ran `down -v`, one didn't). Default preserves user data (volumes kept, DB snapshotted to backups); --purge/--delete-volumes removes volumes+DB+data dirs; --keep-data preserves /etc + /var/lib + backups; --yes/--dry-run added; honors SERVERKIT_DIR. Removes the apt lock-wait drop-in on uninstall. Tests: new scripts/test/test_install.sh (11 cases, source-able install.sh) covers python/node detection, firewall dry-run + state, and the uninstall data-handling matrix. Existing test_update.sh still green. shellcheck clean (error + warning) on all new/changed shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
206 lines
8.7 KiB
Bash
206 lines
8.7 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# scripts/lib/uninstall.sh — the one canonical uninstall routine.
|
|
#
|
|
# Both entry points (the standalone uninstall.sh and `serverkit uninstall`) used
|
|
# to carry their own copy of the teardown logic and had quietly drifted apart —
|
|
# `serverkit uninstall` ran `docker compose down -v` (destroying volumes) while
|
|
# uninstall.sh ran `down` (keeping them), so the same command name deleted your
|
|
# data in one path and not the other. This module is the single source of truth;
|
|
# the callers only handle their own banner + confirmation, then call
|
|
# serverkit_uninstall_core. It is non-interactive (testable) and parameterized
|
|
# entirely through environment variables.
|
|
#
|
|
# Inputs (all optional):
|
|
# SERVERKIT_DIR install dir (default /opt/serverkit)
|
|
# SERVERKIT_PURGE 1 → also remove Docker volumes, the SQLite DB, and all
|
|
# data dirs (/var/lib, /var/serverkit, backups, /etc)
|
|
# SERVERKIT_KEEP_DATA 1 → preserve /var/lib/serverkit, /etc/serverkit and
|
|
# /var/backups/serverkit (overrides the default
|
|
# config-dir removal). Ignored when PURGE=1.
|
|
# SERVERKIT_UNINSTALL_DRY_RUN 1 → print actions, change nothing.
|
|
#
|
|
# Data-handling matrix:
|
|
# default --keep-data --purge
|
|
# code (install dir + slots) remove remove remove
|
|
# systemd/nginx/apt/CLI/firewall remove remove remove
|
|
# logs (/var/log/serverkit) remove remove remove
|
|
# Docker volumes keep keep remove (-v)
|
|
# /etc/serverkit (config) remove keep remove
|
|
# /var/lib/serverkit keep keep remove
|
|
# /var/serverkit (apps) keep keep remove
|
|
# /var/backups/serverkit keep keep remove
|
|
# live SQLite DB backed up backed up remove
|
|
#
|
|
# "Default preserves data" is honored for both deployment shapes: all-Docker DBs
|
|
# live in a volume (kept without -v); hybrid DBs live in the install dir, so we
|
|
# snapshot the DB into /var/backups/serverkit before removing the tree.
|
|
|
|
# Self-contained logging (no dependency on the caller's color helpers).
|
|
_u_info() { printf ' \033[0;34m→\033[0m %s\n' "$1"; }
|
|
_u_ok() { printf ' \033[0;32m✓\033[0m %s\n' "$1"; }
|
|
_u_warn() { printf ' \033[1;33m!\033[0m %s\n' "$1"; }
|
|
|
|
_u_run() {
|
|
if [ "${SERVERKIT_UNINSTALL_DRY_RUN:-0}" = "1" ]; then
|
|
printf ' [uninstall] would run: %s\n' "$*"
|
|
return 0
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
# Remove the firewall rules we (and only we) added, reading the record left in
|
|
# install-state.json. Must run BEFORE the config dir is deleted.
|
|
_u_firewall_cleanup() {
|
|
local lib="$1/scripts/lib"
|
|
[ -f "$lib/firewall.sh" ] || return 0
|
|
[ -f "$lib/state.sh" ] || return 0
|
|
# shellcheck source=/dev/null
|
|
source "$lib/firewall.sh"
|
|
# shellcheck source=/dev/null
|
|
source "$lib/state.sh"
|
|
command -v state_get >/dev/null 2>&1 || return 0
|
|
|
|
local backend ports
|
|
backend="$(state_get firewall_backend 2>/dev/null || true)"
|
|
[ -n "$backend" ] || return 0
|
|
ports="$(state_list firewall_ports 2>/dev/null | tr '\n' ' ')"
|
|
[ -n "$ports" ] || return 0
|
|
|
|
_u_info "Removing firewall rules we added ($backend): $ports"
|
|
if [ "${SERVERKIT_UNINSTALL_DRY_RUN:-0}" = "1" ]; then
|
|
FW_DRY_RUN=1 firewall_close "$backend" $ports
|
|
else
|
|
# shellcheck disable=SC2086
|
|
firewall_close "$backend" $ports || true
|
|
fi
|
|
}
|
|
|
|
serverkit_uninstall_core() {
|
|
local install_dir="${SERVERKIT_DIR:-/opt/serverkit}"
|
|
local purge="${SERVERKIT_PURGE:-0}"
|
|
local keep_data="${SERVERKIT_KEEP_DATA:-0}"
|
|
[ "$purge" = "1" ] && keep_data=0 # purge wins; keeping data makes no sense
|
|
|
|
local data_dir="/var/lib/serverkit"
|
|
local apps_dir="/var/serverkit"
|
|
local config_dir="/etc/serverkit"
|
|
local log_dir="/var/log/serverkit"
|
|
local backup_dir="/var/backups/serverkit"
|
|
|
|
# ---- Stop services -----------------------------------------------------
|
|
_u_info "Stopping services..."
|
|
_u_run systemctl stop serverkit 2>/dev/null || true
|
|
_u_run systemctl disable serverkit 2>/dev/null || true
|
|
_u_ok "Backend service stopped"
|
|
|
|
# ---- Snapshot the live DB (unless purging) -----------------------------
|
|
if [ "$purge" != "1" ]; then
|
|
_u_snapshot_db "$install_dir" "$backup_dir"
|
|
fi
|
|
|
|
# ---- Containers --------------------------------------------------------
|
|
if command -v docker >/dev/null 2>&1 && \
|
|
{ [ -e "$install_dir/docker-compose.yml" ] || [ -e "$install_dir/docker-compose.yaml" ]; }; then
|
|
if [ "$purge" = "1" ]; then
|
|
_u_info "Removing containers and volumes (--purge)..."
|
|
_u_run docker compose --project-directory "$install_dir" down -v --remove-orphans 2>/dev/null || true
|
|
else
|
|
_u_info "Removing containers (volumes preserved)..."
|
|
_u_run docker compose --project-directory "$install_dir" down --remove-orphans 2>/dev/null || true
|
|
fi
|
|
_u_ok "Containers removed"
|
|
else
|
|
_u_warn "Docker or compose file not found — skipping container cleanup"
|
|
fi
|
|
|
|
# ---- Firewall (read state before deleting the config dir) --------------
|
|
_u_firewall_cleanup "$install_dir"
|
|
|
|
# ---- systemd unit ------------------------------------------------------
|
|
_u_info "Removing systemd service..."
|
|
_u_run rm -f /etc/systemd/system/serverkit.service
|
|
_u_run systemctl daemon-reload 2>/dev/null || true
|
|
_u_ok "Systemd service removed"
|
|
|
|
# ---- nginx ServerKit-owned configs -------------------------------------
|
|
_u_info "Removing nginx configuration..."
|
|
_u_run rm -f /etc/nginx/sites-enabled/serverkit.conf
|
|
_u_run rm -f /etc/nginx/sites-available/serverkit.conf
|
|
_u_run rm -f /etc/nginx/sites-available/serverkit-insecure.conf
|
|
_u_run rm -f /etc/nginx/sites-available/example.conf.template
|
|
_u_run rm -f /etc/nginx/conf.d/serverkit-tls.conf
|
|
_u_run rm -rf /etc/nginx/serverkit-conf.d
|
|
if systemctl is-active --quiet nginx 2>/dev/null; then
|
|
_u_run systemctl reload nginx 2>/dev/null || true
|
|
fi
|
|
_u_ok "Nginx config removed"
|
|
|
|
# ---- apt lock-wait drop-in (only the installer creates this) -----------
|
|
_u_run rm -f /etc/apt/apt.conf.d/99-serverkit-lock-wait.conf
|
|
|
|
# ---- CLI symlink -------------------------------------------------------
|
|
_u_run rm -f /usr/local/bin/serverkit
|
|
|
|
# ---- Code: install dir + blue/green slots + backup snapshot ------------
|
|
_u_info "Removing installation files..."
|
|
_u_run rm -rf "$install_dir"
|
|
_u_run rm -rf "${install_dir}-a"
|
|
_u_run rm -rf "${install_dir}-b"
|
|
_u_run rm -rf "${install_dir}.backup"
|
|
_u_ok "Installation directory removed"
|
|
|
|
# ---- Logs (always transient) -------------------------------------------
|
|
_u_run rm -rf "$log_dir"
|
|
|
|
# ---- Data tiers --------------------------------------------------------
|
|
if [ "$purge" = "1" ]; then
|
|
_u_info "Purging data (--purge)..."
|
|
_u_run rm -rf "$data_dir" "$apps_dir" "$backup_dir" "$config_dir"
|
|
_u_ok "Data directories removed"
|
|
else
|
|
# Config dir holds install-state.json + ssl-mode + templates. Removed by
|
|
# default, preserved with --keep-data.
|
|
if [ "$keep_data" = "1" ]; then
|
|
_u_info "Preserving data (--keep-data): $data_dir, $config_dir, $backup_dir"
|
|
else
|
|
_u_run rm -rf "$config_dir"
|
|
_u_info "Preserved user data: $data_dir, $apps_dir, $backup_dir"
|
|
fi
|
|
fi
|
|
|
|
# ---- Telemetry (best-effort) -------------------------------------------
|
|
curl -s "https://serverkit.ai/track/uninstall" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# Snapshot the live SQLite DB into the (preserved) backup dir so a default
|
|
# uninstall never silently destroys the database with the install tree.
|
|
_u_snapshot_db() {
|
|
local install_dir="$1" backup_dir="$2"
|
|
local stamp db_src dest
|
|
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
dest="$backup_dir/serverkit-pre-uninstall-$stamp.db"
|
|
|
|
# Hybrid: DB sits in the install tree.
|
|
db_src="$install_dir/backend/instance/serverkit.db"
|
|
if [ -f "$db_src" ]; then
|
|
_u_run mkdir -p "$backup_dir"
|
|
if _u_run cp "$db_src" "$dest" 2>/dev/null; then
|
|
_u_ok "Database snapshot saved to $dest"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# All-Docker: DB lives in the backend container's volume.
|
|
if command -v docker >/dev/null 2>&1 && \
|
|
docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx 'serverkit-backend'; then
|
|
_u_run mkdir -p "$backup_dir"
|
|
if _u_run docker cp serverkit-backend:/app/instance/serverkit.db "$dest" 2>/dev/null; then
|
|
_u_ok "Database snapshot saved to $dest"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
_u_warn "No live SQLite database found to snapshot — skipping"
|
|
}
|