Files
shell_docker_compose_update.sh/shell_docker_compose_update.sh
2026-03-29 19:05:07 +02:00

420 lines
11 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$BASE_DIR/config.conf"
mkdir -p "$(dirname "$LOG_FILE")"
LOG_LEVEL="${LOG_LEVEL:-INFO}"
level_to_int() {
case "$1" in
DEBUG) echo 0 ;;
INFO) echo 1 ;;
WARN) echo 2 ;;
ERROR) echo 3 ;;
*) echo 1 ;;
esac
}
should_log() {
[ "$(level_to_int "$1")" -ge "$(level_to_int "$LOG_LEVEL")" ]
}
log() {
local level="$1"
shift
local msg="$*"
if should_log "$level"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | $level | $msg" | tee -a "$LOG_FILE"
fi
}
# =============================
# Log begrenzen
# =============================
if [ -f "$LOG_FILE" ]; then
line_count=$(wc -l < "$LOG_FILE")
if [ "$line_count" -gt 1000 ]; then
tail -n 1000 "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE"
fi
fi
# =============================
# Helper
# =============================
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
}
get_image() {
local svc="$1"
docker compose config | awk "/^ $svc:/,/image:/" | grep image | head -n1 | awk '{print $2}'
}
get_container_image_id() {
local svc="$1"
local cid
cid=$(docker compose ps -q "$svc" 2>/dev/null || true)
[ -z "$cid" ] && echo "" && return
docker inspect -f '{{.Image}}' "$cid" 2>/dev/null || echo ""
}
get_local_image_id() {
local image="$1"
docker image inspect -f '{{.Id}}' "$image" 2>/dev/null || echo ""
}
get_container_image_ref() {
local svc="$1"
local cid
cid=$(docker compose ps -q "$svc" 2>/dev/null || true)
[ -z "$cid" ] && echo "none" && return
docker inspect -f '{{.Config.Image}}' "$cid" 2>/dev/null || echo "unknown"
}
get_local_image_digest() {
local image="$1"
docker inspect --format='{{index .RepoDigests 0}}' "$image" 2>/dev/null || echo "unknown"
}
is_running() {
local svc="$1"
local cid
cid=$(docker compose ps -q "$svc" 2>/dev/null || true)
[ -z "$cid" ] && return 1
docker inspect -f '{{.State.Running}}' "$cid" 2>/dev/null | grep -q true
}
run_cmd() {
if [ "$DRY_RUN" = true ]; then
log DEBUG "[DRY RUN] $*"
else
eval "$@"
fi
}
send_ntfy() {
local msg="$1"
local prio="$2"
local image_url="${NTFY_IMAGE_URL:-}"
if [ -n "$image_url" ]; then
curl -s \
-H "Authorization: Bearer $NTFY_TOKEN" \
-H "Title: $NTFY_TITLE" \
-H "Priority: $prio" \
-H "Tags: $NTFY_TAGS" \
-H "Icon: $image_url" \
-d "$msg" \
"$NTFY_URL" > /dev/null || true
else
curl -s \
-H "Authorization: Bearer $NTFY_TOKEN" \
-H "Title: $NTFY_TITLE" \
-H "Priority: $prio" \
-H "Tags: $NTFY_TAGS" \
-d "$msg" \
"$NTFY_URL" > /dev/null || true
fi
}
get_docker_disk_usage() {
local total=0
while read -r size; do
num="${size//[!0-9.]/}"
num="${num:-0}"
if [[ "$size" == *GB ]]; then
total=$(bc <<< "$total + ($num * 1024)")
elif [[ "$size" == *MB ]]; then
total=$(bc <<< "$total + $num")
elif [[ "$size" == *kB ]]; then
total=$(bc <<< "$total + ($num / 1024)")
fi
done < <(docker system df --format '{{.Size}}' 2>/dev/null)
log DEBUG "Docker usage raw: $total MB"
printf "%.0f\n" "$total"
}
# =============================
# Start
# =============================
log INFO "==== Docker Compose Update gestartet ===="
notify_stacks_updated=()
notify_excluded_updates=()
error_flag=false
cd "$COMPOSE_DIR"
while IFS= read -r -d '' file; do
dir=$(dirname "$file")
stack=$(basename "$dir")
if is_stack_excluded "$stack"; then
log INFO "→ Stack $stack übersprungen (excluded)"
continue
fi
log INFO ""
log INFO "→ Prüfe Stack: $stack"
cd "$dir"
mapfile -t services < <(docker compose config --services)
total_services=${#services[@]}
current_index=0
# Running State merken
declare -A was_running
for svc in "${services[@]}"; do
if is_running "$svc"; then
was_running["$svc"]=1
else
was_running["$svc"]=0
fi
done
stack_updated=false
changed_services=()
version_report=()
for svc in "${services[@]}"; do
current_index=$((current_index + 1))
if [ "$current_index" -eq "$total_services" ]; then
prefix="└─"
else
prefix="├─"
fi
image=$(get_image "$svc")
if [ -z "$image" ]; then
log WARN " $prefix $svc → kein Image"
continue
fi
if is_excluded "$svc"; then
docker pull "$image" >/dev/null 2>&1 || true
notify_excluded_updates+=("$stack/$svc")
log INFO " $prefix $svc (excluded)"
continue
fi
log INFO " $prefix $svc ($image)"
before_id=$(get_container_image_id "$svc")
before_ref=$(get_container_image_ref "$svc")
if ! docker pull "$image" >/dev/null 2>&1; then
log ERROR " $prefix ❌ Pull fehlgeschlagen"
error_flag=true
continue
fi
after_id=$(get_local_image_id "$image")
after_digest=$(get_local_image_digest "$image")
if [ -n "$before_id" ] && [ "$before_id" != "$after_id" ]; then
stack_updated=true
changed_services+=("$svc")
if [ "$SHOW_VERSIONS" = true ]; then
log WARN " ⬆️ UPDATE"
log INFO " alt: $before_ref"
log INFO " neu: $after_digest"
version_report+=("$svc: ${before_ref##*@}${after_digest##*@}")
else
log WARN " ⬆️ UPDATE"
fi
fi
done
# =============================
# Update Logik
# =============================
if [ "$stack_updated" = true ]; then
if [ "$total_services" -eq 1 ]; then
svc="${services[0]}"
log WARN " 🔄 Einzelcontainer-Update: $svc"
if [ "${was_running[$svc]}" = 1 ]; then
run_cmd docker compose up -d "$svc" --remove-orphans --no-color >/dev/null 2>&1
else
run_cmd docker compose create "$svc"
fi
log INFO " ✔️ Container aktualisiert"
notify_stacks_updated+=("$stack ($svc)")
else
log WARN " 🔄 Stack wird neu deployt (Trigger: ${changed_services[*]})"
if ! run_cmd docker compose up -d --remove-orphans >/dev/null 2>&1; then
log ERROR " ❌ Stack Update fehlgeschlagen"
error_flag=true
else
log INFO " ✔️ Stack erfolgreich aktualisiert"
# vorher gestoppte wieder stoppen
for svc in "${services[@]}"; do
if [ "${was_running[$svc]}" = 0 ]; then
log INFO " ⏹️ Stoppe $svc (war vorher gestoppt)"
run_cmd docker compose stop "$svc" >/dev/null 2>&1 || true
fi
done
notify_stacks_updated+=("$stack (${changed_services[*]})")
fi
fi
else
log DEBUG " ✔️ Keine Updates"
fi
cd "$COMPOSE_DIR"
done < <(find . -name "$COMPOSE_PATTERN" -print0 | sort -z)
# =============================
# Cleanup (mit Statistik)
# =============================
freed_space="0"
if [ "$ENABLE_CLEANUP" = true ]; then
if [ "$CLEANUP_ONLY_ON_UPDATE" = true ] && \
[ ${#notify_stacks_updated[@]} -eq 0 ]; then
log INFO "🧹 Cleanup übersprungen (keine Updates)"
else
before_size=$(get_docker_disk_usage)
log INFO "🧹 Docker Cleanup läuft..."
if [ "$CLEANUP_IMAGES" = true ]; then
case "$CLEANUP_IMAGES_MODE" in
unused)
log INFO " → Entferne ungenutzte Images"
run_cmd docker image prune -a -f >/dev/null 2>&1
;;
dangling)
log INFO " → Entferne dangling Images"
run_cmd docker image prune -f >/dev/null 2>&1
;;
esac
fi
if [ "$CLEANUP_CONTAINERS" = true ]; then
log INFO " → Entferne gestoppte Container"
run_cmd docker container prune -f >/dev/null 2>&1
fi
if [ "$CLEANUP_VOLUMES" = true ]; then
log WARN " → Entferne ungenutzte Volumes"
run_cmd docker volume prune -f >/dev/null 2>&1
fi
if [ "$CLEANUP_NETWORKS" = true ]; then
log INFO " → Entferne ungenutzte Netzwerke"
run_cmd docker network prune -f >/dev/null 2>&1
fi
after_size=$(get_docker_disk_usage)
freed_space=$(awk "BEGIN {print $before_size - $after_size}")
log INFO "✔️ Cleanup abgeschlossen (freigegeben: ${freed_space} MB)"
fi
fi
# =============================
# Notification
# =============================
if [ "$NTFY_ENABLED" = true ]; then
msg="Docker Compose Update Report"
if [ ${#notify_stacks_updated[@]} -gt 0 ]; then
msg+=$'\n\n🔄 Aktualisierte Stacks'
for s in "${notify_stacks_updated[@]}"; do
msg+=$'\n - '"$s"
done
fi
if [ ${#notify_excluded_updates[@]} -gt 0 ]; then
msg+=$'\n\n⏭ Excluded (Update verfügbar)'
for s in "${notify_excluded_updates[@]}"; do
msg+=$'\n - '"$s"
done
fi
if [ "$error_flag" = true ]; then
msg+=$'\n\n❗ Fehler sind aufgetreten Logs prüfen'
PRIORITY=5
elif [ ${#notify_stacks_updated[@]} -gt 0 ]; then
PRIORITY=3
else
PRIORITY=1
fi
if [ ${#notify_stacks_updated[@]} -eq 0 ] && \
[ ${#notify_excluded_updates[@]} -eq 0 ]; then
msg+=$'\n\n✔ Alles aktuell'
fi
if [ "$freed_space" != "0" ]; then
msg+=$'\n\n🧹 Cleanup: '"${freed_space} MB freigegeben"
fi
if [ "$NTFY_ONLY_ON_CHANGES" = false ] || \
[ ${#notify_stacks_updated[@]} -gt 0 ] || \
[ ${#notify_excluded_updates[@]} -gt 0 ] || \
[ "$error_flag" = true ]; then
send_ntfy "$msg" "$PRIORITY"
log INFO "ntfy Nachricht gesendet (prio=$PRIORITY)"
else
log INFO "keine Änderungen, keine ntfy Nachricht"
fi
fi
log INFO "==== Update beendet ===="