package main import ( "flag" "fmt" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "freeleaps.com/gitea-webhook-ambassador/internal/auth" "freeleaps.com/gitea-webhook-ambassador/internal/config" "freeleaps.com/gitea-webhook-ambassador/internal/database" "freeleaps.com/gitea-webhook-ambassador/internal/handler" "freeleaps.com/gitea-webhook-ambassador/internal/jenkins" "freeleaps.com/gitea-webhook-ambassador/internal/logger" "freeleaps.com/gitea-webhook-ambassador/internal/web" webhandler "freeleaps.com/gitea-webhook-ambassador/internal/web/handler" "freeleaps.com/gitea-webhook-ambassador/internal/worker" ) var ( configFile = flag.String("config", "config.yaml", "Path to configuration file") ) func main() { flag.Parse() // Initialize logger with default configuration logger.Configure(logger.Config{ Level: "info", Format: "text", }) // Load initial configuration if err := config.Load(*configFile); err != nil { logger.Error("Failed to load configuration: %v", err) os.Exit(1) } // Setup application app, err := setupApplication() if err != nil { logger.Error("Failed to setup application: %v", err) os.Exit(1) } defer app.cleanup() // Start HTTP server go app.startServer() // Handle graceful shutdown app.handleShutdown() } type application struct { server *http.Server workerPool *worker.Pool db *database.DB watcher *config.Watcher } func setupApplication() (*application, error) { cfg := config.Get() // Configure logger based on configuration logger.Configure(logger.Config{ Level: cfg.Logging.Level, Format: cfg.Logging.Format, File: cfg.Logging.File, }) // Ensure database directory exists dbDir := filepath.Dir(cfg.Database.Path) if err := os.MkdirAll(dbDir, 0755); err != nil { return nil, fmt.Errorf("failed to create database directory: %v", err) } // Initialize database db, err := setupDatabase(cfg) if err != nil { return nil, fmt.Errorf("failed to setup database: %v", err) } // Create Jenkins client jenkinsClient := jenkins.New(jenkins.Config{ URL: cfg.Jenkins.URL, Username: cfg.Jenkins.Username, Token: cfg.Jenkins.Token, Timeout: time.Duration(cfg.Jenkins.Timeout) * time.Second, }) // Create worker pool workerPool, err := setupWorkerPool(cfg, jenkinsClient, db) if err != nil { return nil, fmt.Errorf("failed to setup worker pool: %v", err) } // Setup config watcher watcher, err := setupConfigWatcher(*configFile) if err != nil { return nil, fmt.Errorf("failed to setup config watcher: %v", err) } if err := watcher.Start(); err != nil { return nil, fmt.Errorf("failed to start config watcher: %v", err) } // Create HTTP server server := setupHTTPServer(cfg, workerPool, db) return &application{ server: server, workerPool: workerPool, db: db, watcher: watcher, }, nil } func setupDatabase(cfg config.Configuration) (*database.DB, error) { return database.New(database.Config{ Path: cfg.Database.Path, }) } func setupWorkerPool(cfg config.Configuration, jenkinsClient *jenkins.Client, db *database.DB) (*worker.Pool, error) { pool, err := worker.New(worker.Config{ PoolSize: cfg.Worker.PoolSize, QueueSize: cfg.Worker.QueueSize, MaxRetries: cfg.Worker.MaxRetries, RetryBackoff: time.Duration(cfg.Worker.RetryBackoff) * time.Second, Client: jenkinsClient, DB: db, }) if err != nil { return nil, err } // Start event cleanup go worker.CleanupEvents(time.Duration(cfg.EventCleanup.ExpireAfter) * time.Second) return pool, nil } func setupConfigWatcher(configPath string) (*config.Watcher, error) { return config.NewWatcher(configPath, func() error { if err := config.Load(configPath); err != nil { return err } newCfg := config.Get() // Update logger configuration logger.Configure(logger.Config{ Level: newCfg.Logging.Level, Format: newCfg.Logging.Format, File: newCfg.Logging.File, }) logger.Info("Configuration reloaded successfully") return nil }) } func setupHTTPServer(cfg config.Configuration, workerPool *worker.Pool, db *database.DB) *http.Server { // Create handlers webhookHandler := handler.NewWebhookHandler(workerPool, db, &cfg) healthHandler := handler.NewHealthHandler(workerPool, &cfg) adminHandler := handler.NewAdminHandler(db, &cfg) projectHandler := handler.NewProjectHandler(db, &cfg) logsHandler := handler.NewLogsHandler(db, &cfg) // Create auth middleware authMiddleware := auth.NewMiddleware(cfg.Server.SecretKey) // Create dashboard handler dashboardHandler, err := webhandler.NewDashboardHandler( web.WebAssets, projectHandler, adminHandler, logsHandler, healthHandler, ) if err != nil { logger.Error("Failed to create dashboard handler: %v", err) os.Exit(1) } // Setup HTTP routes mux := http.NewServeMux() // Static file handlers (not protected by auth) mux.HandleFunc("/css/", dashboardHandler.ServeHTTP) mux.HandleFunc("/js/", dashboardHandler.ServeHTTP) mux.HandleFunc("/img/", dashboardHandler.ServeHTTP) // Webhook endpoint (not protected by auth, uses its own validation) mux.HandleFunc(cfg.Server.WebhookPath, webhookHandler.HandleWebhook) // Login routes - must be defined before protected routes mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { dashboardHandler.ServeHTTP(w, r) } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } }) mux.HandleFunc("/api/auth/login", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { authMiddleware.HandleLogin(w, r) } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } }) // Protected routes mux.Handle("/", authMiddleware.Authenticate(dashboardHandler)) mux.Handle("/dashboard", authMiddleware.Authenticate(dashboardHandler)) // Protected API routes mux.Handle("/api/projects", authMiddleware.Authenticate(http.HandlerFunc(projectHandler.HandleGetProjectMapping))) mux.Handle("/api/admin/api-keys", authMiddleware.Authenticate(http.HandlerFunc(adminHandler.HandleListAPIKeys))) mux.Handle("/api/admin/api-keys/delete", authMiddleware.Authenticate(http.HandlerFunc(adminHandler.HandleDeleteAPIKey))) mux.Handle("/api/logs", authMiddleware.Authenticate(http.HandlerFunc(logsHandler.HandleGetTriggerLogs))) mux.Handle("/api/health", authMiddleware.Authenticate(http.HandlerFunc(healthHandler.HandleHealth))) return &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Server.Port), Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } } func (app *application) startServer() { logger.Info("Server listening on %s", app.server.Addr) if err := app.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("HTTP server error: %v", err) os.Exit(1) } } func (app *application) handleShutdown() { stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop logger.Info("Shutting down server...") app.cleanup() logger.Info("Server shutdown complete") } func (app *application) cleanup() { if app.workerPool != nil { app.workerPool.Release() } if app.db != nil { app.db.Close() } if app.watcher != nil { app.watcher.Stop() } }