## Changes
### port.Deployer interface
- Add PatchProjectSecrets(ctx, projectName, patch) to merge key-value pairs
into all K8s secrets labeled project={projectName}
- Add RestartAll(ctx, projectName) to trigger rolling restart of all deployments
for a project, picking up fresh secrets without waiting for CI
### deployer adapter
- Implement PatchProjectSecrets: lists secrets by label, merges patch into Data,
writes each secret back
- Implement RestartAll: lists deployments by label, sets restartedAt annotation
### domain/credential.go
- Add CredentialCategoryCache = "cache" constant
- Use constant in component_infra.go (was raw string "cache")
### handlers/cache.go (new)
- POST /projects/{projectID}/cache/reprovision
- Calls CreateProjectCache (which handles delete+recreate with new password)
- Updates credential store (REDIS_URL, REDIS_URL_STAGING, REDIS_PREFIX)
- Patches all K8s secrets for the project immediately
- Triggers RestartAll so pods pick up new credentials without waiting for deploy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
4.0 KiB
Go
135 lines
4.0 KiB
Go
// 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",
|
|
})
|
|
}
|