package server import ( "context" "errors" "net/http" "os" "time" ac "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/api" sharedruntime "git.sendico.io/tech/sxchnge/bff/edge/shared/internal/runtime" "git.sendico.io/tech/sxchnge/bff/pkg/db" "git.sendico.io/tech/sxchnge/bff/pkg/merrors" "git.sendico.io/tech/sxchnge/bff/pkg/mlogger" "git.sendico.io/tech/sxchnge/bff/pkg/mservice" mduration "git.sendico.io/tech/sxchnge/bff/pkg/mutil/duration" "github.com/go-chi/chi/v5" "go.uber.org/zap" "gopkg.in/yaml.v3" ) type httpServerConf struct { ListenAddress string `yaml:"listen_address"` ReadHeaderTimeout int `yaml:"read_header_timeout"` ShutdownTimeout int `yaml:"shutdown_timeout"` } // Config represents the server configuration type Config struct { API ac.ConfigProvider DB *db.Config `yaml:"database"` HTTPServer *httpServerConf `yaml:"http_server"` } // Instance represents an instance of the server type Imp struct { lifecycleCtx context.Context lifecycleCancel context.CancelFunc logger mlogger.Logger api mservice.MicroService config *Config db db.Factory httpServer *http.Server runtime ac.RuntimeConfig debug bool file string } // Shutdown stops the server func (i *Imp) Shutdown() { if i.lifecycleCancel != nil { i.lifecycleCancel() } if i.httpServer == nil { return } // Shutdown HTTP server ctx, cancel := context.WithTimeout(context.Background(), mduration.Param2Duration(i.config.HTTPServer.ShutdownTimeout, time.Second)) i.logger.Info("Shutting HTTP server down...") if err := i.httpServer.Shutdown(ctx); err != nil { if !errors.Is(err, http.ErrServerClosed) { i.logger.Warn("Failed to shutdown HTTP server gracefully", zap.Error(err)) cancel() os.Exit(1) } } cancel() } func (i *Imp) Run() error { err := i.httpServer.ListenAndServe() if errors.Is(err, http.ErrServerClosed) { err = nil } i.logger.Info("HTTP Server stopped") if err := i.api.Finish(context.Background()); err != nil { i.logger.Warn("Error when finishing service", zap.Error(err)) } i.db.CloseConnection() if err != nil { return merrors.OpaqueWrap(merrors.ErrInternal, err, "HTTP server stopped unexpectedly") } return nil } // Start starts the server func (i *Imp) Start() error { if i.lifecycleCancel != nil { defer i.lifecycleCancel() } i.logger.Info("Starting...", zap.String("config_file", i.file), zap.Bool("debug_mode", i.debug)) // Load configuration file data, err := os.ReadFile(i.file) if err != nil { i.logger.Error("Could not load configuration", zap.Error(err), zap.String("config_file", i.file)) return err } if err = i.decodeConfig(data); err != nil { i.logger.Error("Failed to parse configuration", zap.Error(err)) return err } if i.db, err = db.NewConnection(i.logger, i.config.DB); err != nil { i.logger.Error("Could not open database connection", zap.Error(err)) return err } rootRouter := chi.NewRouter() apiRouter := chi.NewRouter() if i.api, err = sharedruntime.CreateAPI(i.lifecycleCtx, i.logger, i.config.API.SharedConfig(), i.config.API, i.db, rootRouter, apiRouter, i.debug, i.runtime); err != nil { i.db.CloseConnection() return err } rootRouter.Mount("/", apiRouter) // Startup the HTTP Server in a way that we can gracefully shut it down again i.httpServer = &http.Server{ Addr: i.config.HTTPServer.ListenAddress, Handler: rootRouter, ReadHeaderTimeout: mduration.Param2Duration(i.config.HTTPServer.ReadHeaderTimeout, time.Second), } return i.Run() } func (i *Imp) decodeConfig(data []byte) error { var raw struct { API yaml.Node `yaml:"api"` DB *db.Config `yaml:"database"` HTTPServer *httpServerConf `yaml:"http_server"` } if err := yaml.Unmarshal(data, &raw); err != nil { return err } apiConfig := i.runtime.NewConfig() if apiConfig == nil { return merrors.Internal("runtime config factory returned nil api config") } if raw.API.Kind == 0 { return merrors.InvalidArgument("api configuration is required", "api") } if err := raw.API.Decode(apiConfig); err != nil { return err } if raw.DB == nil || raw.HTTPServer == nil || apiConfig.SharedConfig() == nil || apiConfig.SharedConfig().Mw == nil { return merrors.InvalidArgument("database, http_server and api.middleware configuration are required") } if raw.HTTPServer.ListenAddress == "" || raw.HTTPServer.ReadHeaderTimeout <= 0 || raw.HTTPServer.ShutdownTimeout <= 0 { return merrors.InvalidArgument("HTTP address and positive timeouts are required") } i.config = &Config{ API: apiConfig, DB: raw.DB, HTTPServer: raw.HTTPServer, } return nil } func Create(logger mlogger.Logger, file string, debug bool, runtime ac.RuntimeConfig) (*Imp, error) { lifecycleCtx, lifecycleCancel := context.WithCancel(context.Background()) srv := &Imp{ lifecycleCtx: lifecycleCtx, lifecycleCancel: lifecycleCancel, logger: logger, debug: debug, file: file, runtime: runtime, } return srv, nil }