feat(update): FOOTPRINT_VERSION drift detection — flag when a root re-install is needed
A manager-run 'update apply' refreshes code/apps/WebUI but CANNOT rewrite the root-owned footprint (helpers/wrapper/uninstall/unit/sudoers) — that immutability is the de-sudo boundary. Previously a release that changed those would silently leave them stale. Make it explicit: - init.sh: footprint_version=1 constant, baked at install into /usr/local/lib/libreportal/.footprint_version (root:root 0644) by initRootHelpers. Bump it whenever a root component changes. - make_release.sh: publishes footprint_version in latest.json. - fetch.sh: lpInstalledFootprintVersion (marker) + lpReleaseLatestFootprint (manifest). - check_update.sh: 'update apply' REFUSES when the release's footprint_version exceeds the installed one, directing to a root re-install (which fetches + re-bakes everything atomically). No half-applied updates. - webui_system_update.sh: badge sets footprint_update_needed + clears can_update so the WebUI won't offer a one-click apply for a footprint-bumping release. - docs/DEVELOPMENT.md: the bump rule + the footprint exception explained. Verified: manifest carries footprint_version; drift decision correct both ways (no marker/older -> needs re-install; equal -> no drift). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
d95560c48c
commit
3014965b66
@ -118,6 +118,24 @@ is *code only* (configs/logs/backups live in the other roots), the update just
|
||||
replaces it — no backup/restore dance. `git`/`local` modes keep their existing
|
||||
git-based update path.
|
||||
|
||||
**The footprint exception (important).** `update apply` runs as the *manager*, and
|
||||
the manager is deliberately forbidden from rewriting the **root-owned footprint**
|
||||
(the helpers in `/usr/local/lib/libreportal/`, the CLI wrapper, the uninstall
|
||||
launcher, the systemd unit, the sudoers) — that immutability *is* the de-sudo
|
||||
security boundary. So a manager-run update can refresh **code/apps/WebUI**, but not
|
||||
those. To track when an update touches them, `init.sh` carries a `footprint_version`
|
||||
integer, baked at install into `/usr/local/lib/libreportal/.footprint_version` and
|
||||
published in `latest.json`. When the channel's `footprint_version` exceeds the
|
||||
installed one, the updater **refuses the WebUI apply** and the badge flags
|
||||
`footprint_update_needed` — the user re-runs the installer as root
|
||||
(`curl … install.sh | sudo bash`), which fetches *and* re-bakes the footprint
|
||||
atomically. (Re-running the installer is idempotent.)
|
||||
|
||||
➡️ **BUMP `footprint_version` in `init.sh` whenever you change anything in
|
||||
`scripts/system/*`, the CLI wrapper, the uninstall launcher, the systemd unit, or
|
||||
the sudoers.** Forgetting it means those root components silently stay stale until
|
||||
the next full reinstall.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Versioning:** semver in `VERSION`. Bump before building; `latest.json` carries it.
|
||||
|
||||
13
init.sh
13
init.sh
@ -125,6 +125,13 @@ hostname_file="/etc/hostname"
|
||||
lp_lib_dir="/usr/local/lib/libreportal"
|
||||
command_script="$lp_lib_dir/libreportal"
|
||||
command_symlink="/usr/local/bin/libreportal"
|
||||
# Version of the ROOT-OWNED footprint (helpers, CLI wrapper, uninstall launcher,
|
||||
# systemd unit, sudoers). BUMP THIS whenever you change any of those — a plain
|
||||
# `update apply` runs as the manager and CANNOT rewrite root-owned files, so a bump
|
||||
# tells the updater the new release needs a root re-install (which re-bakes them).
|
||||
# Recorded at install in $lp_lib_dir/.footprint_version. See docs/DEVELOPMENT.md.
|
||||
footprint_version=1
|
||||
footprint_marker="$lp_lib_dir/.footprint_version"
|
||||
|
||||
# Directories — three independently-relocatable roots (see scripts/source/paths.sh
|
||||
# for the canonical description). Defaults below; overridden by --system-dir /
|
||||
@ -940,6 +947,12 @@ initRootHelpers()
|
||||
fi
|
||||
rm -f "$helper_tmp"
|
||||
done
|
||||
|
||||
# Record the footprint version now installed (root-owned, world-readable) so the
|
||||
# manager-run updater can tell when a release bumps it and a root re-install is
|
||||
# needed. Written here because this re-runs on every install/reinstall.
|
||||
echo "$footprint_version" | sudo tee "$footprint_marker" >/dev/null
|
||||
sudo chmod 0644 "$footprint_marker"
|
||||
}
|
||||
|
||||
initFolders()
|
||||
|
||||
@ -26,6 +26,11 @@ REF="${2:-HEAD}"
|
||||
VERSION="$(tr -d ' \t\n\r' < VERSION 2>/dev/null || true)"
|
||||
[[ -n "$VERSION" ]] || { echo "make_release: VERSION file is empty or missing" >&2; exit 1; }
|
||||
|
||||
# Root-owned-footprint version (helpers/wrapper/unit/sudoers). Published in the
|
||||
# manifest so the updater can detect when an update needs a root re-install.
|
||||
FOOTPRINT_VERSION="$(grep -oE '^footprint_version=[0-9]+' init.sh | head -1 | cut -d= -f2)"
|
||||
[[ -n "$FOOTPRINT_VERSION" ]] || FOOTPRINT_VERSION=0
|
||||
|
||||
TARBALL="libreportal-${VERSION}.tar.gz"
|
||||
PREFIX="libreportal-${VERSION}/" # top-level dir inside the tarball
|
||||
OUT="$REPO_ROOT/dist/$CHANNEL"
|
||||
@ -43,6 +48,7 @@ cat > "$OUT/latest.json" <<EOF
|
||||
"channel": "$CHANNEL",
|
||||
"url": "$TARBALL",
|
||||
"sha256": "$SHA",
|
||||
"footprint_version": $FOOTPRINT_VERSION,
|
||||
"notes": ""
|
||||
}
|
||||
EOF
|
||||
|
||||
@ -29,6 +29,17 @@ _lpDownload() { # _lpDownload <url> <outfile|->
|
||||
}
|
||||
_lpSha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1; fi; }
|
||||
_lpJsonStr() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E 's/.*"([^"]*)"$/\1/'; }
|
||||
_lpJsonNum() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*[0-9]+" | head -1 | grep -oE '[0-9]+$'; }
|
||||
|
||||
# Root-owned-footprint version: the one INSTALLED on this box (marker written by
|
||||
# init.sh) vs the one the channel's latest release ships (manifest). When the
|
||||
# latter is greater, a plain manager-run update can't apply it (it can't rewrite
|
||||
# the root-owned helpers/wrapper/unit) — a root re-install is required.
|
||||
lpInstalledFootprintVersion() { local v; v=$(cat /usr/local/lib/libreportal/.footprint_version 2>/dev/null | tr -d ' \t\n\r'); echo "${v:-0}"; }
|
||||
lpReleaseLatestFootprint() {
|
||||
local m; m="$(_lpDownload "$(lpReleaseBaseUrl)/$(lpReleaseChannel)/latest.json" - 2>/dev/null)" || return 1
|
||||
local f; f=$(_lpJsonNum "$m" footprint_version); echo "${f:-0}"
|
||||
}
|
||||
|
||||
# Resolve the latest published version of the configured channel (empty on failure).
|
||||
lpReleaseLatestVersion() {
|
||||
|
||||
@ -46,6 +46,19 @@ webuiRunUpdate()
|
||||
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..."
|
||||
|
||||
@ -62,11 +62,15 @@ webuiSystemUpdateCheck() {
|
||||
# 1 update_available 2 can_update 3 current_version 4 latest_version
|
||||
# 5 current_commit 6 latest_commit 7 behind 8 ahead 9 branch
|
||||
# 10 source 11 error_message (empty = null)
|
||||
# 12 footprint_update_needed (optional, default false) — the release bumps a
|
||||
# root-owned component, so applying it needs a root re-install, not a
|
||||
# manager-run `update apply`.
|
||||
_webuiWriteUpdateStatus() {
|
||||
local _update_available="$1" _can_update="$2"
|
||||
local _current_version="$3" _latest_version="$4"
|
||||
local _current_commit="$5" _latest_commit="$6"
|
||||
local _behind="$7" _ahead="$8" _branch="$9" _source="${10}" _error="${11}"
|
||||
local _footprint_needed="${12:-false}"
|
||||
|
||||
local _error_json="null"
|
||||
[[ -n "$_error" ]] && _error_json="\"$_error\""
|
||||
@ -87,6 +91,7 @@ webuiSystemUpdateCheck() {
|
||||
"git_updates_enabled": ${git_updates},
|
||||
"auto_updates": ${auto_updates},
|
||||
"source": "${_source}",
|
||||
"footprint_update_needed": ${_footprint_needed},
|
||||
"error": ${_error_json},
|
||||
"checked_at": "$(date -Iseconds)"
|
||||
}
|
||||
@ -109,28 +114,37 @@ EOF
|
||||
local _now _last; _now=$(date +%s); _last=$(stat -c '%Y' "$stamp_file" 2>/dev/null || echo 0)
|
||||
(( _now - _last >= fetch_interval )) && do_fetch="true"
|
||||
fi
|
||||
local footprint_needed="false"
|
||||
if [[ "$do_fetch" == "true" ]] && declare -f lpReleaseLatestVersion >/dev/null 2>&1; then
|
||||
local _lv; _lv=$(lpReleaseLatestVersion 2>/dev/null)
|
||||
if [[ -n "$_lv" ]]; then
|
||||
latest_version="$_lv"
|
||||
runAsManager touch "$stamp_file" 2>/dev/null || touch "$stamp_file" 2>/dev/null
|
||||
# Does the published release bump the root-owned footprint? If so,
|
||||
# applying it needs a root re-install, not a WebUI `update apply`.
|
||||
if declare -f lpReleaseLatestFootprint >/dev/null 2>&1; then
|
||||
local _rfp _ifp; _rfp=$(lpReleaseLatestFootprint 2>/dev/null); _ifp=$(lpInstalledFootprintVersion 2>/dev/null)
|
||||
[[ -n "$_rfp" && "${_rfp:-0}" -gt "${_ifp:-0}" ]] && footprint_needed="true"
|
||||
fi
|
||||
else
|
||||
rel_error="Could not reach the update server."
|
||||
fi
|
||||
elif [[ -f "$final_file" ]]; then
|
||||
# Throttled: reuse the last-known latest so the badge doesn't flicker.
|
||||
# Throttled: reuse the last-known latest + footprint flag (no flicker).
|
||||
local _prev; _prev=$(grep -oE '"latest_version"[^,]*' "$final_file" 2>/dev/null | head -1 | sed -E 's/.*"([^"]*)"$/\1/')
|
||||
[[ -n "$_prev" ]] && latest_version="$_prev"
|
||||
grep -q '"footprint_update_needed": true' "$final_file" 2>/dev/null && footprint_needed="true"
|
||||
fi
|
||||
local update_available="false"
|
||||
if declare -f lpVersionGt >/dev/null 2>&1 && lpVersionGt "$latest_version" "$current_version"; then
|
||||
update_available="true"
|
||||
fi
|
||||
# A footprint-bumping release can't be applied from the WebUI (needs root).
|
||||
local can_update="false"
|
||||
[[ "$git_updates" == "true" ]] && can_update="true"
|
||||
[[ "$git_updates" == "true" && "$footprint_needed" != "true" ]] && can_update="true"
|
||||
_webuiWriteUpdateStatus "$update_available" "$can_update" \
|
||||
"$current_version" "$latest_version" \
|
||||
"" "" "0" "0" "${CFG_RELEASE_CHANNEL:-stable}" "release" "$rel_error"
|
||||
"" "" "0" "0" "${CFG_RELEASE_CHANNEL:-stable}" "release" "$rel_error" "$footprint_needed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user