fix(consul): fix gRPC health checks and add API Gateway Consul registration
This commit is contained in:
@@ -2,10 +2,8 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.dcentral.systems/toolz/goplt/internal/client"
|
||||
"git.dcentral.systems/toolz/goplt/pkg/config"
|
||||
@@ -51,10 +49,50 @@ func NewGateway(
|
||||
|
||||
// SetupRoutes configures routes on the Gin router.
|
||||
func (g *Gateway) SetupRoutes(router *gin.Engine) {
|
||||
// Setup route handlers
|
||||
// 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
|
||||
router.Any(route.Path, g.handleRoute(route))
|
||||
|
||||
// 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
|
||||
@@ -66,62 +104,108 @@ func (g *Gateway) SetupRoutes(router *gin.Engine) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleRoute returns a handler function for a route.
|
||||
func (g *Gateway) handleRoute(route RouteConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// TODO: Add authentication middleware if auth_required is true
|
||||
// TODO: Add rate limiting middleware
|
||||
// TODO: Add CORS middleware
|
||||
|
||||
// Discover service instances
|
||||
ctx := c.Request.Context()
|
||||
instances, err := g.registry.Discover(ctx, route.Service)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to discover service",
|
||||
logger.String("service", route.Service),
|
||||
logger.Error(err),
|
||||
)
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Service unavailable",
|
||||
})
|
||||
return
|
||||
// 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
|
||||
}
|
||||
|
||||
if len(instances) == 0 {
|
||||
g.log.Warn("No instances found for service",
|
||||
logger.String("service", route.Service),
|
||||
)
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Service unavailable",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Use first healthy instance (load balancing can be added later)
|
||||
instance := instances[0]
|
||||
targetURL := fmt.Sprintf("http://%s:%d", instance.Address, instance.Port)
|
||||
|
||||
// Create reverse proxy
|
||||
target, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to parse target URL",
|
||||
logger.String("url", targetURL),
|
||||
logger.Error(err),
|
||||
)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal server error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
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 {
|
||||
// For now, return empty routes - will be loaded from config in future
|
||||
// This is a placeholder implementation
|
||||
return []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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user