Initial commit
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# update.sh - aktualisiert das Projekt (git pull --ff-only, falls dieses
|
||||
# Verzeichnis ein Git-Checkout ist) und die Caddy-/HAProxy-Images, validiert
|
||||
# die neue Konfiguration VOR jedem Neustart und macht bei fehlgeschlagenem
|
||||
# Healthcheck automatisch ein Rollback.
|
||||
#
|
||||
# Verzeichnismodell: dieses Verzeichnis IST der Git-Checkout (analog zu
|
||||
# mailcow-dockerized) - kein separates Laufzeitverzeichnis, kein Sync-Schritt.
|
||||
# `git clone <repo> /opt/reverse-proxy` und hier direkt arbeiten.
|
||||
#
|
||||
# Aufruf:
|
||||
# update/update.sh interaktiv: fragt vor dem Anwenden nach
|
||||
# update/update.sh --dry-run zeigt verfügbare Updates, fragt nichts, ändert nichts
|
||||
# update/update.sh -f|--force wendet ohne Rückfrage an (für Cron/systemd)
|
||||
# update/update.sh --yes Alias für --force (Kompatibilität)
|
||||
#
|
||||
# Hooks (optional, siehe update/*_update_hook.sh.example):
|
||||
# update/pre_update_hook.sh läuft ganz am Anfang, falls vorhanden
|
||||
# update/post_update_hook.sh läuft nach einem angewendeten Update
|
||||
# (erfolgreich oder mit Rollback), falls vorhanden
|
||||
#
|
||||
# Exit-Codes (bewusst für Monitoring/CheckMK-Einbindung gewählt):
|
||||
# 0 = keine Änderungen bzw. Dry-Run ohne Befund bzw. vom Nutzer abgelehnt
|
||||
# 1 = Update erfolgreich angewendet
|
||||
# 2 = Update fehlgeschlagen (mit oder ohne erfolgreichem Rollback), oder
|
||||
# Bestätigung erforderlich aber nicht möglich (nicht-interaktiv ohne --force)
|
||||
# - siehe update/update.log
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
LOG_DIR="$PROJECT_DIR/update"
|
||||
LOG_FILE="$LOG_DIR/update.log"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
DRY_RUN=0
|
||||
FORCE=0
|
||||
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}"
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
-f|--force|--yes) FORCE=1 ;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--dry-run] [-f|--force]"
|
||||
echo " --dry-run zeigt verfuegbare Updates, fragt nichts, aendert nichts"
|
||||
echo " -f, --force wendet ohne interaktive Rueckfrage an (fuer Cron/systemd)"
|
||||
echo " --yes Alias fuer --force"
|
||||
echo "Ohne Optionen: interaktiv, fragt vor dem Anwenden nach."
|
||||
exit 0 ;;
|
||||
*)
|
||||
echo "Unbekannte Option: $arg" >&2
|
||||
exit 64 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# .env laden (gleiche Datei, die auch docker-compose.yml verwendet) -----------
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_DIR/.env"
|
||||
set +a
|
||||
fi
|
||||
HAPROXY_IMAGE="${HAPROXY_IMAGE:-haproxy:3.2-alpine}"
|
||||
CADDY_IMAGE="${CADDY_IMAGE:-caddy:2.11-alpine}"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
run_hook() {
|
||||
local hook_file="$PROJECT_DIR/update/$1"
|
||||
if [ -f "$hook_file" ]; then
|
||||
log "Fuehre $1 aus..."
|
||||
if bash "$hook_file" >> "$LOG_FILE" 2>&1; then
|
||||
log "$1 erfolgreich."
|
||||
else
|
||||
log "WARNUNG: $1 ist mit Fehler beendet worden (Exit-Code $?), mache trotzdem weiter."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# einfache Log-Rotation, keine externen Abhängigkeiten (z.B. logrotate) nötig
|
||||
if [ -f "$LOG_FILE" ] && [ "$(wc -c < "$LOG_FILE")" -gt 5242880 ]; then
|
||||
mv "$LOG_FILE" "$LOG_FILE.1"
|
||||
fi
|
||||
|
||||
log "=== Update-Lauf gestartet (dry-run=$DRY_RUN, force=$FORCE) ==="
|
||||
trap 'log "=== Update-Lauf abgebrochen (unerwarteter Fehler, Zeile $LINENO) ==="' ERR
|
||||
|
||||
run_hook "pre_update_hook.sh"
|
||||
|
||||
PROJECT_CHANGED=0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 1. Projekt selbst aktualisieren (git pull --ff-only, falls dieses
|
||||
# Verzeichnis ein Git-Checkout mit konfiguriertem Upstream ist).
|
||||
# Fast-forward-only, damit lokale Änderungen niemals überschrieben
|
||||
# werden - schlägt der Pull fehl, wird nur gewarnt, nicht erzwungen.
|
||||
# -----------------------------------------------------------------------
|
||||
if [ -d "$PROJECT_DIR/.git" ]; then
|
||||
if git fetch --quiet 2>>"$LOG_FILE"; then
|
||||
BEHIND="$(git rev-list 'HEAD..@{u}' --count 2>/dev/null || echo 0)"
|
||||
if [ "$BEHIND" -gt 0 ]; then
|
||||
log "Projekt: $BEHIND neue(r) Commit(s) verfuegbar"
|
||||
else
|
||||
log "Projekt: bereits aktuell"
|
||||
fi
|
||||
else
|
||||
log "Projekt: WARNUNG - git fetch fehlgeschlagen (kein Netzwerk/kein Remote konfiguriert?), Selbst-Update uebersprungen"
|
||||
BEHIND=0
|
||||
fi
|
||||
else
|
||||
log "Projekt: kein Git-Checkout - Selbst-Update uebersprungen (config/sites.yaml muss manuell gepflegt werden)"
|
||||
BEHIND=0
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 2. Aktuellen Image-Stand merken (Grundlage für Rollback) + neue Images
|
||||
# pruefen. Bleibt bewusst auf der in .env gepinnten Minor-Version
|
||||
# (z.B. haproxy:3.2-alpine) - nur neue Patch-/Security-Builds, keine
|
||||
# automatischen Major/Minor-Sprünge. Größere Versionssprünge: Tag in
|
||||
# .env manuell ändern, dann `make reload`.
|
||||
# -----------------------------------------------------------------------
|
||||
OLD_HAPROXY_ID="$(docker compose images -q haproxy 2>/dev/null || true)"
|
||||
OLD_CADDY_ID="$(docker compose images -q caddy 2>/dev/null || true)"
|
||||
|
||||
log "Pruefe auf neue Images ($HAPROXY_IMAGE, $CADDY_IMAGE)..."
|
||||
docker compose pull haproxy caddy >> "$LOG_FILE" 2>&1
|
||||
|
||||
NEW_HAPROXY_ID="$(docker compose images -q haproxy)"
|
||||
NEW_CADDY_ID="$(docker compose images -q caddy)"
|
||||
|
||||
IMAGES_CHANGED=0
|
||||
if [ "$OLD_HAPROXY_ID" != "$NEW_HAPROXY_ID" ]; then
|
||||
log " -> haproxy: neues Image (${OLD_HAPROXY_ID:0:12} -> ${NEW_HAPROXY_ID:0:12})"
|
||||
IMAGES_CHANGED=1
|
||||
else
|
||||
log " -> haproxy: bereits aktuell (${NEW_HAPROXY_ID:0:12})"
|
||||
fi
|
||||
if [ "$OLD_CADDY_ID" != "$NEW_CADDY_ID" ]; then
|
||||
log " -> caddy: neues Image (${OLD_CADDY_ID:0:12} -> ${NEW_CADDY_ID:0:12})"
|
||||
IMAGES_CHANGED=1
|
||||
else
|
||||
log " -> caddy: bereits aktuell (${NEW_CADDY_ID:0:12})"
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log "=== Dry-Run beendet, nichts wurde angewendet ==="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$IMAGES_CHANGED" -eq 0 ] && [ "$BEHIND" -eq 0 ]; then
|
||||
log "=== Keine Aenderungen, fertig ==="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 3. Bestätigung einholen (mailcow-Stil): interaktiv nachfragen, außer
|
||||
# --force/-f/--yes wurde übergeben. Ohne Terminal UND ohne --force wird
|
||||
# sauber abgebrochen statt zu hängen oder blind anzuwenden.
|
||||
# -----------------------------------------------------------------------
|
||||
if [ "$FORCE" -ne 1 ]; then
|
||||
if [ -t 0 ]; then
|
||||
echo ""
|
||||
read -r -p "Update anwenden? Caddy und HAProxy werden neu gestartet. [y/N] " response < /dev/tty
|
||||
if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
log "=== Vom Nutzer abgelehnt, nichts wurde angewendet ==="
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
log "FEHLER: Aenderungen verfuegbar, aber nicht interaktiv und --force nicht gesetzt - breche ab."
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 4. Projekt-Update jetzt tatsächlich ziehen (falls vorhanden)
|
||||
# -----------------------------------------------------------------------
|
||||
if [ "$BEHIND" -gt 0 ]; then
|
||||
if git pull --ff-only --quiet 2>>"$LOG_FILE"; then
|
||||
log "Projekt: aktualisiert ($BEHIND Commit(s))"
|
||||
PROJECT_CHANGED=1
|
||||
else
|
||||
log "Projekt: WARNUNG - git pull --ff-only fehlgeschlagen (lokale Aenderungen? divergierter Branch?), uebersprungen"
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 5. Konfiguration neu generieren (reines Bash+AWK, kein Docker-Image
|
||||
# nötig) und VOR jedem Neustart validieren. Bei Fehlern wird nichts
|
||||
# angefasst.
|
||||
# -----------------------------------------------------------------------
|
||||
log "Generiere Konfiguration aus config/sites.yaml..."
|
||||
if ! "$PROJECT_DIR/generate.sh" >> "$LOG_FILE" 2>&1; then
|
||||
log "FEHLER: Config-Generierung fehlgeschlagen - breche ab, nichts wurde neu gestartet."
|
||||
run_hook "post_update_hook.sh"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
log "Validiere generated/Caddyfile..."
|
||||
if ! docker compose run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >> "$LOG_FILE" 2>&1; then
|
||||
log "FEHLER: generated/Caddyfile ist ungueltig - breche ab, nichts wurde neu gestartet."
|
||||
run_hook "post_update_hook.sh"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
log "Validiere generated/haproxy.cfg mit dem neuen Image..."
|
||||
if ! docker compose run --rm --no-deps haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg >> "$LOG_FILE" 2>&1; then
|
||||
log "FEHLER: generated/haproxy.cfg ist mit dem neuen Image ungueltig - breche ab, nichts wurde neu gestartet."
|
||||
run_hook "post_update_hook.sh"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 6. Neu starten + Healthcheck abwarten
|
||||
# -----------------------------------------------------------------------
|
||||
wait_healthy() {
|
||||
local svc="$1" elapsed=0 cid status
|
||||
cid="$(docker compose ps -q "$svc")"
|
||||
[ -z "$cid" ] && return 1
|
||||
while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do
|
||||
status="$(docker inspect --format='{{.State.Health.Status}}' "$cid" 2>/dev/null || echo none)"
|
||||
[ "$status" = "healthy" ] && return 0
|
||||
[ "$status" = "unhealthy" ] && return 1
|
||||
sleep 3
|
||||
elapsed=$((elapsed + 3))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
log "Starte caddy + haproxy mit aktualisierten Images neu..."
|
||||
docker compose up -d --no-deps caddy haproxy >> "$LOG_FILE" 2>&1
|
||||
|
||||
ALL_HEALTHY=1
|
||||
for svc in caddy haproxy; do
|
||||
if wait_healthy "$svc"; then
|
||||
log " -> $svc: healthy"
|
||||
else
|
||||
log " -> $svc: NICHT healthy innerhalb von ${HEALTH_TIMEOUT}s"
|
||||
ALL_HEALTHY=0
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ALL_HEALTHY" -eq 1 ]; then
|
||||
log "=== Update erfolgreich angewendet ==="
|
||||
run_hook "post_update_hook.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 7. Rollback: alte, lokal noch vorhandene Image-IDs zurück auf den in .env
|
||||
# gepinnten Tag mappen und neu starten.
|
||||
# -----------------------------------------------------------------------
|
||||
log "Healthcheck fehlgeschlagen - fuehre Rollback durch..."
|
||||
|
||||
[ -n "$OLD_HAPROXY_ID" ] && docker tag "$OLD_HAPROXY_ID" "$HAPROXY_IMAGE"
|
||||
[ -n "$OLD_CADDY_ID" ] && docker tag "$OLD_CADDY_ID" "$CADDY_IMAGE"
|
||||
|
||||
docker compose up -d --no-deps caddy haproxy >> "$LOG_FILE" 2>&1
|
||||
|
||||
ROLLBACK_OK=1
|
||||
for svc in caddy haproxy; do
|
||||
if wait_healthy "$svc"; then
|
||||
log " -> $svc: nach Rollback wieder healthy"
|
||||
else
|
||||
log " -> $svc: KRITISCH - auch nach Rollback nicht healthy, bitte manuell pruefen (docker compose logs $svc)"
|
||||
ROLLBACK_OK=0
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ROLLBACK_OK" -eq 1 ]; then
|
||||
log "=== Update fehlgeschlagen, Rollback erfolgreich ==="
|
||||
else
|
||||
log "=== Update fehlgeschlagen, Rollback NICHT vollstaendig erfolgreich - manuelles Eingreifen noetig ==="
|
||||
fi
|
||||
run_hook "post_update_hook.sh"
|
||||
exit 2
|
||||
Reference in New Issue
Block a user