LibrePortal/scripts/update/check_update.sh
librelad bd10a9ab55 feat(update): detect a stale root footprint on git and local installs
footprint_update_needed only ever came from comparing the installed marker
against a channel manifest, so it could not fire on a git or local install —
they have no channel to ask. Those are exactly the installs whose code tree is
synced by hand, i.e. the ones most able to drift, and the drift was silent: the
helpers in /usr/local/lib/libreportal could sit behind the code that calls them
with nothing reporting it. That is how this box ended up running a crowdsec
helper with no bouncer-traefik-rotate action while the tool that needs it
shipped.

init.sh is what bakes the marker, so the install tree's own init.sh is
authoritative for every mode. lpInstallTreeFootprintVersion reads it and
lpFootprintStale compares. Wired into both non-release branches of the WebUI
status generator, and into the local branch of the interactive update check,
which is where a local operator actually looks.

Fails safe: a tree older than the marker, or a missing init.sh, reports current
rather than warning — verified alongside the real stale case.

Also gives webuiSystemUpdateCheck the self-reload guard webuiGenerateAppsToolsConfig
already documents. The WebUI task service sources these once at startup, so
without it an edited generator keeps writing the old JSON from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:44:05 +01:00

274 lines
10 KiB
Bash
Executable File

#!/bin/bash
# webuiRunUpdate — non-interactive LibrePortal update for the WebUI.
#
# Invoked as a task via `libreportal update apply` (see cli_update_commands.sh),
# so it runs under the task processor with stdin closed and
# LIBREPORTAL_NONINTERACTIVE=1. It must therefore never block on a prompt:
# every decision is resolved up front from config, and it bails out cleanly
# (non-zero) instead of asking a question.
#
# Flow: guard -> forced update check -> if behind, run the proven file update
# (gitPerformUpdate) -> redeploy the portal so the new WebUI is live ->
# regenerate WebUI data and refresh the out-of-date status.
webuiRunUpdate()
{
isHeader "LibrePortal Update"
sourceCheckFiles;
local install_mode="${CFG_INSTALL_MODE:-git}"
local git_updates="${CFG_GIT_UPDATES:-true}"
if [[ "$install_mode" == "local" ]]; then
isError "This is a local installation — updates are managed manually, nothing to pull."
# The code tree is synced by hand here, so the root-owned footprint can
# fall behind it without anything saying so — there is no channel
# manifest to trigger the usual footprint_update_needed path.
if declare -f lpFootprintStale >/dev/null 2>&1 && lpFootprintStale; then
isNotice "Root footprint is stale: installed v$(lpInstalledFootprintVersion), install tree carries v$(lpInstallTreeFootprintVersion)."
isNotice "The helpers in /usr/local/lib/libreportal are older than the code that calls them. Re-bake as root:"
isNotice " sudo ${script_dir%/}/init.sh"
fi
return 1
fi
if [[ "$git_updates" != "true" ]]; then
isError "Updates are disabled (CFG_GIT_UPDATES). Enable them to update from the WebUI."
return 1
fi
# Release mode: version-compare the channel's latest against the local VERSION
# and, if newer, fetch + verify the new tarball and redeploy. No config backup
# dance — lpFetchRelease replaces only the install tree; configs/logs live in
# the separate system tree.
if [[ "$install_mode" == "release" ]]; then
webuiSystemUpdateCheck "force"
local cur lat
cur=$(tr -d ' \t\n\r' < "$script_dir/VERSION" 2>/dev/null)
lat=$(lpReleaseLatestVersion 2>/dev/null)
if [[ -z "$lat" ]]; then
isError "Could not reach the release server ($(lpReleaseBaseUrl))."
return 1
fi
if ! lpVersionGt "$lat" "$cur"; then
isSuccessful "LibrePortal is already up to date (v${cur})."
return 0
fi
# If the release bumps the root-owned footprint (helpers/wrapper/unit), a
# manager-run apply can't install it — those are deliberately not
# manager-writable. Require a root re-install, which fetches + re-bakes
# everything atomically.
local inst_fp rel_fp
inst_fp=$(lpInstalledFootprintVersion 2>/dev/null)
rel_fp=$(lpReleaseLatestFootprint 2>/dev/null)
if [[ -n "$rel_fp" && "${rel_fp:-0}" -gt "${inst_fp:-0}" ]]; then
isError "Update v${lat} changes system components — it must be applied as root, not from the WebUI."
isNotice "Run on the host: curl -fsSL $(lpReleaseBaseUrl)/install.sh | sudo bash"
isNotice "(or, from the install tree: sudo ${script_dir}/init.sh --unattended init ... )"
return 1
fi
isNotice "Update found — v${cur} → v${lat}. Fetching the verified release..."
lpFetchRelease "$lat" || { isError "Release fetch failed — install unchanged."; return 1; }
isNotice "Redeploying LibrePortal with the new version..."
dockerInstallApp "libreportal"
# Options added to any app in this release reach only fresh installs
# otherwise — this redeploys LibrePortal, not the other 30-odd apps.
configBackfillAllApps
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
webuiSystemUpdateCheck "force"
webuiSystemVerify "force"
isSuccessful "LibrePortal has been updated to v${lat}."
return 0
fi
# Credential guard — gitReset()/gitCheckGitDetails() would otherwise prompt
# for missing git details and hang the task forever. Fail fast with a clear
# message instead.
if [[ "$install_mode" == "git" ]]; then
if [[ -z "$CFG_GIT_USER" || "$CFG_GIT_USER" == "changeme" ]]; then
isError "Git credentials are not configured. Run the 'libreportal' command once on the host, then retry."
return 1
fi
if [[ "$CFG_GIT_USER" != "empty" && ( -z "$CFG_GIT_KEY" || "$CFG_GIT_KEY" == "changeme" ) ]]; then
isError "Git access token is not configured. Run the 'libreportal' command once on the host, then retry."
return 1
fi
fi
cd "$script_dir" || { isError "Cannot access the install directory ($script_dir)."; return 1; }
runAsManager git config core.fileMode false
# Force a fresh fetch + status write so the decision below (and the badge)
# reflect reality right now, not a stale throttled snapshot.
webuiSystemUpdateCheck "force"
local branch behind
branch=$(runAsManager git -C "$script_dir" rev-parse --abbrev-ref HEAD 2>/dev/null)
[[ -z "$branch" || "$branch" == "HEAD" ]] && branch="main"
behind=$(runAsManager git -C "$script_dir" rev-list --count "HEAD..refs/remotes/origin/$branch" 2>/dev/null)
[[ -z "$behind" ]] && behind=0
if [[ "$behind" -eq 0 ]]; then
isSuccessful "LibrePortal is already up to date."
return 0
fi
isNotice "Update found — $behind commit(s) behind origin/$branch. Updating now..."
# Proven file-level update: back up configs/logs, re-clone, restore.
gitPerformUpdate;
# Redeploy the portal from the freshly pulled source so the new WebUI goes
# live. This is exactly what `libreportal app install libreportal` does, so
# it's already safe to run non-interactively. It restarts the container; the
# task processor runs on the host, so this task survives the restart.
isNotice "Redeploying LibrePortal with the new version..."
dockerInstallApp "libreportal"
# Same as the release branch above: carry newly-added config options into
# apps that are already installed, since nothing else will.
configBackfillAllApps
# Regenerate WebUI data (new version, configs, etc.) and clear the
# out-of-date flag.
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
webuiSystemUpdateCheck "force"
webuiSystemVerify "force"
isSuccessful "LibrePortal has been updated."
}
checkUpdates()
{
local param1="$1"
sourceCheckFiles;
# Skip Git updates if installation mode is local or Git updates are disabled
if [[ $CFG_INSTALL_MODE == "local" ]]; then
isNotice "Local installation detected - Git updates are disabled."
# A local install syncs its code tree by hand, so the root-owned
# footprint can fall behind that code with nothing to say so — there is
# no channel manifest here to drive the usual footprint_update_needed
# path. This is the one place a local operator reliably looks.
if declare -f lpFootprintStale >/dev/null 2>&1 && lpFootprintStale; then
isNotice "Root footprint is stale: installed v$(lpInstalledFootprintVersion), install tree carries v$(lpInstallTreeFootprintVersion)."
isNotice "The helpers in /usr/local/lib/libreportal are older than the code calling them. Re-bake as root:"
isNotice " sudo ${script_dir%/}/init.sh"
fi
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
return
fi
if [[ $CFG_GIT_UPDATES == "true" ]]; then
isHeader "Checking for Updates"
# Ask user if they want to check for updates
while true; do
if [[ $CFG_GIT_UPDATES == "false" ]]; then
isQuestion "Would you like to check for updates? (y/n): "
read -p "" check_updates_choice
elif [[ $CFG_GIT_UPDATES == "true" ]]; then
check_updates_choice=y
fi
case $check_updates_choice in
[yY])
# DNS Query Test with Quad9
isNotice "Testing internet DNS, please wait..."
if command -v dig >/dev/null 2>&1; then
dns_check_cmd="dig +short +time=3 +tries=1 @9.9.9.9 quad9.net"
elif command -v getent >/dev/null 2>&1; then
dns_check_cmd="getent hosts quad9.net"
else
dns_check_cmd="ping -c 1 -W 3 9.9.9.9"
fi
if $dns_check_cmd >/dev/null 2>&1; then
isSuccessful "Internet DNS is working."
else
isError "Internet DNS is not working."
exit 1
fi
cd "$script_dir" || { isError " Cannot navigate to the repository directory"; exit 1; }
# Update Git to ignore changes in file permissions
runAsManager git config core.fileMode false
# Update Git with email address
runAsManager git config --global user.name "$CFG_INSTALL_NAME"
runAsManager git config --global user.email "noreply@${CFG_INSTALL_NAME,,}.libreportal.local"
# Check if there are edited (modified) files
if git status --porcelain | grep -q "^ M"; then
isNotice "There are uncommitted changes in the repository."
while true; do
isQuestion "Do you want to discard these changes and update the repository? (y/n): "
read -p "" customupdatesfound
case $customupdatesfound in
[yY])
remove_changes=true
gitCheckForUpdate;
gitCheckConfigs;
fixPermissionsBeforeStart "" "update";
sourceCheckFiles;
if [[ $init_run_flag == "true" ]]; then
isSuccessful "Starting/Restarting LibrePortal"
startLoad;
fi
;;
[nN])
isNotice "Custom changes will be kept, continuing..."
remove_changes=false
gitCheckForUpdate;
gitCheckConfigs;
fixPermissionsBeforeStart "" "update";
sourceCheckFiles;
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
;;
*)
isNotice "Please provide a valid input (y or n)."
;;
esac
done
fi
# Make sure an update happens after custom code check
if [[ $update_done != "true" ]]; then
gitCheckForUpdate;
gitCheckConfigs;
fixPermissionsBeforeStart "" "update";
sourceCheckFiles;
if [[ $init_run_flag == "true" ]]; then
isSuccessful "Starting/Restarting LibrePortal"
startLoad;
fi
fi
;;
[nN])
echo ""
isNotice "Skipping update check. Starting application..."
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
;;
*)
isNotice "Please enter y or n."
;;
esac
done
else
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
fi
}