Major refactoring to hexagonal (ports & adapters) architecture: - Add service layer (apikey_service, project_service) for business logic - Add webhook system with dispatcher and delivery tracking - Add command queue with priority-based processing - Add rate limiting with sliding window algorithm - Add audit logging for command execution - Add OpenTelemetry integration (traces, metrics, spans) - Add circuit breaker for fault tolerance - Add cached repository wrapper for performance - Add comprehensive validation package - Add Kubernetes client integration for pod management - Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks) - Add network policy and PodDisruptionBudget for k8s - Remove legacy executor and projects/registry packages - Untrack secrets.yaml (now managed via envault) - Add coverage.out to .gitignore - Add e2e test infrastructure with docker-compose - Add comprehensive documentation (API, architecture, operations, plans) - Add golangci-lint config and pre-commit hook Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
156 lines
3.8 KiB
Go
156 lines
3.8 KiB
Go
// 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"`
|
|
}
|