420 lines
11 KiB
Bash
420 lines
11 KiB
Bash
#!/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 ====" |