From 1944a44f9407f1af4ceb3511ca1c127db9c71417 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sat, 26 Sep 2026 11:30:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(uninstall):=20purge=20=E5=89=8D=E9=A2=84?= =?UTF-8?q?=E6=A3=80=20felis=20=E8=A7=92=E8=89=B2=E5=9C=A8=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E5=BA=93=E7=9A=84=E4=BE=9D=E8=B5=96=EF=BC=8CDROP=20RO?= =?UTF-8?q?LE=20=E4=B8=8D=E5=86=8D=E5=8D=8A=E9=80=94=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/uninstall.sh | 34 ++++++++++++++++++++++++++++++++ deploy/uninstall_test.sh | 42 +++++++++++++++++++++++++++++++++++++++- docs/operations.md | 2 +- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/deploy/uninstall.sh b/deploy/uninstall.sh index 98cd541..36cf7bc 100644 --- a/deploy/uninstall.sh +++ b/deploy/uninstall.sh @@ -340,6 +340,39 @@ remove_hba_block() { # file rm -f "$tmp" } +# check_database_purge runs before anything is removed: DROP ROLE refuses a role that +# still owns a database or holds anything in one besides felis (a felis_pgint left by +# `felis db pgint`, a grant made by hand), and by then the units and k3s are already gone. +# It lists what holds the role instead, so the purge either runs to the end or not at all. +check_database_purge() { + [ "$PURGE" = 1 ] || return 0 + systemctl is-active --quiet postgresql 2>/dev/null || return 0 + local sql held err + read -r -d '' sql < OWNER TO postgres', or REASSIGN OWNED BY ${DB_USER} TO postgres; DROP OWNED BY ${DB_USER}; inside that database), or rerun without --purge" +} + purge_database() { [ "$PURGE" = 1 ] || return 0 if ! systemctl is-active --quiet postgresql 2>/dev/null; then @@ -395,6 +428,7 @@ main() { [ -e "$STATE_DIR" ] || [ -e "$HOST_BIN" ] || [ -e "$OPT_DIR" ] \ || die "no Felis install here (${STATE_DIR}, ${HOST_BIN} and ${OPT_DIR} are all absent)" decide_k3s + check_database_purge print_plan confirm final_backup diff --git a/deploy/uninstall_test.sh b/deploy/uninstall_test.sh index 3abd2c6..a625341 100644 --- a/deploy/uninstall_test.sh +++ b/deploy/uninstall_test.sh @@ -89,7 +89,17 @@ run_uninstall() { shift 3 case "$*" in *"SHOW hba_file"*) echo "$ROOT/h/hba.conf" ;; - *) echo "PSQL $* $(cat)" >> "$calls" ;; + *) + sql="$(cat)" + case "$sql" in + # What the felis role still holds: PG_HELD, one line each; PG_CHECK=fail for a + # server that refuses the query. + *pg_shdepend*) + echo "PSQL-CHECK" >> "$calls" + [ "${PG_CHECK:-}" = fail ] && { echo "psql: error: connection refused" >&2; return 2; } + [ -z "${PG_HELD:-}" ] || printf "%s\n" "$PG_HELD" ;; + *) echo "PSQL $* $sql" >> "$calls" ;; + esac ;; esac } docker() { echo "DOCKER $*" >> "$calls"; } @@ -161,6 +171,7 @@ fresh_host out="$(run_uninstall "default felis minecraft" --purge --yes)" calls="$(cat "$root/calls")" refute "purge takes no bundle" "db backup" "$calls" +expect "purge asks what the role holds first" "PSQL-CHECK" "$calls" expect "purge drops the database" "DROP DATABASE IF EXISTS felis;" "$calls" expect "purge drops the role" "DROP ROLE IF EXISTS felis;" "$calls" expect "purge puts listen_addresses back" "ALTER SYSTEM RESET listen_addresses;" "$calls" @@ -179,6 +190,35 @@ case "$hba" in esac expect "purge cleans Docker's build cache" "DOCKER builder prune -af" "$calls" +# --- a purge DROP ROLE would refuse ------------------------------------------------------- +# The VM drill: `felis db pgint` had left felis_pgint owned by felis, the purge removed the +# units and k3s, then stopped at DROP ROLE with half the host gone. +untouched() { # label + [ -d "$root/h/opt" ] && [ -f "$root/h/units/felis-velocity.service" ] && [ -d "$root/h/etc" ] \ + && ! grep -q "k3s-uninstall.sh\|DROP DATABASE\|SYSTEMCTL disable" "$root/calls" \ + && echo "PASS $1" \ + || { echo "FAIL $1: $(cat "$root/calls")"; fails=$((fails + 1)); } +} +fresh_host +out="$(PG_HELD="database felis_pgint (owned) +objects in database shop (privileges)" run_uninstall "default felis minecraft" --purge --yes)" +expect "a purge the role cannot survive is refused" "the felis role still holds database felis_pgint (owned); objects in database shop (privileges)" "$out" +expect "with the way to hand the database over" "ALTER DATABASE OWNER TO postgres" "$out" +untouched "nothing is removed when DROP ROLE would fail" +fresh_host +out="$(PG_HELD="database felis_pgint (owned)" CONFIRM_TTY="$root/no-tty/x" run_uninstall "default felis minecraft" --purge)" +expect "the check comes before the plan and the prompt" "the felis role still holds database felis_pgint" "$out" +refute "so nobody confirms a purge that cannot finish" "this will remove" "$out" +fresh_host +out="$(PG_CHECK=fail run_uninstall "default felis minecraft" --purge --yes)" +expect "a server that cannot be asked stops the purge" "could not ask PostgreSQL what the felis role still holds, so nothing was removed: psql: error: connection refused" "$out" +untouched "nothing is removed when the check cannot run" +fresh_host +PG_HELD="database felis_pgint (owned)" run_uninstall "default felis minecraft" --yes >/dev/null +calls="$(cat "$root/calls")" +refute "keep-data drops no role, so it asks nothing" "PSQL-CHECK" "$calls" +expect "and goes on" "RUN k3s-uninstall.sh" "$calls" + # --- the pieces read before they are removed --------------------------------------------- fresh_host lib() { STATE_DIR="$root/h/etc" OPT_DIR="$root/h/opt" UNIT_DIR="$root/h/units" FELIS_UNINSTALL_SOURCED=1 \ diff --git a/docs/operations.md b/docs/operations.md index 0efa668..8a18281 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -189,7 +189,7 @@ the cluster holds nothing but Felis's namespaces; when it runs anything else onl | | keep data (default) | `--purge` | |---|---|---| | Final database bundle | taken first (`felis db backup -label manual`); a failure stops the uninstall before anything is removed. `--no-backup` skips it | none | -| `felis` database and role | kept | dropped; `listen_addresses` and `pg_hba.conf` go back to how they were | +| `felis` database and role | kept | dropped; `listen_addresses` and `pg_hba.conf` go back to how they were. Checked before anything is removed: a role that still owns another database (a `felis_pgint` left by `felis db pgint`) or holds grants elsewhere stops the purge up front with the list and the `ALTER DATABASE … OWNER TO postgres` to run | | `/etc/felis` (secrets, `felis.toml`, `offsite.env`, tunnel config) | kept; `bootstrap.done` and the per-run records go | deleted, with the tunnel's credentials file | | `/var/lib/felis` (database bundles) | kept | deleted | | Worlds, archives, registry, uploads | moved to `/var/lib/felis/retained/k3s-storage-/` (with `--keep-k3s`: their volumes switch to `Retain` and stay in place) | deleted |