281 lines
8.0 KiB
Go
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
|
|
} |