Compare commits
2 Commits
39a1b51f7f
...
8d193eda28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d193eda28 | ||
|
|
daa336449a |
168
scripts/cli/commands/updater/cli_updater_commands.sh
Normal file
168
scripts/cli/commands/updater/cli_updater_commands.sh
Normal file
@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
|
||||
# App Updater command handler — `libreportal updater <sub>`
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatched automatically by cli_initialize.sh (category -> cliHandleUpdaterCommands).
|
||||
# Subcommands (the features/updater WebUI buttons route to these as tasks):
|
||||
# check refresh the version/CVE data (runs the WebUI generator)
|
||||
# apply <app> update one app — DISASTER-RECOVERY FIRST: snapshot the app
|
||||
# via the backup engine, then pull + recreate; on failure,
|
||||
# roll back to the snapshot automatically
|
||||
# apply-all [a,b] apply to a comma-list (or every update-available app)
|
||||
# rollback <app> restore the app's most recent pre-update snapshot
|
||||
#
|
||||
# State-changing subcommands use the standard task-exec split: invoked normally
|
||||
# they enqueue a task (so the WebUI + CLI share locking + the audit trail);
|
||||
# the task processor re-invokes them with LIBREPORTAL_TASK_EXEC=1 to do the work.
|
||||
|
||||
cliHandleUpdaterCommands()
|
||||
{
|
||||
local sub="$initial_command2"
|
||||
local app="$initial_command3"
|
||||
|
||||
case "$sub" in
|
||||
""|"check")
|
||||
# Quick + safe — just regenerates the read-only data files. Source
|
||||
# the generator explicitly if the lazy loader hasn't mapped it yet
|
||||
# (new file; the array regen self-heals it on deploy, this covers
|
||||
# the gap before that).
|
||||
if ! declare -F webuiUpdaterScan >/dev/null 2>&1; then
|
||||
source "$install_scripts_dir/webui/data/generators/updater/webui_updater_scan.sh" 2>/dev/null
|
||||
fi
|
||||
webuiUpdaterScan
|
||||
;;
|
||||
|
||||
"apply"|"now")
|
||||
if [[ -z "$app" ]]; then isError "Usage: libreportal updater apply <app>"; return 1; fi
|
||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
||||
updaterApplyApp "$app"
|
||||
else
|
||||
cliTaskRun "libreportal updater apply $app" "updater_apply" "$app" ""
|
||||
fi
|
||||
;;
|
||||
|
||||
"apply-all")
|
||||
local list="$app" # optional comma-list in $initial_command3
|
||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
||||
updaterApplyAll "$list"
|
||||
else
|
||||
cliTaskRun "libreportal updater apply-all $list" "updater_apply_all" "updater" ""
|
||||
fi
|
||||
;;
|
||||
|
||||
"rollback")
|
||||
if [[ -z "$app" ]]; then isError "Usage: libreportal updater rollback <app>"; return 1; fi
|
||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
||||
updaterRollbackApp "$app"
|
||||
else
|
||||
cliTaskRun "libreportal updater rollback $app" "updater_rollback" "$app" ""
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
cliShowUpdaterHelp
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Update one app with disaster-recovery: snapshot -> pull -> recreate -> verify,
|
||||
# auto-rolling-back on failure. Uses existing primitives (the backup CLI for the
|
||||
# snapshot, docker compose for the image swap) so it shares their locking/logging.
|
||||
updaterApplyApp()
|
||||
{
|
||||
local app="$1"
|
||||
local app_dir="$containers_dir/$app"
|
||||
if [[ ! -d "$app_dir" ]]; then isError "App '$app' is not installed."; return 1; fi
|
||||
|
||||
isHeader "Updating $app (a recovery snapshot is taken first)"
|
||||
|
||||
# 1. DISASTER RECOVERY — snapshot before touching anything.
|
||||
isNotice "Snapshotting $app before update…"
|
||||
if ! libreportal backup app "$app" >/dev/null 2>&1; then
|
||||
isNotice "Pre-update snapshot did not complete cleanly — continuing is risky; aborting $app update."
|
||||
updaterRecordHistory "$app" "update" "" "" "aborted-no-snapshot"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 2. Capture the current image so we can record from->to / roll back.
|
||||
local before; before="$(grep -m1 -E '^\s*image:' "$app_dir/docker-compose.yml" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')"
|
||||
|
||||
# 3. Pull + recreate (uses the real, install-type-aware compose helpers).
|
||||
isNotice "Pulling new image(s) for $app…"
|
||||
if updaterComposePull "$app" && dockerComposeUp "$app" >/dev/null 2>&1; then
|
||||
local after; after="$(grep -m1 -E '^\s*image:' "$app_dir/docker-compose.yml" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')"
|
||||
updaterRecordHistory "$app" "update" "$before" "$after" "ok"
|
||||
isSuccessful "$app updated. Rollback point retained."
|
||||
webuiUpdaterScan >/dev/null 2>&1 || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 4. Failure -> automatic rollback.
|
||||
isNotice "Update of $app failed — rolling back to the pre-update snapshot…"
|
||||
updaterRollbackApp "$app" "auto"
|
||||
updaterRecordHistory "$app" "update" "$before" "" "rolled-back"
|
||||
return 1
|
||||
}
|
||||
|
||||
updaterApplyAll()
|
||||
{
|
||||
local list="$1" failures=0
|
||||
if [[ -z "$list" ]]; then
|
||||
isNotice "No app list given; nothing to do (the WebUI passes the update-available apps)."
|
||||
return 0
|
||||
fi
|
||||
local IFS=','
|
||||
for app in $list; do
|
||||
[[ -z "$app" ]] && continue
|
||||
updaterApplyApp "$app" || failures=$((failures+1))
|
||||
done
|
||||
[[ $failures -gt 0 ]] && isNotice "$failures app(s) failed and were rolled back." || isSuccessful "All requested apps updated."
|
||||
}
|
||||
|
||||
# Roll an app back to its most recent snapshot. $2='auto' suppresses the header
|
||||
# (called from the failure path of an apply).
|
||||
updaterRollbackApp()
|
||||
{
|
||||
local app="$1" mode="$2"
|
||||
[[ "$mode" != "auto" ]] && isHeader "Rolling $app back to its pre-update snapshot"
|
||||
# Delegate to the backup engine's restore (latest snapshot for this app).
|
||||
if libreportal backup app "$app" restore latest >/dev/null 2>&1; then
|
||||
dockerComposeUp "$app" >/dev/null 2>&1 || true
|
||||
[[ "$mode" != "auto" ]] && updaterRecordHistory "$app" "rollback" "" "" "rolled-back"
|
||||
isSuccessful "$app restored from its pre-update snapshot."
|
||||
return 0
|
||||
fi
|
||||
isError "Could not roll $app back automatically — restore manually from the Backups page."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Force a fresh image pull for an app (mirrors up_app.sh's install-type split).
|
||||
# dockerComposeUp uses --quiet-pull which won't re-fetch a moved tag, so we pull
|
||||
# explicitly first to actually pick up a new image.
|
||||
updaterComposePull()
|
||||
{
|
||||
local app="$1" dir="${containers_dir%/}/$1"
|
||||
[ -d "$dir" ] || return 1
|
||||
if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then
|
||||
dockerCommandRunInstallUser "cd $dir && docker compose pull" >/dev/null 2>&1
|
||||
else
|
||||
( cd "$dir" && docker compose pull >/dev/null 2>&1 )
|
||||
fi
|
||||
}
|
||||
|
||||
# Append an entry to history.json (best-effort; needs jq, skips silently if absent).
|
||||
updaterRecordHistory()
|
||||
{
|
||||
local app="$1" action="$2" from="$3" to="$4" result="$5"
|
||||
local f="$containers_dir/libreportal/frontend/data/updater/generated/history.json"
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[ -f "$f" ] || printf '{ "entries": [] }\n' > "$f"
|
||||
local ts; ts="$(date -Iseconds 2>/dev/null || date)"
|
||||
local tmp; tmp="$(mktemp)"
|
||||
if jq --arg ts "$ts" --arg app "$app" --arg action "$action" --arg from "$from" --arg to "$to" --arg result "$result" \
|
||||
'.entries = ([{ts:$ts, app:$app, action:$action, from:$from, to:$to, result:$result}] + (.entries // []))[0:200]' \
|
||||
"$f" > "$tmp" 2>/dev/null; then
|
||||
runFileOp cp "$tmp" "$f" 2>/dev/null || cp "$tmp" "$f"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
20
scripts/cli/commands/updater/cli_updater_header.sh
Normal file
20
scripts/cli/commands/updater/cli_updater_header.sh
Normal file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# App Updater Commands Header
|
||||
# Shows available `libreportal updater` subcommands.
|
||||
|
||||
cliShowUpdaterHelp()
|
||||
{
|
||||
echo ""
|
||||
echo "Available App Updater Commands:"
|
||||
echo ""
|
||||
echo " libreportal updater check - Refresh per-app version & vulnerability data"
|
||||
echo " libreportal updater apply <app> - Update one app (snapshots it first; auto-rollback on failure)"
|
||||
echo " libreportal updater apply-all [a,b] - Update a comma-list of apps (each snapshotted first)"
|
||||
echo " libreportal updater rollback <app> - Restore an app's most recent pre-update snapshot"
|
||||
echo ""
|
||||
echo "Every update takes a recovery snapshot via the Backup engine before"
|
||||
echo "touching the app, so any update is reversible. These commands back the"
|
||||
echo "WebUI Updates page (features/updater); actions run through the task system."
|
||||
echo ""
|
||||
}
|
||||
93
scripts/webui/data/generators/updater/webui_updater_scan.sh
Normal file
93
scripts/webui/data/generators/updater/webui_updater_scan.sh
Normal file
@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WebUI App Updater — data generator
|
||||
# ---------------------------------------------------------------------------
|
||||
# Writes the read-only JSON the features/updater WebUI reads:
|
||||
# frontend/data/updater/generated/updates.json (per-app version state)
|
||||
# frontend/data/updater/generated/cves.json (per-app known CVEs)
|
||||
# frontend/data/updater/generated/history.json (created/owned by `apply`)
|
||||
#
|
||||
# Run on demand by `libreportal updater check` (NOT on every regen) so a slow
|
||||
# registry/scanner call never stalls the periodic WebUI refresh. The WebUI
|
||||
# degrades gracefully when these files are absent (it derives the app list from
|
||||
# the installed-apps data), so this generator is a bonus, never a dependency.
|
||||
#
|
||||
# Version + CVE discovery is intentionally pluggable: the loop below records
|
||||
# each installed app's CURRENT image (from its compose file) and leaves clearly
|
||||
# marked hooks for an available-version source (registry: `docker manifest` /
|
||||
# skopoe) and a vulnerability scanner (trivy / grype). Until those are wired the
|
||||
# output is honest: scanned=true, update_available=false, cves=[]. Always writes
|
||||
# valid JSON even when data gathering fails — it must never emit a broken file.
|
||||
|
||||
webuiUpdaterScan() {
|
||||
local out_dir="$containers_dir/libreportal/frontend/data/updater/generated"
|
||||
runFileOp mkdir -p "$out_dir" 2>/dev/null || mkdir -p "$out_dir" 2>/dev/null
|
||||
|
||||
local now; now="$(date -Iseconds 2>/dev/null || date)"
|
||||
|
||||
# Installed apps from the apps DB (status=1). Fall back to listing config
|
||||
# dirs if the DB is unavailable, so we still produce useful output.
|
||||
local apps=()
|
||||
if command -v sqlite3 >/dev/null 2>&1 && [ -f "$docker_dir/$db_file" ]; then
|
||||
while IFS= read -r a; do [ -n "$a" ] && apps+=("$a"); done < <(
|
||||
runInstallOp sqlite3 "$docker_dir/$db_file" \
|
||||
"SELECT name FROM apps WHERE status=1 AND name!='libreportal';" 2>/dev/null
|
||||
)
|
||||
fi
|
||||
if [ ${#apps[@]} -eq 0 ]; then
|
||||
while IFS= read -r d; do
|
||||
local b; b="$(basename "$d")"
|
||||
[[ "$b" == "libreportal" ]] && continue
|
||||
[ -f "$d/$b.config" ] && apps+=("$b")
|
||||
done < <(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Build the per-app updates array.
|
||||
local entries="" first=1
|
||||
for app in "${apps[@]}"; do
|
||||
local image="" compose="$containers_dir/$app/docker-compose.yml"
|
||||
if [ -f "$compose" ]; then
|
||||
image="$(grep -m1 -E '^\s*image:' "$compose" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')"
|
||||
fi
|
||||
# --- available-version hook -------------------------------------------
|
||||
# Wire a registry check here (e.g. `docker manifest inspect`/skopeo) to
|
||||
# set available_image + update_available. Honest default: up to date.
|
||||
local available="$image" update_available="false"
|
||||
|
||||
[ $first -eq 0 ] && entries+=","
|
||||
first=0
|
||||
entries+=$(cat <<EOF
|
||||
|
||||
{ "name": "$app", "displayName": "$app", "current_image": "$(printf '%s' "$image" | sed 's/"/\\"/g')", "current_version": "$(printf '%s' "${image##*:}" | sed 's/"/\\"/g')", "available_image": "$(printf '%s' "$available" | sed 's/"/\\"/g')", "available_version": "$(printf '%s' "${available##*:}" | sed 's/"/\\"/g')", "update_available": $update_available, "scanned": true, "last_checked": "$now" }
|
||||
EOF
|
||||
)
|
||||
done
|
||||
|
||||
local tmp; tmp="$(mktemp)"
|
||||
cat > "$tmp" <<EOF
|
||||
{ "generated_at": "$now", "apps": [${entries}
|
||||
] }
|
||||
EOF
|
||||
runFileOp cp "$tmp" "$out_dir/updates.json" 2>/dev/null || cp "$tmp" "$out_dir/updates.json"
|
||||
rm -f "$tmp"
|
||||
|
||||
# CVE data — pluggable. Wire trivy/grype per image here and emit per-app
|
||||
# cves[]. Honest empty-but-valid default until a scanner is configured.
|
||||
if [ ! -f "$out_dir/cves.json" ]; then
|
||||
local ctmp; ctmp="$(mktemp)"
|
||||
printf '{ "generated_at": "%s", "apps": [], "totals": { "critical": 0, "high": 0, "medium": 0, "low": 0 } }\n' "$now" > "$ctmp"
|
||||
runFileOp cp "$ctmp" "$out_dir/cves.json" 2>/dev/null || cp "$ctmp" "$out_dir/cves.json"
|
||||
rm -f "$ctmp"
|
||||
fi
|
||||
# Ensure a valid (possibly empty) history file exists for the WebUI.
|
||||
if [ ! -f "$out_dir/history.json" ]; then
|
||||
local htmp; htmp="$(mktemp)"
|
||||
printf '{ "entries": [] }\n' > "$htmp"
|
||||
runFileOp cp "$htmp" "$out_dir/history.json" 2>/dev/null || cp "$htmp" "$out_dir/history.json"
|
||||
rm -f "$htmp"
|
||||
fi
|
||||
|
||||
# Make the generated tree readable by the container user that serves /data.
|
||||
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$out_dir" 2>/dev/null || true
|
||||
isSuccessful "App updater data refreshed (${#apps[@]} app(s))."
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user