CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 51s
CI / go (push) Successful in 2m19s
CI / bird2 (push) Successful in 13s
CI / release (push) Successful in 4m24s
Added support for portal JWT authentication, enabling single sign-on (SSO) capabilities. Updated the application to handle JWT claims for user permissions and roles, enhancing security and access control. Refactored relevant components and API routes to accommodate the new authentication flow, ensuring a seamless user experience. Updated documentation to reflect the new authentication requirements and configurations. Co-authored-by: Cursor <cursoragent@cursor.com>
275 lines
8.8 KiB
Go
275 lines
8.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"evobgp/internal/jobs"
|
|
"evobgp/internal/observability"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func (s *Server) registerMaintenanceRoutes(m *http.ServeMux) {
|
|
m.HandleFunc("GET /maintenance/policies", s.handleListMaintenancePolicies)
|
|
m.HandleFunc("POST /maintenance/policies", s.handleCreateMaintenancePolicy)
|
|
m.HandleFunc("GET /maintenance/policies/{id}", s.handleGetMaintenancePolicy)
|
|
m.HandleFunc("PATCH /maintenance/policies/{id}", s.handlePatchMaintenancePolicy)
|
|
m.HandleFunc("DELETE /maintenance/policies/{id}", s.handleDeleteMaintenancePolicy)
|
|
m.HandleFunc("GET /maintenance/policies/{id}/hints", s.handleMaintenancePolicyHints)
|
|
m.HandleFunc("GET /maintenance/config-audit", s.handleListMaintenanceConfigAudit)
|
|
m.HandleFunc("POST /maintenance/run", s.handleMaintenanceRun)
|
|
m.HandleFunc("POST /maintenance/dry-run", s.handleMaintenanceDryRun)
|
|
}
|
|
|
|
func maintenancePolicyJSON(p *store.MaintenancePolicy) map[string]any {
|
|
if p == nil {
|
|
return map[string]any{}
|
|
}
|
|
out := map[string]any{
|
|
"id": p.ID,
|
|
"name": p.Name,
|
|
"table_name": p.TableName,
|
|
"condition": p.Condition,
|
|
"vacuum_strategy": p.VacuumStrategy,
|
|
"schedule": p.Schedule,
|
|
"enabled": p.Enabled,
|
|
"dry_run_enabled": p.DryRunEnabled,
|
|
}
|
|
if p.RetentionPeriodSec != nil {
|
|
out["retention_period_sec"] = *p.RetentionPeriodSec
|
|
}
|
|
if p.MaxRows != nil {
|
|
out["max_rows"] = *p.MaxRows
|
|
}
|
|
if p.LastRunAt != nil {
|
|
out["last_run_at"] = p.LastRunAt.UTC().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if p.LastStatus != "" {
|
|
out["last_status"] = p.LastStatus
|
|
}
|
|
if p.LastError != "" {
|
|
out["last_error"] = p.LastError
|
|
}
|
|
if !p.CreatedAt.IsZero() {
|
|
out["created_at"] = p.CreatedAt.UTC().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if !p.UpdatedAt.IsZero() {
|
|
out["updated_at"] = p.UpdatedAt.UTC().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Server) handleListMaintenancePolicies(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requirePostgres(w) {
|
|
return
|
|
}
|
|
cursor := r.URL.Query().Get("cursor")
|
|
limit := parseLimitQuery(r, 20, 100)
|
|
items, next, hasMore, err := s.store.ListMaintenancePolicies(cursor, limit)
|
|
if err != nil {
|
|
writeInternalError(w, "maintenance_policies_list", err)
|
|
return
|
|
}
|
|
out := make([]map[string]any, 0, len(items))
|
|
for _, p := range items {
|
|
out = append(out, maintenancePolicyJSON(p))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": out, "next_cursor": next, "has_more": hasMore})
|
|
}
|
|
|
|
func (s *Server) handleGetMaintenancePolicy(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requirePostgres(w) {
|
|
return
|
|
}
|
|
p, err := s.store.GetMaintenancePolicy(r.PathValue("id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, maintenancePolicyJSON(p))
|
|
}
|
|
|
|
func (s *Server) handleCreateMaintenancePolicy(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) {
|
|
return
|
|
}
|
|
var body store.MaintenancePolicy
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", invalidInputDetail)
|
|
return
|
|
}
|
|
p, err := s.store.CreateMaintenancePolicy(&body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
_ = s.store.AppendMaintenancePolicyConfigAudit(actorPrefix(a), p.ID, "create", nil, maintenancePolicyJSON(p))
|
|
observability.IncMaintenanceConfigChange("create")
|
|
s.reloadMaintenanceConfig(r)
|
|
writeJSON(w, http.StatusCreated, maintenancePolicyJSON(p))
|
|
}
|
|
|
|
func (s *Server) handlePatchMaintenancePolicy(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) {
|
|
return
|
|
}
|
|
id := r.PathValue("id")
|
|
before, err := s.store.GetMaintenancePolicy(id)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
var patch store.MaintenancePolicyPatch
|
|
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", invalidInputDetail)
|
|
return
|
|
}
|
|
updated, err := s.store.UpdateMaintenancePolicy(id, &patch)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
_ = s.store.AppendMaintenancePolicyConfigAudit(actorPrefix(a), id, "update", maintenancePolicyJSON(before), maintenancePolicyJSON(updated))
|
|
observability.IncMaintenanceConfigChange("update")
|
|
s.reloadMaintenanceConfig(r)
|
|
writeJSON(w, http.StatusOK, maintenancePolicyJSON(updated))
|
|
}
|
|
|
|
func (s *Server) handleDeleteMaintenancePolicy(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) {
|
|
return
|
|
}
|
|
id := r.PathValue("id")
|
|
before, err := s.store.GetMaintenancePolicy(id)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
if err := s.store.DeleteMaintenancePolicy(id); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
_ = s.store.AppendMaintenancePolicyConfigAudit(actorPrefix(a), id, "delete", maintenancePolicyJSON(before), nil)
|
|
observability.IncMaintenanceConfigChange("delete")
|
|
s.reloadMaintenanceConfig(r)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleMaintenancePolicyHints(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requirePostgres(w) {
|
|
return
|
|
}
|
|
if s.maintStats == nil {
|
|
writeProblem(w, http.StatusServiceUnavailable, "Unavailable", "postgresql backend required")
|
|
return
|
|
}
|
|
p, err := s.store.GetMaintenancePolicy(r.PathValue("id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
hints, err := s.maintStats.Hints(r.Context(), p.TableName)
|
|
if err != nil {
|
|
writeInternalError(w, "maintenance_policy_hints", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, hints)
|
|
}
|
|
|
|
func (s *Server) handleListMaintenanceConfigAudit(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requirePostgres(w) {
|
|
return
|
|
}
|
|
cursor := r.URL.Query().Get("cursor")
|
|
limit := parseLimitQuery(r, 20, 100)
|
|
items, next, hasMore, err := s.store.ListMaintenancePolicyConfigAudit(cursor, limit)
|
|
if err != nil {
|
|
writeInternalError(w, "maintenance_config_audit", err)
|
|
return
|
|
}
|
|
out := make([]map[string]any, 0, len(items))
|
|
for _, row := range items {
|
|
out = append(out, map[string]any{
|
|
"id": row.ID,
|
|
"policy_id": row.PolicyID,
|
|
"actor_prefix": row.ActorPrefix,
|
|
"action": row.Action,
|
|
"before": row.Before,
|
|
"after": row.After,
|
|
"created_at": row.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": out, "next_cursor": next, "has_more": hasMore})
|
|
}
|
|
|
|
type maintenanceRunBody struct {
|
|
PolicyID string `json:"policy_id"`
|
|
}
|
|
|
|
func (s *Server) handleMaintenanceRun(w http.ResponseWriter, r *http.Request) {
|
|
s.enqueueMaintenancePolicy(w, r, false)
|
|
}
|
|
|
|
func (s *Server) handleMaintenanceDryRun(w http.ResponseWriter, r *http.Request) {
|
|
s.enqueueMaintenancePolicy(w, r, true)
|
|
}
|
|
|
|
func (s *Server) enqueueMaintenancePolicy(w http.ResponseWriter, r *http.Request, dryRun bool) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) {
|
|
return
|
|
}
|
|
var body maintenanceRunBody
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", invalidInputDetail)
|
|
return
|
|
}
|
|
policyID := strings.TrimSpace(body.PolicyID)
|
|
if policyID == "" {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "policy_id is required")
|
|
return
|
|
}
|
|
if _, err := s.store.GetMaintenancePolicy(policyID); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
kind := "maintenance_policy_run"
|
|
if !s.checkPgMaintRateLimit(a.TenantID, kind+":"+policyID) {
|
|
writeProblem(w, http.StatusTooManyRequests, "Too Many Requests", "wait before repeating this maintenance operation")
|
|
return
|
|
}
|
|
idem := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
|
var idemPtr *string
|
|
if idem != "" {
|
|
idemPtr = &idem
|
|
}
|
|
title := "Maintenance policy run"
|
|
if dryRun {
|
|
title = "Maintenance policy dry-run"
|
|
}
|
|
j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindMaintenancePolicyRun, idemPtr, nil, map[string]any{
|
|
"policy_id": policyID, "dry_run": dryRun, "actor_prefix": actorPrefix(a), "job_title": title,
|
|
})
|
|
if err != nil {
|
|
writeInternalError(w, "maintenance_policy_enqueue", err)
|
|
return
|
|
}
|
|
w.Header().Set("Location", "/v1/jobs/"+j.ID)
|
|
snap := j.Snapshot()
|
|
writeJSON(w, http.StatusAccepted, map[string]any{"job_id": snap["job_id"], "status": snap["status"]})
|
|
}
|
|
|
|
func (s *Server) reloadMaintenanceConfig(r *http.Request) {
|
|
if s.maintConfig != nil {
|
|
_ = s.maintConfig.Reload(r.Context())
|
|
}
|
|
}
|