Files
aura-crm/production/backend/internal/modulecontrol/modulecontrol.go
T

102 lines
3.5 KiB
Go

// Package modulecontrol lets core restart this process or flip it into
// maintenance mode — the module side of core's internal/registry
// Restart/SetMaintenance. Authenticated with a shared secret (X-Control-Token,
// the CORE_CONTROL_TOKEN env var issued once when this module was
// registered with core), never a staff JWT — core is the only caller.
//
// Both actions are self-contained to this process on purpose: Restart exits
// the process and relies on docker-compose's own "restart: unless-stopped"
// policy to bring it back, rather than this module (or core) touching
// Docker directly. Maintenance mode is an in-memory flag, not persisted —
// reset to off on every process start, so a crash or an unrelated restart
// can never leave the module stuck refusing traffic with nobody around to
// remember why.
package modulecontrol
import (
"crypto/subtle"
"log"
"os"
"strings"
"sync/atomic"
"time"
"github.com/gofiber/fiber/v2"
)
type Handler struct {
controlToken string
maintenance atomic.Bool
}
// NewHandler reads CORE_CONTROL_TOKEN itself — an empty token means every
// call to Restart/SetMaintenance is rejected (authOK never succeeds against
// an empty stored secret), so control is off by default until an owner
// actually sets one via core's module registration.
func NewHandler() *Handler {
return &Handler{controlToken: os.Getenv("CORE_CONTROL_TOKEN")}
}
func (h *Handler) authOK(c *fiber.Ctx) bool {
if h.controlToken == "" {
return false
}
given := c.Get("X-Control-Token")
return subtle.ConstantTimeCompare([]byte(given), []byte(h.controlToken)) == 1
}
// Restart responds first, then exits after a short delay so the response
// actually reaches core before the process is gone.
func (h *Handler) Restart(c *fiber.Ctx) error {
if !h.authOK(c) {
return c.Status(401).JSON(fiber.Map{"error": "invalid control token"})
}
log.Printf("modulecontrol: restart requested by core")
go func() {
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}()
return c.JSON(fiber.Map{"ok": true, "restarting": true})
}
func (h *Handler) SetMaintenance(c *fiber.Ctx) error {
if !h.authOK(c) {
return c.Status(401).JSON(fiber.Map{"error": "invalid control token"})
}
var body struct {
Enabled bool `json:"enabled"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid request body"})
}
h.maintenance.Store(body.Enabled)
log.Printf("modulecontrol: maintenance mode set to %v by core", body.Enabled)
return c.JSON(fiber.Map{"ok": true, "maintenance": body.Enabled})
}
// Maintenance reports the current in-memory flag — read by both the
// blocking middleware below and coreclient's heartbeat sender (so the
// dashboard shows "maintenance" instead of a misleading "healthy" while
// it's on).
func (h *Handler) Maintenance() bool {
return h.maintenance.Load()
}
// Middleware blocks every route with 503 while maintenance mode is on,
// except /health (so core/an operator can still see the process is alive,
// just deliberately not serving) and the control routes themselves (so
// maintenance mode can always be turned back off — a maintenance flag that
// blocks its own toggle would be a one-way door).
func (h *Handler) Middleware() fiber.Handler {
return func(c *fiber.Ctx) error {
if !h.maintenance.Load() {
return c.Next()
}
path := c.Path()
if path == "/health" || strings.HasPrefix(path, "/api/module-control/") {
return c.Next()
}
return c.Status(503).JSON(fiber.Map{"error": "module is in maintenance mode"})
}
}