244 lines
7.0 KiB
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")
|
|
},
|
|
}
|
|
}
|