package main import ( "context" "errors" "fmt" "log/slog" "net/http" "net/url" "strings" "time" "gitea.dwysokinski.me/twhelp/corev3/internal/chislog" "gitea.dwysokinski.me/twhelp/corev3/internal/health" "gitea.dwysokinski.me/twhelp/corev3/internal/port" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/urfave/cli/v2" ) var ( apiServerPortFlag = &cli.UintFlag{ Name: "api.port", EnvVars: []string{"API_PORT"}, Value: 9234, //nolint:gomnd } apiServerReadTimeoutFlag = &cli.DurationFlag{ Name: "api.readTimeout", EnvVars: []string{"API_READ_TIMEOUT"}, Value: 5 * time.Second, //nolint:gomnd } apiServerReadHeaderTimeoutFlag = &cli.DurationFlag{ Name: "api.readHeaderTimeout", EnvVars: []string{"API_READ_HEADER_TIMEOUT"}, Value: time.Second, } apiServerWriteTimeoutFlag = &cli.DurationFlag{ Name: "api.writeTimeout", EnvVars: []string{"API_WRITE_TIMEOUT"}, Value: 10 * time.Second, //nolint:gomnd } apiServerIdleTimeoutFlag = &cli.DurationFlag{ Name: "api.idleTimeout", EnvVars: []string{"API_IDLE_TIMEOUT"}, Value: 180 * time.Second, //nolint:gomnd } apiServerShutdownTimeoutFlag = &cli.DurationFlag{ Name: "api.shutdownTimeout", EnvVars: []string{"API_SHUTDOWN_TIMEOUT"}, Value: 10 * time.Second, //nolint:gomnd } apiServerOpenAPIEnabledFlag = &cli.BoolFlag{ Name: "api.openApi.enabled", EnvVars: []string{"API_OPENAPI_ENABLED"}, Value: true, } apiServerOpenAPISwaggerEnabledFlag = &cli.BoolFlag{ Name: "api.openApi.swaggerEnabled", EnvVars: []string{"API_OPENAPI_SWAGGER_ENABLED"}, Value: true, } apiServerOpenAPIServersFlag = &cli.StringSliceFlag{ Name: "api.openApi.servers", EnvVars: []string{"API_OPENAPI_SERVERS"}, } apiServerFlags = []cli.Flag{ apiServerPortFlag, apiServerReadTimeoutFlag, apiServerReadHeaderTimeoutFlag, apiServerWriteTimeoutFlag, apiServerIdleTimeoutFlag, apiServerOpenAPIEnabledFlag, apiServerOpenAPISwaggerEnabledFlag, apiServerOpenAPIServersFlag, } ) const ( apiBasePath = "/api" metaBasePath = "/_meta" ) var cmdServe = &cli.Command{ Name: "serve", Usage: "Run the HTTP server", Flags: concatSlices(apiServerFlags, dbFlags), Action: func(c *cli.Context) error { logger := loggerFromCtx(c.Context) bunDB, err := newBunDBFromFlags(c) if err != nil { return err } defer func() { logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) if dbCloseErr := bunDB.Close(); dbCloseErr != nil { logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) } else { logger.Debug("db connections closed") } }() h := health.New() server, err := newHTTPServer( httpServerConfig{ port: c.Uint(apiServerPortFlag.Name), readTimeout: c.Duration(apiServerReadTimeoutFlag.Name), readHeaderTimeout: c.Duration(apiServerReadHeaderTimeoutFlag.Name), writeTimeout: c.Duration(apiServerWriteTimeoutFlag.Name), idleTimeout: c.Duration(apiServerIdleTimeoutFlag.Name), }, func(r chi.Router) error { oapiCfg, oapiCfgErr := newOpenAPIConfigFromFlags(c) if oapiCfgErr != nil { return oapiCfgErr } r.Use(newAPIMiddlewares(logger)...) r.Mount(metaBasePath, port.NewMetaHTTPHandler(h)) r.Mount(apiBasePath, port.NewAPIHTTPHandler(port.WithOpenAPIConfig(oapiCfg))) return nil }, ) if err != nil { return fmt.Errorf("couldn't construct HTTP server: %w", err) } idleConnsClosed := make(chan struct{}) go func() { waitForShutdownSignal(c.Context) logger.Debug("received shutdown signal") ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerShutdownTimeoutFlag.Name)) defer cancel() if err := server.Shutdown(ctx); err != nil { logger.Warn("shutdown failed", slog.Any("error", err)) } close(idleConnsClosed) }() logger.Info("Server is listening on the addr "+server.Addr, slog.String("addr", server.Addr)) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("ListenAndServe failed: %w", err) } <-idleConnsClosed return nil }, } func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) { rawURLs := c.StringSlice(apiServerOpenAPIServersFlag.Name) servers := make([]port.OpenAPIConfigServer, 0, len(rawURLs)) for _, raw := range rawURLs { u, err := url.Parse(raw) if err != nil { return port.OpenAPIConfig{}, err } servers = append(servers, port.OpenAPIConfigServer{ URL: u, }) } return port.OpenAPIConfig{ Enabled: c.Bool(apiServerOpenAPIEnabledFlag.Name), SwaggerEnabled: c.Bool(apiServerOpenAPISwaggerEnabledFlag.Name), BasePath: apiBasePath, Servers: servers, }, nil } func newAPIMiddlewares(logger *slog.Logger) chi.Middlewares { return chi.Middlewares{ chislog.Logger(logger, chislog.WithFilter(func(r *http.Request) bool { return !strings.HasPrefix(r.URL.Path, metaBasePath) })), middleware.Recoverer, } }