package runtime import ( "context" "errors" "fmt" "log/slog" "net/http" "time" "github.com/go-chi/chi/v5" chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/example/remote-access-platform/backend/internal/modules/auth" "github.com/example/remote-access-platform/backend/internal/modules/cluster" "github.com/example/remote-access-platform/backend/internal/modules/identitysource" "github.com/example/remote-access-platform/backend/internal/modules/node" "github.com/example/remote-access-platform/backend/internal/modules/nodeagent" "github.com/example/remote-access-platform/backend/internal/modules/organization" "github.com/example/remote-access-platform/backend/internal/modules/resource" "github.com/example/remote-access-platform/backend/internal/modules/sessionbroker" "github.com/example/remote-access-platform/backend/internal/modules/sessiongateway" "github.com/example/remote-access-platform/backend/internal/modules/worker" "github.com/example/remote-access-platform/backend/internal/platform/authority" "github.com/example/remote-access-platform/backend/internal/platform/config" "github.com/example/remote-access-platform/backend/internal/platform/httpserver" "github.com/example/remote-access-platform/backend/internal/platform/logging" "github.com/example/remote-access-platform/backend/internal/platform/module" postgresplatform "github.com/example/remote-access-platform/backend/internal/platform/postgres" redisplatform "github.com/example/remote-access-platform/backend/internal/platform/redis" "github.com/example/remote-access-platform/backend/internal/platform/secrets" ) type App struct { cfg config.Config logger *slog.Logger httpServer *http.Server workers []backgroundRunner db closeFunc redis closeFunc } type closeFunc func() error type backgroundRunner func(context.Context) error func NewApp(ctx context.Context) (*App, error) { cfg, err := config.Load() if err != nil { return nil, fmt.Errorf("load config: %w", err) } logger := logging.New(cfg.App.Env) db, err := postgresplatform.Open(ctx, cfg.Postgres) if err != nil { return nil, err } redisClient, err := redisplatform.Open(ctx, cfg.Redis) if err != nil { db.Close() return nil, err } authorityVerifier, err := authority.NewVerifier(cfg.Installation) if err != nil { redisClient.Close() db.Close() return nil, fmt.Errorf("create installation authority verifier: %w", err) } deps := module.Dependencies{ Config: module.Config{ App: cfg.App, Auth: cfg.Auth, Installation: cfg.Installation, DataPlane: cfg.DataPlane, Secret: cfg.Secret, Session: cfg.Session, Worker: cfg.Worker, WebSocket: cfg.WebSocket, }, Infra: module.Infra{ Logger: logger, DB: db, Redis: redisClient, }, } workerStore := worker.NewRedisStore(redisClient) workerService := worker.NewService(deps, workerStore) authStore := auth.NewPostgresStore(db) authTx := auth.NewPostgresTransactor(db) authService := auth.NewService(deps, authStore, authTx, authorityVerifier) var resourceSecretStore *secrets.ResourceSecretStore if cfg.Secret.EncryptionKeyBase64 != "" { secretEncryptor, err := secrets.NewEncryptor(cfg.Secret.EncryptionKeyBase64, cfg.Secret.EncryptionKeyID) if err != nil { redisClient.Close() db.Close() return nil, fmt.Errorf("create resource secret encryptor: %w", err) } resourceSecretStore = secrets.NewResourceSecretStore(db, secretEncryptor) } brokerStore := sessionbroker.NewPostgresStore(db, authorityVerifier) brokerTx := sessionbroker.NewPostgresTransactor(db, authorityVerifier) liveStateStore := sessionbroker.NewRedisLiveStateStore(redisClient) brokerService := sessionbroker.NewService(deps, brokerStore, brokerTx, liveStateStore, workerService, resourceSecretStore) workerEvents := worker.NewEventProcessor(redisClient, brokerService) leaseMonitor := worker.NewLeaseMonitor(workerService, brokerService, cfg.Worker.StaleLeaseGracePeriod) brokerModule := sessionbroker.NewModule(brokerService) authModule := auth.NewModule(deps, authService) clusterModule := cluster.NewModule(deps, authorityVerifier) organizationModule := organization.NewModule(deps) identitySourceModule := identitysource.NewModule(deps) resourceModule := resource.NewModule(deps, resourceSecretStore) nodeModule := node.NewModule(deps) nodeAgentModule := nodeagent.NewModule(deps) sessionGatewayModule := sessiongateway.NewModule(deps, brokerModule.Service(), workerService) router := buildRouter( logger, authModule, clusterModule, organizationModule, identitySourceModule, resourceModule, brokerModule, nodeModule, nodeAgentModule, sessionGatewayModule, ) return &App{ cfg: cfg, logger: logger, httpServer: httpserver.New(cfg.HTTP, router), workers: []backgroundRunner{workerEvents.Run, leaseMonitor.Run}, db: func() error { db.Close() return nil }, redis: redisClient.Close, }, nil } func (a *App) Run(ctx context.Context) error { errCh := make(chan error, 1) go func() { a.logger.Info("http server starting", "addr", a.httpServer.Addr, "service", a.cfg.App.Name) if err := a.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err return } errCh <- nil }() for _, runner := range a.workers { runner := runner go func() { if err := runner(ctx); err != nil { errCh <- err } }() } select { case <-ctx.Done(): a.logger.Info("shutdown signal received") case err := <-errCh: if err != nil { return err } return nil } shutdownCtx, cancel := context.WithTimeout(context.Background(), a.cfg.HTTP.ShutdownTimeout) defer cancel() if err := a.httpServer.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("shutdown http server: %w", err) } if err := a.redis(); err != nil { return fmt.Errorf("close redis: %w", err) } if err := a.db(); err != nil { return fmt.Errorf("close postgres: %w", err) } a.logger.Info("app stopped", "at", time.Now().UTC()) return nil } func buildRouter(logger *slog.Logger, modules ...module.Module) http.Handler { router := chi.NewRouter() router.Use(chimiddleware.RequestID) router.Use(chimiddleware.RealIP) router.Use(chimiddleware.Recoverer) router.Use(chimiddleware.Timeout(60 * time.Second)) router.Use(chimiddleware.Heartbeat("/healthz")) router.Get("/readyz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ready")) }) router.Route("/api/v1", func(r chi.Router) { for _, mod := range modules { logger.Info("register module routes", "module", mod.Name()) mod.RegisterRoutes(r) } }) return router }