fix(updater): move lock-step services together, and find the anchor at all
updaterSetAnchorVersion located the anchor by looking for a service literally named "<app>-service". That is a convention, not a rule: matrix names its anchor service matrix-synapse and stoat names its api. For those apps nothing matched, so the rewrite changed no lines and the upgrade aborted at step 2 with "could not set version" — after having already taken a snapshot. Dry runs never showed it because they return before that step. It now finds the anchor by its bare <APP>_VERSION_TAG sentinel, the same way updaterPrimaryImage does. It also moved only the anchor. Some apps are one product shipped as many images: stoat is nine stoatchat services released together, all on v0.15.1, expecting matching versions of each other. Stepping the anchor alone would have put api on v0.16 while events stayed on v0.15.1 — the exact mismatch that once justified keeping the app off automatic updates. The lock-step set is DERIVED from the compose rather than configured, because the compose already states it: a service moves with the anchor when it carries a version sentinel, sits on the SAME tag, and shares the anchor's registry namespace. Both tests are load-bearing and each rejects a real case — livekit-server is same-namespace but on its own cadence, for-web is pinned to a commit hash, element-web is a different namespace entirely, and mongo has no namespace at all. Verified against copies of five composes: stoat moves all eight sibling services and nothing else; matrix, rocketchat and nextcloud move exactly one image. Every sentinel that moved gets its CFG_*_VERSION key set, not just the anchor's, or the next config-driven regeneration would quietly pull the locked-step services back to the old version. One trap worth naming: quotes were stripped with sed 's/["\047]//g', but \047 is an octal escape awk honours and sed does not — in a sed bracket expression it is the literal characters \ 0 4 7, so it deleted every 0, 4 and 7 it saw and v0.15.1 arrived as v.15.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
64ff5f508b
commit
8a997e14dd
@ -27,38 +27,90 @@
|
||||
|
||||
_updaterUpgradeGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated"; }
|
||||
|
||||
# Rewrite the anchor image AND its version sentinel, so the live compose stays
|
||||
# self-consistent. updaterSetAnchorRef preserves the trailing comment verbatim,
|
||||
# which would leave the sentinel advertising the OLD version — and the next
|
||||
# config-driven regeneration would then quietly revert the app. Both or neither.
|
||||
# Rewrite the anchor image AND every image locked in step with it, plus their
|
||||
# version sentinels, so the live compose stays self-consistent.
|
||||
#
|
||||
# THE LOCK-STEP PROBLEM. Some apps are one product shipped as many images:
|
||||
# Stoat is nine stoatchat services released together, all on v0.15.1, and they
|
||||
# expect matching versions of each other. Moving only the anchor would put api
|
||||
# on v0.16 while events stayed on v0.15.1 — precisely the API/events mismatch
|
||||
# that once justified keeping the app off automatic updates. So the set has to
|
||||
# move together or not at all.
|
||||
#
|
||||
# The set is DERIVED, not configured, because the compose already states it:
|
||||
# a service is locked in step with the anchor when it carries a version
|
||||
# sentinel, currently sits on the SAME tag, and lives under the same registry
|
||||
# namespace. Both conditions are needed and each rejects a real case here:
|
||||
# ghcr.io/stoatchat/events:v0.15.1 same ns, same tag -> moves
|
||||
# ghcr.io/stoatchat/livekit-server:v1.9.13 same ns, other tag -> stays
|
||||
# vectorim/element-web:v1.12.25 other ns -> stays
|
||||
# mongo:8.0 no namespace -> stays
|
||||
# A sidecar that coincidentally shares a version number is excluded by the
|
||||
# namespace test; a sibling on its own release cadence by the tag test.
|
||||
#
|
||||
# The anchor itself is found by its BARE <APP>_VERSION_TAG sentinel, not by
|
||||
# guessing "<app>-service". That guess was wrong for every app that names its
|
||||
# services anything else — matrix (matrix-synapse) and stoat (api) among them —
|
||||
# and since nothing matched, the rewrite silently changed no lines and the
|
||||
# upgrade aborted at "could not set version". Dry runs never showed it: they
|
||||
# return before this point.
|
||||
updaterSetAnchorVersion() {
|
||||
local app="$1" newtag="$2"
|
||||
local compose="${containers_dir%/}/$app/docker-compose.yml"
|
||||
[ -f "$compose" ] || return 1
|
||||
local svc="${app//_/-}-service"
|
||||
local up; up="$(printf '%s' "$app" | tr '[:lower:]' '[:upper:]')"
|
||||
|
||||
# Anchor line -> its current tag and registry namespace.
|
||||
local aline; aline="$(grep -E "#LIBREPORTAL\|${up}_VERSION_TAG\|" "$compose" 2>/dev/null | head -1)"
|
||||
[ -n "$aline" ] || return 1
|
||||
# tr, not sed, to strip quotes: in a sed bracket expression \047 is not an
|
||||
# octal escape but the literal characters \ 0 4 7, so it silently deleted
|
||||
# every 0, 4 and 7 in the ref -- v0.15.1 became v.15.1. awk does honour the
|
||||
# escape, which is why the same idiom is fine below.
|
||||
local aref; aref="$(printf '%s' "$aline" | sed -E 's/^[[:space:]]*image:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d "\"' ")"
|
||||
local atag="${aref##*:}"; local arepo="${aref%:*}"
|
||||
case "$aref" in */*:*|*:*) : ;; *) return 1 ;; esac
|
||||
local ans=""; case "$arepo" in */*) ans="${arepo%/*}" ;; esac
|
||||
[ -n "$atag" ] || return 1
|
||||
|
||||
local tmp; tmp="$(mktemp)"
|
||||
awk -v s="$svc" -v tag="$newtag" -v key="${up}_VERSION_TAG" '
|
||||
!done && seen && /^[[:space:]]*image:/ {
|
||||
awk -v newtag="$newtag" -v atag="$atag" -v ans="$ans" '
|
||||
/^[[:space:]]*image:/ && index($0, "#LIBREPORTAL|") && index($0, "_VERSION_TAG|") {
|
||||
match($0,/^[[:space:]]*/); ind=substr($0,1,RLENGTH)
|
||||
line=$0; sub(/^[[:space:]]*image:[[:space:]]*/,"",line)
|
||||
sub(/[[:space:]]*#.*$/,"",line); gsub(/["'"'"']/,"",line)
|
||||
repo=line; sub(/:[^:\/]*$/,"",repo) # strip the old tag
|
||||
printf "%simage: %s:%s #LIBREPORTAL|%s|%s\n", ind, repo, tag, key, tag
|
||||
done=1; next
|
||||
# key = the sentinel this line owns; each keeps its own.
|
||||
k=$0; sub(/^.*#LIBREPORTAL\|/,"",k); sub(/\|.*$/,"",k)
|
||||
ref=$0; sub(/^[[:space:]]*image:[[:space:]]*/,"",ref)
|
||||
sub(/[[:space:]]*#.*$/,"",ref); gsub(/["\047]/,"",ref)
|
||||
if (ref ~ /:/) {
|
||||
tag=ref; sub(/^.*:/,"",tag)
|
||||
repo=ref; sub(/:[^:\/]*$/,"",repo)
|
||||
ns=""; if (repo ~ /\//) { ns=repo; sub(/\/[^\/]*$/,"",ns) }
|
||||
if (tag == atag && ns == ans) {
|
||||
printf "%simage: %s:%s #LIBREPORTAL|%s|%s\n", ind, repo, newtag, k, newtag
|
||||
changed++
|
||||
next
|
||||
}
|
||||
}
|
||||
}
|
||||
$0 ~ ("^[[:space:]]*" s ":") { seen=1 }
|
||||
{ print }
|
||||
END { if (!changed) exit 3 }
|
||||
' "$compose" > "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
grep -q "image:.*:${newtag}" "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
runFileWrite "$compose" < "$tmp"; local rc=$?
|
||||
rm -f "$tmp"
|
||||
|
||||
# Keep the config key in step when the app has one, so the WebUI's Version
|
||||
# field shows what is actually deployed rather than what it used to be.
|
||||
local cfgkey="CFG_${up}_VERSION"
|
||||
if [ -n "${!cfgkey+x}" ] && declare -f updateConfigOption >/dev/null 2>&1; then
|
||||
updateConfigOption "$cfgkey" "$newtag" >/dev/null 2>&1 || true
|
||||
# Keep the config keys in step so the WebUI's Version field shows what is
|
||||
# actually deployed. Every sentinel now on the new tag gets its key set, not
|
||||
# just the anchor's — otherwise the next config-driven regeneration would
|
||||
# quietly pull the locked-step services back to the old version.
|
||||
if declare -f updateConfigOption >/dev/null 2>&1; then
|
||||
local k cfgkey
|
||||
while IFS= read -r k; do
|
||||
[ -n "$k" ] || continue
|
||||
cfgkey="CFG_${k%_VERSION_TAG}_VERSION"
|
||||
[ -n "${!cfgkey+x}" ] && updateConfigOption "$cfgkey" "$newtag" >/dev/null 2>&1
|
||||
done < <(grep -oE "#LIBREPORTAL\|[A-Z0-9_]+_VERSION_TAG\|${newtag}" "$compose" 2>/dev/null \
|
||||
| sed -E 's/#LIBREPORTAL\|//; s/\|.*$//' | sort -u)
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user