Files
skybridge/faas/cmd/server/main.go
2025-08-31 17:01:07 -04:00

281 lines
8.0 KiB
Go

package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/config"
"github.com/RyanCopley/skybridge/faas/internal/database"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/handlers"
"github.com/RyanCopley/skybridge/faas/internal/repository/postgres"
"github.com/RyanCopley/skybridge/faas/internal/services"
)
func main() {
// Initialize configuration
cfg := config.NewConfig()
if err := cfg.Validate(); err != nil {
log.Fatal("Configuration validation failed:", err)
}
// Initialize logger
logger := initLogger(cfg)
defer logger.Sync()
logger.Info("Starting Function-as-a-Service",
zap.String("version", "1.0.0"),
zap.String("environment", cfg.GetString("FAAS_APP_ENV")),
)
// Initialize database
logger.Info("Connecting to database",
zap.String("dsn", cfg.GetDatabaseDSNForLogging()))
db, err := database.NewPostgresProvider(
cfg.GetDatabaseDSN(),
cfg.GetInt("DB_MAX_OPEN_CONNS"),
cfg.GetInt("DB_MAX_IDLE_CONNS"),
cfg.GetString("DB_CONN_MAX_LIFETIME"),
logger,
)
if err != nil {
logger.Fatal("Failed to initialize database",
zap.String("dsn", cfg.GetDatabaseDSNForLogging()),
zap.Error(err))
}
logger.Info("Database connection established successfully")
// Initialize repositories
functionRepo := postgres.NewFunctionRepository(db, logger)
executionRepo := postgres.NewExecutionRepository(db, logger)
// Initialize services
runtimeService := services.NewRuntimeService(logger, nil)
functionService := services.NewFunctionService(functionRepo, runtimeService, logger)
executionService := services.NewExecutionService(executionRepo, functionRepo, runtimeService, logger)
authService := services.NewAuthService(logger) // Mock auth service for now
// Initialize handlers
healthHandler := handlers.NewHealthHandler(db, logger)
functionHandler := handlers.NewFunctionHandler(functionService, authService, logger)
executionHandler := handlers.NewExecutionHandler(executionService, authService, logger)
// Set up router
router := setupRouter(cfg, logger, healthHandler, functionHandler, executionHandler)
// Create HTTP server
srv := &http.Server{
Addr: cfg.GetServerAddress(),
Handler: router,
ReadTimeout: cfg.GetDuration("SERVER_READ_TIMEOUT"),
WriteTimeout: cfg.GetDuration("SERVER_WRITE_TIMEOUT"),
IdleTimeout: cfg.GetDuration("SERVER_IDLE_TIMEOUT"),
}
// Start server in goroutine
go func() {
logger.Info("Starting HTTP server", zap.String("address", srv.Addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start server", zap.Error(err))
}
}()
// Start metrics server if enabled
var metricsSrv *http.Server
if cfg.GetBool("METRICS_ENABLED") {
metricsSrv = startMetricsServer(cfg, logger)
}
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("Shutting down server...")
// Give outstanding requests time to complete
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown main server
if err := srv.Shutdown(ctx); err != nil {
logger.Error("Server forced to shutdown", zap.Error(err))
}
// Shutdown metrics server
if metricsSrv != nil {
if err := metricsSrv.Shutdown(ctx); err != nil {
logger.Error("Metrics server forced to shutdown", zap.Error(err))
}
}
logger.Info("Server exited")
}
func initLogger(cfg config.ConfigProvider) *zap.Logger {
var logger *zap.Logger
var err error
logLevel := cfg.GetString("FAAS_LOG_LEVEL")
if cfg.IsProduction() && logLevel != "debug" {
logger, err = zap.NewProduction()
} else {
// Use development logger for non-production or when debug is explicitly requested
config := zap.NewDevelopmentConfig()
// Set log level based on environment variable
switch strings.ToLower(logLevel) {
case "debug":
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
case "info":
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
case "warn":
config.Level = zap.NewAtomicLevelAt(zap.WarnLevel)
case "error":
config.Level = zap.NewAtomicLevelAt(zap.ErrorLevel)
default:
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) // Default to debug for development
}
logger, err = config.Build()
}
if err != nil {
log.Fatal("Failed to initialize logger:", err)
}
return logger
}
func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *handlers.HealthHandler, functionHandler *handlers.FunctionHandler, executionHandler *handlers.ExecutionHandler) *gin.Engine {
// Set Gin mode based on environment
if cfg.IsProduction() {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
// Add middleware
router.Use(gin.Logger())
router.Use(gin.Recovery())
// CORS middleware
router.Use(func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-User-Email")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
})
// Health check endpoints (no authentication required)
router.GET("/health", healthHandler.Health)
router.GET("/ready", healthHandler.Ready)
// API routes
api := router.Group("/api")
{
// Function Management
api.GET("/functions", functionHandler.List)
api.POST("/functions", functionHandler.Create)
api.GET("/functions/:id", functionHandler.GetByID)
api.PUT("/functions/:id", functionHandler.Update)
api.DELETE("/functions/:id", functionHandler.Delete)
api.POST("/functions/:id/deploy", functionHandler.Deploy)
// Function Execution
api.POST("/functions/:id/execute", executionHandler.Execute)
api.POST("/functions/:id/invoke", executionHandler.Invoke)
// Execution Management
api.GET("/executions", executionHandler.List)
api.GET("/executions/:id", executionHandler.GetByID)
api.DELETE("/executions/:id", executionHandler.Cancel)
api.GET("/executions/:id/logs", executionHandler.GetLogs)
api.GET("/executions/running", executionHandler.GetRunning)
// Runtime information endpoint
api.GET("/runtimes", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"runtimes": domain.GetAvailableRuntimes(),
})
})
// Documentation endpoint
api.GET("/docs", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"service": "Function-as-a-Service",
"version": "1.0.0",
"documentation": "FaaS API Documentation",
"endpoints": map[string]interface{}{
"functions": []string{
"GET /api/functions",
"POST /api/functions",
"GET /api/functions/:id",
"PUT /api/functions/:id",
"DELETE /api/functions/:id",
"POST /api/functions/:id/deploy",
},
"executions": []string{
"POST /api/functions/:id/execute",
"POST /api/functions/:id/invoke",
"GET /api/executions",
"GET /api/executions/:id",
"DELETE /api/executions/:id",
"GET /api/executions/:id/logs",
"GET /api/executions/running",
},
},
})
})
}
return router
}
func startMetricsServer(cfg config.ConfigProvider, logger *zap.Logger) *http.Server {
mux := http.NewServeMux()
// Health endpoint for metrics server
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// Metrics endpoint would go here
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("# FaaS metrics placeholder\n"))
})
srv := &http.Server{
Addr: cfg.GetString("FAAS_SERVER_HOST") + ":" + cfg.GetString("METRICS_PORT"),
Handler: mux,
}
go func() {
logger.Info("Starting metrics server", zap.String("address", srv.Addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("Failed to start metrics server", zap.Error(err))
}
}()
return srv
}