// 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"}) } }