fix(uninstall): purge 前预检 felis 角色在其他库的依赖,DROP ROLE 不再半途失败

This commit is contained in:
Lemon-miaow committed 2026-09-26 11:30:31 +08:00
1 parent 54a533103a
commit 1944a44f94
3 files changed
+76 -2

No files matched your search

+34
View File
@@ -340,6 +340,39 @@ remove_hba_block() { # file
rm -f "$tmp" 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 <<SQL || true
WITH r AS (SELECT oid FROM pg_roles WHERE rolname = '${DB_USER}'),
f AS (SELECT oid FROM pg_database WHERE datname = '${DB_NAME}')
SELECT DISTINCT CASE
WHEN s.classid = 'pg_database'::regclass THEN 'database ' || (SELECT datname FROM pg_database WHERE oid = s.objid)
WHEN s.classid = 'pg_tablespace'::regclass THEN 'tablespace ' || (SELECT spcname FROM pg_tablespace WHERE oid = s.objid)
ELSE 'objects in database ' || (SELECT datname FROM pg_database WHERE oid = s.dbid)
END || CASE s.deptype WHEN 'o' THEN ' (owned)' ELSE ' (privileges)' END
FROM pg_shdepend s
WHERE s.refclassid = 'pg_authid'::regclass AND s.refobjid = (SELECT oid FROM r)
AND s.dbid IS DISTINCT FROM (SELECT oid FROM f)
AND NOT (s.classid = 'pg_database'::regclass AND s.objid IS NOT DISTINCT FROM (SELECT oid FROM f))
ORDER BY 1;
SQL
err="$(mktemp)"
if ! held="$(as_postgres psql -v ON_ERROR_STOP=1 -tAq 2>"$err" <<<"$sql")"; then
held="$(cat "$err")"
rm -f "$err"
die "could not ask PostgreSQL what the ${DB_USER} role still holds, so nothing was removed: ${held}"
fi
rm -f "$err"
[ -n "$held" ] || return 0
die "the ${DB_USER} role still holds $(printf '%s' "$held" | paste -sd ';' - | sed 's/;/; /g'), so DROP ROLE would fail halfway through the purge; nothing was removed. Hand them to postgres first (sudo -u postgres psql -c 'ALTER DATABASE <name> 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_database() {
[ "$PURGE" = 1 ] || return 0 [ "$PURGE" = 1 ] || return 0
if ! systemctl is-active --quiet postgresql 2>/dev/null; then if ! systemctl is-active --quiet postgresql 2>/dev/null; then
@@ -395,6 +428,7 @@ main() {
[ -e "$STATE_DIR" ] || [ -e "$HOST_BIN" ] || [ -e "$OPT_DIR" ] \ [ -e "$STATE_DIR" ] || [ -e "$HOST_BIN" ] || [ -e "$OPT_DIR" ] \
|| die "no Felis install here (${STATE_DIR}, ${HOST_BIN} and ${OPT_DIR} are all absent)" || die "no Felis install here (${STATE_DIR}, ${HOST_BIN} and ${OPT_DIR} are all absent)"
decide_k3s decide_k3s
check_database_purge
print_plan print_plan
confirm confirm
final_backup final_backup
+41 -1
View File
@@ -89,7 +89,17 @@ run_uninstall() {
shift 3 shift 3
case "$*" in case "$*" in
*"SHOW hba_file"*) echo "$ROOT/h/hba.conf" ;; *"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 esac
} }
docker() { echo "DOCKER $*" >> "$calls"; } docker() { echo "DOCKER $*" >> "$calls"; }
@@ -161,6 +171,7 @@ fresh_host
out="$(run_uninstall "default felis minecraft" --purge --yes)" out="$(run_uninstall "default felis minecraft" --purge --yes)"
calls="$(cat "$root/calls")" calls="$(cat "$root/calls")"
refute "purge takes no bundle" "db backup" "$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 database" "DROP DATABASE IF EXISTS felis;" "$calls"
expect "purge drops the role" "DROP ROLE 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" expect "purge puts listen_addresses back" "ALTER SYSTEM RESET listen_addresses;" "$calls"
@@ -179,6 +190,35 @@ case "$hba" in
esac esac
expect "purge cleans Docker's build cache" "DOCKER builder prune -af" "$calls" 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 <name> 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 --------------------------------------------- # --- the pieces read before they are removed ---------------------------------------------
fresh_host fresh_host
lib() { STATE_DIR="$root/h/etc" OPT_DIR="$root/h/opt" UNIT_DIR="$root/h/units" FELIS_UNINSTALL_SOURCED=1 \ lib() { STATE_DIR="$root/h/etc" OPT_DIR="$root/h/opt" UNIT_DIR="$root/h/units" FELIS_UNINSTALL_SOURCED=1 \
+1 -1
View File
@@ -189,7 +189,7 @@ the cluster holds nothing but Felis's namespaces; when it runs anything else onl
| | keep data (default) | `--purge` | | | 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 | | 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 | | `/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 | | `/var/lib/felis` (database bundles) | kept | deleted |
| Worlds, archives, registry, uploads | moved to `/var/lib/felis/retained/k3s-storage-<stamp>/` (with `--keep-k3s`: their volumes switch to `Retain` and stay in place) | deleted | | Worlds, archives, registry, uploads | moved to `/var/lib/felis/retained/k3s-storage-<stamp>/` (with `--keep-k3s`: their volumes switch to `Retain` and stay in place) | deleted |