Merge claude/2
This commit is contained in:
commit
06bd49d768
@ -3,4 +3,5 @@
|
|||||||
# @icon 🔄
|
# @icon 🔄
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - Minutes between automatic app update/CVE/improvement scans. 0 disables.
|
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]
|
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"
|
isError " Invalid crontab format for $var_name"
|
||||||
fi
|
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
|
# Validate numeric values
|
||||||
if ! echo "$var_value" | grep -qE '^[0-9]+$'; then
|
if ! echo "$var_value" | grep -qE '^[0-9]+$'; then
|
||||||
isError " $var_name must be a positive integer"
|
isError " $var_name must be a positive integer"
|
||||||
|
|||||||
@ -31,17 +31,85 @@
|
|||||||
# hyphens in the service name, e.g. libreportal_catalog → libreportal-catalog-
|
# hyphens in the service name, e.g. libreportal_catalog → libreportal-catalog-
|
||||||
# service), then take the first `image:` under it. Falls back to the first
|
# service), then take the first `image:` under it. Falls back to the first
|
||||||
# image line for anything that doesn't follow the convention.
|
# 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() {
|
updaterPrimaryImage() {
|
||||||
local app="$1" compose="$2" svc img
|
local app="$1" compose="$2" svc img
|
||||||
svc="${app//_/-}-service"
|
svc="${app//_/-}-service"
|
||||||
img="$(awk -v s="$svc" '
|
img="$(awk -v s="$svc" '
|
||||||
m && /^[[:space:]]*image:/ { print; exit }
|
m && /^[[:space:]]*image:/ { print; exit }
|
||||||
$0 ~ ("^[[:space:]]*" s ":") { m=1 }
|
$0 ~ ("^[[:space:]]*" s ":") { m=1 }
|
||||||
' "$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 | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')"
|
[ -n "$img" ] || img="$(grep -m1 -E '^\s*image:' "$compose" 2>/dev/null | _updaterCleanImageRef)"
|
||||||
printf '%s' "$img"
|
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() {
|
webuiUpdaterScan() {
|
||||||
local out_dir="$containers_dir/libreportal/frontend/data/updater/generated"
|
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
|
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)
|
done < <(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build the per-app updates array.
|
# Registry lookups are the expensive step, so throttle them SEPARATELY from
|
||||||
local entries="" first=1
|
# 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
|
for app in "${apps[@]}"; do
|
||||||
local image="" compose="$containers_dir/$app/docker-compose.yml"
|
local compose="$containers_dir/$app/docker-compose.yml"
|
||||||
[ -f "$compose" ] && image="$(updaterPrimaryImage "$app" "$compose")"
|
[ -f "$compose" ] || continue
|
||||||
# --- available-version hook -------------------------------------------
|
local anchor; anchor="$(updaterPrimaryImage "$app" "$compose")"
|
||||||
# Wire a registry check here (e.g. `docker manifest inspect`/skopeo) to
|
local channel; channel="$(updaterTagOf "$anchor")"; [ -n "$channel" ] || channel="latest"
|
||||||
# set available_image + update_available. Honest default: up to date.
|
local vtype; vtype="$(updaterClassifyTag "$channel")"
|
||||||
local available="$image" update_available="false"
|
|
||||||
|
|
||||||
[ $first -eq 0 ] && entries+=","
|
# local running identity of the anchor image
|
||||||
first=0
|
local li dig oci; li="$(updaterInspectLocal "$anchor")"; dig="${li%%|*}"; oci="${li#*|}"
|
||||||
entries+=$(cat <<EOF
|
|
||||||
|
|
||||||
{ "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" }
|
# available (registry) digest for the anchor's channel
|
||||||
EOF
|
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
|
done
|
||||||
|
[ "$do_registry" = "1" ] && touch "$reg_stamp" 2>/dev/null || true
|
||||||
|
|
||||||
local tmp; tmp="$(mktemp)"
|
local tmp; tmp="$(mktemp)"
|
||||||
cat > "$tmp" <<EOF
|
if [ "$have_jq" = "1" ]; then
|
||||||
{ "generated_at": "$now", "apps": [${entries}
|
jq -s --arg now "$now" '{generated_at:$now, apps:.}' "$objs" > "$tmp" 2>/dev/null \
|
||||||
] }
|
|| printf '{ "generated_at": "%s", "apps": [] }\n' "$now" > "$tmp"
|
||||||
EOF
|
else
|
||||||
# Write as the container user that owns out_dir. cp'ing the manager-owned
|
{ printf '{ "generated_at": "%s", "apps": [' "$now"
|
||||||
# mktemp would fail (the container user can't read a 600 /tmp file), so the
|
paste -sd, "$objs"; printf '] }\n'; } > "$tmp"
|
||||||
# old `runFileOp cp || cp` fell through to a manager cp that EACCES'd on the
|
fi
|
||||||
# container-owned dir. runFileWrite reads the tmp in this (manager) shell and
|
rm -f "$objs"
|
||||||
# tees it as the container user — works in both modes.
|
# 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"
|
runFileWrite "$out_dir/updates.json" < "$tmp"
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user