Files
docker-stack-template/gitea-auto-update.sh
T
bruno 1c6506e319 Correction et enrichissement complet des scripts Gitea
- Corrections de bugs (14): grep portable, df robuste, rollback reel,
  retention intelligente, nettoyage cible, lock anti-concurrence, etc.
- Nouveaux fichiers: setup.sh (installateur tout-en-un), restore.sh
  (restauration standalone), gitea-scripts.conf (config unifiee)
- Features: backup distant (rsync/SCP), lock files, logrotate, dry-run,
  healthchecks Docker, notifications Telegram/ntfy.sh
- Documentation enrichie avec diagrammes d'architecture mermaid
2026-06-26 11:53:28 -04:00

279 lines
12 KiB
Bash
Executable File

#!/bin/bash
# =============================================================================
# gitea-auto-update.sh — Vérifie et applique les mises à jour Gitea Docker
# =============================================================================
# À placer dans /usr/local/bin/ ou ~/scripts/
# Cron recommandé : 0 4 * * * /usr/local/bin/gitea-auto-update.sh
#
# Paramètres configurables : voir gitea-scripts.conf
# =============================================================================
set -euo pipefail
# ─── Charger la configuration commune ─────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/gitea-scripts.conf}"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
# ─── Configuration locale (après chargement conf, permet surcharge env) ──────
# Chemins et conteneurs
COMPOSE_DIR="${COMPOSE_DIR:-$HOME/dev/git/outils/docker/gitea}"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
APP_SERVICE="${APP_SERVICE:-gitea_server}"
APP_CONTAINER="${APP_CONTAINER:-gitea_server}"
DB_CONTAINER="${DB_CONTAINER:-gitea_db}"
IMAGE_NAME="${IMAGE_NAME:-gitea/gitea}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
# Base de données
DB_NAME="${DB_NAME:-gitea}"
DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-gitea}"
# Logs
LOG_FILE="${LOG_FILE:-/var/log/gitea-update.log}"
LOG_DIR="$(dirname "$LOG_FILE")"
# Notifications
ENABLE_NOTIFICATIONS="${ENABLE_NOTIFICATIONS:-false}"
NOTIFY_METHOD="${NOTIFY_METHOD:-log}"
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
NTPY_TOPIC="${NTPY_TOPIC:-}"
# Santé
HEALTH_CHECK_URL="${HEALTH_CHECK_URL:-http://localhost:3000}"
HEALTH_CHECK_TIMEOUT="${HEALTH_CHECK_TIMEOUT:-30}"
# Lock file
LOCK_DIR="${LOCK_DIR:-/tmp}"
LOCK_FILE="${LOCK_DIR}/gitea-auto-update.lock"
# ─── Prérequis : s'assurer que le dossier de log existe ──────────────────────
mkdir -p "$LOG_DIR" 2>/dev/null || true
# ─── Logging ─────────────────────────────────────────────────────────────────
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
echo "$msg"
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
}
# ─── Notification ────────────────────────────────────────────────────────────
notify() {
local msg="$1"
case "$NOTIFY_METHOD" in
telegram)
if [[ -n "$TELEGRAM_BOT_TOKEN" ]] && [[ -n "$TELEGRAM_CHAT_ID" ]]; then
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=$TELEGRAM_CHAT_ID" -d "text=$msg" > /dev/null 2>&1 || true
fi
;;
ntfy)
if [[ -n "$NTPY_TOPIC" ]]; then
curl -s -H "Title: Gitea Update" -d "$msg" "https://ntfy.sh/$NTPY_TOPIC" > /dev/null 2>&1 || true
fi
;;
esac
}
# ─── Lock file ───────────────────────────────────────────────────────────────
acquire_lock() {
if [[ -f "$LOCK_FILE" ]]; then
local pid
pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
# Vérifier si le processus existe encore
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
log "⚠️ Une autre instance (PID $pid) est déjà en cours d'exécution — arrêt"
exit 0
fi
# Stale lock, on le nettoie
log "⚠️ Lock stale détecté (PID $pid n'existe plus), on continue"
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
}
release_lock() {
rm -f "$LOCK_FILE"
}
# ─── Nettoyage en cas d'erreur ───────────────────────────────────────────────
cleanup() {
release_lock
}
trap cleanup EXIT
# ─── Récupération du digest (compatible POSIX grep) ──────────────────────────
get_image_digest() {
local image="$1"
local digest
# Essayer d'abord avec -E (ERE, disponible presque partout)
digest=$(docker image inspect "$image" --format '{{.RepoDigests}}' 2>/dev/null | grep -oE 'sha256:[a-f0-9]{64}' | head -1 || echo "")
if [[ -z "$digest" ]]; then
# Fallback : utiliser awk
digest=$(docker image inspect "$image" --format '{{.RepoDigests}}' 2>/dev/null | grep -o 'sha256:[a-f0-9]\{64\}' | head -1 || echo "")
fi
echo "$digest"
}
# ═════════════════════════════════════════════════════════════════════════════
# MAIN
# ═════════════════════════════════════════════════════════════════════════════
acquire_lock
log "════════════════════════════════════════════════════════"
log "🔄 Début vérification mise à jour Gitea"
# ─── Pré-vérifications ──────────────────────────────────────────────────────
cd "$COMPOSE_DIR" || { log "❌ Répertoire $COMPOSE_DIR introuvable"; exit 1; }
if ! command -v docker &>/dev/null; then
log "❌ Docker n'est pas installé"
exit 1
fi
# Vérifier si docker compose (plugin) ou docker-compose (standalone) est dispo
if docker compose version &>/dev/null; then
DOCKER_COMPOSE="docker compose"
elif command -v docker-compose &>/dev/null; then
DOCKER_COMPOSE="docker-compose"
else
log "❌ Ni 'docker compose' ni 'docker-compose' n'est disponible"
exit 1
fi
# ─── Récupérer l'image digest actuelle ──────────────────────────────────────
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
OLD_DIGEST=$(get_image_digest "$FULL_IMAGE")
log "📦 Image actuelle : $FULL_IMAGE"
log "🔍 Digest actuel : ${OLD_DIGEST:-non disponible}"
# ─── Pull de la nouvelle image ──────────────────────────────────────────────
log "📥 Pull de la nouvelle image..."
PULL_OUTPUT=$($DOCKER_COMPOSE -f "$COMPOSE_FILE" pull "$APP_SERVICE" 2>&1) || {
PULL_EXIT=$?
log "❌ Échec du pull (code $PULL_EXIT) : $PULL_OUTPUT"
exit 1
}
NEW_DIGEST=$(get_image_digest "$FULL_IMAGE")
log "🔍 Nouveau digest : ${NEW_DIGEST:-non disponible}"
# ─── Comparer les digests ───────────────────────────────────────────────────
if [[ -n "$OLD_DIGEST" ]] && [[ -n "$NEW_DIGEST" ]] && [[ "$OLD_DIGEST" == "$NEW_DIGEST" ]]; then
log "✅ Aucune mise à jour disponible (digest identique)"
log "═══ Fin ───────────────────────────────────────────"
exit 0
fi
# ─── MISE À JOUR NÉCESSAIRE ─────────────────────────────────────────────────
log "🔔 Nouvelle version détectée ! Mise à jour en cours..."
# Sauvegarder l'image actuelle pour rollback éventuel
log "💾 Sauvegarde de l'image actuelle pour rollback..."
OLD_IMAGE_ID=$(docker image inspect "$FULL_IMAGE" --format '{{.ID}}' 2>/dev/null || echo "")
ROLLBACK_TAG="${IMAGE_NAME}:rollback-$(date +%Y%m%d-%H%M%S)"
if [[ -n "$OLD_IMAGE_ID" ]]; then
docker tag "$OLD_IMAGE_ID" "$ROLLBACK_TAG" 2>/dev/null || {
log "⚠️ Impossible de tagger l'image pour rollback (on continue)"
ROLLBACK_TAG=""
}
else
ROLLBACK_TAG=""
fi
# Backup rapide DB avant mise à jour
BACKUP_PRE_DIR="${BACKUP_PRE_DIR:-/tmp/gitea-pre-update-$(date +%Y%m%d-%H%M%S)}"
mkdir -p "$BACKUP_PRE_DIR"
log "💾 Backup DB pré-update vers $BACKUP_PRE_DIR"
if docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then
docker exec "$DB_CONTAINER" mysqldump \
-u root -p"$DB_ROOT_PASSWORD" \
--single-transaction \
"$DB_NAME" > "$BACKUP_PRE_DIR/gitea_db.sql" 2>/dev/null || {
log "⚠️ Backup pré-update DB échoué, on continue quand même"
}
else
log "⚠️ Conteneur $DB_CONTAINER non trouvé — pas de backup pré-update"
fi
# Recréer le conteneur avec la nouvelle image
log "🚀 Redémarrage du conteneur avec la nouvelle image..."
UP_OUTPUT=$($DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d "$APP_SERVICE" 2>&1) || {
UP_EXIT=$?
log "❌ Échec du redémarrage (code $UP_EXIT) : $UP_OUTPUT"
# Rollback réel : restaurer l'ancienne image
if [[ -n "$ROLLBACK_TAG" ]]; then
log "🔄 ROLLBACK : restauration de l'image $ROLLBACK_TAG..."
docker tag "$ROLLBACK_TAG" "$FULL_IMAGE" 2>/dev/null || true
$DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d "$APP_SERVICE" 2>&1 || true
docker rmi "$ROLLBACK_TAG" 2>/dev/null || true
else
log "⚠️ Rollback impossible (pas d'image sauvegardée)"
$DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d "$APP_SERVICE" 2>&1 || true
fi
notify "❌ GITEA UPDATE FAILED sur $(hostname) — rollback effectué"
exit 1
}
# Nettoyer le tag de rollback si tout va bien
if [[ -n "$ROLLBACK_TAG" ]]; then
docker rmi "$ROLLBACK_TAG" 2>/dev/null || true
fi
# ─── Vérification santé ─────────────────────────────────────────────────────
log "🏥 Vérification santé (timeout=${HEALTH_CHECK_TIMEOUT}s)..."
HEALTH_OK=false
for i in $(seq 1 "$HEALTH_CHECK_TIMEOUT"); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_CHECK_URL" 2>/dev/null || echo "000")
if [[ "$STATUS" == "200" || "$STATUS" == "302" || "$STATUS" == "303" ]]; then
log "✅ Gitea répond (HTTP $STATUS) après ${i}s"
HEALTH_OK=true
break
fi
if [[ $i -eq "$HEALTH_CHECK_TIMEOUT" ]]; then
log "❌ Gitea ne répond pas après ${HEALTH_CHECK_TIMEOUT}s"
notify "⚠️ GITEA UPDATE : service lent à démarrer sur $(hostname) — vérifier"
exit 1
fi
sleep 1
done
# ─── Rapport final ──────────────────────────────────────────────────────────
GITEA_VERSION=$(curl -s "$HEALTH_CHECK_URL/api/v1/version" 2>/dev/null | grep -oE '"version":"[^"]+"' | cut -d'"' -f4 || echo "inconnue")
log "✅ Mise à jour réussie !"
log "📋 Nouvelle version : $GITEA_VERSION"
log "📋 Ancien digest : ${OLD_DIGEST:-N/A}"
log "📋 Nouveau digest : ${NEW_DIGEST:-N/A}"
log "═══ Fin ───────────────────────────────────────────"
notify "✅ Gitea mis à jour vers $GITEA_VERSION sur $(hostname)"
# ─── Nettoyage ciblé : seulement les vieilles images Gitea ──────────────────
log "🧹 Nettoyage des anciennes images Gitea..."
# Trouver toutes les images gitea/gitea sauf la dernière utilisée
CURRENT_IMAGE_ID=$(docker image inspect "$FULL_IMAGE" --format '{{.ID}}' 2>/dev/null || echo "")
if [[ -n "$CURRENT_IMAGE_ID" ]]; then
# Lister les images Gitea non taggées ou anciennes
docker images "$IMAGE_NAME" --format '{{.ID}} {{.Tag}}' 2>/dev/null | while read -r img_id img_tag; do
if [[ "$img_id" != "$CURRENT_IMAGE_ID" ]] && [[ "$img_tag" != "$IMAGE_TAG" ]]; then
log " 🗑️ Suppression ancienne image : $img_id ($img_tag)"
docker rmi "$img_id" 2>/dev/null || true
fi
done
fi
# Nettoyer aussi les images dangling (sans tag)
docker image prune -f 2>/dev/null || true
exit 0