Files
goplt/services/gateway/gateway.go
0x1d 988adf6cc5
Some checks failed
CI / Test (pull_request) Failing after 50s
CI / Lint (pull_request) Failing after 32s
CI / Build (pull_request) Successful in 17s
CI / Format Check (pull_request) Failing after 2s
Fix gRPC health checks and add API Gateway Consul registration
- Fix gRPC health checks: Set serving status for default service (empty string) in all services
  - Consul checks the default service by default, not specific service names
  - All services now set both default and specific service status to SERVING

- Update Consul registration logic to automatically detect HTTP vs gRPC services
  - HTTP services (API Gateway) use HTTP health checks
  - gRPC services use gRPC health checks
  - Detection based on service tags and metadata

- Add API Gateway Consul registration
  - Register with Docker service name in Docker environment
  - Use HTTP health checks for API Gateway
  - Proper host/port configuration handling

- Add API Gateway HTTP-to-gRPC handlers
  - Implement service-specific handlers for Auth and Identity services
  - Translate HTTP requests to gRPC calls
  - Map gRPC error codes to HTTP status codes
2025-11-06 22:04:55 +01:00

212 lines
5.3 KiB
Go

// Package gateway provides API Gateway implementation.
package gateway
import (
"net/http"
"strings"
"git.dcentral.systems/toolz/goplt/internal/client"
"git.dcentral.systems/toolz/goplt/pkg/config"
"git.dcentral.systems/toolz/goplt/pkg/logger"
"git.dcentral.systems/toolz/goplt/pkg/registry"
"github.com/gin-gonic/gin"
)
// Gateway handles routing requests to backend services.
type Gateway struct {
config config.ConfigProvider
log logger.Logger
clientFactory *client.ServiceClientFactory
registry registry.ServiceRegistry
routes []RouteConfig
}
// RouteConfig defines a route configuration.
type RouteConfig struct {
Path string
Service string
AuthRequired bool
}
// NewGateway creates a new API Gateway instance.
func NewGateway(
cfg config.ConfigProvider,
log logger.Logger,
clientFactory *client.ServiceClientFactory,
reg registry.ServiceRegistry,
) (*Gateway, error) {
// Load route configurations
routes := loadRoutes(cfg)
return &Gateway{
config: cfg,
log: log,
clientFactory: clientFactory,
registry: reg,
routes: routes,
}, nil
}
// SetupRoutes configures routes on the Gin router.
func (g *Gateway) SetupRoutes(router *gin.Engine) {
// Register routes with wildcard support
// Gin uses /*path for wildcards, so we convert /** to /*path
for _, route := range g.routes {
route := route // Capture for closure
// Convert /** wildcard to Gin's /*path format
ginPath := route.Path
hasWildcard := strings.HasSuffix(ginPath, "/**")
if hasWildcard {
ginPath = strings.TrimSuffix(ginPath, "/**") + "/*path"
}
// Register all HTTP methods for this route
router.Any(ginPath, func(c *gin.Context) {
// Extract the remaining path
var remainingPath string
if hasWildcard {
// Extract from Gin's path parameter
pathParam := c.Param("path")
if pathParam != "" {
remainingPath = "/" + pathParam
} else {
remainingPath = "/"
}
} else {
// Exact match - no remaining path
remainingPath = "/"
}
// Route to appropriate service handler
switch route.Service {
case "auth-service":
g.handleAuthService(c, route, remainingPath)
case "identity-service":
g.handleIdentityService(c, route, remainingPath)
default:
g.log.Warn("Unknown service",
logger.String("service", route.Service),
)
c.JSON(http.StatusNotFound, gin.H{
"error": "Service not found",
})
}
})
}
// Default handler for unmatched routes
router.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": "Route not found",
"path": c.Request.URL.Path,
})
})
}
// matchRoute finds the matching route configuration for a given path.
func (g *Gateway) matchRoute(path string) *RouteConfig {
for _, route := range g.routes {
if g.pathMatches(path, route.Path) {
return &route
}
}
return nil
}
// pathMatches checks if a request path matches a route pattern.
// Supports wildcard matching: "/api/v1/auth/**" matches "/api/v1/auth/login", etc.
func (g *Gateway) pathMatches(requestPath, routePath string) bool {
// Remove trailing slashes for comparison
requestPath = strings.TrimSuffix(requestPath, "/")
routePath = strings.TrimSuffix(routePath, "/")
// Exact match
if requestPath == routePath {
return true
}
// Wildcard match: routePath ends with "/**"
if strings.HasSuffix(routePath, "/**") {
prefix := strings.TrimSuffix(routePath, "/**")
return strings.HasPrefix(requestPath, prefix+"/") || requestPath == prefix
}
return false
}
// extractRemainingPath extracts the path segment after the route prefix.
// Example: path="/api/v1/auth/login", route="/api/v1/auth/**" -> returns "/login"
func (g *Gateway) extractRemainingPath(requestPath, routePath string) string {
// Remove trailing slashes
requestPath = strings.TrimSuffix(requestPath, "/")
routePath = strings.TrimSuffix(routePath, "/")
// Handle wildcard routes
if strings.HasSuffix(routePath, "/**") {
prefix := strings.TrimSuffix(routePath, "/**")
if strings.HasPrefix(requestPath, prefix) {
remaining := strings.TrimPrefix(requestPath, prefix)
if remaining == "" {
return "/"
}
return remaining
}
}
// Exact match - no remaining path
if requestPath == routePath {
return "/"
}
return ""
}
// loadRoutes loads route configurations from config.
func loadRoutes(cfg config.ConfigProvider) []RouteConfig {
if cfg == nil {
return []RouteConfig{}
}
// Get routes from config
routesInterface := cfg.Get("gateway.routes")
if routesInterface == nil {
return []RouteConfig{}
}
// Convert to slice of RouteConfig
routesSlice, ok := routesInterface.([]interface{})
if !ok {
return []RouteConfig{}
}
routes := make([]RouteConfig, 0, len(routesSlice))
for _, routeInterface := range routesSlice {
routeMap, ok := routeInterface.(map[string]interface{})
if !ok {
continue
}
route := RouteConfig{}
if path, ok := routeMap["path"].(string); ok {
route.Path = path
}
if service, ok := routeMap["service"].(string); ok {
route.Service = service
}
if authRequired, ok := routeMap["auth_required"].(bool); ok {
route.AuthRequired = authRequired
}
// Only add route if it has required fields
if route.Path != "" && route.Service != "" {
routes = append(routes, route)
}
}
return routes
}