Files
HellreigN/backend/internal/handlers/graph.go
T
shinyzero0 7aa25b02c5
ci-agent / build (push) Has been cancelled
feat(backend): add service graph yaml
2026-04-05 07:48:55 +03:00

150 lines
3.6 KiB
Go

package handlers
import (
"io"
"log"
"net/http"
"os"
"sync"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/graph"
"github.com/gin-gonic/gin"
)
// GraphHandlers manages the service dependency graph.
type GraphHandlers struct {
path string
mu sync.RWMutex
yamlData []byte
loaded *graph.Graph
}
// NewGraphHandlers loads the graph from the given YAML file path.
func NewGraphHandlers(yamlPath string) *GraphHandlers {
h := &GraphHandlers{path: yamlPath}
if err := h.reload(); err != nil {
if _, ok := err.(*os.PathError); ok {
log.Printf("[graph] no graph file at %q, starting with empty graph", yamlPath)
h.loaded = graph.New()
h.yamlData = []byte("nodes: {}\n")
} else {
log.Fatalf("[graph] failed to load graph from %q: %v", yamlPath, err)
}
}
return h
}
func (h *GraphHandlers) reload() error {
data, err := os.ReadFile(h.path)
if err != nil {
return err
}
g, err := graph.ParseYAML(data)
if err != nil {
return err
}
h.mu.Lock()
h.yamlData = data
h.loaded = g
h.mu.Unlock()
return nil
}
// GetGraph returns the current parsed graph.
func (h *GraphHandlers) GetGraph() *graph.Graph {
h.mu.RLock()
defer h.mu.RUnlock()
return h.loaded
}
// GetYAML returns the raw YAML content.
// @Summary Get dependency graph YAML
// @Description Returns the service dependency graph as raw YAML text
// @Tags graph
// @Produce plain
// @Success 200 {string} string "YAML content"
// @Security Bearer
// @Router /graph [get]
func (h *GraphHandlers) GetYAML(c *gin.Context) {
h.mu.RLock()
defer h.mu.RUnlock()
c.Data(http.StatusOK, "text/yaml", h.yamlData)
}
// UpdateYAML updates the graph from new YAML text.
// @Summary Update dependency graph YAML
// @Description Replaces the service dependency graph YAML and reloads it
// @Tags graph
// @Accept plain
// @Produce json
// @Param body body string true "New YAML content"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Security Bearer
// @Router /graph [put]
func (h *GraphHandlers) UpdateYAML(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read body"})
return
}
g, err := graph.ParseYAML(body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := os.WriteFile(h.path, body, 0o644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write graph file"})
return
}
h.mu.Lock()
h.yamlData = body
h.loaded = g
h.mu.Unlock()
log.Printf("[graph] updated graph from admin, saved to %s", h.path)
c.JSON(http.StatusOK, gin.H{"message": "graph updated"})
}
// StartupOrder returns the computed service startup order.
// @Summary Get startup order
// @Description Returns the topologically sorted service startup order
// @Tags graph
// @Produce json
// @Success 200 {array} string
// @Failure 400 {object} map[string]string
// @Security Bearer
// @Router /graph/order [get]
func (h *GraphHandlers) StartupOrder(c *gin.Context) {
h.mu.RLock()
g := h.loaded
h.mu.RUnlock()
order, err := g.TopologicalSort()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, order)
}
// CycleCheck checks if the graph has cycles.
// @Summary Check for cycles
// @Description Returns whether the dependency graph contains cycles
// @Tags graph
// @Produce json
// @Success 200 {object} map[string]bool
// @Security Bearer
// @Router /graph/cycle [get]
func (h *GraphHandlers) CycleCheck(c *gin.Context) {
h.mu.RLock()
g := h.loaded
h.mu.RUnlock()
c.JSON(http.StatusOK, gin.H{"has_cycle": g.HasCycle()})
}