package app import ( "context" "fmt" "net/http" "sync" "time" "git.linuxforward.com/byop/byop-engine/auth" "git.linuxforward.com/byop/byop-engine/clients" "git.linuxforward.com/byop/byop-engine/config" "git.linuxforward.com/byop/byop-engine/dbstore" "git.linuxforward.com/byop/byop-engine/handlers" mw "git.linuxforward.com/byop/byop-engine/middleware" "git.linuxforward.com/byop/byop-engine/services" "github.com/gin-gonic/gin" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) type App struct { entry *logrus.Entry cnf *config.Config ctx context.Context cancelFunc context.CancelFunc rtr *gin.Engine // Database database *dbstore.SQLiteStore // Services authService auth.Service tokenStore auth.TokenStore // Clients buildkitClient clients.BuildMachineClient // BuildKit client for build operations registryClient clients.RegistryClient // Docker registry client for pushing images // Handlers authHandler *handlers.AuthHandler userHandler *handlers.UserHandler clientHandler *handlers.ClientHandler componentHandler *handlers.ComponentHandler // Renamed from appHandler appHandler *handlers.AppsHandler // Renamed from templateHandler deploymentHandler *handlers.DeploymentHandler previewHandler *handlers.PreviewHandler // Preview Service previewService services.PreviewService builderService *services.Builder // Resource Handlers providerHandler *handlers.ProviderHandler // ticketHandler *handlers.TicketHandler stopped bool wg sync.WaitGroup // monitoringHandler *handlers.MonitoringHandler } func NewApp(cnf *config.Config) (*App, error) { ctx, cancelFunc := context.WithCancel(context.Background()) app := &App{ entry: logrus.WithField("component", "app"), cnf: cnf, ctx: ctx, cancelFunc: cancelFunc, } // Initialize router first if cnf.Debug { // gin.SetMode(gin.ReleaseMode) // gin.SetMode(gin.DebugMode) logrus.SetLevel(logrus.DebugLevel) } else { // Set gin to release mode for production // This will disable debug logs and enable performance optimizations gin.SetMode(gin.ReleaseMode) } app.rtr = gin.New() // Disable automatic redirection of trailing slashes // This prevents 301 redirects that can cause CORS issues app.rtr.RedirectTrailingSlash = false app.rtr.RedirectFixedPath = false app.rtr.Use(gin.Recovery()) // app.rtr.Use(mw.Logger) // Add CORS middleware to handle cross-origin requests app.rtr.Use(mw.CORS()) // Initialize clients app.buildkitClient = clients.NewDockerfileBuilder(cnf.BuildkitHost) app.entry.Info("Dockerfile builder client initialized") app.registryClient = clients.NewSimpleRegistryClient(app.buildkitClient) app.entry.Info("Registry client initialized") // Initialize services and handlers if err := app.initCommonServices(); err != nil { return nil, errors.Wrap(err, "initialize services") } if err := app.initHandlers(); err != nil { return nil, errors.Wrap(err, "initialize handlers") } // Set up routes after all handlers are initialized app.setupRoutes() return app, nil } // Shutdown gracefully shuts down the application and its resources. func (a *App) Shutdown(ctx context.Context) error { a.entry.Info("Initiating graceful shutdown...") // Signal all goroutines to stop a.cancelFunc() // This will close a.ctx // Close services if a.previewService != nil { a.entry.Info("Closing preview service...") a.previewService.Close(ctx) // Assuming Close() is idempotent or handles multiple calls gracefully a.entry.Info("Preview service closed.") } if a.tokenStore != nil { if closer, ok := a.tokenStore.(interface{ Close() error }); ok { a.entry.Info("Closing token store...") if err := closer.Close(); err != nil { a.entry.WithError(err).Error("Failed to close token store") // Potentially return this error or aggregate errors } else { a.entry.Info("Token store closed.") } } } // Close database connection if a.database != nil { a.entry.Info("Closing database connection...") if err := a.database.Close(); err != nil { a.entry.WithError(err).Error("Failed to close database connection") // Potentially return this error or aggregate errors } else { a.entry.Info("Database connection closed.") } } // Wait for any other background goroutines managed by the app to finish // a.wg.Wait() // Uncomment if you use sync.WaitGroup for other app-managed goroutines a.entry.Info("Graceful shutdown completed.") return nil } func (a *App) Run() error { // The main HTTP server instance for the application. // This will be shut down by the logic in main.go. srv := &http.Server{ Addr: fmt.Sprintf(":%d", a.cnf.Server.Port), Handler: a.rtr, } // This goroutine will block until the server is shut down or an error occurs. // The actual shutdown signal is handled in main.go. go func() { select { case <-a.ctx.Done(): // Listen for the app context cancellation a.entry.Info("App context cancelled, initiating server shutdown from Run()...") // Context for server shutdown, can be different from app context if needed shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 15*time.Second) defer cancelShutdown() if err := srv.Shutdown(shutdownCtx); err != nil { a.entry.WithError(err).Error("HTTP server shutdown error in Run() after context cancellation") } return } }() a.entry.WithField("address", srv.Addr).Infof("Starting server on port %d", a.cnf.Server.Port) var err error if a.cnf.Server.Tls.Enabled { a.entry.Info("Starting server with TLS...") err = srv.ListenAndServeTLS(a.cnf.Server.Tls.CertFile, a.cnf.Server.Tls.KeyFile) } else { a.entry.Info("Starting server without TLS...") err = srv.ListenAndServe() } if err != nil && err != http.ErrServerClosed { a.entry.WithError(err).Errorf("Server ListenAndServe error") return fmt.Errorf("failed to start server: %w", err) } a.entry.Info("Server Run() method finished.") return nil // http.ErrServerClosed is a normal error on shutdown }