// Package handlers provides HTTP handlers for the rdev API. package handlers import ( "context" "database/sql" "net/http" "strings" "time" "github.com/orchard9/rdev/pkg/api" k8sclient "k8s.io/client-go/kubernetes" ) // HealthHandler handles health and readiness checks. type HealthHandler struct { serviceName string db *sql.DB k8sClient *k8sclient.Clientset } // NewHealthHandler creates a new health handler with dependencies. func NewHealthHandler(serviceName string, db *sql.DB, k8sClient *k8sclient.Clientset) *HealthHandler { return &HealthHandler{ serviceName: serviceName, db: db, k8sClient: k8sClient, } } // Health returns a simple liveness check. // This should be lightweight and only fail if the process is unhealthy. // GET /health func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) { api.WriteSuccess(w, r, map[string]string{ "status": "ok", "service": h.serviceName, }) } // Ready returns a readiness check with dependency health. // This checks all required dependencies (database, k8s) and returns // 503 if any are unhealthy. // GET /ready func (h *HealthHandler) Ready(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() checks := make(map[string]CheckResult) allHealthy := true // Database check if h.db != nil { dbCheck := h.checkDatabase(ctx) checks["database"] = dbCheck if !dbCheck.Healthy { allHealthy = false } } // Kubernetes check if h.k8sClient != nil { k8sCheck := h.checkKubernetes(ctx) checks["kubernetes"] = k8sCheck if !k8sCheck.Healthy { allHealthy = false } } response := ReadinessResponse{ Status: "ready", Service: h.serviceName, Checks: checks, } if !allHealthy { response.Status = "not_ready" api.WriteError(w, r, http.StatusServiceUnavailable, "NOT_READY", "Service not ready - one or more checks failed") return } api.WriteSuccess(w, r, response) } // checkDatabase performs a database health check. func (h *HealthHandler) checkDatabase(ctx context.Context) CheckResult { start := time.Now() err := h.db.PingContext(ctx) latency := time.Since(start) if err != nil { return CheckResult{ Healthy: false, Message: "connection failed: " + err.Error(), Latency: latency.String(), LastCheck: time.Now().UTC(), } } return CheckResult{ Healthy: true, Message: "connected", Latency: latency.String(), LastCheck: time.Now().UTC(), } } // checkKubernetes performs a Kubernetes API health check. func (h *HealthHandler) checkKubernetes(ctx context.Context) CheckResult { start := time.Now() // Try to get server version - lightweight API call _, err := h.k8sClient.Discovery().ServerVersion() latency := time.Since(start) if err != nil { // Check if it's a timeout or connection error msg := err.Error() if strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline") { msg = "connection timeout" } else if strings.Contains(msg, "refused") { msg = "connection refused" } return CheckResult{ Healthy: false, Message: msg, Latency: latency.String(), LastCheck: time.Now().UTC(), } } return CheckResult{ Healthy: true, Message: "connected", Latency: latency.String(), LastCheck: time.Now().UTC(), } } // CheckResult represents the result of a health check. type CheckResult struct { Healthy bool `json:"healthy"` Message string `json:"message"` Latency string `json:"latency,omitempty"` LastCheck time.Time `json:"last_check"` } // ReadinessResponse is the response for the /ready endpoint. type ReadinessResponse struct { Status string `json:"status"` Service string `json:"service"` Checks map[string]CheckResult `json:"checks,omitempty"` }