Version initiale : script, README et schéma d'architecture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-02 09:43:36 +02:00
commit 719a533130
4 files changed
+3970

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env bash
#
# update-stacks.sh — Met à jour les stacks docker compose d'une racine donnée.
#
# Pour chaque sous-répertoire de la racine contenant un fichier compose :
# 1. télécharge les images (pull) — sans toucher aux conteneurs en cours ;
# 2. compare l'image utilisée par chaque conteneur à l'image fraîchement tirée ;
# 3. si au moins une image a changé : down puis up -d de toute la stack.
# Les sous-répertoires sans fichier compose, ou avec un fichier compose vide,
# sont ignorés sans rien afficher. Un fichier compose invalide est signalé en erreur.
#
# Usage : update-stacks.sh [-v | -q] [-n] [-f] [-p] [-a] [RACINE]
# -v verbose : détaille chaque conteneur et chaque commande docker
# -q quiet : n'affiche qu'une ligne par stack (nom, répertoire, état)
# -n dry-run : détecte les mises à jour mais ne relance rien
# -f force : down + up -d de chaque stack, même sans mise à jour
# -p prune : supprime les images orphelines (dangling) à la fin
# -a all : traite aussi les stacks arrêtées (par défaut : ignorées)
# RACINE : répertoire contenant les stacks (défaut : /home)
#
# Couleurs : vert = à jour, jaune = mise(s) à jour détectée(s) ou forcée(s), rouge = erreur.
# Désactivées automatiquement hors terminal (cron, redirection) ou si NO_COLOR est défini.
set -uo pipefail
VERBOSITY=1 # 0 = quiet, 1 = normal, 2 = verbose
DRY_RUN=0
FORCE=0
PRUNE=0
INCLUDE_STOPPED=0
while getopts "vqnfpah" opt; do
case "$opt" in
v) VERBOSITY=2 ;;
q) VERBOSITY=0 ;;
n) DRY_RUN=1 ;;
f) FORCE=1 ;;
p) PRUNE=1 ;;
a) INCLUDE_STOPPED=1 ;;
h|*) sed -n '2,23p' "$0"; exit 0 ;;
esac
done
shift $((OPTIND - 1))
ROOT="${1:-/home}"
ROOT="${ROOT%/}"
COMPOSE_FILES=(compose.yaml compose.yml docker-compose.yaml docker-compose.yml)
LOCK_FILE="/tmp/update-stacks.lock"
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
GREEN=$'\e[32m' YELLOW=$'\e[33m' RED=$'\e[31m' BOLD=$'\e[1m' DIM=$'\e[2m' RESET=$'\e[0m'
else
GREEN="" YELLOW="" RED="" BOLD="" DIM="" RESET=""
fi
ts() { date '+%F %T'; }
log() { (( VERBOSITY >= 1 )) && printf '[%s] %s\n' "$(ts)" "$*"; }
debug() { (( VERBOSITY >= 2 )) && printf '[%s] %s%s%s\n' "$(ts)" "$DIM" "$*" "$RESET"; }
ok() { log " ${GREEN}✔ $*${RESET}"; }
warn() { log " ${YELLOW}↑ $*${RESET}"; }
error() { printf '[%s] %s✘ %s%s\n' "$(ts)" "$RED" "$*" "$RESET" >&2; }
# En mode quiet : une seule ligne par stack, l'état s'affiche au bout
quiet_status() { (( VERBOSITY == 0 )) && printf '%s%s%s\n' "$1" "$2" "$RESET"; }
# Exécute une commande docker. En verbose, affiche la commande et sa sortie ;
# sinon la sortie est gardée dans RUN_OUTPUT pour être affichée par fail().
RUN_OUTPUT=""
run() {
RUN_OUTPUT=""
if (( VERBOSITY >= 2 )); then
debug " \$ $*"
"$@"
else
RUN_OUTPUT=$("$@" 2>&1)
fi
}
# Signale l'échec d'une stack, avec la sortie de la commande fautive
fail() {
quiet_status "$RED" "✘ erreur ($1)"
(( VERBOSITY >= 1 )) && error "$name : erreur ($1)"
[[ -n "$RUN_OUTPUT" ]] && printf '%s\n' "$RUN_OUTPUT" | sed 's/^/ /' >&2
failed_stacks+=("$name")
}
# Empêche deux exécutions simultanées (utile en cron)
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
error "Une autre exécution est déjà en cours, abandon."
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
error "'docker compose' (v2) introuvable."
exit 1
fi
[[ -d "$ROOT" ]] || { error "$ROOT n'est pas un répertoire."; exit 1; }
# Renvoie 0 si au moins un conteneur de la stack tourne sur une image obsolète
stack_has_updates() {
local cid running_id image_ref latest_id service updated=1
for cid in $(docker compose ps -q 2>/dev/null); do
running_id=$(docker inspect -f '{{.Image}}' "$cid")
image_ref=$(docker inspect -f '{{.Config.Image}}' "$cid")
service=$(docker inspect -f '{{index .Config.Labels "com.docker.compose.service"}}' "$cid")
latest_id=$(docker image inspect -f '{{.Id}}' "$image_ref" 2>/dev/null) || {
debug " · $service ($image_ref) : image locale introuvable, ignoré"
continue
}
if [[ "$running_id" != "$latest_id" ]]; then
warn "$service ($image_ref) : nouvelle image disponible"
debug " actuelle : ${running_id:7:12} → nouvelle : ${latest_id:7:12}"
updated=0
else
debug " · $service ($image_ref) : à jour (${running_id:7:12})"
fi
done
return $updated
}
updated_stacks=()
failed_stacks=()
log "${BOLD}Racine : $ROOT${RESET}$( (( DRY_RUN )) && echo ' [dry-run]')$( (( FORCE )) && echo ' [force]')"
for dir in "$ROOT"/*/; do
dir="${dir%/}"
name=$(basename "$dir")
found=""
for f in "${COMPOSE_FILES[@]}"; do
[[ -f "$dir/$f" ]] && { found="$f"; break; }
done
[[ -n "$found" ]] || continue
pushd "$dir" >/dev/null || continue
# Valide le fichier compose avant tout affichage : un fichier vide est ignoré
# comme un répertoire sans stack, un fichier invalide est signalé.
config_ok=1
RUN_OUTPUT=$(docker compose config -q 2>&1) || config_ok=0
if (( ! config_ok )) && [[ "$RUN_OUTPUT" == *"empty compose file"* ]]; then
popd >/dev/null; continue
fi
log "${BOLD}== Stack : $name${RESET} ($dir/$found)"
(( VERBOSITY == 0 )) && printf '%-25s %-45s ' "$name" "$dir"
if (( ! config_ok )); then
fail "fichier compose invalide"
popd >/dev/null; continue
fi
if [[ -z "$(docker compose ps -q 2>/dev/null)" && $INCLUDE_STOPPED -eq 0 ]]; then
log " ${DIM}stack arrêtée, ignorée (utiliser -a pour l'inclure)${RESET}"
quiet_status "$DIM" "arrêtée"
popd >/dev/null; continue
fi
# Le pull ne modifie pas les conteneurs en cours : il sert à la détection
debug " Téléchargement des images…"
if ! run docker compose pull --ignore-buildable; then
fail "pull"
popd >/dev/null; continue
fi
# Stack arrêtée incluse via -a : aucun conteneur à comparer, on la lance.
# stack_has_updates est toujours appelé pour afficher le détail, même avec -f.
has_updates=0
if [[ -z "$(docker compose ps -q 2>/dev/null)" ]] || stack_has_updates; then
has_updates=1
fi
if (( has_updates || FORCE )); then
if (( has_updates )); then
label="mise à jour" done_label="↑ mise à jour appliquée"
else
label="redémarrage forcé" done_label="↻ redémarrage forcé effectué"
fi
if [[ $DRY_RUN -eq 1 ]]; then
warn "[dry-run] $label nécessaire, rien n'est relancé"
quiet_status "$YELLOW" "↑ $label à faire"
updated_stacks+=("$name")
elif ! run docker compose down; then
fail "down"
elif ! run docker compose up -d; then
fail "up -d"
else
log " ${YELLOW}✔ stack relancée (down + up -d) : $label${RESET}"
quiet_status "$YELLOW" "$done_label"
updated_stacks+=("$name")
fi
else
ok "à jour"
quiet_status "$GREEN" "✔ à jour"
fi
popd >/dev/null
done
if [[ $PRUNE -eq 1 && $DRY_RUN -eq 0 ]]; then
log "Nettoyage des images orphelines…"
run docker image prune -f
fi
if (( ${#failed_stacks[@]} )); then
color=$RED
elif (( ${#updated_stacks[@]} )); then
color=$YELLOW
else
color=$GREEN
fi
log "${color}Terminé. Stacks relancées : ${#updated_stacks[@]} ${updated_stacks[*]:-} | Échecs : ${#failed_stacks[@]} ${failed_stacks[*]:-}${RESET}"
[[ ${#failed_stacks[@]} -eq 0 ]]