246 lines
7.0 KiB
Go
246 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 (
|
|
apiServerPortFlag = &cli.UintFlag{
|
|
Name: "api.port",
|
|
EnvVars: []string{"API_PORT"},
|
|
Value: 9234, //nolint:gomnd
|
|
}
|
|
apiServerHandlerTimeoutFlag = &cli.DurationFlag{
|
|
Name: "api.handlerTimeout",
|
|
EnvVars: []string{"API_HANDLER_TIMEOUT"},
|
|
Value: 5 * time.Second, //nolint:gomnd
|
|
Usage: "https://pkg.go.dev/net/http#TimeoutHandler",
|
|
}
|
|
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,
|
|
apiServerHandlerTimeoutFlag,
|
|
apiServerReadTimeoutFlag,
|
|
apiServerReadHeaderTimeoutFlag,
|
|
apiServerWriteTimeoutFlag,
|
|
apiServerIdleTimeoutFlag,
|
|
apiServerOpenAPIEnabledFlag,
|
|
apiServerOpenAPISwaggerEnabledFlag,
|
|
apiServerOpenAPIServersFlag,
|
|
}
|
|
)
|
|
|
|
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(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.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.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(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(apiServerHandlerTimeoutFlag.Name), "Timeout")
|
|
},
|
|
}
|
|
}
|