The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
3.1 KiB
Bash
68 lines
3.1 KiB
Bash
#!/bin/bash
|
|
|
|
# Read-only network-drift scan — the shared detection used by both the WebUI
|
|
# status generator (webuiSystemNetworkCheck) and the heal verb (network heal),
|
|
# so the two never diverge.
|
|
#
|
|
# networkScanConflicts sets these globals (call it DIRECTLY, never in $(...) —
|
|
# a subshell would drop the globals):
|
|
# NET_DAEMON_OK "true"/"false" — docker daemon reachable
|
|
# NET_PRESENT "true"/"false" — the shared network ($CFG_NETWORK_NAME) exists
|
|
# NET_DOCKER_SUBNET — its real subnet CIDR (e.g. 10.123.154.0/24)
|
|
# NET_SCAN_ERROR — human note when the daemon/network is off (else "")
|
|
# NET_CONFLICTS (array) — one "app|service|ip" entry per active IP that
|
|
# no longer falls inside the docker network's
|
|
# real subnet (the "network recreated with a
|
|
# different /24, app stranded" drift).
|
|
# Gateway-routed services (no live shared-net ipv4 in their deployed compose,
|
|
# e.g. gluetun-routed service-1) are skipped, so they don't false-positive.
|
|
#
|
|
# Nothing here mutates state.
|
|
|
|
# Is this app/service NOT live on the shared network? Routed via a gateway, or
|
|
# its ipv4 simply isn't present uncommented in the deployed compose -> skip it.
|
|
# We key on the IP (unique per service): a routed service has its whole
|
|
# `ipv4_address:` block commented out (GLUETUN_OFF region), so an uncommented
|
|
# assignment carrying this exact IP means it IS live on the shared net.
|
|
_netServiceIsRouted() {
|
|
local app="$1" ip="$2"
|
|
local compose="$(appDir "$app")/docker-compose.yml"
|
|
[[ -f "$compose" ]] || return 1 # no compose to consult -> don't skip
|
|
local esc_ip="${ip//./\\.}"
|
|
grep -Eq "^[[:space:]]*ipv4_address:[[:space:]]*${esc_ip}([[:space:]]|#|$)" "$compose" && return 1
|
|
return 0
|
|
}
|
|
|
|
networkScanConflicts() {
|
|
NET_DAEMON_OK="false"; NET_PRESENT="false"; NET_DOCKER_SUBNET=""; NET_SCAN_ERROR=""
|
|
NET_CONFLICTS=()
|
|
|
|
# Distinguish "daemon down" (transient/benign — never alarm on what we can't
|
|
# verify) from "daemon up but our network is gone" (a real conflict).
|
|
if ! dockerCommandRun "docker info" >/dev/null 2>&1; then
|
|
NET_SCAN_ERROR="docker daemon unreachable"
|
|
return 0
|
|
fi
|
|
NET_DAEMON_OK="true"
|
|
|
|
NET_DOCKER_SUBNET=$(dockerCommandRun "docker network inspect $CFG_NETWORK_NAME --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'" 2>/dev/null | tr -d '[:space:]')
|
|
if [[ -z "$NET_DOCKER_SUBNET" ]]; then
|
|
NET_SCAN_ERROR="network '$CFG_NETWORK_NAME' not found"
|
|
return 0
|
|
fi
|
|
NET_PRESENT="true"
|
|
|
|
local rows app service ip
|
|
rows=$(runInstallOp sqlite3 "$docker_dir/$db_file" \
|
|
"SELECT app_name, service_name, resource_value FROM network_resources WHERE resource_type='ip' AND status='active';" 2>/dev/null)
|
|
[[ -z "$rows" ]] && return 0
|
|
|
|
while IFS='|' read -r app service ip; do
|
|
[[ -z "$app" || -z "$ip" ]] && continue
|
|
_netServiceIsRouted "$app" "$ip" && continue
|
|
if ! ipInSubnet "$ip" "$NET_DOCKER_SUBNET"; then
|
|
NET_CONFLICTS+=("${app}|${service}|${ip}")
|
|
fi
|
|
done <<< "$rows"
|
|
}
|