package main import ( "context" "flag" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/api" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" "gitea.dooplex.hu/admin/felhom-controller/internal/web" ) var ( // Set at build time via ldflags Version = "dev" BuildTime = "unknown" GitCommit = "unknown" ) func main() { configPath := flag.String("config", "/opt/docker/felhom-controller/controller.yaml", "Path to configuration file") showVersion := flag.Bool("version", false, "Show version and exit") flag.Parse() if *showVersion { fmt.Printf("felhom-controller %s (built %s, commit %s)\n", Version, BuildTime, GitCommit) os.Exit(0) } // --- Load configuration --- cfg, err := config.Load(*configPath) if err != nil { log.Fatalf("[FATAL] Failed to load config from %s: %v", *configPath, err) } logger := setupLogger(cfg) logger.Printf("[INFO] felhom-controller %s starting (customer: %s, domain: %s)", Version, cfg.Customer.ID, cfg.Customer.Domain) // --- Initialize stack manager --- stackMgr, err := stacks.NewManager(cfg, logger) if err != nil { logger.Fatalf("[FATAL] Failed to initialize stack manager: %v", err) } // Initial stack scan if err := stackMgr.ScanStacks(); err != nil { logger.Printf("[WARN] Initial stack scan failed: %v", err) } // --- Initialize API router --- apiRouter := api.NewRouter(cfg, stackMgr, logger) // --- Initialize web server --- webServer := web.NewServer(cfg, stackMgr, logger, Version) // --- Build HTTP mux --- mux := http.NewServeMux() // API routes (no auth for health endpoint, auth for everything else) mux.HandleFunc("/api/health", apiRouter.HealthHandler) mux.Handle("/api/", webServer.RequireAuth(http.HandlerFunc(apiRouter.ServeHTTP))) // Web UI routes (auth required) mux.Handle("/", webServer.RequireAuth(http.HandlerFunc(webServer.ServeHTTP))) // --- Start HTTP server --- server := &http.Server{ Addr: cfg.Web.Listen, Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, } // --- Graceful shutdown --- ctx, cancel := context.WithCancel(context.Background()) defer cancel() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-sigCh logger.Printf("[INFO] Received signal %v, shutting down...", sig) cancel() shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second) defer shutdownCancel() if err := server.Shutdown(shutdownCtx); err != nil { logger.Printf("[ERROR] HTTP server shutdown error: %v", err) } }() // --- Start background tasks --- // Periodic stack status refresh go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: if err := stackMgr.RefreshStatus(); err != nil { logger.Printf("[WARN] Status refresh failed: %v", err) } } } }() logger.Printf("[INFO] Web UI listening on %s", cfg.Web.Listen) if err := server.ListenAndServe(); err != http.ErrServerClosed { logger.Fatalf("[FATAL] HTTP server error: %v", err) } logger.Println("[INFO] felhom-controller stopped") } func setupLogger(cfg *config.Config) *log.Logger { // For now, log to stdout. File logging will be added later. logger := log.New(os.Stdout, "", log.LstdFlags) if cfg.Logging.Level == "debug" { logger.SetFlags(log.LstdFlags | log.Lshortfile) } return logger }