diff --git a/scripts/cli/commands/updater/cli_updater_commands.sh b/scripts/cli/commands/updater/cli_updater_commands.sh index ce61d55..0feb9e3 100644 --- a/scripts/cli/commands/updater/cli_updater_commands.sh +++ b/scripts/cli/commands/updater/cli_updater_commands.sh @@ -97,6 +97,46 @@ cliHandleUpdaterCommands() esac } +# Digest (sha256:…) of a locally-present image ref; "" if absent. Rootless-aware. +updaterRefDigest() +{ + local ref="$1" out + out="$(dockerCommandRun "docker inspect --format '{{index .RepoDigests 0}}' $ref" 2>/dev/null | tr -d '\r' | head -1)" + out="${out##*@}"; case "$out" in sha256:*) printf '%s' "$out";; esac +} + +# Rewrite the app's ANCHOR (-service) image line to $newref, preserving the +# original indent and any trailing ` #LIBREPORTAL|…` version sentinel. Targets the +# anchor by service name (not the first image line — some apps list a companion +# first), so it's correct for ollama et al. +updaterSetAnchorRef() +{ + local app="$1" newref="$2" + local compose="${containers_dir%/}/$app/docker-compose.yml" + [ -f "$compose" ] || return 1 + local tmp; tmp="$(mktemp)" + awk -v s="${app//_/-}-service" -v ref="$newref" ' + !done && seen && /^[[:space:]]*image:/ { + match($0,/^[[:space:]]*/); ind=substr($0,1,RLENGTH) + cmt=""; if (match($0,/#.*/)) cmt=" " substr($0,RSTART) + print ind "image: " ref cmt; done=1; next + } + $0 ~ ("^[[:space:]]*" s ":") { seen=1 } + { print } + ' "$compose" > "$tmp" || { rm -f "$tmp"; return 1; } + runFileWrite "$compose" < "$tmp"; local rc=$? + rm -f "$tmp"; return $rc +} + +# The image ref (repo:tag@sha256:…) the app ran BEFORE its last successful update +# — i.e. the roll-back target. Read from history.json's most recent update/ok. +updaterLastUpdateFrom() +{ + local app="$1" hist="$containers_dir/libreportal/frontend/data/updater/generated/history.json" + [ -f "$hist" ] && command -v jq >/dev/null 2>&1 || return 0 + jq -r --arg a "$app" 'first(.entries[]? | select(.app==$a and .action=="update" and .result=="ok") | .from) // ""' "$hist" 2>/dev/null +} + # 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. @@ -121,13 +161,21 @@ updaterApplyApp() 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')" + # 2. Capture the current ANCHOR image + its running digest, so from->to is an + # exact build reference (repo:tag@sha256:…) even for a floating tag. Anchor is + # the -service image (updaterPrimaryImage), NOT the first line. If a + # prior rollback pinned a digest, unpin it first so we track the channel again. + local anchor; anchor="$(updaterPrimaryImage "$app" "$app_dir/docker-compose.yml")" + case "$anchor" in *@sha256:*) updaterSetAnchorRef "$app" "${anchor%%@*}"; anchor="${anchor%%@*}";; esac + local before_dig; before_dig="$(updaterRefDigest "$anchor")" + local before="$anchor${before_dig:+@$before_dig}" # 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')" + local after_ref; after_ref="$(updaterPrimaryImage "$app" "$app_dir/docker-compose.yml")"; after_ref="${after_ref%%@*}" + local after_dig; after_dig="$(updaterRefDigest "$after_ref")" + local after="$after_ref${after_dig:+@$after_dig}" updaterRecordHistory "$app" "update" "$before" "$after" "ok" isSuccessful "$app updated. Rollback point retained." webuiUpdaterScan >/dev/null 2>&1 || true @@ -162,6 +210,15 @@ updaterRollbackApp() { local app="$1" mode="$2" [[ "$mode" != "auto" ]] && isHeader "Rolling $app back to its pre-update snapshot" + # Re-pin the ANCHOR image to the exact build the app ran before the last + # update, so `up` below runs the OLD code — not the current channel head. + # Without this, restoring the data snapshot but recreating on the new `latest` + # image is "new code on old data", the hole a floating tag makes invisible. + # (Preserves the version sentinel; apply un-pins it again on the next update.) + local prev_ref; prev_ref="$(updaterLastUpdateFrom "$app")" + if [[ -n "$prev_ref" && "$prev_ref" == *@sha256:* ]]; then + updaterSetAnchorRef "$app" "$prev_ref" && isNotice "Pinned $app back to its pre-update build." + fi # Delegate to the restore engine (latest snapshot for this app). Call the # function directly — the old `backup app "$app" restore latest` CLI form was # malformed (parsed as action="$app") so it silently did nothing yet exited 0.