292 lines
10 KiB
Go
292 lines
10 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/cache"
|
|
"github.com/atlasos/calypso/internal/common/config"
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
"github.com/atlasos/calypso/internal/audit"
|
|
"github.com/atlasos/calypso/internal/auth"
|
|
"github.com/atlasos/calypso/internal/iam"
|
|
"github.com/atlasos/calypso/internal/monitoring"
|
|
"github.com/atlasos/calypso/internal/scst"
|
|
"github.com/atlasos/calypso/internal/storage"
|
|
"github.com/atlasos/calypso/internal/system"
|
|
"github.com/atlasos/calypso/internal/tape_physical"
|
|
"github.com/atlasos/calypso/internal/tape_vtl"
|
|
"github.com/atlasos/calypso/internal/tasks"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// NewRouter creates and configures the HTTP router
|
|
func NewRouter(cfg *config.Config, db *database.DB, log *logger.Logger) *gin.Engine {
|
|
if cfg.Logging.Level == "debug" {
|
|
gin.SetMode(gin.DebugMode)
|
|
} else {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
r := gin.New()
|
|
|
|
// Initialize cache if enabled
|
|
var responseCache *cache.Cache
|
|
if cfg.Server.Cache.Enabled {
|
|
responseCache = cache.NewCache(cfg.Server.Cache.DefaultTTL)
|
|
log.Info("Response caching enabled", "default_ttl", cfg.Server.Cache.DefaultTTL)
|
|
}
|
|
|
|
// Middleware
|
|
r.Use(ginLogger(log))
|
|
r.Use(gin.Recovery())
|
|
r.Use(securityHeadersMiddleware(cfg))
|
|
r.Use(rateLimitMiddleware(cfg, log))
|
|
r.Use(corsMiddleware(cfg))
|
|
|
|
// Cache control headers (always applied)
|
|
r.Use(cacheControlMiddleware())
|
|
|
|
// Response caching middleware (if enabled)
|
|
if cfg.Server.Cache.Enabled {
|
|
cacheConfig := CacheConfig{
|
|
Enabled: cfg.Server.Cache.Enabled,
|
|
DefaultTTL: cfg.Server.Cache.DefaultTTL,
|
|
MaxAge: cfg.Server.Cache.MaxAge,
|
|
}
|
|
r.Use(cacheMiddleware(cacheConfig, responseCache))
|
|
}
|
|
|
|
// Initialize monitoring services
|
|
eventHub := monitoring.NewEventHub(log)
|
|
alertService := monitoring.NewAlertService(db, log)
|
|
alertService.SetEventHub(eventHub) // Connect alert service to event hub
|
|
metricsService := monitoring.NewMetricsService(db, log)
|
|
healthService := monitoring.NewHealthService(db, log, metricsService)
|
|
|
|
// Start event hub in background
|
|
go eventHub.Run()
|
|
|
|
// Start metrics broadcaster in background
|
|
go func() {
|
|
ticker := time.NewTicker(30 * time.Second) // Broadcast metrics every 30 seconds
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if metrics, err := metricsService.CollectMetrics(context.Background()); err == nil {
|
|
eventHub.BroadcastMetrics(metrics)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Initialize and start alert rule engine
|
|
alertRuleEngine := monitoring.NewAlertRuleEngine(db, log, alertService)
|
|
|
|
// Register default alert rules
|
|
alertRuleEngine.RegisterRule(monitoring.NewAlertRule(
|
|
"storage-capacity-warning",
|
|
"Storage Capacity Warning",
|
|
monitoring.AlertSourceStorage,
|
|
&monitoring.StorageCapacityCondition{ThresholdPercent: 80.0},
|
|
monitoring.AlertSeverityWarning,
|
|
true,
|
|
"Alert when storage repositories exceed 80% capacity",
|
|
))
|
|
alertRuleEngine.RegisterRule(monitoring.NewAlertRule(
|
|
"storage-capacity-critical",
|
|
"Storage Capacity Critical",
|
|
monitoring.AlertSourceStorage,
|
|
&monitoring.StorageCapacityCondition{ThresholdPercent: 95.0},
|
|
monitoring.AlertSeverityCritical,
|
|
true,
|
|
"Alert when storage repositories exceed 95% capacity",
|
|
))
|
|
alertRuleEngine.RegisterRule(monitoring.NewAlertRule(
|
|
"task-failure",
|
|
"Task Failure",
|
|
monitoring.AlertSourceTask,
|
|
&monitoring.TaskFailureCondition{LookbackMinutes: 60},
|
|
monitoring.AlertSeverityWarning,
|
|
true,
|
|
"Alert when tasks fail within the last hour",
|
|
))
|
|
|
|
// Start alert rule engine in background
|
|
ctx := context.Background()
|
|
go alertRuleEngine.Start(ctx)
|
|
|
|
// Health check (no auth required) - enhanced
|
|
r.GET("/api/v1/health", func(c *gin.Context) {
|
|
health := healthService.CheckHealth(c.Request.Context())
|
|
statusCode := 200
|
|
if health.Status == "unhealthy" {
|
|
statusCode = 503
|
|
} else if health.Status == "degraded" {
|
|
statusCode = 200 // Still 200 but with degraded status
|
|
}
|
|
c.JSON(statusCode, health)
|
|
})
|
|
|
|
// API v1 routes
|
|
v1 := r.Group("/api/v1")
|
|
{
|
|
// Auth routes (public)
|
|
authHandler := auth.NewHandler(db, cfg, log)
|
|
v1.POST("/auth/login", authHandler.Login)
|
|
v1.POST("/auth/logout", authHandler.Logout)
|
|
|
|
// Audit middleware for mutating operations (applied to all v1 routes)
|
|
auditMiddleware := audit.NewMiddleware(db, log)
|
|
v1.Use(auditMiddleware.LogRequest())
|
|
|
|
// Protected routes
|
|
protected := v1.Group("")
|
|
protected.Use(authMiddleware(authHandler))
|
|
protected.Use(func(c *gin.Context) {
|
|
// Store DB in context for permission middleware
|
|
c.Set("db", db)
|
|
c.Next()
|
|
})
|
|
{
|
|
// Auth
|
|
protected.GET("/auth/me", authHandler.Me)
|
|
|
|
// Tasks
|
|
taskHandler := tasks.NewHandler(db, log)
|
|
protected.GET("/tasks/:id", taskHandler.GetTask)
|
|
|
|
// Storage
|
|
storageHandler := storage.NewHandler(db, log)
|
|
storageGroup := protected.Group("/storage")
|
|
storageGroup.Use(requirePermission("storage", "read"))
|
|
{
|
|
storageGroup.GET("/disks", storageHandler.ListDisks)
|
|
storageGroup.POST("/disks/sync", storageHandler.SyncDisks)
|
|
storageGroup.GET("/volume-groups", storageHandler.ListVolumeGroups)
|
|
storageGroup.GET("/repositories", storageHandler.ListRepositories)
|
|
storageGroup.GET("/repositories/:id", storageHandler.GetRepository)
|
|
storageGroup.POST("/repositories", requirePermission("storage", "write"), storageHandler.CreateRepository)
|
|
storageGroup.DELETE("/repositories/:id", requirePermission("storage", "write"), storageHandler.DeleteRepository)
|
|
// ZFS Pools
|
|
storageGroup.GET("/zfs/pools", storageHandler.ListZFSPools)
|
|
storageGroup.GET("/zfs/pools/:id", storageHandler.GetZFSPool)
|
|
storageGroup.POST("/zfs/pools", requirePermission("storage", "write"), storageHandler.CreateZPool)
|
|
storageGroup.DELETE("/zfs/pools/:id", requirePermission("storage", "write"), storageHandler.DeleteZFSPool)
|
|
storageGroup.POST("/zfs/pools/:id/spare", requirePermission("storage", "write"), storageHandler.AddSpareDisk)
|
|
// ZFS Datasets
|
|
storageGroup.GET("/zfs/pools/:id/datasets", storageHandler.ListZFSDatasets)
|
|
storageGroup.POST("/zfs/pools/:id/datasets", requirePermission("storage", "write"), storageHandler.CreateZFSDataset)
|
|
storageGroup.DELETE("/zfs/pools/:id/datasets/:dataset", requirePermission("storage", "write"), storageHandler.DeleteZFSDataset)
|
|
}
|
|
|
|
// SCST
|
|
scstHandler := scst.NewHandler(db, log)
|
|
scstGroup := protected.Group("/scst")
|
|
scstGroup.Use(requirePermission("iscsi", "read"))
|
|
{
|
|
scstGroup.GET("/targets", scstHandler.ListTargets)
|
|
scstGroup.GET("/targets/:id", scstHandler.GetTarget)
|
|
scstGroup.POST("/targets", scstHandler.CreateTarget)
|
|
scstGroup.POST("/targets/:id/luns", scstHandler.AddLUN)
|
|
scstGroup.POST("/targets/:id/initiators", scstHandler.AddInitiator)
|
|
scstGroup.POST("/config/apply", scstHandler.ApplyConfig)
|
|
scstGroup.GET("/handlers", scstHandler.ListHandlers)
|
|
}
|
|
|
|
// Physical Tape Libraries
|
|
tapeHandler := tape_physical.NewHandler(db, log)
|
|
tapeGroup := protected.Group("/tape/physical")
|
|
tapeGroup.Use(requirePermission("tape", "read"))
|
|
{
|
|
tapeGroup.GET("/libraries", tapeHandler.ListLibraries)
|
|
tapeGroup.POST("/libraries/discover", tapeHandler.DiscoverLibraries)
|
|
tapeGroup.GET("/libraries/:id", tapeHandler.GetLibrary)
|
|
tapeGroup.POST("/libraries/:id/inventory", tapeHandler.PerformInventory)
|
|
tapeGroup.POST("/libraries/:id/load", tapeHandler.LoadTape)
|
|
tapeGroup.POST("/libraries/:id/unload", tapeHandler.UnloadTape)
|
|
}
|
|
|
|
// Virtual Tape Libraries
|
|
vtlHandler := tape_vtl.NewHandler(db, log)
|
|
vtlGroup := protected.Group("/tape/vtl")
|
|
vtlGroup.Use(requirePermission("tape", "read"))
|
|
{
|
|
vtlGroup.GET("/libraries", vtlHandler.ListLibraries)
|
|
vtlGroup.POST("/libraries", vtlHandler.CreateLibrary)
|
|
vtlGroup.GET("/libraries/:id", vtlHandler.GetLibrary)
|
|
vtlGroup.DELETE("/libraries/:id", vtlHandler.DeleteLibrary)
|
|
vtlGroup.GET("/libraries/:id/drives", vtlHandler.GetLibraryDrives)
|
|
vtlGroup.GET("/libraries/:id/tapes", vtlHandler.GetLibraryTapes)
|
|
vtlGroup.POST("/libraries/:id/tapes", vtlHandler.CreateTape)
|
|
vtlGroup.POST("/libraries/:id/load", vtlHandler.LoadTape)
|
|
vtlGroup.POST("/libraries/:id/unload", vtlHandler.UnloadTape)
|
|
}
|
|
|
|
// System Management
|
|
systemHandler := system.NewHandler(log, tasks.NewEngine(db, log))
|
|
systemGroup := protected.Group("/system")
|
|
systemGroup.Use(requirePermission("system", "read"))
|
|
{
|
|
systemGroup.GET("/services", systemHandler.ListServices)
|
|
systemGroup.GET("/services/:name", systemHandler.GetServiceStatus)
|
|
systemGroup.POST("/services/:name/restart", systemHandler.RestartService)
|
|
systemGroup.GET("/services/:name/logs", systemHandler.GetServiceLogs)
|
|
systemGroup.POST("/support-bundle", systemHandler.GenerateSupportBundle)
|
|
}
|
|
|
|
// IAM (admin only)
|
|
iamHandler := iam.NewHandler(db, cfg, log)
|
|
iamGroup := protected.Group("/iam")
|
|
iamGroup.Use(requireRole("admin"))
|
|
{
|
|
iamGroup.GET("/users", iamHandler.ListUsers)
|
|
iamGroup.GET("/users/:id", iamHandler.GetUser)
|
|
iamGroup.POST("/users", iamHandler.CreateUser)
|
|
iamGroup.PUT("/users/:id", iamHandler.UpdateUser)
|
|
iamGroup.DELETE("/users/:id", iamHandler.DeleteUser)
|
|
}
|
|
|
|
// Monitoring
|
|
monitoringHandler := monitoring.NewHandler(db, log, alertService, metricsService, eventHub)
|
|
monitoringGroup := protected.Group("/monitoring")
|
|
monitoringGroup.Use(requirePermission("monitoring", "read"))
|
|
{
|
|
// Alerts
|
|
monitoringGroup.GET("/alerts", monitoringHandler.ListAlerts)
|
|
monitoringGroup.GET("/alerts/:id", monitoringHandler.GetAlert)
|
|
monitoringGroup.POST("/alerts/:id/acknowledge", monitoringHandler.AcknowledgeAlert)
|
|
monitoringGroup.POST("/alerts/:id/resolve", monitoringHandler.ResolveAlert)
|
|
|
|
// Metrics
|
|
monitoringGroup.GET("/metrics", monitoringHandler.GetMetrics)
|
|
|
|
// WebSocket (no permission check needed, handled by auth middleware)
|
|
monitoringGroup.GET("/events", monitoringHandler.WebSocketHandler)
|
|
}
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// ginLogger creates a Gin middleware for logging
|
|
func ginLogger(log *logger.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Next()
|
|
|
|
log.Info("HTTP request",
|
|
"method", c.Request.Method,
|
|
"path", c.Request.URL.Path,
|
|
"status", c.Writer.Status(),
|
|
"client_ip", c.ClientIP(),
|
|
"latency_ms", c.Writer.Size(),
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
|