Files
middleware-manager/api/server.go
hhftechnologies 88a2a02a0d update
2025-04-29 11:13:11 +05:30

268 lines
7.8 KiB
Go

package api
import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/hhftechnology/middleware-manager/api/handlers"
"github.com/hhftechnology/middleware-manager/services"
)
// Server represents the API server
type Server struct {
db *sql.DB
router *gin.Engine
srv *http.Server
middlewareHandler *handlers.MiddlewareHandler
resourceHandler *handlers.ResourceHandler
configHandler *handlers.ConfigHandler
dataSourceHandler *handlers.DataSourceHandler
configManager *services.ConfigManager
}
// ServerConfig contains configuration options for the server
type ServerConfig struct {
Port string
UIPath string
Debug bool
AllowCORS bool
CORSOrigin string
}
// NewServer creates a new API server
func NewServer(db *sql.DB, config ServerConfig, configManager *services.ConfigManager) *Server {
// Set gin mode based on debug flag
if !config.Debug {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
// Use recovery and logger middleware
router.Use(gin.Recovery())
if config.Debug {
router.Use(gin.Logger())
} else {
// In production, use a custom minimal logger
router.Use(minimalLogger())
}
// CORS middleware if enabled
if config.AllowCORS {
corsConfig := cors.DefaultConfig()
// If a specific origin is provided, use it
if config.CORSOrigin != "" {
corsConfig.AllowOrigins = []string{config.CORSOrigin}
} else {
corsConfig.AllowAllOrigins = true
}
corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"}
corsConfig.ExposeHeaders = []string{"Content-Length"}
corsConfig.AllowCredentials = true
corsConfig.MaxAge = 12 * time.Hour
router.Use(cors.New(corsConfig))
}
// Create request handlers
middlewareHandler := handlers.NewMiddlewareHandler(db)
resourceHandler := handlers.NewResourceHandler(db)
configHandler := handlers.NewConfigHandler(db)
dataSourceHandler := handlers.NewDataSourceHandler(configManager)
// Setup server
server := &Server{
db: db,
router: router,
middlewareHandler: middlewareHandler,
resourceHandler: resourceHandler,
configHandler: configHandler,
dataSourceHandler: dataSourceHandler,
configManager: configManager,
srv: &http.Server{
Addr: ":" + config.Port,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
},
}
// Configure routes
server.setupRoutes(config.UIPath)
return server
}
// setupRoutes configures all the routes for the API server
func (s *Server) setupRoutes(uiPath string) {
// Health check endpoint
s.router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// API routes
api := s.router.Group("/api")
{
// Middleware routes
middlewares := api.Group("/middlewares")
{
middlewares.GET("", s.middlewareHandler.GetMiddlewares)
middlewares.POST("", s.middlewareHandler.CreateMiddleware)
middlewares.GET("/:id", s.middlewareHandler.GetMiddleware)
middlewares.PUT("/:id", s.middlewareHandler.UpdateMiddleware)
middlewares.DELETE("/:id", s.middlewareHandler.DeleteMiddleware)
}
// Resource routes
resources := api.Group("/resources")
{
resources.GET("", s.resourceHandler.GetResources)
resources.GET("/:id", s.resourceHandler.GetResource)
resources.DELETE("/:id", s.resourceHandler.DeleteResource)
resources.POST("/:id/middlewares", s.resourceHandler.AssignMiddleware)
resources.POST("/:id/middlewares/bulk", s.resourceHandler.AssignMultipleMiddlewares)
resources.DELETE("/:id/middlewares/:middlewareId", s.resourceHandler.RemoveMiddleware)
// Router configuration routes
resources.PUT("/:id/config/http", s.configHandler.UpdateHTTPConfig)
resources.PUT("/:id/config/tls", s.configHandler.UpdateTLSConfig)
resources.PUT("/:id/config/tcp", s.configHandler.UpdateTCPConfig)
resources.PUT("/:id/config/headers", s.configHandler.UpdateHeadersConfig)
resources.PUT("/:id/config/priority", s.configHandler.UpdateRouterPriority)
}
// Data source routes
datasource := api.Group("/datasource")
{
datasource.GET("", s.dataSourceHandler.GetDataSources)
datasource.GET("/active", s.dataSourceHandler.GetActiveDataSource)
datasource.PUT("/active", s.dataSourceHandler.SetActiveDataSource)
datasource.PUT("/:name", s.dataSourceHandler.UpdateDataSource)
datasource.POST("/:name/test", s.dataSourceHandler.TestDataSourceConnection)
}
}
// Serve the React app
uiPathToUse := uiPath
if uiPathToUse == "" {
// Default UI path
uiPathToUse = "/app/ui/build"
}
// Check if UI path exists and is a directory
if stat, err := os.Stat(uiPathToUse); err == nil && stat.IsDir() {
s.router.Use(static.Serve("/", static.LocalFile(uiPathToUse, false)))
// Handle all other routes by serving the index.html file
s.router.NoRoute(func(c *gin.Context) {
// API routes should 404 when not found
if len(c.Request.URL.Path) >= 4 && c.Request.URL.Path[:4] == "/api" {
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
return
}
// Non-API routes serve the SPA
c.File(uiPathToUse + "/index.html")
})
} else {
log.Printf("Warning: UI path %s doesn't exist or is not a directory. Web UI will not be available.", uiPathToUse)
}
}
// Start starts the API server with graceful shutdown
func (s *Server) Start() error {
// Channel to listen for errors coming from the listener.
serverErrors := make(chan error, 1)
// Start the server
go func() {
log.Printf("API server listening on %s", s.srv.Addr)
serverErrors <- s.srv.ListenAndServe()
}()
// Channel to listen for an interrupt or terminate signal from the OS.
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
// Blocking select waiting for either a server error or a signal.
select {
case err := <-serverErrors:
// Non-nil error from ListenAndServe.
return err
case <-shutdown:
log.Println("Shutdown signal received")
// Give outstanding requests a deadline for completion.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Asking listener to shut down and shed load.
if err := s.srv.Shutdown(ctx); err != nil {
// Error from closing listeners, or context timeout.
log.Printf("Graceful shutdown failed: %v", err)
if err := s.srv.Close(); err != nil {
log.Printf("Error during forced shutdown: %v", err)
}
return err
}
log.Println("API server stopped gracefully")
}
return nil
}
// Stop gracefully stops the API server
func (s *Server) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := s.srv.Shutdown(ctx); err != nil {
log.Printf("Failed to gracefully shutdown server: %v", err)
if err := s.srv.Close(); err != nil {
log.Printf("Error during forced shutdown: %v", err)
}
} else {
log.Println("API server stopped gracefully")
}
}
// minimalLogger returns a Gin middleware for minimal request logging
func minimalLogger() gin.HandlerFunc {
return func(c *gin.Context) {
// Start timer
start := time.Now()
// Process request
c.Next()
// Log only when path is not being probed by health checkers
if c.Request.URL.Path != "/health" && c.Request.URL.Path != "/ping" {
// Log only requests with errors or non-standard responses
if c.Writer.Status() >= 400 || len(c.Errors) > 0 {
log.Printf("[GIN] %s | %d | %v | %s | %s",
c.Request.Method,
c.Writer.Status(),
time.Since(start),
c.ClientIP(),
c.Request.URL.Path,
)
}
}
}
}