mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-08 16:02:24 +00:00
179 lines
5.4 KiB
Go
179 lines
5.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// Fleet placement as the operator sees it: every worker against its capacity
|
|
// row, the decision log the control loops write, and the dedicated bindings.
|
|
|
|
// AdminFleetCapacity lists every worker with its capacity-view row, hottest first.
|
|
//
|
|
// GET /admin/fleet/capacity
|
|
func (h *Handler) AdminFleetCapacity(c *gin.Context) {
|
|
if h.AdminFleetRepo == nil {
|
|
errx.JSON(c, errx.New(errx.NotImplemented, "fleet view is not available on this instance"))
|
|
return
|
|
}
|
|
rows, err := h.AdminFleetRepo.Capacity(c.Request.Context())
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, err.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": rows})
|
|
}
|
|
|
|
// AdminFleetDecisions lists decision_log newest first.
|
|
//
|
|
// GET /admin/fleet/decisions?kind=&worker_id=&limit=
|
|
func (h *Handler) AdminFleetDecisions(c *gin.Context) {
|
|
if h.AdminFleetRepo == nil {
|
|
errx.JSON(c, errx.New(errx.NotImplemented, "fleet view is not available on this instance"))
|
|
return
|
|
}
|
|
var workerID *uuid.UUID
|
|
if raw := c.Query("worker_id"); raw != "" {
|
|
id, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid worker_id"))
|
|
return
|
|
}
|
|
workerID = &id
|
|
}
|
|
limit := 0
|
|
if raw := c.Query("limit"); raw != "" {
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil || n <= 0 {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid limit"))
|
|
return
|
|
}
|
|
limit = n
|
|
}
|
|
rows, err := h.AdminFleetRepo.Decisions(c.Request.Context(), c.Query("kind"), workerID, limit)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, err.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": rows})
|
|
}
|
|
|
|
// AdminFleetDedicated lists active worker-to-organization bindings.
|
|
//
|
|
// GET /admin/fleet/dedicated
|
|
func (h *Handler) AdminFleetDedicated(c *gin.Context) {
|
|
if h.AdminFleetRepo == nil {
|
|
errx.JSON(c, errx.New(errx.NotImplemented, "fleet view is not available on this instance"))
|
|
return
|
|
}
|
|
rows, err := h.AdminFleetRepo.DedicatedAssignments(c.Request.Context())
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, err.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": rows})
|
|
}
|
|
|
|
// AdminFleetReleaseDedicated is the inverse of AdminConvertWorkerToDedicated:
|
|
// the org's mailboxes go back to shared premium workers, the binding is
|
|
// released, and the worker re-enters the shared pool once nothing binds it.
|
|
//
|
|
// POST /admin/fleet/dedicated/:orgId/release
|
|
func (h *Handler) AdminFleetReleaseDedicated(c *gin.Context) {
|
|
orgID, ok := parseUUIDParam(c, "orgId")
|
|
if !ok {
|
|
return
|
|
}
|
|
if h.WorkerAssignmentService == nil || h.WorkerRepo == nil {
|
|
errx.JSON(c, errx.New(errx.NotImplemented, "worker placement is not available on this instance"))
|
|
return
|
|
}
|
|
ctx := c.Request.Context()
|
|
|
|
assignment, err := h.WorkerRepo.GetActiveDedicatedAssignment(ctx, orgID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, err.Error()))
|
|
return
|
|
}
|
|
if assignment == nil {
|
|
errx.JSON(c, errx.New(errx.NotFound, "organization has no active dedicated worker"))
|
|
return
|
|
}
|
|
workerID := assignment.WorkerID
|
|
|
|
before, err := h.WorkerRepo.GetEmailAccountsByWorkerID(ctx, workerID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "list accounts: "+err.Error()))
|
|
return
|
|
}
|
|
|
|
// Moves every org mailbox onto a live shared premium worker and releases
|
|
// the binding; a mailbox with no shared target stays put rather than failing.
|
|
if err := h.WorkerAssignmentService.MigrateOrgToShared(ctx, orgID); err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "migrate to shared: "+err.Error()))
|
|
return
|
|
}
|
|
// MigrateOrgToShared swallows its own release error, so release the exact
|
|
// row read above by id: a binding created meanwhile is never touched.
|
|
if _, err := h.WorkerRepo.ReleaseDedicatedAssignmentByID(ctx, assignment.ID); err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "release assignment: "+err.Error()))
|
|
return
|
|
}
|
|
|
|
after, err := h.WorkerRepo.GetEmailAccountsByWorkerID(ctx, workerID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "list accounts: "+err.Error()))
|
|
return
|
|
}
|
|
moved := len(before) - len(after)
|
|
if moved < 0 {
|
|
moved = 0
|
|
}
|
|
|
|
// The worker only returns to the shared pool when no other org binds it.
|
|
stillBound := false
|
|
if h.AdminFleetRepo != nil {
|
|
active, err := h.AdminFleetRepo.DedicatedAssignments(ctx)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "list assignments: "+err.Error()))
|
|
return
|
|
}
|
|
for _, a := range active {
|
|
if a.WorkerID == workerID {
|
|
stillBound = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
returnedToShared := false
|
|
if !stillBound {
|
|
if err := h.WorkerRepo.SetWorkerType(ctx, workerID, models.WorkerTypeShared); err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "set type: "+err.Error()))
|
|
return
|
|
}
|
|
returnedToShared = true
|
|
}
|
|
|
|
h.audit(c, "release_dedicated", models.AuditEntityWorker, &workerID, map[string]string{
|
|
"organization_id": orgID.String(),
|
|
"subscription_id": assignment.SubscriptionID.String(),
|
|
"assignment_id": assignment.ID.String(),
|
|
"accounts_moved": itoa(moved),
|
|
"accounts_remaining": itoa(len(after)),
|
|
"returned_to_shared": boolStr(returnedToShared),
|
|
})
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ok": true,
|
|
"worker_id": workerID,
|
|
"accounts_moved": moved,
|
|
"accounts_remaining": len(after),
|
|
"returned_to_shared": returnedToShared,
|
|
})
|
|
}
|