LibrePortal/containers/gluetun/scripts/gluetun_route_apps.sh
librelad 7fae6bc308 fix(updater): stop a callee blanking the app name mid-update
First real end-to-end auto-update on a live install failed like this:

  Automatically updating trivy (a recovery snapshot is taken first)
  Snapshotting trivy before update…
  Pulling new image(s) for …
  Update of  failed — rolling back…
  Could not roll  back automatically

The app name went empty after the snapshot. Cause: bash is dynamically
scoped, so a callee assigning an undeclared variable writes the CALLER's
local of that name — and a `while read app` loop leaves it EMPTY at EOF.
webuiBackupAppStatus's dashboard generator runs at the end of every backup
and did exactly that to updaterApplyApp's `app`.

Nothing was damaged: the pull ran against an empty name, failed before
touching the image, and the rollback was a no-op on a nonexistent app.

Fixed both ends. The generator (and three gluetun loops with the same
latent leak) now declare `local app`. updaterApplyApp/updaterRollbackApp
hold the name in `_upd_app` so they no longer depend on every callee's
hygiene, and updaterApplyAll stops leaking its own loop var.

This is exactly the untested path the roadmap flagged: "apply/revert not
yet exercised end-to-end on a live install with a pending update."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:37:14 +01:00

100 lines
4.0 KiB
Bash

#!/bin/bash
# Walks installed apps and offers to flip CFG_<APP>_NETWORK to gluetun for each
# eligible one. Honors the curated category list + CFG_REQUIREMENT_GLUETUN_FOR_ALL
# override flag. Skips itself, Traefik, LibrePortal, fail2ban.
gluetunRouteExistingAppsPrompt()
{
local categories_file="${containers_dir}libreportal/frontend/data/apps/gluetun-eligible-categories.json"
local override="${CFG_REQUIREMENT_GLUETUN_FOR_ALL:-false}"
if ! command -v sqlite3 >/dev/null 2>&1 || [[ ! -f "$docker_dir/$db_file" ]]; then
isNotice "Database not available; skipping per-app routing prompt."
return 0
fi
local allow_csv=""
if [[ -f "$categories_file" ]] && command -v jq >/dev/null 2>&1; then
allow_csv=$(jq -r '.categories | join(",")' "$categories_file" 2>/dev/null | tr '[:upper:]' '[:lower:]')
fi
local installed
installed=$(runInstallOp sqlite3 "$docker_dir/$db_file" \
"SELECT name FROM apps WHERE status = 1 AND name NOT IN ('gluetun','libreportal','traefik','fail2ban') ORDER BY name;" 2>/dev/null)
if [[ -z "$installed" ]]; then
isNotice "No other apps installed yet. Skipping."
return 0
fi
local eligible=()
local app # local: bash is dynamically scoped, and a while-read loop
# leaves an undeclared name EMPTY in the CALLER's scope at EOF.
while IFS= read -r app; do
[[ -z "$app" ]] && continue
local cfg_file="${containers_dir}${app}/${app}.config"
[[ -f "$cfg_file" ]] || continue
local category
category=$(grep -E "^CFG_${app^^}_CATEGORY=" "$cfg_file" 2>/dev/null \
| head -1 | cut -d'=' -f2 | tr -d '"' | tr '[:upper:]' '[:lower:]')
local is_eligible="false"
if [[ "$override" == "true" ]]; then
is_eligible="true"
elif [[ -n "$allow_csv" ]]; then
IFS=',' read -ra cats <<< "$allow_csv"
for c in "${cats[@]}"; do
if [[ "$c" == "$category" ]]; then is_eligible="true"; break; fi
done
fi
[[ "$is_eligible" == "true" ]] && eligible+=("$app")
done <<< "$installed"
if [[ ${#eligible[@]} -eq 0 ]]; then
isNotice "No eligible installed apps. (Enable CFG_REQUIREMENT_GLUETUN_FOR_ALL to expose every app.)"
return 0
fi
echo " The following installed apps can be routed through Gluetun:"
for app in "${eligible[@]}"; do
local cfg_file="${containers_dir}${app}/${app}.config"
local current
current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2)
[[ -z "$current" ]] && current="default"
echo " - ${app} (current: ${current})"
done
echo ""
isQuestion "Apply gluetun routing now? Tick each app to flip. (y/N): "
local resp
read -rp "" resp
echo ""
case "$resp" in
[yY]*) ;;
*) isNotice "Skipped. You can apply later via the WebUI gluetun page or per-app config."; return 0 ;;
esac
for app in "${eligible[@]}"; do
local cfg_file="${containers_dir}${app}/${app}.config"
local current
current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2)
[[ -z "$current" ]] && current="default"
isQuestion " Route '$app' through gluetun? (current: $current) [y/n/skip]: "
local r
read -rp "" r
case "$r" in
[yY]*)
updateConfigOption "CFG_${app^^}_NETWORK" "gluetun" "$cfg_file"
isSuccessful " Set CFG_${app^^}_NETWORK=gluetun. Re-running install for $app..."
dockerInstallApp "$app"
;;
[nN]*)
if [[ "$current" == "gluetun" ]]; then
updateConfigOption "CFG_${app^^}_NETWORK" "default" "$cfg_file"
isSuccessful " Reverted $app to default network. Re-running install..."
dockerInstallApp "$app"
fi
;;
*) ;;
esac
done
}