mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 08:36:55 +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>
106 lines
3.0 KiB
Bash
106 lines
3.0 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# scripts/lib/state.sh — tiny install-state tracker.
|
|
#
|
|
# ServerKit makes a handful of global, host-wide changes during install
|
|
# (firewall ports, an apt lock-wait drop-in, an nginx TLS snippet). For uninstall
|
|
# to be reliable and for re-runs to be idempotent, we record exactly what we
|
|
# touched in /etc/serverkit/install-state.json and undo only those things on the
|
|
# way out — never a rule or file the operator added themselves (Goal G8).
|
|
#
|
|
# 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.
|
|
#
|
|
# API:
|
|
# state_set <key> <value> set a scalar
|
|
# state_get <key> print a scalar ("" if absent)
|
|
# state_append <key> <value> append to an array (deduped)
|
|
# state_list <key> print array items, one per line
|
|
# state_unset <key> remove a key entirely
|
|
#
|
|
# Override the file location with SERVERKIT_STATE_FILE (used by tests).
|
|
|
|
SERVERKIT_STATE_FILE="${SERVERKIT_STATE_FILE:-/etc/serverkit/install-state.json}"
|
|
|
|
_state_have_py() { command -v python3 >/dev/null 2>&1; }
|
|
|
|
_state_py() {
|
|
# Args: <action> <key> [value]
|
|
SERVERKIT_STATE_FILE="$SERVERKIT_STATE_FILE" python3 - "$@" <<'PY'
|
|
import json, os, sys
|
|
|
|
path = os.environ["SERVERKIT_STATE_FILE"]
|
|
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 ""
|
|
|
|
try:
|
|
with open(path) as fh:
|
|
data = json.load(fh)
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
except (FileNotFoundError, ValueError):
|
|
data = {}
|
|
|
|
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)
|
|
|
|
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)
|
|
PY
|
|
}
|
|
|
|
state_set() {
|
|
_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_append() {
|
|
_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_unset() {
|
|
_state_have_py || return 0
|
|
_state_py unset "$1"
|
|
}
|