LibrePortal/scripts/update/check_update.sh
librelad 741edfdeb1 config: carry newly-added app options into existing installs
An app's deployed config is written once, on first install, and never
touched again — dockerConfigSetupToContainer copies only when the file is
absent, precisely so an update can never overwrite values someone has
edited. Right default, unchosen consequence: an app that gains a CFG_
option in a new release has it on every fresh install and on no existing
one.

The failure is silent, which is the worst part. Nothing errors. The key
reads as empty and whatever depends on it quietly does something else.

Two halves, because there were two gaps. Per-app, when a config is set up,
options present in the template and missing from the deployed file are
appended with their comment blocks — the comment is the only explanation
of a new option that exists, and a bare key at the end of a documented file
is not actionable. And a sweep across every installed app after an update,
because an update redeploys LibrePortal itself and nothing else, so without
it a new option would reach an app only when someone next reinstalled it —
which, for an app that is working, may be never.

Existing values are never touched, and keys the deployed file has but the
template no longer does are left alone: a removed option is usually a
rename, and deleting someone's value is not recoverable. Deliberately not a
regenerate-from-template, which would place new keys in their proper
section and refresh the docs, but would put a whole-file rewrite of every
app config in the path of every app action — appending cannot lose a line.

Backfilled RANDOMIZED* defaults are generated in both paths. A placeholder
left in place would otherwise be a credential identical on every install
that took the upgrade.

The sweep is driven from the template directory, not the container one:
under rootless the container tree is drwxr-x--x and owned by the docker
user, so the manager can traverse it but not list it, and a glob there
expands to nothing — the sweep would report success having examined no apps.

Run against this install it found real drift beyond the test fixtures:
mattermost was missing CFG_MATTERMOST_ADMIN_PASSWORD, whose own comment
notes that without it the password-reset tool has nothing to write to, and
speedtest was missing its password key entirely.

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

257 lines
9.3 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."
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."
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
}