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>
257 lines
9.3 KiB
Bash
Executable File
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
|
|
}
|