core/cmd/twhelp/cmd_serve.go

244 lines
7.0 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"slices"
"time"
"gitea.dwysokinski.me/Kichiyaki/chiclientip"
"gitea.dwysokinski.me/twhelp/core/internal/adapter"
"gitea.dwysokinski.me/twhelp/core/internal/app"
"gitea.dwysokinski.me/twhelp/core/internal/chislog"
"gitea.dwysokinski.me/twhelp/core/internal/health"
"gitea.dwysokinski.me/twhelp/core/internal/port"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/realclientip/realclientip-go"
"github.com/urfave/cli/v2"
)
var (
apiServerFlagPort = &cli.UintFlag{
Name: "api.port",
EnvVars: []string{"API_PORT"},
Value: 9234, //nolint:gomnd
}
apiServerFlagHandlerTimeout = &cli.DurationFlag{
Name: "api.handlerTimeout",
EnvVars: []string{"API_HANDLER_TIMEOUT"},
Value: 5 * time.Second, //nolint:gomnd
Usage: "https://pkg.go.dev/net/http#TimeoutHandler",
}
apiServerFlagReadTimeout = &cli.DurationFlag{
Name: "api.readTimeout",
EnvVars: []string{"API_READ_TIMEOUT"},
Value: 5 * time.Second, //nolint:gomnd
}
apiServerFlagReadHeaderTimeout = &cli.DurationFlag{
Name: "api.readHeaderTimeout",
EnvVars: []string{"API_READ_HEADER_TIMEOUT"},
Value: time.Second,
}
apiServerFlagWriteTimeout = &cli.DurationFlag{
Name: "api.writeTimeout",
EnvVars: []string{"API_WRITE_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd
}
apiServerFlagIdleTimeout = &cli.DurationFlag{
Name: "api.idleTimeout",
EnvVars: []string{"API_IDLE_TIMEOUT"},
Value: 180 * time.Second, //nolint:gomnd
}
apiServerFlagShutdownTimeout = &cli.DurationFlag{
Name: "api.shutdownTimeout",
EnvVars: []string{"API_SHUTDOWN_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd
}
apiServerFlagOpenAPIEnabled = &cli.BoolFlag{
Name: "api.openApi.enabled",
EnvVars: []string{"API_OPENAPI_ENABLED"},
Value: true,
}
apiServerFlagOpenAPISwaggerEnabled = &cli.BoolFlag{
Name: "api.openApi.swaggerEnabled",
EnvVars: []string{"API_OPENAPI_SWAGGER_ENABLED"},
Value: true,
}
apiServerFlagOpenAPIServers = &cli.StringSliceFlag{
Name: "api.openApi.servers",
EnvVars: []string{"API_OPENAPI_SERVERS"},
}
apiServerFlags = []cli.Flag{
apiServerFlagPort,
apiServerFlagHandlerTimeout,
apiServerFlagReadTimeout,
apiServerFlagReadHeaderTimeout,
apiServerFlagWriteTimeout,
apiServerFlagIdleTimeout,
apiServerFlagOpenAPIEnabled,
apiServerFlagOpenAPISwaggerEnabled,
apiServerFlagOpenAPIServers,
}
)
const (
apiBasePath = "/api"
metaBasePath = "/_meta"
)
var cmdServe = &cli.Command{
Name: "serve",
Usage: "Run the HTTP server",
Flags: slices.Concat(apiServerFlags, dbFlags),
Action: func(c *cli.Context) error {
logger := loggerFromCtx(c.Context)
// deps
bunDB, err := newBunDBFromFlags(c)
if err != nil {
return err
}
defer closeBunDB(bunDB, logger)
// adapters
versionRepo := adapter.NewVersionBunRepository(bunDB)
serverRepo := adapter.NewServerBunRepository(bunDB)
tribeRepo := adapter.NewTribeBunRepository(bunDB)
playerRepo := adapter.NewPlayerBunRepository(bunDB)
villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB)
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB)
// services
versionSvc := app.NewVersionService(versionRepo)
serverSvc := app.NewServerService(serverRepo, nil, nil)
tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
tribeChangeSvc := app.NewTribeChangeService(tribeChangeRepo)
playerSvc := app.NewPlayerService(playerRepo, tribeChangeSvc, nil, nil)
villageSvc := app.NewVillageService(villageRepo, nil, nil)
ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil)
tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, nil)
playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, nil)
// health
h := health.New()
server, err := newHTTPServer(
httpServerConfig{
port: c.Uint(apiServerFlagPort.Name),
readTimeout: c.Duration(apiServerFlagReadTimeout.Name),
readHeaderTimeout: c.Duration(apiServerFlagReadHeaderTimeout.Name),
writeTimeout: c.Duration(apiServerFlagWriteTimeout.Name),
idleTimeout: c.Duration(apiServerFlagIdleTimeout.Name),
},
func(r chi.Router) error {
oapiCfg, oapiCfgErr := newOpenAPIConfigFromFlags(c)
if oapiCfgErr != nil {
return oapiCfgErr
}
r.Group(func(r chi.Router) {
r.Use(middleware.Recoverer)
r.Mount(metaBasePath, port.NewMetaHTTPHandler(h))
})
r.Group(func(r chi.Router) {
r.Use(newAPIMiddlewares(c, logger)...)
r.Mount(apiBasePath, port.NewAPIHTTPHandler(
versionSvc,
serverSvc,
tribeSvc,
playerSvc,
villageSvc,
ennoblementSvc,
tribeChangeSvc,
tribeSnapshotSvc,
playerSnapshotSvc,
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)
ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerFlagShutdownTimeout.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(apiServerFlagOpenAPIServers.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(apiServerFlagOpenAPIEnabled.Name),
SwaggerEnabled: c.Bool(apiServerFlagOpenAPISwaggerEnabled.Name),
BasePath: apiBasePath,
Servers: servers,
}, nil
}
func newAPIMiddlewares(c *cli.Context, logger *slog.Logger) chi.Middlewares {
return chi.Middlewares{
chiclientip.ClientIP(
realclientip.NewChainStrategy(
realclientip.Must(realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For")),
realclientip.RemoteAddrStrategy{},
),
),
chislog.Logger(logger, chislog.WithIPExtractor(func(r *http.Request) string {
clientIP, _ := chiclientip.ClientIPFromContext(r.Context())
return clientIP
})),
middleware.Recoverer,
func(next http.Handler) http.Handler {
return http.TimeoutHandler(next, c.Duration(apiServerFlagHandlerTimeout.Name), "Timeout")
},
}
}