You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
198 lines
5.5 KiB
Go
198 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
|
redisv9 "github.com/redis/go-redis/v9"
|
|
|
|
gconfig "knowfoolery/backend/services/gateway-service/internal/infra/config"
|
|
"knowfoolery/backend/services/gateway-service/internal/infra/proxy"
|
|
httpapi "knowfoolery/backend/services/gateway-service/internal/interfaces/http"
|
|
"knowfoolery/backend/services/gateway-service/internal/interfaces/http/middleware"
|
|
"knowfoolery/backend/shared/infra/auth/zitadel"
|
|
"knowfoolery/backend/shared/infra/observability/logging"
|
|
sharedmetrics "knowfoolery/backend/shared/infra/observability/metrics"
|
|
"knowfoolery/backend/shared/infra/observability/tracing"
|
|
"knowfoolery/backend/shared/infra/utils/serviceboot"
|
|
)
|
|
|
|
func main() {
|
|
cfg := gconfig.FromEnv()
|
|
|
|
logger := logging.NewLogger(cfg.Logging)
|
|
if err := cfg.Upstreams.Validate(); err != nil {
|
|
logger.WithError(err).Fatal("invalid gateway upstream configuration")
|
|
}
|
|
|
|
sharedmetrics.NewMetrics(cfg.Metrics)
|
|
|
|
tracer, err := tracing.NewTracer(cfg.Tracing)
|
|
if err != nil {
|
|
logger.Fatal("failed to initialize tracer")
|
|
}
|
|
defer func() {
|
|
_ = tracer.Shutdown(context.Background())
|
|
}()
|
|
|
|
redisClient := initRedis(cfg, logger)
|
|
if redisClient != nil {
|
|
defer func() { _ = redisClient.Close() }()
|
|
}
|
|
|
|
bootCfg := serviceboot.Config{
|
|
AppName: cfg.AppName,
|
|
ServiceSlug: "gateway",
|
|
PortEnv: "GATEWAY_INTERNAL_PORT",
|
|
DefaultPort: cfg.Port,
|
|
}
|
|
app := serviceboot.NewFiberApp(bootCfg)
|
|
serviceboot.RegisterHealth(app, bootCfg.ServiceSlug)
|
|
serviceboot.RegisterReadiness(app, 2*time.Second, readinessChecks(cfg, redisClient)...)
|
|
app.Get("/metrics", adaptor.HTTPHandler(sharedmetrics.Handler()))
|
|
|
|
app.Use(middleware.RequestContext(logger))
|
|
app.Use(middleware.SecurityHeaders(cfg.Security))
|
|
app.Use(middleware.CORS(cfg.CORS))
|
|
|
|
authMiddleware := buildAuthMiddleware(cfg)
|
|
rateLimitMiddleware := middleware.RateLimitMiddleware(redisClient, cfg.Rate, cfg.PublicPrefix, logger)
|
|
|
|
httpapi.RegisterRoutes(app, httpapi.Options{
|
|
PublicPrefix: cfg.PublicPrefix,
|
|
Upstreams: cfg.Upstreams,
|
|
Proxy: proxy.New(cfg.UpstreamTimeout, logger),
|
|
AuthMiddleware: authMiddleware,
|
|
RateLimitMiddleware: rateLimitMiddleware,
|
|
})
|
|
|
|
addr := serviceboot.ListenAddress(bootCfg.PortEnv, bootCfg.DefaultPort)
|
|
log.Fatal(serviceboot.Run(app, addr))
|
|
}
|
|
|
|
func initRedis(cfg gconfig.Config, logger *logging.Logger) *redisv9.Client {
|
|
opt := &redisv9.Options{
|
|
Addr: cfg.Redis.Addr(),
|
|
Password: cfg.Redis.Password,
|
|
DB: cfg.Redis.DB,
|
|
PoolSize: cfg.Redis.PoolSize,
|
|
MinIdleConns: cfg.Redis.MinIdleConns,
|
|
DialTimeout: cfg.Redis.DialTimeout,
|
|
ReadTimeout: cfg.Redis.ReadTimeout,
|
|
WriteTimeout: cfg.Redis.WriteTimeout,
|
|
}
|
|
|
|
client := redisv9.NewClient(opt)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
logger.WithError(err).Warn("redis unavailable; rate limiting running in degraded fail-open mode")
|
|
_ = client.Close()
|
|
return nil
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
func readinessChecks(cfg gconfig.Config, redisClient *redisv9.Client) []serviceboot.ReadyCheck {
|
|
checks := []serviceboot.ReadyCheck{
|
|
{
|
|
Name: "redis",
|
|
Required: false,
|
|
Probe: func(ctx context.Context) error {
|
|
if redisClient == nil {
|
|
return fmt.Errorf("redis unavailable")
|
|
}
|
|
return redisClient.Ping(ctx).Err()
|
|
},
|
|
},
|
|
{
|
|
Name: "game-session",
|
|
Required: false,
|
|
Probe: makeHealthProbe(cfg.Upstreams.GameSession, cfg.UpstreamTimeout),
|
|
},
|
|
{
|
|
Name: "question-bank",
|
|
Required: false,
|
|
Probe: makeHealthProbe(cfg.Upstreams.QuestionBank, cfg.UpstreamTimeout),
|
|
},
|
|
{
|
|
Name: "user",
|
|
Required: false,
|
|
Probe: makeHealthProbe(cfg.Upstreams.User, cfg.UpstreamTimeout),
|
|
},
|
|
{
|
|
Name: "leaderboard",
|
|
Required: false,
|
|
Probe: makeHealthProbe(cfg.Upstreams.Leaderboard, cfg.UpstreamTimeout),
|
|
},
|
|
{
|
|
Name: "admin",
|
|
Required: false,
|
|
Probe: makeHealthProbe(cfg.Upstreams.Admin, cfg.UpstreamTimeout),
|
|
},
|
|
}
|
|
return checks
|
|
}
|
|
|
|
func makeHealthProbe(baseURL string, timeout time.Duration) func(ctx context.Context) error {
|
|
if timeout <= 0 {
|
|
timeout = 2 * time.Second
|
|
}
|
|
|
|
return func(ctx context.Context) error {
|
|
healthURL := strings.TrimRight(baseURL, "/") + "/health"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := &http.Client{Timeout: timeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Errorf("upstream returned %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func buildAuthMiddleware(cfg gconfig.Config) fiber.Handler {
|
|
skipPaths := []string{
|
|
"/health",
|
|
"/ready",
|
|
"/metrics",
|
|
cfg.PublicPrefix + "/questions",
|
|
cfg.PublicPrefix + "/leaderboard/top10",
|
|
cfg.PublicPrefix + "/leaderboard/stats",
|
|
cfg.PublicPrefix + "/admin/auth",
|
|
cfg.PublicPrefix + "/users/register",
|
|
cfg.PublicPrefix + "/users/verify-email",
|
|
}
|
|
|
|
return zitadel.BuildJWTMiddleware(zitadel.MiddlewareFactoryConfig{
|
|
BaseURL: cfg.ZitadelBaseURL,
|
|
ClientID: cfg.ZitadelClientID,
|
|
ClientSecret: cfg.ZitadelSecret,
|
|
Issuer: cfg.ZitadelIssuer,
|
|
Audience: cfg.ZitadelAudience,
|
|
RequiredClaims: []string{
|
|
"sub",
|
|
},
|
|
AdminEndpoints: []string{cfg.PublicPrefix + "/admin"},
|
|
SkipPaths: skipPaths,
|
|
Timeout: 10 * time.Second,
|
|
})
|
|
}
|