This commit is contained in:
2026-04-01 13:27:26 +02:00
parent f93fb53638
commit d67d0f936e
2 changed files with 74 additions and 121 deletions

View File

@@ -142,18 +142,6 @@ services:
- composeupdater.mode=update - composeupdater.mode=update
``` ```
#### 📦 Stack-Level Label
Du kannst das Verhalten auch für den gesamten Stack setzen:
```yml
labels:
- composeupdater.mode=ignore
```
Das Stack-Level Label definiert den Standard für alle Services im Stack
Service-Labels können diesen Standard überschreiben
--- ---
## 🗑️ Prune / Cleanup ## 🗑️ Prune / Cleanup
@@ -321,25 +309,26 @@ chmod +x script.sh
## 📄 Beispiel Ausgabe ## 📄 Beispiel Ausgabe
``` ```
Prüfe Stack: rss 🔍 Prüfe Stack: rss
├─ read (phpdockerio/readability-js-server) ├─ read (phpdockerio/readability-js-server) [Mode: 🔄 update]
├─ merc (wangqiru/mercury-parser-api) ├─ merc (wangqiru/mercury-parser-api) [Mode: 🔄 update]
├─ full-text-rss (heussd/fivefilters-full-text-rss:latest) ├─ full-text-rss (heussd/fivefilters-full-text-rss:latest) [Mode: 🔄 update]
├─ rss-bridge (rssbridge/rss-bridge:latest) ├─ rss-bridge (rssbridge/rss-bridge:latest) [Mode: 🔄 update]
⬆ UPDATE ⬆ UPDATE
alt: rssbridge/rss-bridge:latest@sha256:55215923cf81b2fa6fbb7ecc1bd2555405f4fc06029ae9876e91164a735c7b9d alt: rssbridge/rss-bridge:latest@sha256:55215923cf81b2fa6fbb7ecc1bd2555405f4fc06029ae9876e91164a735c7b9d
neu: rssbridge/rss-bridge:latest@sha256:f3f0218c8b075cbc7c559c8e6852888e95fa6d68258436da6195efc5ab98b025 neu: rssbridge/rss-bridge:latest@sha256:f3f0218c8b075cbc7c559c8e6852888e95fa6d68258436da6195efc5ab98b025
└─ freshrss (freshrss/freshrss:latest) └─ freshrss (freshrss/freshrss:latest) [Mode: 🔄 update]
🔄 Stack wird neu deployt (Trigger: rss-bridge) ♻️ Stack wird neu deployt (Trigger: rss-bridge)
⏳ Deploy läuft... ⏳ Deploy läuft...
Stack erfolgreich aktualisiert Stack erfolgreich aktualisiert
Keine Healthchecks definiert → überspringe warten 💤 Warte 60s nach Deploy
⏱ Dauer: 18s Keine Healthchecks definiert → überspringe warten
🕒 Dauer: 18s
→ Prüfe Stack: tinymediamanager → Prüfe Stack: tinymediamanager
└─ tinymediamanager (tinymediamanager/tinymediamanager:latest) └─ tinymediamanager [Mode: 🚫 ignore]
Dauer: 1s 🕒 Dauer: 1s
``` ```
--- ---

View File

@@ -18,6 +18,9 @@ level_to_int() {
esac esac
} }
INDENT=" "
SUBINDENT=" "
should_log() { should_log() {
[ "$(level_to_int "$1")" -ge "$(level_to_int "$LOG_LEVEL")" ] [ "$(level_to_int "$1")" -ge "$(level_to_int "$LOG_LEVEL")" ]
} }
@@ -46,45 +49,6 @@ fi
get_service_mode() { get_service_mode() {
local svc="$1" local svc="$1"
echo "$compose_json" \
| jq -r --arg svc "$svc" '
.services[$svc].labels // []
| map(select(startswith("composeupdater.mode=")))
| .[0] // "composeupdater.mode=update"
| split("=")[1]
'
}
get_stack_mode() {
echo "$compose_json" \
| jq -r '
.labels // []
| map(select(startswith("composeupdater.mode=")))
| .[0] // "composeupdater.mode=update"
| split("=")[1]
'
}
is_excluded() {
local svc="$1"
for ex in "${EXCLUDE_SERVICES[@]}"; do
[[ "$svc" == "$ex" ]] && return 0
done
return 1
}
is_stack_excluded() {
local stack="$1"
for ex in "${EXCLUDE_STACKS[@]}"; do
[[ "$stack" == "$ex" ]] && return 0
done
return 1
}
is_label_excluded() {
local svc="$1"
echo "$compose_json" \ echo "$compose_json" \
| jq -r --arg svc "$svc" ' | jq -r --arg svc "$svc" '
.services[$svc].labels // {} .services[$svc].labels // {}
@@ -92,11 +56,11 @@ is_label_excluded() {
then map(split("=") | {(.[0]): .[1]}) | add then map(split("=") | {(.[0]): .[1]}) | add
else . else .
end) end)
| .["composeupdater.enable"] // empty | .["composeupdater.mode"] // "update"
' \ '
| grep -qi "^false$"
} }
get_image() { get_image() {
local svc="$1" local svc="$1"
@@ -174,20 +138,20 @@ wait_for_healthy() {
local start local start
start=$(date +%s) start=$(date +%s)
# 👉 Containerliste bestimmen # Containerliste bestimmen
if [ ${#services[@]} -gt 0 ]; then if [ ${#services[@]} -gt 0 ]; then
mapfile -t cids < <(docker compose ps -aq "${services[@]}") mapfile -t cids < <(docker compose ps -aq "${services[@]}")
else else
mapfile -t cids < <(docker compose ps -aq) mapfile -t cids < <(docker compose ps -aq)
fi fi
# 👉 keine Container → nichts zu tun # keine Container → nichts zu tun
if [ ${#cids[@]} -eq 0 ]; then if [ ${#cids[@]} -eq 0 ]; then
log INFO " Keine Container gefunden → überspringe Healthcheck" log INFO "${SUBINDENT} Keine Container gefunden → überspringe Healthcheck"
return 0 return 0
fi fi
# 👉 prüfen ob überhaupt Healthchecks existieren # prüfen ob überhaupt Healthchecks existieren
local has_healthcheck=false local has_healthcheck=false
for cid in "${cids[@]}"; do for cid in "${cids[@]}"; do
@@ -199,13 +163,13 @@ wait_for_healthy() {
fi fi
done done
# 👉 wenn keiner vorhanden → direkt raus # wenn keiner vorhanden → direkt raus
if [ "$has_healthcheck" = false ]; then if [ "$has_healthcheck" = false ]; then
log INFO " Keine Healthchecks definiert → überspringe warten" log INFO "${SUBINDENT} Keine Healthchecks → überspringe warten"
return 0 return 0
fi fi
log INFO " ⏳ Warte auf healthy Container (max ${timeout}s)" log INFO "${SUBINDENT}⏳ Warte auf healthy Container (max ${timeout}s)"
while true; do while true; do
local all_ok=true local all_ok=true
@@ -220,7 +184,7 @@ wait_for_healthy() {
done done
if [ "$all_ok" = true ]; then if [ "$all_ok" = true ]; then
log INFO " ✔️ Alle Container healthy" log INFO "${SUBINDENT}💚 Alle Container healthy"
return 0 return 0
fi fi
@@ -228,7 +192,7 @@ wait_for_healthy() {
now=$(date +%s) now=$(date +%s)
if [ $((now - start)) -ge "$timeout" ]; then if [ $((now - start)) -ge "$timeout" ]; then
log WARN " ⚠️ Healthcheck Timeout erreicht" log WARN "${SUBINDENT}⚠️ Healthcheck Timeout erreicht"
return 1 return 1
fi fi
@@ -288,7 +252,7 @@ while IFS= read -r -d '' file; do
stack=$(basename "$dir") stack=$(basename "$dir")
log INFO "" log INFO ""
log INFO " Prüfe Stack: $stack" log INFO "🔍 Prüfe Stack: $stack"
stack_start=$(date +%s) stack_start=$(date +%s)
@@ -296,8 +260,6 @@ while IFS= read -r -d '' file; do
compose_json=$(docker compose config --format json) compose_json=$(docker compose config --format json)
stack_mode=$(get_stack_mode)
mapfile -t services < <(docker compose config --services) mapfile -t services < <(docker compose config --services)
total_services=${#services[@]} total_services=${#services[@]}
current_index=0 current_index=0
@@ -331,33 +293,36 @@ while IFS= read -r -d '' file; do
fi fi
# ============================= # =============================
# Mode bestimmen (Service > Stack) # Mode (nur Service!)
# ============================= # =============================
mode=$(get_service_mode "$svc") mode=$(get_service_mode "$svc")
if [ -z "$mode" ]; then case "$mode" in
mode="$stack_mode" update) mode_label="Mode: 🔄 update" ;;
fi notify-only) mode_label="Mode: 🔔 notify-only" ;;
ignore) mode_label="Mode: 🚫 ignore" ;;
*)
mode_label="Mode: ❓ unknown"
mode="update"
;;
esac
case "$mode" in case "$mode" in
ignore) ignore)
log INFO " $prefix $svc (ignore)" log INFO "${INDENT}$prefix $svc [$mode_label]"
continue continue
;; ;;
notify-only) notify-only)
log INFO " $prefix $svc (notify-only)" log INFO "${INDENT}$prefix $svc ($image) [$mode_label]"
before_id=$(get_container_image_id "$svc") before_id=$(get_container_image_id "$svc")
# 👉 ohne Container kein Vergleich sinnvoll
if [ -z "$before_id" ]; then if [ -z "$before_id" ]; then
continue continue
fi fi
# 👉 Pull nur einmal pro Image
if [ -z "${pulled_images[$image]:-}" ]; then if [ -z "${pulled_images[$image]:-}" ]; then
pull_with_retry "$image" || true pull_with_retry "$image" || true
pulled_images[$image]=1 pulled_images[$image]=1
@@ -373,21 +338,15 @@ while IFS= read -r -d '' file; do
;; ;;
update) update)
# normal weiterlaufen lassen log INFO "${INDENT}$prefix $svc ($image) [$mode_label]"
;; ;;
esac
*)
log WARN " $prefix $svc (unbekannter mode: $mode → fallback=update)"
;;
esac
log INFO " $prefix $svc ($image)"
before_id=$(get_container_image_id "$svc") before_id=$(get_container_image_id "$svc")
# 👉 Skip wenn kein Container und nicht erlaubt
if [ -z "$before_id" ] && [ "$UPDATE_INCLUDE_STOPPED" = false ]; then if [ -z "$before_id" ] && [ "$UPDATE_INCLUDE_STOPPED" = false ]; then
log INFO " ⏭️ übersprungen (kein Container vorhanden)" log INFO "${SUBINDENT}⏭️ übersprungen (kein Container vorhanden)"
continue continue
fi fi
@@ -397,13 +356,13 @@ while IFS= read -r -d '' file; do
if [ -z "${pulled_images[$image]:-}" ]; then if [ -z "${pulled_images[$image]:-}" ]; then
if ! pull_with_retry "$image"; then if ! pull_with_retry "$image"; then
log ERROR " ❌ Pull fehlgeschlagen" log ERROR "${INDENT}❌ Pull fehlgeschlagen"
error_flag=true error_flag=true
continue continue
fi fi
pulled_images[$image]=1 pulled_images[$image]=1
else else
log DEBUG " ⏩ Pull übersprungen (bereits gemacht)" log DEBUG "${SUBINDENT}⏩ Pull übersprungen (bereits gemacht)"
fi fi
after_id=$(get_local_image_id "$image") after_id=$(get_local_image_id "$image")
@@ -427,9 +386,9 @@ while IFS= read -r -d '' file; do
stack_updated=true stack_updated=true
changed_services+=("$svc") changed_services+=("$svc")
log INFO " ⬆️ UPDATE" log INFO "${SUBINDENT}⬆️ UPDATE"
log INFO " alt: ${image}@${before_id}" log INFO "${SUBINDENT} alt: ${image}@${before_id}"
log INFO " neu: ${image}@${after_id}" log INFO "${SUBINDENT} neu: ${image}@${after_id}"
short_before="${before_id#sha256:}" short_before="${before_id#sha256:}"
short_before="${short_before:0:6}" short_before="${short_before:0:6}"
@@ -448,7 +407,9 @@ while IFS= read -r -d '' file; do
if [ "$total_services" -eq 1 ]; then if [ "$total_services" -eq 1 ]; then
svc="${services[0]}" svc="${services[0]}"
log INFO " 🔄 Einzelcontainer-Update: $svc"
log INFO ""
log INFO "${INDENT}🔄 Einzelcontainer-Update: $svc"
if [ "${was_running[$svc]}" = 1 ]; then if [ "${was_running[$svc]}" = 1 ]; then
run_cmd docker compose up -d "$svc" --remove-orphans run_cmd docker compose up -d "$svc" --remove-orphans
@@ -460,44 +421,44 @@ while IFS= read -r -d '' file; do
fi fi
fi fi
log INFO " ✔️ Container $svc aktualisiert" log INFO "${SUBINDENT} Container aktualisiert"
# 👉 feste Wartezeit
if [ "${REDEPLOY_WAIT:-0}" -gt 0 ]; then if [ "${REDEPLOY_WAIT:-0}" -gt 0 ]; then
log INFO " Warte ${REDEPLOY_WAIT}s nach Deploy" log INFO "${SUBINDENT}💤 Warte ${REDEPLOY_WAIT}s nach Deploy"
sleep "$REDEPLOY_WAIT" sleep "$REDEPLOY_WAIT"
fi fi
# 👉 optionaler Healthcheck
if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then
wait_for_healthy "$REDEPLOY_WAIT_HEALTHY_TIMEOUT" "$svc" wait_for_healthy "$REDEPLOY_WAIT_HEALTHY_TIMEOUT" "$svc"
else
log INFO "${SUBINDENT} Keine Healthchecks → überspringe warten"
fi fi
else else
log INFO " 🔄 Stack wird neu deployt (Trigger: ${changed_services[*]})" log INFO ""
log INFO " ⏳ Deploy läuft..." log INFO "${INDENT}♻️ Stack wird neu deployt (Trigger: ${changed_services[*]})"
log INFO "${SUBINDENT}⏳ Deploy läuft..."
if ! run_cmd docker compose up -d --remove-orphans >/dev/null 2>&1; then if ! run_cmd docker compose up -d --remove-orphans >/dev/null 2>&1; then
log ERROR " ❌ Stack Update fehlgeschlagen" log ERROR "${SUBINDENT}❌ Stack Update fehlgeschlagen"
error_flag=true error_flag=true
else else
log INFO " ✔️ Stack erfolgreich aktualisiert" log INFO "${SUBINDENT} Stack erfolgreich aktualisiert"
# 👉 feste Wartezeit
if [ "${REDEPLOY_WAIT:-0}" -gt 0 ]; then if [ "${REDEPLOY_WAIT:-0}" -gt 0 ]; then
log INFO " Warte ${REDEPLOY_WAIT}s nach Deploy" log INFO "${SUBINDENT}💤 Warte ${REDEPLOY_WAIT}s nach Deploy"
sleep "$REDEPLOY_WAIT" sleep "$REDEPLOY_WAIT"
fi fi
# 👉 optionaler Healthcheck
if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then
wait_for_healthy "$REDEPLOY_WAIT_HEALTHY_TIMEOUT" "${changed_services[@]}" wait_for_healthy "$REDEPLOY_WAIT_HEALTHY_TIMEOUT" "${changed_services[@]}"
else
log INFO "${SUBINDENT} Keine Healthchecks → überspringe warten"
fi fi
# 👉 gestoppte wieder stoppen
for svc in "${services[@]}"; do for svc in "${services[@]}"; do
if [ "${was_running[$svc]}" = 0 ]; then if [ "${was_running[$svc]}" = 0 ]; then
log INFO " ⏹️ Stoppe $svc (war vorher gestoppt)" log INFO "${SUBINDENT}⏹️ Stoppe $svc (war vorher gestoppt)"
run_cmd docker compose stop "$svc" >/dev/null 2>&1 || true run_cmd docker compose stop "$svc" >/dev/null 2>&1 || true
fi fi
done done
@@ -536,7 +497,7 @@ while IFS= read -r -d '' file; do
cd "$PATH_COMPOSE_DIR" cd "$PATH_COMPOSE_DIR"
stack_end=$(date +%s) stack_end=$(date +%s)
log INFO " Dauer: $((stack_end - stack_start))s" log INFO "${INDENT}🕒 Dauer: $((stack_end - stack_start))s"
done < <(find . -name "$PATH_COMPOSE_PATTERN" -print0 | sort -z) done < <(find . -name "$PATH_COMPOSE_PATTERN" -print0 | sort -z)
@@ -546,6 +507,8 @@ done < <(find . -name "$PATH_COMPOSE_PATTERN" -print0 | sort -z)
freed_space="0" freed_space="0"
log INFO ""
if [ "$CLEANUP_ENABLED" = true ]; then if [ "$CLEANUP_ENABLED" = true ]; then
if [ "$CLEANUP_ONLY_ON_UPDATE" = true ] && \ if [ "$CLEANUP_ONLY_ON_UPDATE" = true ] && \
@@ -554,7 +517,8 @@ if [ "$CLEANUP_ENABLED" = true ]; then
else else
before_size=$(get_docker_disk_usage) before_size=$(get_docker_disk_usage)
log INFO "🧹 Docker Cleanup läuft..." log INFO ""
log INFO "${INDENT}🧹 Docker Cleanup läuft..."
if [ "$CLEANUP_IMAGES_ENABLED" = true ]; then if [ "$CLEANUP_IMAGES_ENABLED" = true ]; then
case "$CLEANUP_IMAGES_MODE" in case "$CLEANUP_IMAGES_MODE" in
@@ -578,7 +542,7 @@ if [ "$CLEANUP_ENABLED" = true ]; then
after_size=$(get_docker_disk_usage) after_size=$(get_docker_disk_usage)
freed_space=$((after_size < before_size ? before_size - after_size : 0)) freed_space=$((after_size < before_size ? before_size - after_size : 0))
log INFO "✔️ Cleanup abgeschlossen (${freed_space} MB freigegeben)" log INFO "${INDENT} Cleanup abgeschlossen (${freed_space} MB freigegeben)"
fi fi
fi fi
@@ -625,9 +589,9 @@ if [ "$NTFY_ENABLED" = true ]; then
fi fi
send_ntfy "$msg" "$PRIORITY" send_ntfy "$msg" "$PRIORITY"
log INFO "ntfy Nachricht gesendet (prio=$PRIORITY)" log INFO "📨 ntfy Nachricht gesendet (prio=$PRIORITY)"
fi fi
script_end=$(date +%s) script_end=$(date +%s)
log INFO " Gesamtzeit: $((script_end - script_start))s" log INFO "🕒 Gesamtzeit: $((script_end - script_start))s"
log INFO "==== Update beendet ====" log INFO "==== Update beendet ===="