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 }