Stop misreporting a reinstall's admin account and stale firewall rows

Bookstack: create-admin fails on a reinstall because the account is
already provisioned. That took the generic-failure branch, which printed
the upstream defaults (admin@admin.com / password) as "the" login — those
credentials were replaced on the first install, so the one line a user
would act on was the wrong one. Detect "already exists" and say the
existing account was kept and its password not reset.

Firewall: uninstall deleted only resource_type='port', orphaning the
port_tag_*/traefik_managed/url_accessible rows the rebuild reads. Every
rebuild then walked ports for long-gone apps and printed "Skipped: <app>
(app not found)" per row. Widen the uninstall delete to all non-'ip' rows
(the source), and prune already-orphaned rows in the rebuild (the
self-heal). Pruning requires both no container dir AND status != 1 in the
apps table, so a mid-flight install can't prune itself.

Verified on a live bookstack reinstall: admin path reports correctly,
firewall pruned nextcloud + stalwart once, second run silent, 11 rules
added / 0 failed throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-18 22:09:37 +01:00
parent 133f54cd4f
commit 861a51a22c
3 changed files with 40 additions and 2 deletions

View File

@ -69,6 +69,15 @@ PHP
echo " Email : ${bookstack_target_email}" echo " Email : ${bookstack_target_email}"
echo " Password : ${bookstack_target_pass}" echo " Password : ${bookstack_target_pass}"
echo "" echo ""
elif [[ "$bookstack_create_output" == *"already exists"* ]]; then
# Reinstall over existing data: the account is already provisioned, so
# create-admin refuses. NOT a failure, and printing the upstream
# defaults here would be actively wrong — those credentials were
# replaced on the first install, and the password in the config was
# never re-applied to the live account.
isSuccessful "Bookstack admin account already exists (email: $bookstack_target_email) — kept as-is."
isNotice "Its password was NOT reset by this install. Sign in with the credentials from the original install, or reset from inside Bookstack."
echo ""
else else
isNotice "Bookstack admin auto-create failed (exit $bookstack_create_rc). Output:" isNotice "Bookstack admin auto-create failed (exit $bookstack_create_rc). Output:"
echo "$bookstack_create_output" | sed 's/^/ /' echo "$bookstack_create_output" | sed 's/^/ /'

View File

@ -43,6 +43,27 @@ firewallRebuildFromDatabase()
WHERE nr1.resource_type = 'port_tag_external' WHERE nr1.resource_type = 'port_tag_external'
AND nr1.status = 'active'" AND nr1.status = 'active'"
# Self-heal rows orphaned by an uninstall from before portsRemoveFromDatabase
# widened its DELETE (it removed only resource_type='port', leaving the
# port_tag_* rows behind). Without this, every rebuild walked the ports of
# long-gone apps and printed a "Skipped: <app> (app not found)" line each.
# Both signals are required before deleting — no container dir AND not marked
# installed in the apps table — so an install still mid-flight (tag rows
# written, dir not yet created) is never pruned out from under itself.
local stale_apps=() _stale_app
while IFS= read -r _stale_app; do
[[ -n "$_stale_app" ]] || continue
[[ -d "$containers_dir/$_stale_app" ]] && continue
[[ "$(sqlite3 "$docker_dir/$db_file" "SELECT status FROM apps WHERE name = '$_stale_app';" 2>/dev/null)" == "1" ]] && continue
stale_apps+=("$_stale_app")
done <<< "$(sqlite3 "$docker_dir/$db_file" "SELECT DISTINCT app_name FROM network_resources;" 2>/dev/null)"
if ((${#stale_apps[@]} > 0)); then
for _stale_app in "${stale_apps[@]}"; do
runInstallOp sqlite3 "$docker_dir/$db_file" "DELETE FROM network_resources WHERE app_name = '$_stale_app';" 2>/dev/null
done
isNotice "Pruned stale network entries for uninstalled apps: ${stale_apps[*]}"
fi
local total_added=0 local total_added=0
local total_failed=0 local total_failed=0

View File

@ -8,8 +8,16 @@ portsRemoveFromDatabase()
if [[ -z "$app_name" ]]; then if [[ -z "$app_name" ]]; then
isError "App name is required for port removal" isError "App name is required for port removal"
else else
# Delete port allocations for this app # Delete EVERY port-side row, not just resource_type='port'. The port-tag
local result; result=$(sqlite3 "$docker_dir/$db_file" "DELETE FROM network_resources WHERE app_name = '$app_name' AND resource_type = 'port';" 2>/dev/null) # writer (portsUpdateComposeTags) also inserts port_tag_combined /
# port_tag_external / port_tag_internal / traefik_managed /
# url_accessible for the same app; deleting only 'port' orphaned the rest
# at uninstall, and the firewall rebuild — which reads port_tag_external —
# then walked ports for apps that no longer exist on every run.
# 'ip' is excluded: it is ipRemoveFromDatabase's row, and uninstall calls
# that first. Matching by "not ip" rather than an explicit type list so a
# future port-side resource_type is cleaned up without another edit here.
local result; result=$(sqlite3 "$docker_dir/$db_file" "DELETE FROM network_resources WHERE app_name = '$app_name' AND resource_type != 'ip';" 2>/dev/null)
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
isSuccessful "Removed port allocations for $app_name" isSuccessful "Removed port allocations for $app_name"