check_cluster.sh

#!/usr/bin/env bash
# Revisión rápida de salud del clúster. Solo lectura: no cambia nada.
#
# Uso: ./scripts/check_cluster.sh
# Requiere kubectl apuntando al clúster y, para la parte de keepalived,
# acceso SSH (usuario $SSH_USER, por defecto admin) a los masters.
set -uo pipefail

SSH_USER="${SSH_USER:-admin}"
VIP="192.0.2.250"
MASTERS=(192.0.2.112 192.0.2.139 192.0.2.129)

ok()   { printf '  \e[32m✔\e[0m %s\n' "$*"; }
warn() { printf '  \e[33m⚠\e[0m %s\n' "$*"; }
bad()  { printf '  \e[31m✘\e[0m %s\n' "$*"; }
section() { printf '\n\e[1m%s\e[0m\n' "$*"; }

section "Nodos"
kubectl get nodes --no-headers | while read -r name status _; do
  [[ "$status" == "Ready" ]] && ok "$name" || bad "$name: $status"
done

section "API por la VIP ($VIP)"
if curl -sk --max-time 3 "https://$VIP:6443/healthz" -o /dev/null -w '%{http_code}' | grep -qE '200|401'; then
  ok "responde"
else
  bad "no responde"
fi

section "keepalived en los masters"
KEEPALIVED_STATE="journalctl -u keepalived -n 200 --no-pager 2>/dev/null | grep -oiE 'entering (master|backup|fault) state' | tail -1"
for ip in "${MASTERS[@]}"; do
  if ip -4 -o addr show | grep -qF " $ip/"; then
    state=$(bash -c "$KEEPALIVED_STATE")          # este mismo equipo: sin SSH
  else
    state=$(ssh -o BatchMode=yes -o ConnectTimeout=4 "$SSH_USER@$ip" "$KEEPALIVED_STATE" 2>/dev/null)
  fi
  case "${state,,}" in
    *master*) ok "$ip MASTER (tiene la VIP)" ;;
    *backup*) ok "$ip BACKUP" ;;
    *fault*)  bad "$ip FAULT: no puede tomar la VIP" ;;
    *)        warn "$ip: estado desconocido (sin acceso SSH o sin logs)" ;;
  esac
done

section "Pods que no están Running/Completed"
pods=$(kubectl get pods -A --no-headers | grep -vE 'Running|Completed')
[[ -z "$pods" ]] && ok "ninguno" || echo "$pods" | sed 's/^/  /'

section "Volúmenes Longhorn no sanos"
vols=$(kubectl -n longhorn-system get volumes.longhorn.io --no-headers \
  -o custom-columns='N:.metadata.name,PVC:.status.kubernetesStatus.pvcName,S:.status.state,R:.status.robustness' \
  | awk '$3=="attached" && $4!="healthy"')
[[ -z "$vols" ]] && ok "todos los volúmenes en uso están healthy" || echo "$vols" | sed 's/^/  /'

section "StorageClass por defecto"
n=$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}{"\n"}{end}' | grep -c true)
[[ "$n" -eq 1 ]] && ok "una sola" || warn "$n StorageClasses marcadas como default"

section "Velero"
# backups.velero.io: «backup» a secas es ambiguo (Longhorn tiene backups.longhorn.io) y
# kubectl elegía el de Longhorn, vacío en este namespace: salía «no existe ningún Backup».
last=$(kubectl -n velero get backups.velero.io --sort-by=.metadata.creationTimestamp --no-headers \
  -o custom-columns='N:.metadata.name,P:.status.phase' 2>/dev/null | tail -1)
if [[ -z "$last" ]]; then
  bad "no existe ningún Backup"
elif [[ "$last" == *Completed ]]; then
  ok "último: $last"
else
  warn "último: $last"
fi

section "Cargas pausadas a propósito"
kubectl get ds -A -o jsonpath='{range .items[?(@.spec.template.spec.nodeSelector.paused-by)]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' \
  | sed 's/^/  ⏸ DaemonSet /'
kubectl get deploy -A -o jsonpath='{range .items[?(@.spec.replicas==0)]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' \
  | sed 's/^/  ⏸ Deployment a 0 réplicas: /'
echo