Traefik_Control/backend/internal/api/handlers/traefik_api.go

188 lines
5.1 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"sort"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/traefik/traefik-gui/backend/internal/api/middleware"
"github.com/traefik/traefik-gui/backend/internal/traefik"
)
// TraefikAPIHandler handles /api/traefik/* routes via the typed Traefik client
type TraefikAPIHandler struct {
api traefik.TraefikAPI
cache map[string]cacheEntry
mu sync.RWMutex
ttl time.Duration
}
type cacheEntry struct {
body []byte
status int
expiry time.Time
}
// NewTraefikAPIHandler creates a handler with 15s cache
func NewTraefikAPIHandler(api traefik.TraefikAPI) *TraefikAPIHandler {
return &TraefikAPIHandler{
api: api,
cache: make(map[string]cacheEntry),
ttl: 15 * time.Second,
}
}
// for tests to inject custom ttl or clear cache
func (h *TraefikAPIHandler) clearCache() {
h.mu.Lock()
defer h.mu.Unlock()
h.cache = make(map[string]cacheEntry)
}
func (h *TraefikAPIHandler) cached(c *gin.Context, path string, fn func() (interface{}, error)) {
// role check: viewer+ for reads
user := middleware.GetUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
if user.Role != "viewer" && user.Role != "operator" && user.Role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"})
return
}
// ?refresh=1 bypasses cache
if c.Query("refresh") == "1" {
h.mu.Lock()
delete(h.cache, path)
h.mu.Unlock()
} else {
h.mu.RLock()
if e, ok := h.cache[path]; ok && time.Now().Before(e.expiry) {
h.mu.RUnlock()
c.Data(e.status, "application/json", e.body)
return
}
h.mu.RUnlock()
}
data, err := fn()
if err != nil {
if traefik.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "resource not found"})
return
}
if traefik.IsUnauthorized(err) {
c.JSON(http.StatusBadGateway, gin.H{"error": "traefik API unauthorized - check if Traefik API is enabled or on a private entrypoint"})
return
}
if apiErr, ok := err.(*traefik.APIError); ok {
status := apiErr.StatusCode
if status < 400 || status >= 600 {
status = http.StatusBadGateway
}
c.JSON(status, gin.H{"error": apiErr.Message})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "upstream API error"})
return
}
// Marshal to cache
// Use gin to marshal via c.JSON would not give us bytes for cache; we mimic JSON marshal
// Instead we use c.JSON and also cache the body by re-marshaling
// Simplify: use c.JSON and store via recording? For now marshal manually
// We'll just call c.JSON and also store the marshaled bytes via helper
// To avoid double marshal, we directly marshal and cache
// Use gin's JSON rendering via helper
body, err := marshalJSON(data)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "marshal error"})
return
}
h.mu.Lock()
h.cache[path] = cacheEntry{body: body, status: http.StatusOK, expiry: time.Now().Add(h.ttl)}
h.mu.Unlock()
c.Data(http.StatusOK, "application/json", body)
}
func marshalJSON(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (h *TraefikAPIHandler) Health(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
raw, err := h.api.GetHealthz(c.Request.Context())
if err != nil {
return nil, err
}
return gin.H{"healthy": true, "raw": raw}, nil
})
}
func (h *TraefikAPIHandler) Overview(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
return h.api.GetOverview(c.Request.Context())
})
}
func (h *TraefikAPIHandler) Entrypoints(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
return h.api.GetEntrypoints(c.Request.Context())
})
}
func (h *TraefikAPIHandler) Routers(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
return h.api.GetRouters(c.Request.Context())
})
}
func (h *TraefikAPIHandler) Services(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
return h.api.GetServices(c.Request.Context())
})
}
func (h *TraefikAPIHandler) Middlewares(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
return h.api.GetMiddlewares(c.Request.Context())
})
}
func (h *TraefikAPIHandler) Providers(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
return h.api.GetProviders(c.Request.Context())
})
}
func (h *TraefikAPIHandler) Certificates(c *gin.Context) {
h.cached(c, c.Request.URL.Path, func() (interface{}, error) {
certs, err := h.api.GetCertificates(c.Request.Context())
if err != nil {
return nil, err
}
// Sort ascending by notAfter
sort.Slice(certs, func(i, j int) bool {
return certs[i].NotAfter.Before(certs[j].NotAfter)
})
now := time.Now()
type certWithExpiry struct {
traefik.Certificate
DaysUntilExpiry float64 `json:"days_until_expiry"`
Expired bool `json:"expired"`
}
out := make([]certWithExpiry, len(certs))
for i, cert := range certs {
out[i] = certWithExpiry{
Certificate: cert,
DaysUntilExpiry: cert.NotAfter.Sub(now).Hours() / 24.0,
Expired: cert.NotAfter.Before(now),
}
}
return out, nil
})
}