From a9d365daa449827bc303af6e8b5c5b5953b12c4e Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 18 Nov 2024 08:50:49 +0100 Subject: [PATCH 1/4] feat: add additional routes from defined directory --- internal/config.go | 2 +- internal/extra_config.go | 47 ++++++++++++++++++++++++++++++++++ internal/gateway_type.go | 9 ++++--- internal/route.go | 15 ++++++++--- internal/route_config.go | 55 ++++++++++++++++++++++++++++++++++++++++ internal/route_type.go | 5 ++++ internal/server.go | 2 +- internal/types.go | 5 ++++ internal/var.go | 10 +++++--- 9 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 internal/extra_config.go create mode 100644 internal/route_config.go diff --git a/internal/config.go b/internal/config.go index 6fdcbe3..7b2f569 100644 --- a/internal/config.go +++ b/internal/config.go @@ -76,7 +76,7 @@ func (GatewayServer) Config(configFile string, ctx context.Context) (*GatewaySer } logger.Info("Generating new configuration file...") - // check if config directory does exist + // check if config Directory does exist if !util.FolderExists(ConfigDir) { err := os.MkdirAll(ConfigDir, os.ModePerm) if err != nil { diff --git a/internal/extra_config.go b/internal/extra_config.go new file mode 100644 index 0000000..60d72f0 --- /dev/null +++ b/internal/extra_config.go @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Jonas Kaninda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package pkg + +import ( + "fmt" + "os" + "path/filepath" +) + +// loadExtraFiles loads routes files in .yml and .yaml based on defined directory +func loadExtraFiles(routePath string) ([]string, error) { + // Slice to store YAML/YML files + var yamlFiles []string + // Walk through the Directory + err := filepath.Walk(routePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // Check for .yaml or .yml file extension + if !info.IsDir() && (filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml") { + yamlFiles = append(yamlFiles, path) + } + return nil + }) + + if err != nil { + //log.Fatalf("error walking the path %v: %v", routePath, err) + return nil, fmt.Errorf("error loading extra route files: %v", err) + } + return yamlFiles, nil +} diff --git a/internal/gateway_type.go b/internal/gateway_type.go index 7118f4e..f3e55a3 100644 --- a/internal/gateway_type.go +++ b/internal/gateway_type.go @@ -34,10 +34,11 @@ type Gateway struct { // RateLimit Defines the number of request peer minutes RateLimit int `yaml:"rateLimit" env:"GOMA_RATE_LIMIT, overwrite"` // BlockCommonExploits enable, disable block common exploits - BlockCommonExploits bool `yaml:"blockCommonExploits"` - AccessLog string `yaml:"accessLog" env:"GOMA_ACCESS_LOG, overwrite"` - ErrorLog string `yaml:"errorLog" env:"GOMA_ERROR_LOG=, overwrite"` - LogLevel string `yaml:"logLevel" env:"GOMA_LOG_LEVEL, overwrite"` + BlockCommonExploits bool `yaml:"blockCommonExploits"` + AccessLog string `yaml:"accessLog" env:"GOMA_ACCESS_LOG, overwrite"` + ErrorLog string `yaml:"errorLog" env:"GOMA_ERROR_LOG=, overwrite"` + LogLevel string `yaml:"logLevel" env:"GOMA_LOG_LEVEL, overwrite"` + ExtraRoutes ExtraRouteConfig `yaml:"extraRoutes"` // DisableHealthCheckStatus enable and disable routes health check DisableHealthCheckStatus bool `yaml:"disableHealthCheckStatus"` // DisableRouteHealthCheckError allows enabling and disabling backend healthcheck errors diff --git a/internal/route.go b/internal/route.go index 4bb75bf..137dccc 100644 --- a/internal/route.go +++ b/internal/route.go @@ -35,18 +35,27 @@ func init() { // Initialize initializes the routes func (gatewayServer GatewayServer) Initialize() *mux.Router { gateway := gatewayServer.gateway + dynamicRoutes = gateway.Routes + // Load Extra Routes + if len(gateway.ExtraRoutes.Directory) != 0 { + extraRoutes, err := loadExtraRoutes(gateway.ExtraRoutes.Directory) + if err != nil { + logger.Error(err.Error()) + } + dynamicRoutes = append(dynamicRoutes, extraRoutes...) + } m := gatewayServer.middlewares redisBased := false if len(gateway.Redis.Addr) != 0 { redisBased = true } // Routes background healthcheck - routesHealthCheck(gateway.Routes) + routesHealthCheck(dynamicRoutes) r := mux.NewRouter() heath := HealthCheckRoute{ DisableRouteHealthCheckError: gateway.DisableRouteHealthCheckError, - Routes: gateway.Routes, + Routes: dynamicRoutes, } if gateway.EnableMetrics { // Prometheus endpoint @@ -80,7 +89,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { // Add rate limit middlewares r.Use(limiter.RateLimitMiddleware()) } - for rIndex, route := range gateway.Routes { + for rIndex, route := range dynamicRoutes { if len(route.Path) != 0 { // Checks if route destination and backend are empty if len(route.Destination) == 0 && len(route.Backends) == 0 { diff --git a/internal/route_config.go b/internal/route_config.go new file mode 100644 index 0000000..07e337c --- /dev/null +++ b/internal/route_config.go @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Jonas Kaninda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package pkg + +import ( + "fmt" + "github.com/jkaninda/goma-gateway/pkg/logger" + "gopkg.in/yaml.v3" + "os" +) + +// loadExtraRoutes loads additional routes +func loadExtraRoutes(routePath string) ([]Route, error) { + logger.Info("Loading additional routes from %s", routePath) + yamlFiles, err := loadExtraFiles(routePath) + if err != nil { + return nil, fmt.Errorf("error loading extra files: %v", err) + } + var extraRoutes []Route + for _, yamlFile := range yamlFiles { + buf, err := os.ReadFile(yamlFile) + if err != nil { + return nil, fmt.Errorf("error loading extra file: %v", err) + } + ex := &ExtraRoute{} + err = yaml.Unmarshal(buf, ex) + if err != nil { + return nil, fmt.Errorf("in file %q: %w", ConfigFile, err) + } + extraRoutes = append(extraRoutes, ex.Routes...) + + } + if len(extraRoutes) == 0 { + return nil, fmt.Errorf("no extra routes found in %s", routePath) + } else { + logger.Info("Loaded %d extra routes from %s", len(extraRoutes), routePath) + + } + return extraRoutes, nil +} diff --git a/internal/route_type.go b/internal/route_type.go index b09e894..b544eee 100644 --- a/internal/route_type.go +++ b/internal/route_type.go @@ -56,3 +56,8 @@ type Route struct { // Middlewares Defines route middlewares from Middleware names Middlewares []string `yaml:"middlewares"` } + +type ExtraRoute struct { + // Routes holds proxy routes + Routes []Route `yaml:"routes"` +} diff --git a/internal/server.go b/internal/server.go index 947f1ec..340b177 100644 --- a/internal/server.go +++ b/internal/server.go @@ -41,7 +41,7 @@ func (gatewayServer GatewayServer) Start() error { } if !gatewayServer.gateway.DisableDisplayRouteOnStart { - printRoute(gatewayServer.gateway.Routes) + printRoute(dynamicRoutes) } httpServer := gatewayServer.createServer(":8080", route, nil) diff --git a/internal/types.go b/internal/types.go index ca0c724..8f05891 100644 --- a/internal/types.go +++ b/internal/types.go @@ -168,3 +168,8 @@ type Redis struct { Addr string `yaml:"addr"` Password string `yaml:"password"` } + +type ExtraRouteConfig struct { + Directory string `yaml:"directory"` + Watch bool `yaml:"watch"` +} diff --git a/internal/var.go b/internal/var.go index c83716a..4a71ff5 100644 --- a/internal/var.go +++ b/internal/var.go @@ -8,7 +8,9 @@ const AccessMiddleware = "access" // access middlewares const BasicAuth = "basic" // basic authentication middlewares const JWTAuth = "jwt" // JWT authentication middlewares const OAuth = "oauth" // OAuth authentication middlewares -// Round-robin counter -var counter uint32 - -var Routes *[]Route +var ( + // Round-robin counter + counter uint32 + // dynamicRoutes routes + dynamicRoutes []Route +) From 762fbe85112045193ef982a57a831e92f331bc34 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 18 Nov 2024 09:20:12 +0100 Subject: [PATCH 2/4] feat: add track duplicate route names --- internal/route.go | 7 +++++++ internal/route_config.go | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/internal/route.go b/internal/route.go index 137dccc..7dc7aeb 100644 --- a/internal/route.go +++ b/internal/route.go @@ -44,6 +44,13 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { } dynamicRoutes = append(dynamicRoutes, extraRoutes...) } + //find duplicated route name + duplicates := findDuplicateRouteNames(dynamicRoutes) + if len(duplicates) != 0 { + for _, duplicate := range duplicates { + logger.Error("Duplicate route name found: %s ", duplicate) + } + } m := gatewayServer.middlewares redisBased := false if len(gateway.Redis.Addr) != 0 { diff --git a/internal/route_config.go b/internal/route_config.go index 07e337c..77fd94f 100644 --- a/internal/route_config.go +++ b/internal/route_config.go @@ -53,3 +53,18 @@ func loadExtraRoutes(routePath string) ([]Route, error) { } return extraRoutes, nil } + +func findDuplicateRouteNames(routes []Route) []string { + // Create a map to track occurrences of names + nameMap := make(map[string]int) + var duplicates []string + + for _, route := range routes { + nameMap[route.Name]++ + // If the count is ==2, it's a duplicate + if nameMap[route.Name] == 2 { + duplicates = append(duplicates, route.Name) + } + } + return duplicates +} From 8f8f9d4d6cd50bbfed251cd44844b32259115bf6 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 18 Nov 2024 10:03:58 +0100 Subject: [PATCH 3/4] chore: add extra route config tests --- internal/extra_config.go | 1 - internal/route.go | 6 +- internal/route_test.go | 178 +++++++++++++++++++++++++++++++++++++++ internal/server_test.go | 14 ++- 4 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 internal/route_test.go diff --git a/internal/extra_config.go b/internal/extra_config.go index 60d72f0..4781e58 100644 --- a/internal/extra_config.go +++ b/internal/extra_config.go @@ -40,7 +40,6 @@ func loadExtraFiles(routePath string) ([]string, error) { }) if err != nil { - //log.Fatalf("error walking the path %v: %v", routePath, err) return nil, fmt.Errorf("error loading extra route files: %v", err) } return yamlFiles, nil diff --git a/internal/route.go b/internal/route.go index 7dc7aeb..50a6d4b 100644 --- a/internal/route.go +++ b/internal/route.go @@ -40,15 +40,15 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { if len(gateway.ExtraRoutes.Directory) != 0 { extraRoutes, err := loadExtraRoutes(gateway.ExtraRoutes.Directory) if err != nil { - logger.Error(err.Error()) + logger.Error("Error: %v", err.Error()) } dynamicRoutes = append(dynamicRoutes, extraRoutes...) } - //find duplicated route name + // find duplicated route name duplicates := findDuplicateRouteNames(dynamicRoutes) if len(duplicates) != 0 { for _, duplicate := range duplicates { - logger.Error("Duplicate route name found: %s ", duplicate) + logger.Error("Duplicated route named was found: %s ", duplicate) } } m := gatewayServer.middlewares diff --git a/internal/route_test.go b/internal/route_test.go new file mode 100644 index 0000000..3917a01 --- /dev/null +++ b/internal/route_test.go @@ -0,0 +1,178 @@ +/* + * Copyright 2024 Jonas Kaninda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package pkg + +import ( + "fmt" + "github.com/jkaninda/goma-gateway/util" + "gopkg.in/yaml.v3" + "os" +) + +// initExtraRoute create extra routes +func initExtraRoute(path string) error { + + conf := &ExtraRoute{ + Routes: []Route{ + { + Name: "Extra1", + Path: "/", + Methods: []string{"GET"}, + Destination: "https://extra-example.com", + Rewrite: "/", + HealthCheck: RouteHealthCheck{ + Path: "/", + Interval: "30s", + Timeout: "10s", + HealthyStatuses: []int{200, 404}, + }, + DisableHostFording: true, + Middlewares: []string{"block-access"}, + }, + // Duplicate route name + { + Name: "Load balancer", + Path: "/protected", + Backends: []string{ + "https://example.com", + "https://example2.com", + "https://example3.com", + }, + Rewrite: "/", + HealthCheck: RouteHealthCheck{}, + Cors: Cors{ + Origins: []string{"http://localhost:3000", "https://dev.example.com"}, + Headers: map[string]string{ + "Access-Control-Allow-Headers": "Origin, Authorization", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "1728000", + }, + }, + Middlewares: []string{"basic-auth", "block-access"}, + }, + }, + } + yamlData, err := yaml.Marshal(&conf) + if err != nil { + return fmt.Errorf("serializing configuration %v\n", err.Error()) + } + err = os.WriteFile(fmt.Sprintf("%s/extra.yaml", path), yamlData, 0644) + if err != nil { + return fmt.Errorf("unable to write config file %s\n", err) + } + return nil +} + +// initConfig initializes configs +func initConfiguration(configFile string) error { + conf := &GatewayConfig{ + Version: util.ConfigVersion, + GatewayConfig: Gateway{ + WriteTimeout: 15, + ReadTimeout: 15, + IdleTimeout: 30, + AccessLog: "/dev/Stdout", + ErrorLog: "/dev/stderr", + DisableRouteHealthCheckError: false, + DisableDisplayRouteOnStart: false, + RateLimit: 0, + InterceptErrors: []int{405, 500}, + ExtraRoutes: ExtraRouteConfig{ + Directory: extraRoutePath, + Watch: false, + }, + Cors: Cors{ + Origins: []string{"http://localhost:8080", "https://example.com"}, + Headers: map[string]string{ + "Access-Control-Allow-Headers": "Origin, Authorization, Accept, Content-Type, Access-Control-Allow-Headers", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "1728000", + }, + }, + Routes: []Route{ + { + Name: "Example", + Path: "/", + Methods: []string{"GET"}, + Destination: "https://example.com", + Rewrite: "/", + HealthCheck: RouteHealthCheck{ + Path: "/", + Interval: "30s", + Timeout: "10s", + HealthyStatuses: []int{200, 404}, + }, + DisableHostFording: true, + Middlewares: []string{"block-access"}, + }, + { + Name: "Load balancer", + Path: "/protected", + Backends: []string{ + "https://example.com", + "https://example2.com", + "https://example3.com", + }, + Rewrite: "/", + HealthCheck: RouteHealthCheck{}, + Cors: Cors{ + Origins: []string{"http://localhost:3000", "https://dev.example.com"}, + Headers: map[string]string{ + "Access-Control-Allow-Headers": "Origin, Authorization", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "1728000", + }, + }, + Middlewares: []string{"basic-auth", "block-access"}, + }, + }, + }, + Middlewares: []Middleware{ + { + Name: "basic-auth", + Type: BasicAuth, + Paths: []string{ + "/*", + }, + Rule: BasicRuleMiddleware{ + Username: "admin", + Password: "admin", + }, + }, + { + Name: "block-access", + Type: AccessMiddleware, + Paths: []string{ + "/swagger-ui/*", + "/v2/swagger-ui/*", + "/api-docs/*", + "/actuator/*", + }, + }, + }, + } + yamlData, err := yaml.Marshal(&conf) + if err != nil { + return fmt.Errorf("serializing configuration %v\n", err.Error()) + } + err = os.WriteFile(configFile, yamlData, 0644) + if err != nil { + return fmt.Errorf("unable to write config file %s\n", err) + } + return nil +} diff --git a/internal/server_test.go b/internal/server_test.go index 23d752f..4fc0ce6 100644 --- a/internal/server_test.go +++ b/internal/server_test.go @@ -10,6 +10,7 @@ import ( ) const testPath = "./tests" +const extraRoutePath = "./tests/extra" var configFile = filepath.Join(testPath, "goma.yml") @@ -18,11 +19,15 @@ func TestInit(t *testing.T) { if err != nil { t.Error(err) } + err = os.MkdirAll(extraRoutePath, os.ModePerm) + if err != nil { + t.Error(err) + } } func TestCheckConfig(t *testing.T) { TestInit(t) - err := initConfig(configFile) + err := initConfiguration(configFile) if err != nil { t.Fatal("Error init config:", err) } @@ -35,10 +40,15 @@ func TestCheckConfig(t *testing.T) { func TestStart(t *testing.T) { TestInit(t) - err := initConfig(configFile) + err := initConfiguration(configFile) if err != nil { t.Fatalf("Error initializing config: %s", err.Error()) } + + err = initExtraRoute(extraRoutePath) + if err != nil { + t.Fatalf("Error creating extra routes file: %s", err.Error()) + } ctx := context.Background() g := GatewayServer{} gatewayServer, err := g.Config(configFile, ctx) From e2b9bda5ec8893217c9222f9182c76f1d3064b12 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 18 Nov 2024 10:05:08 +0100 Subject: [PATCH 4/4] chore: update logging text --- internal/route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/route.go b/internal/route.go index 50a6d4b..77fea94 100644 --- a/internal/route.go +++ b/internal/route.go @@ -48,7 +48,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { duplicates := findDuplicateRouteNames(dynamicRoutes) if len(duplicates) != 0 { for _, duplicate := range duplicates { - logger.Error("Duplicated route named was found: %s ", duplicate) + logger.Error("Duplicated route name was found: %s ", duplicate) } } m := gatewayServer.middlewares