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>
274 lines
10 KiB
Bash
Executable File
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
|
|
}
|