#!/bin/bash # Automatic app updates — policy resolution + the auto-apply enqueuer. # --------------------------------------------------------------------------- # The decision half of the updater: `webui_updater_scan.sh` answers "is a new # build available?", `updaterApplyApp` answers "how do I install it safely?", # and this file answers "should I install it without being asked?". # # Policy is per app, with a fleet-wide master switch: # CFG__UPDATE_TYPE = auto | manual (per app, default auto) # CFG_UPDATER_AUTO = true | false (master switch, default true) # Precedence mirrors the established backup-strategy shape (per-app override on # top of a global default): the master switch can only ever make things MORE # manual, so "turn auto-updates off" is one flip, not 33 config edits. # # What it deliberately does NOT do: # * It never applies anything inline. Like artifactApplyAuto, it only enqueues # the ordinary `updater_apply` task, so an automatic update is the exact # same code path (snapshot -> pull -> up -> auto-rollback on failure), with # the same task log, the same History entry, and the same Roll back button # as a hand-pressed Update. The only difference is who pressed it, which is # recorded as the history entry's `trigger`. # * It never retries a build that already failed. Each auto-attempt stamps the # target digest under generated/auto/; the same digest is skipped # forever after. Without this a broken upstream build would be re-attempted # (and rolled back) on every scan, snapshotting the app each time. A newer # build changes the digest and is attempted normally; the Update button # stays available for a manual retry of the skipped one. _updaterAutoGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated"; } _updaterAutoDir() { echo "$(_updaterAutoGenDir)/auto"; } _updaterAutoStamp() { echo "$(_updaterAutoDir)/$1.digest"; } # $1=app # Effective update policy for one app -> "auto" | "manual". # 1. master switch off -> manual (everything, no exceptions) # 2. per-app CFG__UPDATE_TYPE # 3. unset / unrecognised -> auto (the documented default) # App configs are sourced globally (sourceScanFiles app_configs), so the per-app # key is read by indirect expansion exactly like backupResolveStrategy does. updaterAppPolicy() { local app="$1" [[ -n "$app" ]] || { echo manual; return 0; } [[ "${CFG_UPDATER_AUTO:-true}" == "true" ]] || { echo manual; return 0; } local key="CFG_${app^^}_UPDATE_TYPE" case "$(printf '%s' "${!key-}" | tr 'A-Z' 'a-z')" in manual|off|false) echo manual ;; *) echo auto ;; esac } # True when an updater task for this app is already queued or in flight, so a # scan that lands while the previous update is still running doesn't stack a # second one. Cheap: one jq over the task files, and only for apps that got # this far (update available + policy auto). updaterAutoTaskPending() { local app="$1" dir="${containers_dir%/}/libreportal/frontend/data/tasks" command -v jq >/dev/null 2>&1 || return 1 local files=( "$dir"/task_*.json ) [[ -e "${files[0]}" ]] || return 1 local n n="$(jq -rs --arg a "$app" ' [ .[] | select((.app // "") == $a and ((.type // "") | startswith("updater_")) and ((.status // "") == "queued" or (.status // "") == "pending" or (.status // "") == "running")) ] | length ' "${files[@]}" 2>/dev/null)" [[ "${n:-0}" -gt 0 ]] } # updaterApplyAuto — enqueue an `updater apply` task for every app that has an # update available and is set to auto. Called from `updater check` right after # the scan that produced updates.json, so it always acts on fresh data. updaterApplyAuto() { if [[ "${CFG_UPDATER_AUTO:-true}" != "true" ]]; then isNotice "Automatic app updates are off (CFG_UPDATER_AUTO=false)." return 0 fi if ! command -v jq >/dev/null 2>&1; then isNotice "Automatic app updates need jq to read the scan results — skipping (updates are still listed in the WebUI)." return 0 fi local upd; upd="$(_updaterAutoGenDir)/updates.json" [[ -f "$upd" ]] || return 0 local auto_dir; auto_dir="$(_updaterAutoDir)" [[ -d "$auto_dir" ]] || runFileOp mkdir -p "$auto_dir" 2>/dev/null local app dig stamp enqueued=0 skipped=0 while IFS=$'\t' read -r app dig; do [[ -n "$app" ]] || continue [[ "$(updaterAppPolicy "$app")" == "auto" ]] || continue # An update we already queued may still be waiting its turn — that is # in-flight, not stuck, so it must be tested BEFORE the attempted-digest # stamp (which the enqueue already wrote) or it would be miscounted as a # failure below. updaterAutoTaskPending "$app" && continue # Already auto-attempted this exact build? (failed last time — see header) stamp="$(_updaterAutoStamp "$app")" if [[ -n "$dig" && -f "$stamp" ]] && [[ "$(cat "$stamp" 2>/dev/null)" == "$dig" ]]; then skipped=$((skipped + 1)) continue fi # Stamp BEFORE enqueueing: if the update fails and rolls back, the stamp # is what stops the next scan from trying the same build again. A crash # between stamp and enqueue costs one skipped auto-update, never a loop. printf '%s' "$dig" | runFileWrite "$stamp" 2>/dev/null || true cliTaskRun "libreportal updater apply $app auto" "updater_apply" "$app" "--detach" enqueued=$((enqueued + 1)) done < <(jq -r '.apps[]? | select(.update_available == true) | "\(.name)\t\(.available_digest // "")"' "$upd" 2>/dev/null) if (( enqueued > 0 )); then isSuccessful "Queued $enqueued automatic app update(s) — each is snapshotted before it is applied." elif (( skipped > 0 )); then isNotice "$skipped app(s) have an update that a previous automatic attempt could not apply — update them from the WebUI to see why." fi return 0 }