Files
flatrender/services/render/internal/handlers/nodes.go
T
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:29:31 +03:30

227 lines
6.7 KiB
Go

package handlers
import (
"net/http"
"time"
"github.com/flatrender/render-svc/internal/db"
"github.com/flatrender/render-svc/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type NodeHandler struct {
store *db.Store
}
func NewNodeHandler(store *db.Store) *NodeHandler {
return &NodeHandler{store: store}
}
// GET /v1/nodes
func (h *NodeHandler) List(c *gin.Context) {
region := c.Query("region")
status := c.Query("status")
nodes, err := h.store.ListNodes(c.Request.Context(), region, status)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
if nodes == nil {
nodes = []*models.RenderNode{}
}
c.JSON(http.StatusOK, gin.H{"data": nodes})
}
// POST /v1/nodes
func (h *NodeHandler) Create(c *gin.Context) {
var req models.NodeCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()})
return
}
node, err := h.store.CreateNode(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
c.JSON(http.StatusCreated, node)
}
// GET /v1/nodes/:node_id
func (h *NodeHandler) Get(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
node, err := h.store.GetNodeByID(c.Request.Context(), nodeID)
if err != nil {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()})
return
}
c.JSON(http.StatusOK, node)
}
// PATCH /v1/nodes/:node_id
func (h *NodeHandler) Patch(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
var req models.NodePatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()})
return
}
node, err := h.store.PatchNode(c.Request.Context(), nodeID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
c.JSON(http.StatusOK, node)
}
// POST /v1/nodes/:node_id/restart — Stub: in production this calls the node agent
func (h *NodeHandler) Restart(c *gin.Context) {
_, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
c.Status(http.StatusAccepted)
}
// POST /v1/nodes/:node_id/release
func (h *NodeHandler) Release(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
if err := h.store.ReleaseNode(c.Request.Context(), nodeID); err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// POST /v1/nodes/:node_id/close-ae — Stub: signals the node agent to kill AE process
func (h *NodeHandler) CloseAE(c *gin.Context) {
_, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
c.Status(http.StatusNoContent)
}
// GET /v1/nodes/:node_id/health
func (h *NodeHandler) Health(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
node, err := h.store.GetNodeByID(c.Request.Context(), nodeID)
if err != nil {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"node_id": node.ID,
"recorded_at": node.LastHeartbeatAt,
"status": node.Status,
"cpu_pct": node.LastCPUPct,
"ram_available_mb": node.LastRAMAvailableMB,
"ae_running": node.AERunning,
"current_job_id": node.CurrentJobID,
"cache_used_gb": node.CacheUsedGB,
})
}
// GET /v1/nodes/:node_id/health/history
func (h *NodeHandler) HealthHistory(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
fromStr := c.Query("from")
toStr := c.Query("to")
from := time.Now().Add(-24 * time.Hour)
to := time.Now()
if fromStr != "" {
if t, err := time.Parse(time.RFC3339, fromStr); err == nil {
from = t
}
}
if toStr != "" {
if t, err := time.Parse(time.RFC3339, toStr); err == nil {
to = t
}
}
logs, err := h.store.ListNodeHealthHistory(c.Request.Context(), nodeID, from, to)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
if logs == nil {
logs = []*models.NodeHealthLog{}
}
c.JSON(http.StatusOK, gin.H{"data": logs})
}
// GET /v1/nodes/:node_id/crashes
func (h *NodeHandler) Crashes(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
crashes, err := h.store.ListNodeCrashes(c.Request.Context(), nodeID)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
if crashes == nil {
crashes = []*models.NodeCrash{}
}
c.JSON(http.StatusOK, gin.H{"data": crashes})
}
// GET /v1/node-updates
func (h *NodeHandler) ListUpdates(c *gin.Context) {
updates, err := h.store.ListNodeUpdates(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
if updates == nil {
updates = []*models.NodeUpdate{}
}
c.JSON(http.StatusOK, gin.H{"data": updates})
}
// POST /v1/node-updates/:update_id/rollout
func (h *NodeHandler) Rollout(c *gin.Context) {
updateID, err := uuid.Parse(c.Param("update_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid update_id"})
return
}
var body struct {
NodeIDs []uuid.UUID `json:"node_ids"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()})
return
}
if err := h.store.QueueUpdateRollout(c.Request.Context(), updateID, body.NodeIDs); err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
c.Status(http.StatusAccepted)
}