mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
fix(lib,uninstall): warn-and-no-op state ops, best-effort teardown, remnants fallback (L1-L4,L8,L10-L13)
- state.sh mutating ops genuinely warn-and-no-op on OSError; reads quiet-empty; save refuses to clobber unread state (L1) - uninstall lib: guarded state_list pipeline (L2), _u_try wrapper so rm/mkdir failures never abort mid-teardown (L3), telemetry respects dry-run + --max-time 5 (L8) - root uninstall.sh: inline best-effort fallback teardown when the lib is missing — remnants-only boxes can now be uninstalled (L4) - pkg_refresh matches its never-propagate docstring (L10); ufw detect capture-then-case (L11); init/firewall zero-arg safety, init cache actually used, init_reload contract documented (L13) - 18 new proving assertions in test_lib.sh (31 passed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a2d2ffd83
commit
ccfd9ed227
7 files changed
+519
-132
No files matched your search
+20
-5
@@ -47,8 +47,16 @@ firewall_detect() {
|
||||
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
||||
printf 'firewalld\n'; return 0
|
||||
fi
|
||||
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qiE '^Status:[[:space:]]*active'; then
|
||||
printf 'ufw\n'; return 0
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
# Capture-then-case rather than `ufw status | grep -q`: grep -q exits
|
||||
# at the first match, ufw can then die of SIGPIPE, and under a caller's
|
||||
# pipefail the pipeline goes non-zero — misdetecting an active ufw.
|
||||
local ufw_out
|
||||
ufw_out="$(ufw status 2>/dev/null || true)"
|
||||
case "$ufw_out" in
|
||||
*[Ss]tatus:*[Ii]nactive*) : ;; # "inactive" contains "active" — rule it out first
|
||||
*[Ss]tatus:*[Aa]ctive*) printf 'ufw\n'; return 0 ;;
|
||||
esac
|
||||
fi
|
||||
if command -v nft >/dev/null 2>&1 && nft list ruleset >/dev/null 2>&1; then
|
||||
printf 'nftables\n'; return 0
|
||||
@@ -64,7 +72,11 @@ _fw_port() { printf '%s' "${1%%/*}"; }
|
||||
_fw_proto() { case "$1" in */*) printf '%s' "${1##*/}" ;; *) printf 'tcp' ;; esac; }
|
||||
|
||||
firewall_open() {
|
||||
local backend="$1"; shift
|
||||
# No backend argument (a zero-arg call under a `set -u` caller) is a
|
||||
# no-op, not an unbound-variable abort.
|
||||
local backend="${1:-}"
|
||||
[ -n "$backend" ] || return 0
|
||||
shift
|
||||
local spec port proto
|
||||
case "$backend" in
|
||||
firewalld)
|
||||
@@ -102,7 +114,10 @@ firewall_open() {
|
||||
}
|
||||
|
||||
firewall_close() {
|
||||
local backend="$1"; shift
|
||||
# Same zero-arg guard as firewall_open.
|
||||
local backend="${1:-}"
|
||||
[ -n "$backend" ] || return 0
|
||||
shift
|
||||
local spec port proto
|
||||
case "$backend" in
|
||||
firewalld)
|
||||
@@ -134,6 +149,6 @@ firewall_close() {
|
||||
|
||||
# Warn-and-document fallback text for when no firewall could be driven.
|
||||
firewall_manual_hint() {
|
||||
local ports="$1"
|
||||
local ports="${1:-}"
|
||||
printf 'Open these ports manually so the panel is reachable: %s\n' "$ports"
|
||||
}
|
||||
+43
-37
@@ -18,7 +18,11 @@
|
||||
# init_stop <service> → stop a service
|
||||
# init_enable <service> → enable at boot
|
||||
# init_disable <service> → disable at boot
|
||||
# init_reload [service] → daemon-reload (no arg) / reload a service
|
||||
# init_reload [service] → with a service: reload it, propagating the
|
||||
# tool's exit exactly like init_start/stop;
|
||||
# with no arg: systemd daemon-reload (a
|
||||
# successful no-op on every other init system,
|
||||
# where the concept does not exist)
|
||||
# init_is_active <service> → return 0 if running, 1 otherwise (no sudo)
|
||||
#
|
||||
# Honors:
|
||||
@@ -30,9 +34,9 @@
|
||||
# enable/disable (which is symlink-based) never aborts even if the symlink op
|
||||
# fails.
|
||||
|
||||
# Cache of the detected init system. Populated lazily, but every public function
|
||||
# still calls init_detect so that INIT_OVERRIDE/INIT_DRY_RUN flipped per-call in
|
||||
# tests always take effect — init_detect honours the override before this cache.
|
||||
# Cache of the detected init system: probing /proc and PATH once per process is
|
||||
# enough. init_detect honours INIT_OVERRIDE *before* this cache, so an override
|
||||
# flipped per-call in tests always takes effect.
|
||||
_INIT_DETECTED=""
|
||||
|
||||
# Prepend sudo when we are not already root and sudo exists. Used only for the
|
||||
@@ -71,6 +75,10 @@ init_detect() {
|
||||
printf '%s\n' "$INIT_OVERRIDE"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$_INIT_DETECTED" ]; then
|
||||
printf '%s\n' "$_INIT_DETECTED"
|
||||
return 0
|
||||
fi
|
||||
local comm=""
|
||||
if [ -d /run/systemd/system ]; then
|
||||
_INIT_DETECTED="systemd"; printf 'systemd\n'; return 0
|
||||
@@ -97,14 +105,21 @@ init_detect() {
|
||||
# Print "No init system detected" and fail. Centralised so every mutating
|
||||
# function reports the same actionable message.
|
||||
_init_no_init() {
|
||||
local action="$1" svc="$2"
|
||||
local action="${1:-}" svc="${2:-}"
|
||||
printf ' [init] No init system detected — cannot %s %s; start it manually\n' \
|
||||
"$action" "$svc" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# A zero-arg call under a `set -u` caller must warn-and-fail, never die on an
|
||||
# unbound $1 expansion. Callers get the same return 1 as any other failure.
|
||||
_init_no_svc() {
|
||||
printf ' [init] %s requires a service name\n' "${1:-}" >&2
|
||||
}
|
||||
|
||||
init_start() {
|
||||
local svc="$1" init
|
||||
local svc="${1:-}" init
|
||||
[ -n "$svc" ] || { _init_no_svc init_start; return 1; }
|
||||
init="$(init_detect)"
|
||||
case "$init" in
|
||||
systemd) _init_run "$(_init_sudo)" systemctl start "$svc" ;;
|
||||
@@ -116,7 +131,8 @@ init_start() {
|
||||
}
|
||||
|
||||
init_stop() {
|
||||
local svc="$1" init
|
||||
local svc="${1:-}" init
|
||||
[ -n "$svc" ] || { _init_no_svc init_stop; return 1; }
|
||||
init="$(init_detect)"
|
||||
case "$init" in
|
||||
systemd) _init_run "$(_init_sudo)" systemctl stop "$svc" ;;
|
||||
@@ -128,7 +144,8 @@ init_stop() {
|
||||
}
|
||||
|
||||
init_enable() {
|
||||
local svc="$1" init
|
||||
local svc="${1:-}" init
|
||||
[ -n "$svc" ] || { _init_no_svc init_enable; return 1; }
|
||||
init="$(init_detect)"
|
||||
case "$init" in
|
||||
systemd)
|
||||
@@ -158,7 +175,8 @@ init_enable() {
|
||||
}
|
||||
|
||||
init_disable() {
|
||||
local svc="$1" init
|
||||
local svc="${1:-}" init
|
||||
[ -n "$svc" ] || { _init_no_svc init_disable; return 1; }
|
||||
init="$(init_detect)"
|
||||
case "$init" in
|
||||
systemd)
|
||||
@@ -189,41 +207,29 @@ init_disable() {
|
||||
init_reload() {
|
||||
local svc="${1:-}" init
|
||||
init="$(init_detect)"
|
||||
# Bare `init_reload` means "systemd daemon-reload"; the concept does not
|
||||
# exist on other init systems, where it is a successful no-op.
|
||||
if [ -z "$svc" ]; then
|
||||
[ "$init" = "systemd" ] || return 0
|
||||
_init_run "$(_init_sudo)" systemctl daemon-reload
|
||||
return
|
||||
fi
|
||||
# A named reload behaves exactly like init_start/stop: it propagates the
|
||||
# tool's exit, and warns-and-fails when no init system can be driven.
|
||||
case "$init" in
|
||||
systemd)
|
||||
if [ -n "$svc" ]; then
|
||||
_init_run "$(_init_sudo)" systemctl reload "$svc"
|
||||
else
|
||||
_init_run "$(_init_sudo)" systemctl daemon-reload
|
||||
fi
|
||||
;;
|
||||
openrc)
|
||||
[ -n "$svc" ] && _init_run "$(_init_sudo)" rc-service "$svc" reload
|
||||
return 0
|
||||
;;
|
||||
runit)
|
||||
[ -n "$svc" ] && _init_run "$(_init_sudo)" sv reload "$svc"
|
||||
return 0
|
||||
;;
|
||||
sysvinit)
|
||||
[ -n "$svc" ] && _init_run "$(_init_sudo)" service "$svc" reload
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
# daemon-reload has no meaning without an init system; a named reload
|
||||
# is the only mutating intent worth warning about.
|
||||
if [ -n "$svc" ]; then
|
||||
_init_no_init "reload" "$svc"
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
systemd) _init_run "$(_init_sudo)" systemctl reload "$svc" ;;
|
||||
openrc) _init_run "$(_init_sudo)" rc-service "$svc" reload ;;
|
||||
runit) _init_run "$(_init_sudo)" sv reload "$svc" ;;
|
||||
sysvinit) _init_run "$(_init_sudo)" service "$svc" reload ;;
|
||||
*) _init_no_init "reload" "$svc" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Report whether a service is currently running. Read-only: no sudo, and the
|
||||
# tool's own output is discarded so callers can rely purely on the exit status.
|
||||
init_is_active() {
|
||||
local svc="$1" init
|
||||
local svc="${1:-}" init
|
||||
[ -n "$svc" ] || return 1 # no name → not running (read-only, stay quiet)
|
||||
init="$(init_detect)"
|
||||
case "$init" in
|
||||
systemd) systemctl is-active --quiet "$svc" ;;
|
||||
|
||||
+10
-8
@@ -80,19 +80,21 @@ pkg_detect() {
|
||||
|
||||
# Refresh the package index for the detected manager. Best-effort: index
|
||||
# refreshes routinely fail on a flaky mirror and that should not abort an
|
||||
# install, so we never propagate the manager's exit here.
|
||||
# install, so we never propagate the manager's exit here — every arm is
|
||||
# guarded and the function always returns 0.
|
||||
pkg_refresh() {
|
||||
local mgr
|
||||
mgr="$(pkg_detect)"
|
||||
case "$mgr" in
|
||||
apt) _pkg_run apt-get update ;;
|
||||
dnf) _pkg_run dnf makecache --refresh ;;
|
||||
yum) _pkg_run yum makecache ;;
|
||||
zypper) _pkg_run zypper --non-interactive refresh ;;
|
||||
pacman) _pkg_run pacman -Sy --noconfirm ;;
|
||||
apk) _pkg_run apk update ;;
|
||||
*) return 0 ;;
|
||||
apt) _pkg_run apt-get update || true ;;
|
||||
dnf) _pkg_run dnf makecache --refresh || true ;;
|
||||
yum) _pkg_run yum makecache || true ;;
|
||||
zypper) _pkg_run zypper --non-interactive refresh || true ;;
|
||||
pacman) _pkg_run pacman -Sy --noconfirm || true ;;
|
||||
apk) _pkg_run apk update || true ;;
|
||||
*) : ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# Install one or more packages non-interactively for the detected manager.
|
||||
|
||||
+76
-39
@@ -10,7 +10,12 @@
|
||||
#
|
||||
# Backed by python3, which is a hard ServerKit dependency (the whole backend is
|
||||
# Python), so it is present on any installed box. If python3 is somehow missing
|
||||
# the helpers degrade to warn-and-no-op rather than aborting the caller.
|
||||
# the helpers degrade to warn-and-no-op rather than aborting the caller. The
|
||||
# same contract holds when python3 IS present but the state file can't be
|
||||
# used (read-only fs, ENOSPC, unwritable parent, path-is-a-directory):
|
||||
# mutating ops warn to stderr and exit 0, read ops stay quiet and print
|
||||
# nothing — recording state is best-effort bookkeeping and must never abort
|
||||
# a `set -e` caller.
|
||||
#
|
||||
# API:
|
||||
# state_set <key> <value> set a scalar
|
||||
@@ -35,6 +40,17 @@ action = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
key = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
value = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||
|
||||
def warn(msg):
|
||||
try:
|
||||
sys.stderr.write(" [state] %s\n" % msg)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Reads are quiet-empty on any I/O problem: a missing, unreadable or
|
||||
# corrupt state file means "empty state" — an observation never aborts.
|
||||
# We remember when an EXISTING file could not be read so a later save()
|
||||
# can refuse to clobber state it never saw.
|
||||
load_failed = False
|
||||
try:
|
||||
with open(path) as fh:
|
||||
data = json.load(fh)
|
||||
@@ -42,64 +58,85 @@ try:
|
||||
data = {}
|
||||
except (FileNotFoundError, ValueError):
|
||||
data = {}
|
||||
except OSError:
|
||||
data = {}
|
||||
load_failed = True
|
||||
|
||||
def save():
|
||||
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w") as fh:
|
||||
json.dump(data, fh, indent=2, sort_keys=True)
|
||||
fh.write("\n")
|
||||
os.replace(tmp, path)
|
||||
# Mutations are warn-and-no-op on failure (read-only fs, ENOSPC,
|
||||
# unwritable parent, path-is-a-directory): the header promises this
|
||||
# degrades rather than aborting a `set -e` caller.
|
||||
if load_failed:
|
||||
warn("cannot read %s — not recording %s" % (path, key))
|
||||
return
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w") as fh:
|
||||
json.dump(data, fh, indent=2, sort_keys=True)
|
||||
fh.write("\n")
|
||||
os.replace(tmp, path)
|
||||
except OSError as exc:
|
||||
warn("cannot write %s (%s) — not recording %s" % (path, exc, key))
|
||||
|
||||
if action == "set":
|
||||
data[key] = value
|
||||
save()
|
||||
elif action == "get":
|
||||
v = data.get(key, "")
|
||||
if v is not None:
|
||||
sys.stdout.write(str(v))
|
||||
elif action == "append":
|
||||
arr = data.get(key)
|
||||
if not isinstance(arr, list):
|
||||
arr = []
|
||||
if value not in arr:
|
||||
arr.append(value)
|
||||
data[key] = arr
|
||||
save()
|
||||
elif action == "list":
|
||||
arr = data.get(key)
|
||||
if isinstance(arr, list):
|
||||
for item in arr:
|
||||
sys.stdout.write(str(item) + "\n")
|
||||
elif action == "unset":
|
||||
data.pop(key, None)
|
||||
save()
|
||||
else:
|
||||
sys.exit(2)
|
||||
try:
|
||||
if action == "set":
|
||||
data[key] = value
|
||||
save()
|
||||
elif action == "get":
|
||||
v = data.get(key, "")
|
||||
if v is not None:
|
||||
sys.stdout.write(str(v))
|
||||
elif action == "append":
|
||||
arr = data.get(key)
|
||||
if not isinstance(arr, list):
|
||||
arr = []
|
||||
if value not in arr:
|
||||
arr.append(value)
|
||||
data[key] = arr
|
||||
save()
|
||||
elif action == "list":
|
||||
arr = data.get(key)
|
||||
if isinstance(arr, list):
|
||||
for item in arr:
|
||||
sys.stdout.write(str(item) + "\n")
|
||||
elif action == "unset":
|
||||
data.pop(key, None)
|
||||
save()
|
||||
else:
|
||||
sys.exit(2)
|
||||
except BrokenPipeError:
|
||||
# A downstream consumer closed the pipe early (state_list | head ...).
|
||||
# Not an error for an observation; silence the interpreter's exit-time
|
||||
# flush and leave with 0 so a pipefail caller is unaffected.
|
||||
try:
|
||||
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
||||
except OSError:
|
||||
pass
|
||||
PY
|
||||
}
|
||||
|
||||
state_set() {
|
||||
_state_have_py || { printf ' [state] python3 unavailable — not recording %s\n' "$1" >&2; return 0; }
|
||||
_state_py set "$1" "$2"
|
||||
_state_have_py || { printf ' [state] python3 unavailable — not recording %s\n' "${1:-}" >&2; return 0; }
|
||||
_state_py set "${1:-}" "${2:-}"
|
||||
}
|
||||
|
||||
state_get() {
|
||||
_state_have_py || return 0
|
||||
_state_py get "$1"
|
||||
_state_py get "${1:-}"
|
||||
}
|
||||
|
||||
state_append() {
|
||||
_state_have_py || { printf ' [state] python3 unavailable — not recording %s\n' "$1" >&2; return 0; }
|
||||
_state_py append "$1" "$2"
|
||||
_state_have_py || { printf ' [state] python3 unavailable — not recording %s\n' "${1:-}" >&2; return 0; }
|
||||
_state_py append "${1:-}" "${2:-}"
|
||||
}
|
||||
|
||||
state_list() {
|
||||
_state_have_py || return 0
|
||||
_state_py list "$1"
|
||||
_state_py list "${1:-}"
|
||||
}
|
||||
|
||||
state_unset() {
|
||||
_state_have_py || return 0
|
||||
_state_py unset "$1"
|
||||
_state_py unset "${1:-}"
|
||||
}
|
||||
+39
-21
@@ -49,6 +49,18 @@ _u_run() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
# Best-effort teardown step (rule R7): run it and, on failure, warn and keep
|
||||
# going. By the time these steps run the services are already stopped, so an
|
||||
# uninstall must never die mid-teardown and strand the box half-removed over a
|
||||
# single EBUSY (live bind-mount), EROFS or ENOSPC rm/mkdir.
|
||||
_u_try() {
|
||||
if _u_run "$@"; then
|
||||
return 0
|
||||
fi
|
||||
_u_warn "Step failed (continuing): $*"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 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() {
|
||||
@@ -64,7 +76,10 @@ _u_firewall_cleanup() {
|
||||
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' ' ')"
|
||||
# `|| true` mirrors the state_get line above: under the entry points'
|
||||
# pipefail a failing state_list would otherwise abort the WHOLE uninstall
|
||||
# before anything was removed.
|
||||
ports="$(state_list firewall_ports 2>/dev/null | tr '\n' ' ' || true)"
|
||||
[ -n "$ports" ] || return 0
|
||||
|
||||
_u_info "Removing firewall rules we added ($backend): $ports"
|
||||
@@ -119,44 +134,44 @@ serverkit_uninstall_core() {
|
||||
|
||||
# ---- systemd unit ------------------------------------------------------
|
||||
_u_info "Removing systemd service..."
|
||||
_u_run rm -f /etc/systemd/system/serverkit.service
|
||||
_u_try 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
|
||||
_u_try rm -f /etc/nginx/sites-enabled/serverkit.conf
|
||||
_u_try rm -f /etc/nginx/sites-available/serverkit.conf
|
||||
_u_try rm -f /etc/nginx/sites-available/serverkit-insecure.conf
|
||||
_u_try rm -f /etc/nginx/sites-available/example.conf.template
|
||||
_u_try rm -f /etc/nginx/conf.d/serverkit-tls.conf
|
||||
_u_try 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
|
||||
_u_try rm -f /etc/apt/apt.conf.d/99-serverkit-lock-wait.conf
|
||||
|
||||
# ---- CLI symlink -------------------------------------------------------
|
||||
_u_run rm -f /usr/local/bin/serverkit
|
||||
_u_try 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_try rm -rf "$install_dir"
|
||||
_u_try rm -rf "${install_dir}-a"
|
||||
_u_try rm -rf "${install_dir}-b"
|
||||
_u_try rm -rf "${install_dir}.backup"
|
||||
_u_ok "Installation directory removed"
|
||||
|
||||
# ---- Logs (always transient) -------------------------------------------
|
||||
_u_run rm -rf "$log_dir"
|
||||
_u_try 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_try 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
|
||||
@@ -164,13 +179,16 @@ serverkit_uninstall_core() {
|
||||
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_try 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
|
||||
# ---- Telemetry (best-effort; bounded so an air-gapped box never hangs,
|
||||
# ---- and a --dry-run never phones home) ---------------------------------
|
||||
if [ "${SERVERKIT_UNINSTALL_DRY_RUN:-0}" != "1" ]; then
|
||||
curl -s --max-time 5 "https://serverkit.ai/track/uninstall" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Snapshot the live SQLite DB into the (preserved) backup dir so a default
|
||||
@@ -184,7 +202,7 @@ _u_snapshot_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"
|
||||
_u_try mkdir -p "$backup_dir"
|
||||
if _u_run cp "$db_src" "$dest" 2>/dev/null; then
|
||||
_u_ok "Database snapshot saved to $dest"
|
||||
return 0
|
||||
@@ -194,7 +212,7 @@ _u_snapshot_db() {
|
||||
# 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"
|
||||
_u_try 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
|
||||
|
||||
+269
-9
@@ -1,24 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Unit tests for the multi-distro abstraction libs:
|
||||
# scripts/lib/pkg.sh — package-manager abstraction
|
||||
# scripts/lib/init.sh — init-system / service control
|
||||
# scripts/lib/env.sh — container / WSL / systemd detection
|
||||
# scripts/lib/pkg.sh — package-manager abstraction
|
||||
# scripts/lib/init.sh — init-system / service control
|
||||
# scripts/lib/env.sh — container / WSL / systemd detection
|
||||
# scripts/lib/state.sh — install-state tracker (warn-and-no-op contract)
|
||||
# scripts/lib/firewall.sh — firewall abstraction (detection under pipefail)
|
||||
# scripts/lib/uninstall.sh — canonical teardown (never dies mid-teardown)
|
||||
# plus the root uninstall.sh entry point's remnants-only fallback path.
|
||||
#
|
||||
# All three are pure, override-friendly, and dry-run aware, so they test
|
||||
# deterministically on any host (including this Windows/Git-Bash dev box)
|
||||
# without touching a real package manager, init system, or /proc.
|
||||
# Everything is pure, override-friendly, and dry-run aware — or exercised
|
||||
# against throwaway fixtures and PATH stubs — so it tests deterministically on
|
||||
# any host (including this Windows/Git-Bash dev box) without touching a real
|
||||
# package manager, init system, firewall, or /etc.
|
||||
#
|
||||
# Each unit-under-test runs in a subshell that re-enables `set -Eeuo pipefail`,
|
||||
# so an unguarded command silently aborting under set -e is caught here as a
|
||||
# failed assertion rather than on someone's server.
|
||||
#
|
||||
# Run: bash scripts/test/test_lib.sh
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LIB_DIR="$(cd "$SCRIPT_DIR/../lib" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
ok() { PASS=$((PASS + 1)); printf ' \033[32m✔\033[0m %s\n' "$1"; }
|
||||
bad() { FAIL=$((FAIL + 1)); printf ' \033[31m✘\033[0m %s\n' "$1"; }
|
||||
SKIP=0
|
||||
ok() { PASS=$((PASS + 1)); printf ' \033[32m✔\033[0m %s\n' "$1"; }
|
||||
bad() { FAIL=$((FAIL + 1)); printf ' \033[31m✘\033[0m %s\n' "$1"; }
|
||||
skip() { SKIP=$((SKIP + 1)); printf ' \033[33m∼\033[0m %s (skipped)\n' "$1"; }
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
@@ -147,6 +159,254 @@ else
|
||||
bad "has_systemd force-override broken"
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# init.sh — L13: zero-arg safety under set -u, the detection cache, and the
|
||||
# init_reload contract.
|
||||
# ==========================================================================
|
||||
errf="$WORK/init-noargs.err"
|
||||
if ( set -Eeuo pipefail; init_start ) 2>"$errf"; then
|
||||
bad "init_start with no args must return non-zero"
|
||||
elif grep -q 'unbound variable' "$errf"; then
|
||||
bad "init_start with no args died on an unbound \$1 instead of a clean warn"
|
||||
elif grep -q 'requires a service name' "$errf"; then
|
||||
ok "init_start with no args warns and returns 1 (set -u safe)"
|
||||
else
|
||||
bad "init_start with no args returned 1 but without the missing-name warning"
|
||||
fi
|
||||
if ( set -Eeuo pipefail; init_is_active ) 2>"$errf"; then
|
||||
bad "init_is_active with no args must return non-zero"
|
||||
elif grep -q 'unbound variable' "$errf"; then
|
||||
bad "init_is_active with no args died on an unbound \$1 under set -u"
|
||||
else
|
||||
ok "init_is_active with no args quietly returns 1 (set -u safe)"
|
||||
fi
|
||||
|
||||
# The lazily-populated cache must be read back — and the override must still
|
||||
# beat it (that ordering is what keeps per-call INIT_OVERRIDE tests working).
|
||||
got="$( set -Eeuo pipefail; INIT_OVERRIDE="" _INIT_DETECTED=openrc init_detect )"
|
||||
got2="$( set -Eeuo pipefail; INIT_OVERRIDE=runit _INIT_DETECTED=openrc init_detect )"
|
||||
if [ "$got" = "openrc" ] && [ "$got2" = "runit" ]; then
|
||||
ok "init_detect serves the _INIT_DETECTED cache, but INIT_OVERRIDE still wins"
|
||||
else
|
||||
bad "init_detect cache/override order wrong (cache→[$got], override→[$got2])"
|
||||
fi
|
||||
|
||||
# init_reload: a NAMED reload behaves like init_start (emits the tool command,
|
||||
# warns-and-fails under 'none'); a BARE reload is a systemd-only no-op.
|
||||
out="$( set -Eeuo pipefail; INIT_OVERRIDE=openrc INIT_DRY_RUN=1 init_reload serverkit 2>&1 )"
|
||||
reload_ok=1
|
||||
printf '%s' "$out" | grep -qF "rc-service serverkit reload" || { bad "init_reload openrc dry-run: [$out]"; reload_ok=0; }
|
||||
( set -Eeuo pipefail; INIT_OVERRIDE=openrc init_reload ) >/dev/null 2>&1 || { bad "bare init_reload must be a no-op 0 on non-systemd inits"; reload_ok=0; }
|
||||
( set -Eeuo pipefail; INIT_OVERRIDE=none init_reload serverkit ) >/dev/null 2>&1 && { bad "named init_reload under 'none' must return non-zero like its siblings"; reload_ok=0; }
|
||||
[ "$reload_ok" = "1" ] && ok "init_reload: named reload matches the siblings' contract; bare reload is systemd-only"
|
||||
|
||||
# ==========================================================================
|
||||
# pkg.sh — L10: pkg_refresh promises "never propagate the manager's exit".
|
||||
# ==========================================================================
|
||||
t="$WORK/pkgrefresh"; mkdir -p "$t/bin"
|
||||
printf '#!/bin/sh\nexit 7\n' > "$t/bin/apt-get"; chmod +x "$t/bin/apt-get"
|
||||
# sudo shim: on a non-root Linux host _pkg_sudo would prepend sudo; keep the
|
||||
# stub chain deterministic everywhere.
|
||||
printf '#!/bin/sh\nexec "$@"\n' > "$t/bin/sudo"; chmod +x "$t/bin/sudo"
|
||||
if ( set -Eeuo pipefail; PATH="$t/bin:$PATH" PKG_MGR_OVERRIDE=apt PKG_DRY_RUN=0 pkg_refresh ); then
|
||||
ok "pkg_refresh returns 0 when the manager's refresh fails (best-effort doc contract)"
|
||||
else
|
||||
bad "pkg_refresh propagated a failing package manager's exit (L10 regression)"
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# firewall.sh — L11: ufw detection is capture-then-case (no `| grep -q`
|
||||
# SIGPIPE race under a pipefail caller), and "inactive" is never misread as
|
||||
# active. L13: zero-arg calls are safe no-ops under set -u.
|
||||
# ==========================================================================
|
||||
t="$WORK/fw"; mkdir -p "$t/on" "$t/off"
|
||||
# The active stub prints a rule table far bigger than a 64K pipe buffer: with
|
||||
# the old `ufw status | grep -q` shape, grep exits at the first line and the
|
||||
# still-writing ufw dies of SIGPIPE — flipping the pipeline non-zero under
|
||||
# pipefail and misdetecting the box. Capture-then-case must shrug this off.
|
||||
cat > "$t/on/ufw" <<'UFW_EOF'
|
||||
#!/bin/sh
|
||||
echo "Status: active"
|
||||
i=0
|
||||
while [ "$i" -lt 2000 ]; do
|
||||
echo "22/tcp ALLOW Anywhere # filler $i"
|
||||
i=$((i+1))
|
||||
done
|
||||
UFW_EOF
|
||||
chmod +x "$t/on/ufw"
|
||||
printf '#!/bin/sh\necho "Status: inactive"\n' > "$t/off/ufw"; chmod +x "$t/off/ufw"
|
||||
got="$( set -Eeuo pipefail; source "$LIB_DIR/firewall.sh"; PATH="$t/on" FIREWALL_BACKEND="" firewall_detect )"
|
||||
got2="$( set -Eeuo pipefail; source "$LIB_DIR/firewall.sh"; PATH="$t/off" FIREWALL_BACKEND="" firewall_detect )"
|
||||
if [ "$got" = "ufw" ] && [ "$got2" = "none" ]; then
|
||||
ok "firewall_detect sees an active ufw under pipefail and never misreads 'inactive'"
|
||||
else
|
||||
bad "firewall_detect wrong: active→[$got] (want ufw), inactive→[$got2] (want none)"
|
||||
fi
|
||||
|
||||
errf="$WORK/fw-noargs.err"
|
||||
if ( set -Eeuo pipefail; source "$LIB_DIR/firewall.sh"
|
||||
firewall_open && firewall_close && firewall_manual_hint >/dev/null ) 2>"$errf"; then
|
||||
if grep -q 'unbound variable' "$errf"; then
|
||||
bad "zero-arg firewall helpers leaked an unbound-variable error"
|
||||
else
|
||||
ok "firewall_open/close/manual_hint with zero args are safe no-ops under set -u"
|
||||
fi
|
||||
else
|
||||
bad "zero-arg firewall helpers aborted under set -u: $(tr -d '\r' < "$errf")"
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# state.sh — L1: the header promises warn-and-no-op, so a PRESENT python3
|
||||
# with an unusable state file (path is a directory, parent is a file,
|
||||
# unwritable dir) must warn and exit 0 for mutations, and stay quiet-empty
|
||||
# for reads — never abort a `set -Eeuo pipefail` caller.
|
||||
# ==========================================================================
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
sdir="$WORK/state-as-dir"; mkdir -p "$sdir"
|
||||
errf="$WORK/state-l1.err"
|
||||
if ( set -Eeuo pipefail; source "$LIB_DIR/state.sh"
|
||||
SERVERKIT_STATE_FILE="$sdir" state_set firewall_backend ufw ) 2>"$errf"; then
|
||||
if grep -q '\[state\]' "$errf"; then
|
||||
ok "state_set warns and exits 0 when the state path is a directory"
|
||||
else
|
||||
bad "state_set exited 0 on a directory state path but never warned"
|
||||
fi
|
||||
else
|
||||
bad "state_set ABORTED when the state path is a directory (L1 regression)"
|
||||
fi
|
||||
|
||||
if out="$( set -Eeuo pipefail; source "$LIB_DIR/state.sh"
|
||||
SERVERKIT_STATE_FILE="$sdir" state_get firewall_backend 2>/dev/null )" \
|
||||
&& [ -z "$out" ]; then
|
||||
ok "state_get is quiet-empty (exit 0) on an unreadable state path"
|
||||
else
|
||||
bad "state_get aborted or printed [$out] on an unreadable state path"
|
||||
fi
|
||||
|
||||
if ( set -Eeuo pipefail; source "$LIB_DIR/state.sh"
|
||||
ports="$(SERVERKIT_STATE_FILE="$sdir" state_list firewall_ports | tr '\n' ' ')"
|
||||
[ -z "${ports// /}" ] ); then
|
||||
ok "state_list | tr survives set -Eeuo pipefail on an unreadable state path"
|
||||
else
|
||||
bad "state_list | tr aborted under pipefail (the L2-shaped uninstall killer)"
|
||||
fi
|
||||
|
||||
: > "$WORK/state-blocker"
|
||||
if ( set -Eeuo pipefail; source "$LIB_DIR/state.sh"
|
||||
SERVERKIT_STATE_FILE="$WORK/state-blocker/state.json" state_append firewall_ports 80/tcp ) 2>/dev/null; then
|
||||
ok "state_append warns and exits 0 when the state parent is a regular file"
|
||||
else
|
||||
bad "state_append ABORTED when the state parent is a regular file (L1 regression)"
|
||||
fi
|
||||
|
||||
# Unwritable parent dir. As root (distro-container CI) or on Windows the
|
||||
# write may legitimately succeed — the contract under test is only
|
||||
# "never non-zero", which must hold either way.
|
||||
ro="$WORK/state-ro"; mkdir -p "$ro"; chmod 555 "$ro" 2>/dev/null || true
|
||||
if ( set -Eeuo pipefail; source "$LIB_DIR/state.sh"
|
||||
SERVERKIT_STATE_FILE="$ro/state.json" state_set k v ) 2>/dev/null; then
|
||||
ok "state_set exits 0 with an unwritable state directory"
|
||||
else
|
||||
bad "state_set ABORTED on an unwritable state directory (L1 regression)"
|
||||
fi
|
||||
chmod 755 "$ro" 2>/dev/null || true
|
||||
else
|
||||
skip "state.sh warn-and-no-op contract — python3 unavailable here"
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# lib/uninstall.sh — L2: a failing `state_list | tr` must not abort the whole
|
||||
# uninstall; L3: failed rm/mkdir teardown steps warn and continue (_u_try);
|
||||
# L8: telemetry curl is bounded with --max-time and skipped under --dry-run.
|
||||
# ==========================================================================
|
||||
t="$WORK/ucore"; mkdir -p "$t/bin" "$t/inst/scripts/lib"
|
||||
# Hostile state fixture: state_list emits one port then FAILS mid-pipe —
|
||||
# exactly the shape that used to kill the uninstall before it removed anything.
|
||||
cat > "$t/inst/scripts/lib/state.sh" <<'HOSTILE_EOF'
|
||||
state_get() { printf 'iptables'; }
|
||||
state_list() { printf '80/tcp\n'; return 1; }
|
||||
HOSTILE_EOF
|
||||
printf 'firewall_close() { return 0; }\n' > "$t/inst/scripts/lib/firewall.sh"
|
||||
# rm always fails (EBUSY/EROFS stand-in); docker/systemctl fail hermetically;
|
||||
# curl logs its argv so the telemetry contract is observable.
|
||||
printf '#!/bin/sh\nexit 1\n' > "$t/bin/rm"; chmod +x "$t/bin/rm"
|
||||
printf '#!/bin/sh\nexit 1\n' > "$t/bin/docker"; chmod +x "$t/bin/docker"
|
||||
printf '#!/bin/sh\nexit 1\n' > "$t/bin/systemctl"; chmod +x "$t/bin/systemctl"
|
||||
CURL_LOG="$t/curl.log"; : > "$CURL_LOG"
|
||||
cat > "$t/bin/curl" <<EOF
|
||||
#!/bin/sh
|
||||
printf '%s\n' "\$*" >> "$CURL_LOG"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$t/bin/curl"
|
||||
|
||||
out="$(
|
||||
set -Eeuo pipefail
|
||||
source "$LIB_DIR/uninstall.sh"
|
||||
export PATH="$t/bin:$PATH"
|
||||
export SERVERKIT_DIR="$t/inst"
|
||||
serverkit_uninstall_core 2>&1
|
||||
)"
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -qF 'Removing firewall rules we added (iptables): 80/tcp'; then
|
||||
ok "uninstall survives a state_list failing mid-pipe and still gets its firewall record (L2)"
|
||||
else
|
||||
bad "uninstall rc=$rc or never reached the firewall step past the failing state_list: [$out]"
|
||||
fi
|
||||
if printf '%s' "$out" | grep -qF 'Step failed (continuing): rm' && \
|
||||
printf '%s' "$out" | grep -qF 'Preserved user data'; then
|
||||
ok "uninstall warns on every failed rm and continues to the last teardown step (L3/_u_try)"
|
||||
else
|
||||
bad "uninstall did not warn-and-continue past failing rm steps (L3 regression): [$out]"
|
||||
fi
|
||||
if grep -q -- '--max-time 5' "$CURL_LOG"; then
|
||||
ok "telemetry curl is bounded with --max-time (no air-gapped hang) (L8)"
|
||||
else
|
||||
bad "telemetry curl ran unbounded; curl saw: $(tr '\n' ';' < "$CURL_LOG")"
|
||||
fi
|
||||
|
||||
: > "$CURL_LOG"
|
||||
if (
|
||||
set -Eeuo pipefail
|
||||
source "$LIB_DIR/uninstall.sh"
|
||||
export PATH="$t/bin:$PATH"
|
||||
export SERVERKIT_DIR="$t/inst" SERVERKIT_UNINSTALL_DRY_RUN=1
|
||||
serverkit_uninstall_core
|
||||
) >/dev/null 2>&1 && [ ! -s "$CURL_LOG" ]; then
|
||||
ok "uninstall --dry-run never phones home (L8)"
|
||||
else
|
||||
bad "uninstall --dry-run failed or still called curl: $(tr '\n' ';' < "$CURL_LOG")"
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# root uninstall.sh — L4: a remnants-only box (no scripts/lib anywhere) must
|
||||
# still be uninstallable via the inline fallback teardown, honoring --dry-run.
|
||||
# ==========================================================================
|
||||
t="$WORK/fallback"; mkdir -p "$t/remnants" "$t/remnants-a"
|
||||
printf '1.2.3\n' > "$t/remnants/VERSION"
|
||||
printf 'x\n' > "$t/remnants/leftover.txt"
|
||||
cp "$ROOT_DIR/uninstall.sh" "$t/uninstall.sh" # copied away from the repo so no scripts/lib is findable
|
||||
|
||||
out="$( cd "$t" && SERVERKIT_UNINSTALL_ALLOW_NONROOT=1 SERVERKIT_UNINSTALL_LOG="$t/un.log" \
|
||||
SERVERKIT_DIR="$t/remnants" bash ./uninstall.sh --dry-run --yes 2>&1 )"
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -qi 'fallback' && \
|
||||
printf '%s' "$out" | grep -qF "would run: rm -rf $t/remnants" && \
|
||||
[ -d "$t/remnants" ] && [ -d "$t/remnants-a" ]; then
|
||||
ok "root uninstall.sh with no lib announces the fallback; --dry-run changes nothing (L4)"
|
||||
else
|
||||
bad "fallback dry-run rc=$rc, or it mutated the fixture: [$out]"
|
||||
fi
|
||||
|
||||
out="$( cd "$t" && SERVERKIT_UNINSTALL_ALLOW_NONROOT=1 SERVERKIT_UNINSTALL_LOG="$t/un.log" \
|
||||
SERVERKIT_DIR="$t/remnants" bash ./uninstall.sh --yes 2>&1 )"
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ] && [ ! -e "$t/remnants" ] && [ ! -e "$t/remnants-a" ]; then
|
||||
ok "root uninstall.sh fallback removes the remnants and exits 0 (L4)"
|
||||
else
|
||||
bad "fallback teardown rc=$rc; remnants left: $(ls -d "$t"/remnants* 2>/dev/null | tr '\n' ' '): [$out]"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
printf '\n%d passed, %d failed\n\n' "$PASS" "$FAIL"
|
||||
printf '\n%d passed, %d failed, %d skipped\n\n' "$PASS" "$FAIL" "$SKIP"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
+62
-13
@@ -20,7 +20,9 @@ set -Eeuo pipefail
|
||||
########################################
|
||||
|
||||
INSTALL_DIR="${SERVERKIT_DIR:-/opt/serverkit}"
|
||||
LOG_FILE="/var/log/serverkit-uninstall.log"
|
||||
# SERVERKIT_UNINSTALL_LOG is a test hook (scripts/test/test_lib.sh) — real
|
||||
# uninstalls always log to /var/log.
|
||||
LOG_FILE="${SERVERKIT_UNINSTALL_LOG:-/var/log/serverkit-uninstall.log}"
|
||||
|
||||
PURGE=0
|
||||
KEEP_DATA=0
|
||||
@@ -58,8 +60,11 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p /var/log
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
# Logging is best-effort: an unwritable log location must not stop an
|
||||
# uninstall (or eat its output through a dead tee) — just skip the log file.
|
||||
if mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null && touch "$LOG_FILE" 2>/dev/null; then
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
fi
|
||||
|
||||
########################################
|
||||
# Colors / masthead
|
||||
@@ -107,7 +112,9 @@ print_header() {
|
||||
# Root check
|
||||
########################################
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
# SERVERKIT_UNINSTALL_ALLOW_NONROOT=1 is a test hook (scripts/test/test_lib.sh
|
||||
# exercises the script against throwaway fixtures) — real uninstalls need root.
|
||||
if [[ $EUID -ne 0 && "${SERVERKIT_UNINSTALL_ALLOW_NONROOT:-0}" != "1" ]]; then
|
||||
print_error "Please run as root (sudo)"
|
||||
exit 1
|
||||
fi
|
||||
@@ -127,10 +134,45 @@ for d in "$SELF_DIR/scripts/lib" "$INSTALL_DIR/scripts/lib"; do
|
||||
fi
|
||||
done
|
||||
if [ -z "$UNINSTALL_LIB" ]; then
|
||||
print_error "Cannot find scripts/lib/uninstall.sh — is ServerKit installed at $INSTALL_DIR?"
|
||||
exit 1
|
||||
# Remnants-only box: the install died early or /opt/serverkit was deleted
|
||||
# by hand, so the canonical routine is gone. Refusing here used to make
|
||||
# such a box impossible to clean up — fall back to a minimal teardown.
|
||||
print_warning "Cannot find scripts/lib/uninstall.sh (partial or broken install at $INSTALL_DIR)."
|
||||
print_warning "Falling back to a minimal cleanup of ServerKit remnants."
|
||||
fi
|
||||
|
||||
########################################
|
||||
# Minimal fallback teardown (no lib found)
|
||||
########################################
|
||||
|
||||
# Every step is best-effort and only ever removes serverkit-named paths; data
|
||||
# dirs (/var/lib/serverkit, /var/serverkit, backups) are never touched, so the
|
||||
# default "preserve user data" promise holds here too.
|
||||
_fb_run() {
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo " [uninstall] would run: $*"
|
||||
return 0
|
||||
fi
|
||||
"$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
fallback_teardown() {
|
||||
echo "Removing ServerKit remnants (minimal fallback)..."
|
||||
_fb_run systemctl stop serverkit
|
||||
_fb_run systemctl disable serverkit
|
||||
_fb_run rm -f /etc/systemd/system/serverkit.service
|
||||
_fb_run rm -f /etc/nginx/sites-enabled/serverkit.conf
|
||||
_fb_run rm -f /etc/nginx/sites-available/serverkit.conf
|
||||
_fb_run rm -f /etc/nginx/sites-available/serverkit-insecure.conf
|
||||
_fb_run rm -f /etc/nginx/conf.d/serverkit-tls.conf
|
||||
_fb_run rm -rf /etc/nginx/serverkit-conf.d
|
||||
_fb_run rm -f /etc/apt/apt.conf.d/99-serverkit-lock-wait.conf
|
||||
_fb_run rm -f /usr/local/bin/serverkit
|
||||
_fb_run rm -rf "$INSTALL_DIR" "${INSTALL_DIR}-a" "${INSTALL_DIR}-b" "${INSTALL_DIR}.backup"
|
||||
_fb_run systemctl daemon-reload
|
||||
echo "Minimal fallback cleanup finished."
|
||||
}
|
||||
|
||||
########################################
|
||||
# Confirm
|
||||
########################################
|
||||
@@ -157,15 +199,22 @@ fi
|
||||
# Run the canonical teardown
|
||||
########################################
|
||||
|
||||
# shellcheck source=scripts/lib/uninstall.sh
|
||||
source "$UNINSTALL_LIB"
|
||||
if [ -n "$UNINSTALL_LIB" ]; then
|
||||
# shellcheck source=scripts/lib/uninstall.sh
|
||||
source "$UNINSTALL_LIB"
|
||||
|
||||
export SERVERKIT_DIR="$INSTALL_DIR"
|
||||
export SERVERKIT_PURGE="$PURGE"
|
||||
export SERVERKIT_KEEP_DATA="$KEEP_DATA"
|
||||
export SERVERKIT_UNINSTALL_DRY_RUN="$DRY_RUN"
|
||||
export SERVERKIT_DIR="$INSTALL_DIR"
|
||||
export SERVERKIT_PURGE="$PURGE"
|
||||
export SERVERKIT_KEEP_DATA="$KEEP_DATA"
|
||||
export SERVERKIT_UNINSTALL_DRY_RUN="$DRY_RUN"
|
||||
|
||||
serverkit_uninstall_core
|
||||
serverkit_uninstall_core
|
||||
else
|
||||
if [ "$PURGE" = "1" ]; then
|
||||
print_warning "--purge is unavailable in fallback mode — data dirs are left in place"
|
||||
fi
|
||||
fallback_teardown
|
||||
fi
|
||||
|
||||
########################################
|
||||
# Finish
|
||||
|
||||
Reference in new issue
Block a user