From 0f39ead97cbef8446448b5712e27a0c815fd442f Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Sat, 9 Nov 2024 10:59:17 +0100 Subject: [PATCH 1/2] feat: add routes loadblancing --- internal/handler.go | 2 +- internal/healthCheck.go | 2 +- internal/proxy.go | 15 ++++++++++++++- internal/route.go | 5 +++++ internal/types.go | 12 ++++++++++++ internal/var.go | 2 ++ 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/internal/handler.go b/internal/handler.go index 0fe9cdd..33fec61 100644 --- a/internal/handler.go +++ b/internal/handler.go @@ -73,7 +73,7 @@ func (heathRoute HealthCheckRoute) HealthCheckHandler(w http.ResponseWriter, r * go func() { defer wg.Done() if route.HealthCheck != "" { - err := HealthCheck(route.Destination + route.HealthCheck) + err := healthCheck(route.Destination + route.HealthCheck) if err != nil { if heathRoute.DisableRouteHealthCheckError { routes = append(routes, HealthCheckRouteResponse{Name: route.Name, Status: "unhealthy", Error: "Route healthcheck errors disabled"}) diff --git a/internal/healthCheck.go b/internal/healthCheck.go index 9d0ecd8..931f1a4 100644 --- a/internal/healthCheck.go +++ b/internal/healthCheck.go @@ -23,7 +23,7 @@ import ( "net/url" ) -func HealthCheck(healthURL string) error { +func healthCheck(healthURL string) error { healthCheckURL, err := url.Parse(healthURL) if err != nil { return fmt.Errorf("error parsing HealthCheck URL: %v ", err) diff --git a/internal/proxy.go b/internal/proxy.go index e114f25..92d82c3 100644 --- a/internal/proxy.go +++ b/internal/proxy.go @@ -23,6 +23,7 @@ import ( "net/url" "slices" "strings" + "sync/atomic" ) // ProxyHandler proxies requests to the backend @@ -76,8 +77,13 @@ func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc { r.Header.Set("X-Real-IP", getRealIP(r)) r.Host = targetURL.Host } + backendURL, _ := url.Parse(proxyRoute.destination) + if len(proxyRoute.backends) > 0 { + // Select the next backend URL + backendURL = getNextBackend(proxyRoute.backends) + } // Create proxy - proxy := httputil.NewSingleHostReverseProxy(targetURL) + proxy := httputil.NewSingleHostReverseProxy(backendURL) // Rewrite if proxyRoute.path != "" && proxyRoute.rewrite != "" { // Rewrite the path @@ -92,3 +98,10 @@ func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc { proxy.ServeHTTP(w, r) } } + +// getNextBackend selects the next backend in a round-robin fashion +func getNextBackend(backendURLs []string) *url.URL { + idx := atomic.AddUint32(&counter, 1) % uint32(len(backendURLs)) + backendURL, _ := url.Parse(backendURLs[idx]) + return backendURL +} diff --git a/internal/route.go b/internal/route.go index ecfdfac..3f227f3 100644 --- a/internal/route.go +++ b/internal/route.go @@ -53,7 +53,10 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { } for _, route := range gateway.Routes { if route.Path != "" { + if route.Destination == "" && len(route.Backends) == 0 { + logger.Fatal("Route %s : destination or backends should not be empty", route.Name) + } // Apply middlewares to route for _, mid := range route.Middlewares { if mid != "" { @@ -84,6 +87,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { path: route.Path, rewrite: route.Rewrite, destination: route.Destination, + backends: route.Backends, disableXForward: route.DisableHeaderXForward, methods: route.Methods, cors: route.Cors, @@ -190,6 +194,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { path: route.Path, rewrite: route.Rewrite, destination: route.Destination, + backends: route.Backends, methods: route.Methods, disableXForward: route.DisableHeaderXForward, cors: route.Cors, diff --git a/internal/types.go b/internal/types.go index 26963a5..671161d 100644 --- a/internal/types.go +++ b/internal/types.go @@ -143,6 +143,8 @@ type Route struct { Rewrite string `yaml:"rewrite"` // Destination Defines backend URL Destination string `yaml:"destination"` + // + Backends []string `yaml:"backends"` // Cors contains the route cors headers Cors Cors `yaml:"cors"` //RateLimit int `yaml:"rateLimit"` @@ -197,6 +199,14 @@ type Gateway struct { // Routes holds proxy routes Routes []Route `yaml:"routes"` } + +type RouteHealthCheck struct { + Path string `yaml:"path"` + Interval int `yaml:"interval"` + Timeout int `yaml:"timeout"` + HealthyStatuses []int `yaml:"healthyStatuses"` + UnhealthyStatuses []int `yaml:"unhealthyStatuses"` +} type GatewayConfig struct { Version string `yaml:"version"` // GatewayConfig holds Gateway config @@ -220,6 +230,8 @@ type ProxyRoute struct { path string rewrite string destination string + backends []string + healthCheck RouteHealthCheck methods []string cors Cors disableXForward bool diff --git a/internal/var.go b/internal/var.go index 204fb80..5c87d6f 100644 --- a/internal/var.go +++ b/internal/var.go @@ -9,3 +9,5 @@ const AccessMiddleware = "access" // access middleware const BasicAuth = "basic" // basic authentication middleware const JWTAuth = "jwt" // JWT authentication middleware const OAuth = "oauth" // OAuth authentication middleware +// Round-robin counter +var counter uint32 From bc057dd43510e8c69a393fb83dda483c76a89c69 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Sun, 10 Nov 2024 07:56:46 +0100 Subject: [PATCH 2/2] feat: add multiple hosts rounting capabilities --- internal/config.go | 4 ++-- internal/route.go | 8 +++++--- internal/types.go | 8 +++++--- util/constants.go | 2 ++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/config.go b/internal/config.go index 50b59c8..e02e780 100644 --- a/internal/config.go +++ b/internal/config.go @@ -118,7 +118,7 @@ func initConfig(configFile string) { configFile = GetConfigPaths() } conf := &GatewayConfig{ - Version: util.Version, + Version: util.ConfigVersion, GatewayConfig: Gateway{ WriteTimeout: 15, ReadTimeout: 15, @@ -165,7 +165,7 @@ func initConfig(configFile string) { }, { Name: "Hostname example", - Host: "http://example.localhost", + Hosts: []string{"example.com", "example.localhost"}, Path: "/", Destination: "https://example.com", Rewrite: "/", diff --git a/internal/route.go b/internal/route.go index ecfdfac..9de202b 100644 --- a/internal/route.go +++ b/internal/route.go @@ -197,14 +197,16 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { router := r.PathPrefix(route.Path).Subrouter() // Apply route Cors router.Use(CORSHandler(route.Cors)) - if route.Host != "" { - router.Host(route.Host).PathPrefix("").Handler(proxyRoute.ProxyHandler()) + if len(route.Hosts) > 0 { + for _, host := range route.Hosts { + router.Host(host).PathPrefix("").Handler(proxyRoute.ProxyHandler()) + } } else { router.PathPrefix("").Handler(proxyRoute.ProxyHandler()) } } else { logger.Error("Error, path is empty in route %s", route.Name) - logger.Debug("Route path ignored: %s", route.Path) + logger.Error("Route path ignored: %s", route.Path) } } // Apply global Cors middlewares diff --git a/internal/types.go b/internal/types.go index 26963a5..c10ed3d 100644 --- a/internal/types.go +++ b/internal/types.go @@ -131,12 +131,14 @@ type MiddlewareName struct { // Route defines gateway route type Route struct { + // Path defines route path + Path string `yaml:"path"` // Name defines route name Name string `yaml:"name"` //Host Domain/host based request routing - Host string `yaml:"host"` - // Path defines route path - Path string `yaml:"path"` + //Host string `yaml:"host"` + //Hosts Domains/hosts based request routing + Hosts []string `yaml:"hosts"` // Rewrite rewrites route path to desired path // // E.g. /cart to / => It will rewrite /cart path to / diff --git a/util/constants.go b/util/constants.go index 5a15dff..6757d9b 100644 --- a/util/constants.go +++ b/util/constants.go @@ -15,6 +15,8 @@ import ( var Version string +const ConfigVersion = "1.0" + func VERSION(def string) string { build := os.Getenv("VERSION") if build == "" {