// Package handlers provides HTTP handlers for the rdev API. package handlers import ( "context" "log/slog" "net/http" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/auth" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/pkg/api" ) // CacheHandler handles cache reprovision endpoints. type CacheHandler struct { cacheProvisioner port.CacheProvisioner // may be nil if not configured credStore port.CredentialStore // may be nil deployer port.Deployer // may be nil logger *slog.Logger } // NewCacheHandler creates a new cache handler. func NewCacheHandler( cacheProvisioner port.CacheProvisioner, credStore port.CredentialStore, deployer port.Deployer, logger *slog.Logger, ) *CacheHandler { return &CacheHandler{ cacheProvisioner: cacheProvisioner, credStore: credStore, deployer: deployer, logger: logger, } } // Mount registers the cache routes. func (h *CacheHandler) Mount(r api.Router) { r.Route("/projects/{projectID}/cache", func(r chi.Router) { r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)). Post("/reprovision", h.Reprovision) }) } // Reprovision recreates the Redis ACL user with a new password, updates the credential // store, patches all K8s secrets for the project, and triggers rolling restarts so // pods pick up the new credentials immediately. // POST /projects/{projectID}/cache/reprovision func (h *CacheHandler) Reprovision(w http.ResponseWriter, r *http.Request) { if h.cacheProvisioner == nil { api.WriteError(w, r, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", "cache provisioner not configured") return } projectID := chi.URLParam(r, "projectID") if projectID == "" { api.WriteBadRequest(w, r, "project ID is required") return } ctx, cancel := context.WithTimeout(r.Context(), TimeoutStandard) defer cancel() log := logging.FromContext(ctx).WithHandler("CacheReprovision") // CreateProjectCache handles delete+recreate: if the ACL user already exists it // is deleted first, then a new one is created with a fresh password. creds, err := h.cacheProvisioner.CreateProjectCache(ctx, projectID) if err != nil { log.Error("failed to reprovision cache", logging.FieldError, err, logging.FieldProjectID, projectID, ) api.WriteInternalError(w, r, "failed to reprovision cache") return } // Persist new credentials in the credential store. if h.credStore != nil { for _, kv := range []struct{ key, val string }{ {"REDIS_URL", creds.URL}, {"REDIS_URL_STAGING", creds.URLStaging}, {"REDIS_PREFIX", creds.Prefix}, } { if err := h.credStore.Set(ctx, domain.Credential{ Key: projectID + ":" + kv.key, Value: kv.val, Category: domain.CredentialCategoryCache, }); err != nil { log.Error("failed to store cache credential", logging.FieldError, err, logging.FieldProjectID, projectID, "credential_key", kv.key, ) } } } // Update K8s secrets and restart all project deployments so pods pick up the // new credentials without waiting for the next CI deploy. if h.deployer != nil { patch := map[string]string{ "REDIS_URL": creds.URL, "REDIS_URL_STAGING": creds.URLStaging, "REDIS_PREFIX": creds.Prefix, } if err := h.deployer.PatchProjectSecrets(ctx, projectID, patch); err != nil { log.Warn("failed to patch K8s secrets; pods will get new creds on next CI deploy", logging.FieldError, err, logging.FieldProjectID, projectID, ) } else if err := h.deployer.RestartAll(ctx, projectID); err != nil { log.Warn("failed to restart project deployments", logging.FieldError, err, logging.FieldProjectID, projectID, ) } } log.Info("cache reprovisioned", logging.FieldProjectID, projectID, "prefix", creds.Prefix, "username", creds.Username, ) api.WriteSuccess(w, r, map[string]string{ "prefix": creds.Prefix, "username": creds.Username, "status": "reprovisioned", }) }