admin.go 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. // M13a W5: admin HTTP routes for archiverd. One route is
  2. // registered when BA_AUTHD_JWT_SECRET is set:
  3. //
  4. // POST /v1/admin/archiver/run — trigger an immediate archiver
  5. // pass (instead of waiting for the next ticker fire). Destructive:
  6. // takes the Postgres advisory lock and runs a full drain. Any
  7. // authenticated user can fire it; the existing runLock
  8. // guarantees no two pass can overlap.
  9. //
  10. // When BA_AUTHD_JWT_SECRET is unset, no admin routes are
  11. // registered. The /health and /metrics endpoints are owned by
  12. // the shared metricsHandler and continue to work as before.
  13. package main
  14. import (
  15. "encoding/json"
  16. "log/slog"
  17. "net/http"
  18. "git3.techno-world.net/lrosales/broad-announce/internal/authd"
  19. )
  20. // wireAdminRoutes wires the admin routes onto mux. The triggerCh
  21. // is a buffered channel of size 1; a non-blocking send coalesces
  22. // concurrent trigger requests (a run that's already in progress
  23. // will complete, and any queued trigger is dropped — the next
  24. // ticker will fire normally).
  25. //
  26. // When the JWT gate is disabled (env not set), the function logs
  27. // a warning and returns without registering anything — backward
  28. // compat for non-M13 deploys.
  29. func wireAdminRoutes(mux *http.ServeMux, triggerCh chan<- struct{}, logger *slog.Logger) {
  30. if !authd.EnvEnabled() {
  31. logger.Warn("admin routes are DISABLED (set BA_AUTHD_JWT_SECRET to enable)")
  32. return
  33. }
  34. ad, err := authd.NewFromEnv()
  35. if err != nil {
  36. logger.Error("BA_AUTHD_JWT_SECRET set but authd init failed; admin routes DISABLED", "err", err)
  37. return
  38. }
  39. logger.Info("admin routes enabled with JWT gate")
  40. // Any authenticated user can fire an archiver run; it's not
  41. // state-destroying (the archiver is idempotent thanks to the
  42. // Postgres advisory lock + the SELECT/DELETE contract).
  43. mux.Handle("POST /v1/admin/archiver/run",
  44. ad.RequireAuth(http.HandlerFunc(handleRunNow(triggerCh, logger))))
  45. }
  46. // handleRunNow signals the runLoop to fire a pass. The send is
  47. // non-blocking: if a trigger is already pending (a previous
  48. // request fired and the loop hasn't picked it up yet), we report
  49. // it back to the caller instead of queueing duplicates.
  50. func handleRunNow(triggerCh chan<- struct{}, logger *slog.Logger) http.HandlerFunc {
  51. return func(w http.ResponseWriter, r *http.Request) {
  52. actor := ""
  53. if c := authd.ClaimsFromContext(r.Context()); c != nil {
  54. actor = c.UserID
  55. }
  56. select {
  57. case triggerCh <- struct{}{}:
  58. logger.Info("admin archiver run triggered", "actor", actor)
  59. w.Header().Set("Content-Type", "application/json")
  60. _ = json.NewEncoder(w).Encode(map[string]any{
  61. "triggered": true,
  62. })
  63. default:
  64. // Trigger already pending or run in progress.
  65. // Coalesce — return 202 Accepted with a hint.
  66. logger.Info("admin archiver run coalesced (already pending)", "actor", actor)
  67. w.Header().Set("Content-Type", "application/json")
  68. w.WriteHeader(http.StatusAccepted)
  69. _ = json.NewEncoder(w).Encode(map[string]any{
  70. "triggered": false,
  71. "reason": "already_pending",
  72. })
  73. }
  74. }
  75. }