#!/usr/bin/env bash set -uo pipefail # repair_pipeline.sh # Purpose: # - Validate cluster readiness and common service health # - Repair or re-deploy safe-to-recreate services when anomalies are detected SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck disable=SC1090 source "$SCRIPT_DIR/prole_cfg.sh" ACTION="repair" NAMESPACE_OVERRIDE="" DB_NAMESPACE_OVERRIDE="" usage() { cat <<'USAGE' Usage: repair_pipeline.sh [-n|--namespace NS] [--db-namespace NS] [-m|--mode MODE] [repair] Runs a best-effort repair pass across common services and cluster add-ons. USAGE } while [[ $# -gt 0 ]]; do case "$1" in -m|--mode) shift prole_set_mode "${1:-}" ;; -m=*|--mode=*) prole_set_mode "${1#*=}" ;; -n|--namespace) shift NAMESPACE_OVERRIDE="${1:-}" ;; --db-namespace) shift DB_NAMESPACE_OVERRIDE="${1:-}" ;; --db-namespace=*) DB_NAMESPACE_OVERRIDE="${1#*=}" ;; -n=*|--namespace=*) NAMESPACE_OVERRIDE="${1#*=}" ;; repair) ACTION="repair" ;; -h|--help) usage exit 0 ;; *) usage exit 2 ;; esac shift done BASE_NAMESPACE="${NAMESPACE:-}" if [[ -n "$NAMESPACE_OVERRIDE" ]]; then SERVICE_NAMESPACE="$NAMESPACE_OVERRIDE" elif [[ -n "${SERVICE_NAMESPACE:-}" ]]; then SERVICE_NAMESPACE="$SERVICE_NAMESPACE" elif [[ -n "${NAMESPACE:-}" ]]; then SERVICE_NAMESPACE="$NAMESPACE" else SERVICE_NAMESPACE="default" fi if [[ -n "$DB_NAMESPACE_OVERRIDE" ]]; then DB_NAMESPACE="$DB_NAMESPACE_OVERRIDE" elif [[ -n "${PROLE_DB_NAMESPACE:-}" ]]; then DB_NAMESPACE="$PROLE_DB_NAMESPACE" elif [[ -n "$BASE_NAMESPACE" ]]; then DB_NAMESPACE="$BASE_NAMESPACE" else DB_NAMESPACE="$SERVICE_NAMESPACE" fi ARGOCD_NAMESPACE="${ARGOCD_NAMESPACE:-argocd}" export SERVICE_NAMESPACE export DB_NAMESPACE export NAMESPACE="$SERVICE_NAMESPACE" log() { printf '%s\n' "$*"; } warn() { printf 'WARN: %s\n' "$*" >&2; } err() { printf 'ERROR: %s\n' "$*" >&2; } have() { command -v "$1" >/dev/null 2>&1; } run_with_timeout() { local timeout_s="$1" shift local cmd=( "$@" ) "${cmd[@]}" & local pid=$! local start=$SECONDS while kill -0 "$pid" >/dev/null 2>&1; do if (( SECONDS - start > timeout_s )); then warn "Command timed out after ${timeout_s}s: ${cmd[*]}" kill "$pid" >/dev/null 2>&1 || true return 124 fi sleep 2 done wait "$pid" return $? } ensure_kubectl() { have kubectl || { err "kubectl not found"; exit 1; } } detect_mode() { local mode mode="${PROLE_MODE:-}" if have prole_normalize_mode; then mode="$(prole_normalize_mode "$mode")" fi printf '%s' "$mode" } ensure_kubeconfig_if_needed() { local mode mode="$(detect_mode)" if [[ "$mode" == "k3s" || -n "${PROLE_K3S_SERVER:-}" || -n "${K3S_SERVER_URL:-}" ]]; then prole_ensure_kubeconfig >/dev/null 2>&1 || true fi } cluster_ready() { ensure_kubectl if ! kubectl cluster-info >/dev/null 2>&1; then return 1 fi return 0 } k3d_cluster_ready() { local cluster="${K3D_CLUSTER:-knoe-dev-cluster}" if have k3d; then if k3d cluster list --no-headers 2>/dev/null | grep -q "^${cluster}[[:space:]]"; then return 0 fi return 1 fi return 0 } resource_exists() { local kind="$1" name="$2" ns="$3" kubectl -n "$ns" get "$kind" "$name" >/dev/null 2>&1 } resource_ready() { local kind="$1" name="$2" ns="$3" local desired ready if ! resource_exists "$kind" "$name" "$ns"; then return 2 fi desired=$(kubectl -n "$ns" get "$kind" "$name" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "") ready=$(kubectl -n "$ns" get "$kind" "$name" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "") desired=${desired:-1} ready=${ready:-0} if [[ "$ready" -ge "$desired" && "$desired" -gt 0 ]]; then return 0 fi return 1 } authority_context_exists() { local base if [[ -n "${PROLE_HOME:-}" ]]; then if [[ -d "$PROLE_HOME/authority" || -d "$PROLE_HOME/prole/authority" ]]; then return 0 fi fi base="$SCRIPT_DIR/.." if [[ -d "$base/authority" || -d "$base/prole/authority" ]]; then return 0 fi return 1 } repair_dashboard() { local ns="kubernetes-dashboard" if ! kubectl get ns "$ns" >/dev/null 2>&1; then warn "Dashboard namespace missing; installing via Helm" if have helm; then helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/ >/dev/null 2>&1 || true if ! run_with_timeout 120 bash -c 'helm repo update >/dev/null 2>&1'; then warn "Helm repo update timed out; continuing with cached index" fi if ! run_with_timeout 300 helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard \ --create-namespace --namespace "$ns" \ --set kong.image.tag=3.8 --set kong.image.repository=docker.io/library/kong; then warn "Helm install timed out or failed" fi else warn "helm not found; cannot install kubernetes-dashboard" return 1 fi fi local kong_pods kong_pods=$(kubectl -n "$ns" get pods --no-headers 2>/dev/null | awk '{print $1}' | grep -E 'kong' || true) if [[ -z "$kong_pods" ]]; then warn "Dashboard Kong pod missing; re-applying Helm release" if have helm; then if ! run_with_timeout 300 helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard \ --create-namespace --namespace "$ns" \ --set kong.image.tag=3.8 --set kong.image.repository=docker.io/library/kong; then warn "Helm install timed out or failed" fi else warn "helm not found; cannot reinstall kubernetes-dashboard" return 1 fi else local unhealthy unhealthy=$(kubectl -n "$ns" get pods --no-headers 2>/dev/null | grep -E 'kong' | awk '$3 != "Running" {print $1}' || true) if [[ -n "$unhealthy" ]]; then warn "Dashboard Kong pod unhealthy; deleting pods for restart" kubectl -n "$ns" delete pod $unhealthy --wait=false >/dev/null 2>&1 || true fi fi } repair_argocd() { local ns="$1" local need_fix=0 local items=( "deploy argocd-server" "deploy argocd-repo-server" "deploy argocd-dex-server" "deploy argocd-applicationset-controller" "deploy argocd-notifications-controller" "deploy argocd-redis" "statefulset argocd-application-controller" ) local item kind name status for item in "${items[@]}"; do kind="${item%% *}" name="${item##* }" resource_ready "$kind" "$name" "$ns"; status=$? if [[ "$status" -ne 0 ]]; then need_fix=1 break fi done if [[ "$need_fix" -eq 1 ]]; then warn "ArgoCD not ready; re-deploying" REGISTRY_NAMESPACE="${REGISTRY_NAMESPACE:-default}" \ "$SCRIPT_DIR/init_registry.sh" -n "$ns" --registry-namespace "${REGISTRY_NAMESPACE:-default}" stop || true REGISTRY_NAMESPACE="${REGISTRY_NAMESPACE:-default}" \ "$SCRIPT_DIR/init_registry.sh" -n "$ns" --registry-namespace "${REGISTRY_NAMESPACE:-default}" update || true else log "ArgoCD: OK" fi } repair_openbao() { local ns="$1" local status resource_ready deploy openbao "$ns"; status=$? if [[ "$status" -eq 2 ]]; then resource_ready statefulset openbao "$ns"; status=$? fi if [[ "$status" -ne 0 ]]; then warn "OpenBao not ready; restarting pods and re-applying" kubectl -n "$ns" delete pod -l app=openbao --wait=false >/dev/null 2>&1 || true "$SCRIPT_DIR/init_openbao.sh" -n "$ns" update || true else log "OpenBao: OK" fi } repair_opentofu() { local ns="$1" local status resource_ready deploy opentofu "$ns"; status=$? if [[ "$status" -ne 0 ]]; then warn "OpenTofu not ready; re-deploying" kubectl -n "$ns" delete deploy opentofu --ignore-not-found >/dev/null 2>&1 || true "$SCRIPT_DIR/init_opentofu.sh" -n "$ns" update || true else log "OpenTofu: OK" fi } repair_garage() { local ns="$1" local status resource_ready statefulset garage "$ns"; status=$? if [[ "$status" -ne 0 ]]; then warn "Garage not ready; restarting pods (PVCs preserved)" kubectl -n "$ns" delete pod -l app=garage --wait=false >/dev/null 2>&1 || true "$SCRIPT_DIR/init_garage_store.sh" start || true else log "Garage: OK" fi } repair_cnpg() { local ns="$1" local cluster="${CNPG_CLUSTER_NAME:-prole-db}" if ! kubectl -n "$ns" get cluster "$cluster" >/dev/null 2>&1; then warn "CNPG cluster '$cluster' not found in namespace '$ns'" return 0 fi local total ready bad total=$(kubectl -n "$ns" get pods -l "cnpg.io/cluster=$cluster" --no-headers 2>/dev/null | wc -l | tr -d ' ') if [[ "$total" -eq 0 ]]; then warn "CNPG cluster '$cluster' has no pods; skipping rollout" return 0 fi ready=$(kubectl -n "$ns" get pods -l "cnpg.io/cluster=$cluster" --no-headers 2>/dev/null | awk '$2 ~ /^([0-9]+)\/\\1$/ {c++} END{print c+0}') bad=$(kubectl -n "$ns" get pods -l "cnpg.io/cluster=$cluster" --no-headers 2>/dev/null | awk '$3 ~ /CrashLoopBackOff|Error|ImagePullBackOff|ErrImagePull/ {c++} END{print c+0}') if [[ "$ready" -lt "$total" || "$bad" -gt 0 ]]; then warn "CNPG pods unhealthy ($ready/$total ready, $bad bad); running rollout" run_with_timeout 360 env NAMESPACE="$ns" "$SCRIPT_DIR/init_cloudnative_pg.sh" rollout || true else log "CNPG: OK" fi } repair_barman_plugin() { local ns="cnpg-system" if ! kubectl -n "$ns" get deploy barman-cloud >/dev/null 2>&1; then warn "Barman Cloud plugin missing; reinstalling" run_with_timeout 300 env NAMESPACE="$DB_NAMESPACE" "$SCRIPT_DIR/init_cloudnative_pg.sh" install-barman-plugin || true return 0 fi local ready ready=$(kubectl -n "$ns" get deploy barman-cloud -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") if [[ -z "$ready" || "$ready" == "0" ]]; then warn "Barman Cloud plugin not ready; reinstalling and restarting" run_with_timeout 300 env NAMESPACE="$DB_NAMESPACE" "$SCRIPT_DIR/init_cloudnative_pg.sh" install-barman-plugin || true kubectl -n "$ns" rollout restart deploy/barman-cloud >/dev/null 2>&1 || true else log "Barman Cloud plugin: OK" fi } repair_kdc() { local ns="$1" local status if ! authority_context_exists; then warn "Authority Docker context missing; skipping KDC rebuild" return 0 fi resource_ready deploy auth "$ns"; status=$? if [[ "$status" -ne 0 ]]; then warn "KDC (auth) not ready; re-deploying" kubectl -n "$ns" delete deploy auth --ignore-not-found >/dev/null 2>&1 || true kubectl -n "$ns" delete svc auth --ignore-not-found >/dev/null 2>&1 || true "$SCRIPT_DIR/init_kdc.sh" update || true else log "KDC (auth): OK" fi } case "${ACTION}" in repair) ensure_kubectl ensure_kubeconfig_if_needed log "== Repair Pipeline ==" log "Namespace: $NAMESPACE" if ! cluster_ready; then err "Cluster not reachable; aborting repair" exit 2 fi if [[ "$(detect_mode)" == "k3d" ]]; then if ! k3d_cluster_ready; then err "k3d cluster not running" exit 2 fi fi log "-- Dashboard (Kong) --" repair_dashboard || true log "-- ArgoCD --" repair_argocd "$ARGOCD_NAMESPACE" log "-- OpenBao --" repair_openbao "$SERVICE_NAMESPACE" log "-- OpenTofu --" repair_opentofu "$SERVICE_NAMESPACE" log "-- Garage --" repair_garage "$SERVICE_NAMESPACE" if [[ "${KERBEROS_ENABLED:-}" == "0" || "${KERBEROS_ENABLED:-}" == "false" || "${KERBEROS_ENABLED:-}" == "False" ]]; then log "KDC (auth): skipped (Kerberos disabled)" elif [[ "${PROLE_KDC_ENABLED:-1}" != "0" ]]; then log "-- KDC (auth) --" repair_kdc "$SERVICE_NAMESPACE" else log "KDC (auth): skipped (disabled)" fi log "-- Barman Cloud Plugin --" repair_barman_plugin log "-- CNPG --" repair_cnpg "$DB_NAMESPACE" log "-- Common Services Status --" if [[ "${KERBEROS_ENABLED:-}" == "1" || "${KERBEROS_ENABLED:-}" == "true" || "${KERBEROS_ENABLED:-}" == "True" ]]; then "$SCRIPT_DIR/status_common_services.sh" -n "$SERVICE_NAMESPACE" -k || true else "$SCRIPT_DIR/status_common_services.sh" -n "$SERVICE_NAMESPACE" || true fi log "Repair pipeline complete." ;; *) usage exit 2 ;; esac