Merge claude/2
This commit is contained in:
commit
06bd49d768
@ -3,4 +3,5 @@
|
||||
# @icon 🔄
|
||||
# ================================================================================
|
||||
CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - Minutes between automatic app update/CVE/improvement scans. 0 disables.
|
||||
CFG_UPDATER_REGISTRY_INTERVAL=360 # Registry Check Interval - Minutes between registry lookups for new image builds (the expensive step; the local scan still refreshes every scan). 0 = never (local-only).
|
||||
CFG_HOTFIX_AUTO=security-breakage # Hotfix Auto-Apply - Which signed hotfix severities apply automatically on the update check [security-breakage|all|off]
|
||||
|
||||
@ -72,7 +72,7 @@ webuiValidateConfigValue() {
|
||||
isError " Invalid crontab format for $var_name"
|
||||
fi
|
||||
;;
|
||||
CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL|CFG_UPDATER_CHECK|CFG_UPDATER_SCAN_INTERVAL|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC)
|
||||
CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL|CFG_UPDATER_CHECK|CFG_UPDATER_SCAN_INTERVAL|CFG_UPDATER_REGISTRY_INTERVAL|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC)
|
||||
# Validate numeric values
|
||||
if ! echo "$var_value" | grep -qE '^[0-9]+$'; then
|
||||
isError " $var_name must be a positive integer"
|
||||
|
||||
@ -31,17 +31,85 @@
|
||||
# hyphens in the service name, e.g. libreportal_catalog → libreportal-catalog-
|
||||
# service), then take the first `image:` under it. Falls back to the first
|
||||
# image line for anything that doesn't follow the convention.
|
||||
# Strips `image:`, quotes, and any trailing ` #comment` (e.g. the version
|
||||
# sentinel added to anchor lines) plus surrounding whitespace.
|
||||
_updaterCleanImageRef() { sed -E 's/^\s*image:\s*//; s/["'"'"']//g; s/[[:space:]]*#.*$//; s/[[:space:]]*$//'; }
|
||||
updaterPrimaryImage() {
|
||||
local app="$1" compose="$2" svc img
|
||||
svc="${app//_/-}-service"
|
||||
img="$(awk -v s="$svc" '
|
||||
m && /^[[:space:]]*image:/ { print; exit }
|
||||
$0 ~ ("^[[:space:]]*" s ":") { m=1 }
|
||||
' "$compose" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')"
|
||||
[ -n "$img" ] || img="$(grep -m1 -E '^\s*image:' "$compose" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')"
|
||||
' "$compose" 2>/dev/null | _updaterCleanImageRef)"
|
||||
[ -n "$img" ] || img="$(grep -m1 -E '^\s*image:' "$compose" 2>/dev/null | _updaterCleanImageRef)"
|
||||
printf '%s' "$img"
|
||||
}
|
||||
|
||||
# All service→image pairs in an app's compose, one "service<TAB>image" per line,
|
||||
# in file order (skips commented lines). Used to build updates.json services[].
|
||||
updaterAllServiceImages() {
|
||||
awk '
|
||||
/^[[:space:]]*#/ { next }
|
||||
/^[[:space:]]{2,4}[a-z0-9_-]+:[[:space:]]*(#|$)/ {
|
||||
s=$1; sub(/:.*/,"",s); gsub(/[[:space:]]/,"",s); svc=s; next
|
||||
}
|
||||
/^[[:space:]]*image:/ {
|
||||
img=$0; sub(/^[[:space:]]*image:[[:space:]]*/,"",img); gsub(/["'"'"']/,"",img); sub(/[[:space:]]*#.*/,"",img)
|
||||
if (svc!="") { print svc "\t" img; svc="" }
|
||||
}
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Tag portion of an image ref (after the last colon IN the final path segment;
|
||||
# empty when untagged). Strips any @digest first.
|
||||
updaterTagOf() {
|
||||
local ref="${1%%@*}" last; last="${ref##*/}"
|
||||
case "$last" in *:*) printf '%s' "${last##*:}";; *) printf '';; esac
|
||||
}
|
||||
# repo:tag with any @digest suffix removed
|
||||
updaterRepoTag() { printf '%s' "${1%%@*}"; }
|
||||
|
||||
# Classify a tag: rolling (floating channel / OS-variant / untagged) vs
|
||||
# versioned (carries a comparable version number). See the roadmap doc §2.
|
||||
updaterClassifyTag() {
|
||||
local tag; tag="$(printf '%s' "$1" | tr 'A-Z' 'a-z')"
|
||||
[ -z "$tag" ] && { echo rolling; return; }
|
||||
case "$tag" in
|
||||
latest|stable|main|master|edge|nightly|rolling|dev|develop|canary|release) echo rolling; return;;
|
||||
latest-*|stable-*|main-*|master-*|edge-*|nightly-*|develop-*|canary-*|release-*) echo rolling; return;;
|
||||
esac
|
||||
case "$tag" in *[0-9]*) echo versioned;; *) echo rolling;; esac
|
||||
}
|
||||
|
||||
# One docker inspect of a LOCAL image → "sha256:<repodigest>|<oci-version-label>".
|
||||
# Empty (both sides) when the image isn't present. Rootless-aware.
|
||||
updaterInspectLocal() {
|
||||
local ref="$1" out
|
||||
out="$(dockerCommandRun "docker inspect --format '{{index .RepoDigests 0}}|{{index .Config.Labels \"org.opencontainers.image.version\"}}' $ref" 2>/dev/null | tr -d '\r' | head -1)"
|
||||
local dig="${out%%|*}" lbl="${out#*|}"
|
||||
dig="${dig##*@}"; case "$dig" in sha256:*) : ;; *) dig="";; esac
|
||||
case "$lbl" in "<no value>"|"<nil>"|"$out") lbl="";; esac
|
||||
printf '%s|%s' "$dig" "$lbl"
|
||||
}
|
||||
|
||||
# Registry index digest for repo:tag WITHOUT pulling (buildx imagetools). This is
|
||||
# the same identity docker records as the local RepoDigest, so a straight compare
|
||||
# is exact. "" on any failure (offline / rate-limited) — caller treats "" as
|
||||
# "unknown", never as "changed". Rootless-aware.
|
||||
updaterRegistryDigest() {
|
||||
dockerCommandRun "docker buildx imagetools inspect $1 --format '{{.Manifest.Digest}}'" 2>/dev/null \
|
||||
| tr -d '\r' | grep -oE 'sha256:[0-9a-f]{64}' | head -1
|
||||
}
|
||||
|
||||
# Human-readable version for display: OCI label → tag (versioned) → tag·shortdigest.
|
||||
updaterDisplayVersion() {
|
||||
local oci="$1" channel="$2" vtype="$3" digest="$4"
|
||||
if [ -n "$oci" ]; then printf '%s' "$oci"; return; fi
|
||||
if [ "$vtype" = "versioned" ] && [ -n "$channel" ]; then printf '%s' "$channel"; return; fi
|
||||
local short="${digest#sha256:}"; short="${short:0:7}"
|
||||
if [ -n "$short" ]; then printf '%s · %s' "${channel:-latest}" "$short"; else printf '%s' "${channel:-latest}"; fi
|
||||
}
|
||||
|
||||
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
|
||||
@ -65,35 +133,110 @@ webuiUpdaterScan() {
|
||||
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
|
||||
# Registry lookups are the expensive step, so throttle them SEPARATELY from
|
||||
# the cheap local scan: at most once per CFG_UPDATER_REGISTRY_INTERVAL
|
||||
# minutes (default 360 = 6h; 0 = never, local-only). Between windows we reuse
|
||||
# the previous available_digest from the existing updates.json, so the app
|
||||
# list + running versions still refresh every scan. UPDATER_REGISTRY_FORCE=1
|
||||
# (the "Check now" button) forces a live pull.
|
||||
local prev_json="$out_dir/updates.json"
|
||||
local reg_interval="${CFG_UPDATER_REGISTRY_INTERVAL:-360}"
|
||||
local reg_stamp="/tmp/libreportal_updater_registry_checked"
|
||||
local do_registry=1
|
||||
[ "$reg_interval" = "0" ] && do_registry=0
|
||||
if [ -z "${UPDATER_REGISTRY_FORCE:-}" ] && [ "$do_registry" = "1" ] && [ -f "$reg_stamp" ]; then
|
||||
local _rn _rl; _rn=$(date +%s); _rl=$(stat -c '%Y' "$reg_stamp" 2>/dev/null || echo 0)
|
||||
(( _rn - _rl < reg_interval * 60 )) && do_registry=0
|
||||
fi
|
||||
local have_jq=0; command -v jq >/dev/null 2>&1 && have_jq=1
|
||||
|
||||
# Build the per-app updates array (one compact JSON object per line → jq -s).
|
||||
local objs; objs="$(mktemp)"
|
||||
for app in "${apps[@]}"; do
|
||||
local image="" compose="$containers_dir/$app/docker-compose.yml"
|
||||
[ -f "$compose" ] && image="$(updaterPrimaryImage "$app" "$compose")"
|
||||
# --- 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"
|
||||
local compose="$containers_dir/$app/docker-compose.yml"
|
||||
[ -f "$compose" ] || continue
|
||||
local anchor; anchor="$(updaterPrimaryImage "$app" "$compose")"
|
||||
local channel; channel="$(updaterTagOf "$anchor")"; [ -n "$channel" ] || channel="latest"
|
||||
local vtype; vtype="$(updaterClassifyTag "$channel")"
|
||||
|
||||
[ $first -eq 0 ] && entries+=","
|
||||
first=0
|
||||
entries+=$(cat <<EOF
|
||||
# local running identity of the anchor image
|
||||
local li dig oci; li="$(updaterInspectLocal "$anchor")"; dig="${li%%|*}"; oci="${li#*|}"
|
||||
|
||||
{ "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
|
||||
)
|
||||
# available (registry) digest for the anchor's channel
|
||||
local avail_dig=""
|
||||
if [ "$do_registry" = "1" ]; then
|
||||
avail_dig="$(updaterRegistryDigest "$(updaterRepoTag "$anchor")")"
|
||||
fi
|
||||
# reuse the prior value when we didn't (or couldn't) reach the registry
|
||||
if [ -z "$avail_dig" ] && [ "$have_jq" = "1" ] && [ -f "$prev_json" ]; then
|
||||
avail_dig="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.available_digest)//""' "$prev_json" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
local update_available=false
|
||||
[ -n "$dig" ] && [ -n "$avail_dig" ] && [ "$dig" != "$avail_dig" ] && update_available=true
|
||||
|
||||
local cur_ver; cur_ver="$(updaterDisplayVersion "$oci" "$channel" "$vtype" "$dig")"
|
||||
local avail_ver=""
|
||||
if [ "$update_available" = "true" ]; then
|
||||
local sa="${avail_dig#sha256:}"; sa="${sa:0:7}"
|
||||
avail_ver="${channel} · ${sa}"
|
||||
fi
|
||||
|
||||
# services[] — every image line, anchored flag + per-service digest
|
||||
local svcs="[]"
|
||||
if [ "$have_jq" = "1" ]; then
|
||||
local sobjs; sobjs="$(mktemp)"
|
||||
local anchor_svc="${app//_/-}-service"
|
||||
while IFS="$(printf '\t')" read -r sname simg; do
|
||||
[ -n "$simg" ] || continue
|
||||
local sli sdig; sli="$(updaterInspectLocal "$simg")"; sdig="${sli%%|*}"
|
||||
local stag; stag="$(updaterTagOf "$simg")"; [ -n "$stag" ] || stag="latest"
|
||||
local is_anchor=false; [ "$sname" = "$anchor_svc" ] && is_anchor=true
|
||||
jq -cn --arg service "$sname" --arg image "$simg" --arg channel "$stag" \
|
||||
--arg type "$(updaterClassifyTag "$stag")" --arg pinned "$sdig" \
|
||||
--argjson anchor "$is_anchor" \
|
||||
'{service:$service,image:$image,channel:$channel,type:$type,pinned_digest:$pinned,is_anchor:$anchor}' \
|
||||
>> "$sobjs" 2>/dev/null
|
||||
done < <(updaterAllServiceImages "$compose")
|
||||
svcs="$(jq -cs '.' "$sobjs" 2>/dev/null || echo '[]')"; rm -f "$sobjs"
|
||||
fi
|
||||
|
||||
if [ "$have_jq" = "1" ]; then
|
||||
jq -cn \
|
||||
--arg name "$app" --arg displayName "$app" --arg type "$vtype" \
|
||||
--arg channel "$channel" --arg current_image "$anchor" \
|
||||
--arg current_version "$cur_ver" --arg current_digest "$dig" \
|
||||
--arg available_image "$anchor" --arg available_version "$avail_ver" \
|
||||
--arg available_digest "$avail_dig" --argjson update_available "$update_available" \
|
||||
--arg last_checked "$now" --argjson services "$svcs" \
|
||||
'{name:$name,displayName:$displayName,type:$type,channel:$channel,
|
||||
current_image:$current_image,current_version:$current_version,current_digest:$current_digest,
|
||||
available_image:$available_image,
|
||||
available_version:($available_version|select(.!="")),
|
||||
available_digest:$available_digest,update_available:$update_available,
|
||||
scanned:true,last_checked:$last_checked,services:$services}' \
|
||||
>> "$objs" 2>/dev/null
|
||||
else
|
||||
# jq-less fallback: minimal but valid object
|
||||
printf '{"name":"%s","displayName":"%s","current_image":"%s","current_version":"%s","update_available":false,"scanned":true,"last_checked":"%s"}\n' \
|
||||
"$app" "$app" "$(printf '%s' "$anchor" | sed 's/"/\\"/g')" \
|
||||
"$(printf '%s' "$cur_ver" | sed 's/"/\\"/g')" "$now" >> "$objs"
|
||||
fi
|
||||
done
|
||||
[ "$do_registry" = "1" ] && touch "$reg_stamp" 2>/dev/null || true
|
||||
|
||||
local tmp; tmp="$(mktemp)"
|
||||
cat > "$tmp" <<EOF
|
||||
{ "generated_at": "$now", "apps": [${entries}
|
||||
] }
|
||||
EOF
|
||||
# Write as the container user that owns out_dir. cp'ing the manager-owned
|
||||
# mktemp would fail (the container user can't read a 600 /tmp file), so the
|
||||
# old `runFileOp cp || cp` fell through to a manager cp that EACCES'd on the
|
||||
# container-owned dir. runFileWrite reads the tmp in this (manager) shell and
|
||||
# tees it as the container user — works in both modes.
|
||||
if [ "$have_jq" = "1" ]; then
|
||||
jq -s --arg now "$now" '{generated_at:$now, apps:.}' "$objs" > "$tmp" 2>/dev/null \
|
||||
|| printf '{ "generated_at": "%s", "apps": [] }\n' "$now" > "$tmp"
|
||||
else
|
||||
{ printf '{ "generated_at": "%s", "apps": [' "$now"
|
||||
paste -sd, "$objs"; printf '] }\n'; } > "$tmp"
|
||||
fi
|
||||
rm -f "$objs"
|
||||
# Write as the container user that owns out_dir (runFileWrite reads the
|
||||
# manager-owned tmp here and tees it as the container user — works in both
|
||||
# rootless and rooted modes; a plain cp would EACCES on the container dir).
|
||||
runFileWrite "$out_dir/updates.json" < "$tmp"
|
||||
rm -f "$tmp"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user