159 lines
5.3 KiB
Bash
159 lines
5.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Vérifie les conteneurs Compose, leur healthcheck, leur redémarrage et leurs routes Traefik.
|
|
# En cas d'écart, un redémarrage Compose ciblé est tenté une fois avant le prochain cycle.
|
|
set -Eeuo pipefail
|
|
|
|
readonly STATE_DIR="/var/lib/manus-apps-health"
|
|
readonly REPORT_FILE="${STATE_DIR}/latest.txt"
|
|
readonly LOCK_FILE="/run/manus-apps-health.lock"
|
|
readonly MODE="${1:---repair}"
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
exec 9>"$LOCK_FILE"
|
|
flock -n 9 || exit 0
|
|
|
|
declare -A PROJECT_CONFIGS=()
|
|
declare -A PROJECT_NAMES=()
|
|
declare -A SEEN_HTTP_HOSTS=()
|
|
declare -A FAILED_PROJECTS=()
|
|
declare -a REPORT_LINES=()
|
|
HAS_FAILURE=false
|
|
|
|
log_line() {
|
|
local level="$1"
|
|
local message="$2"
|
|
local line="[$(date -Is)] [$level] $message"
|
|
REPORT_LINES+=("$line")
|
|
echo "$line"
|
|
}
|
|
|
|
mark_failure() {
|
|
HAS_FAILURE=true
|
|
log_line "KO" "$1"
|
|
}
|
|
|
|
mark_project_failure() {
|
|
FAILED_PROJECTS["$1"]=1
|
|
mark_failure "$2"
|
|
}
|
|
|
|
is_healthy_http_status() {
|
|
[[ "$1" =~ ^2[0-9][0-9]$ || "$1" =~ ^3[0-9][0-9]$ ]]
|
|
}
|
|
|
|
check_http_route() {
|
|
local container_id="$1"
|
|
local project="$2"
|
|
local workdir="$3"
|
|
local labels host_rule host status
|
|
|
|
labels="$(docker inspect -f '{{range $key, $value := .Config.Labels}}{{$key}}={{$value}}{{"\n"}}{{end}}' "$container_id")"
|
|
host_rule="$(printf '%s\n' "$labels" | grep -oE 'Host\(`[^`]+`\)' | head -n 1 || true)"
|
|
[[ -n "$host_rule" ]] || return 0
|
|
|
|
host="${host_rule#Host(\`}"
|
|
host="${host%\`)}"
|
|
[[ -n "$host" ]] || return 0
|
|
[[ -z "${SEEN_HTTP_HOSTS[$host]:-}" ]] || return 0
|
|
SEEN_HTTP_HOSTS["$host"]=1
|
|
|
|
status="$(curl --silent --show-error --location --max-redirs 3 --connect-timeout 10 --max-time 25 \
|
|
--output /dev/null --write-out '%{http_code}' "https://${host}" 2>/dev/null || true)"
|
|
if is_healthy_http_status "$status"; then
|
|
log_line "OK" "${project}: HTTPS ${host} → ${status}"
|
|
else
|
|
mark_project_failure "$workdir" "${project}: HTTPS ${host} → ${status:-erreur réseau}"
|
|
fi
|
|
}
|
|
|
|
check_containers() {
|
|
local container_id name project workdir config_files status health restart_policy
|
|
mapfile -t container_ids < <(docker ps -aq --filter label=com.docker.compose.project)
|
|
|
|
if [[ "${#container_ids[@]}" -eq 0 ]]; then
|
|
mark_failure "Aucun conteneur Docker Compose détecté"
|
|
return
|
|
fi
|
|
|
|
for container_id in "${container_ids[@]}"; do
|
|
name="$(docker inspect -f '{{.Name}}' "$container_id" | sed 's#^/##')"
|
|
project="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project"}}' "$container_id")"
|
|
workdir="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$container_id")"
|
|
config_files="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.config_files"}}' "$container_id")"
|
|
status="$(docker inspect -f '{{.State.Status}}' "$container_id")"
|
|
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id")"
|
|
restart_policy="$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$container_id")"
|
|
|
|
PROJECT_NAMES["$workdir"]="$project"
|
|
PROJECT_CONFIGS["$workdir"]="$config_files"
|
|
|
|
if [[ "$status" != "running" ]]; then
|
|
mark_project_failure "$workdir" "${project}/${name}: état Docker ${status}"
|
|
continue
|
|
fi
|
|
if [[ "$health" == "unhealthy" ]]; then
|
|
mark_project_failure "$workdir" "${project}/${name}: healthcheck unhealthy"
|
|
continue
|
|
fi
|
|
if [[ "$restart_policy" != "unless-stopped" && "$restart_policy" != "always" ]]; then
|
|
mark_project_failure "$workdir" "${project}/${name}: politique de redémarrage ${restart_policy:-none}"
|
|
continue
|
|
fi
|
|
|
|
log_line "OK" "${project}/${name}: running, health=${health}, restart=${restart_policy}"
|
|
check_http_route "$container_id" "$project" "$workdir"
|
|
done
|
|
}
|
|
|
|
repair_projects() {
|
|
local workdir config_files project config
|
|
for workdir in "${!FAILED_PROJECTS[@]}"; do
|
|
project="${PROJECT_NAMES[$workdir]}"
|
|
[[ -d "$workdir" ]] || continue
|
|
config_files="${PROJECT_CONFIGS[$workdir]}"
|
|
local -a compose_args=()
|
|
if [[ -n "$config_files" ]]; then
|
|
IFS=',' read -r -a configs <<< "$config_files"
|
|
for config in "${configs[@]}"; do
|
|
compose_args+=("-f" "$config")
|
|
done
|
|
elif [[ -f "$workdir/docker-compose.yml" ]]; then
|
|
compose_args+=("-f" "$workdir/docker-compose.yml")
|
|
else
|
|
mark_failure "${project}: fichier Compose introuvable pour la réparation"
|
|
continue
|
|
fi
|
|
|
|
log_line "INFO" "${project}: tentative de réparation Compose"
|
|
if (cd "$workdir" && docker compose "${compose_args[@]}" up -d --remove-orphans); then
|
|
log_line "INFO" "${project}: réparation Compose demandée"
|
|
else
|
|
mark_failure "${project}: échec de réparation Compose"
|
|
fi
|
|
done
|
|
}
|
|
|
|
check_containers
|
|
|
|
if [[ "$HAS_FAILURE" == true && "$MODE" == "--repair" ]]; then
|
|
repair_projects
|
|
log_line "INFO" "Nouvelle vérification après réparation"
|
|
sleep 15
|
|
exec "$0" --check-only
|
|
fi
|
|
|
|
{
|
|
echo "Supervision Santinova / Itinova"
|
|
echo "Généré : $(date -Is)"
|
|
echo "Statut : $([[ "$HAS_FAILURE" == true ]] && echo KO || echo OK)"
|
|
printf '%s\n' "${REPORT_LINES[@]}"
|
|
} > "${REPORT_FILE}.tmp"
|
|
mv "${REPORT_FILE}.tmp" "$REPORT_FILE"
|
|
|
|
if [[ "$HAS_FAILURE" == true ]]; then
|
|
logger -p daemon.err -t manus-apps-health "Anomalie détectée : consulter ${REPORT_FILE}"
|
|
exit 1
|
|
fi
|
|
|
|
logger -p daemon.info -t manus-apps-health "Toutes les applications contrôlées sont opérationnelles"
|