Initial commit

This commit is contained in:
2026-07-31 22:17:22 +02:00
commit c6fa5d3201
14 changed files with 1459 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# =============================================================================
# post_update_hook.sh - läuft am Ende von update.sh, falls diese Datei
# existiert (Name exakt "post_update_hook.sh", ohne ".example") UND
# tatsächlich ein Update angewendet wurde (erfolgreich oder mit Rollback -
# bei "keine Änderungen" oder --dry-run läuft dieser Hook NICHT).
#
# Beispiele: Benachrichtigung über Erfolg/Fehlschlag, CheckMK-Spooler
# anstoßen, Wartungsfenster wieder schließen.
# =============================================================================
set -euo pipefail
echo "post_update_hook.sh: nichts zu tun (Vorlage - anpassen oder loeschen)"
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# =============================================================================
# pre_update_hook.sh - läuft ganz am Anfang von update.sh, falls diese Datei
# existiert (Name exakt "pre_update_hook.sh", ohne ".example").
#
# Beispiele: Benachrichtigung an Slack/Mail, Backup von generated/acl/,
# Wartungsfenster in eurem Monitoring markieren.
#
# Ein Fehler hier bricht das Update NICHT ab (nur eine Warnung im Log) -
# wenn ein Fehlschlag hier das Update verhindern soll, das explizit selbst
# mit "exit 1" tun.
# =============================================================================
set -euo pipefail
echo "pre_update_hook.sh: nichts zu tun (Vorlage - anpassen oder loeschen)"
+23
View File
@@ -0,0 +1,23 @@
[Unit]
Description=Reverse-Proxy Update (Caddy/HAProxy Images + Projekt)
Wants=network-online.target
After=network-online.target docker.service
Requires=docker.service
[Service]
Type=oneshot
# Der Git-Checkout IST das Laufzeitverzeichnis (kein separates Deployment-
# Ziel). Pfad an euren tatsächlichen Klon-Ort anpassen, falls abweichend
# von /opt/reverse-proxy:
WorkingDirectory=/opt/reverse-proxy
ExecStart=/opt/reverse-proxy/update/update.sh --force
# Muss Docker ansprechen können - entweder root oder ein Nutzer in der
# docker-Gruppe. Läuft der Rest eurer Infra unter einem Service-User,
# hier entsprechend eintragen:
User=root
# Exit-Code 1 (= Update erfolgreich angewendet) und 2 (= fehlgeschlagen/
# Rollback) sollen NICHT als systemd-Fehlschlag gewertet werden, nur ein
# echter Absturz (>2) soll das:
SuccessExitStatus=0 1 2
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Woechentliches Update fuer den Reverse-Proxy-Stack (Caddy/HAProxy)
[Timer]
# Sonntags 04:00 Uhr, mit bis zu 30 Minuten zufälligem Versatz
OnCalendar=Sun *-*-* 04:00:00
RandomizedDelaySec=1800
Persistent=true
[Install]
WantedBy=timers.target
+281
View File
@@ -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