chore: add route gateway verification

This commit is contained in:
Jonas Kaninda
2024-10-27 07:24:50 +01:00
parent 1923506e0a
commit ff3dbe2a27
6 changed files with 151 additions and 72 deletions

View File

@@ -11,7 +11,7 @@
```
Goma Gateway is a lightweight API Gateway and Reverse Proxy.
[![Build](https://github.com/jkaninda/goma-gateway/actions/workflows/release.yml/badge.svg)](https://github.com/jkaninda/goma/actions/workflows/release.yml)
[![Build](https://github.com/jkaninda/goma-gateway/actions/workflows/release.yml/badge.svg)](https://github.com/jkaninda/goma-gateway/actions/workflows/release.yml)
[![Go Report](https://goreportcard.com/badge/github.com/jkaninda/goma-gateway)](https://goreportcard.com/report/github.com/jkaninda/goma-gateway)
[![Go Reference](https://pkg.go.dev/badge/github.com/jkaninda/goma-gateway.svg)](https://pkg.go.dev/github.com/jkaninda/goma-gateway)
![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/jkaninda/goma-gateway?style=flat-square)
@@ -65,8 +65,13 @@ docker run --rm --name goma-gateway \
```
### 4. Healthcheck
- Goma Gateway readiness: `/readyz`
- Routes health check: `/healthz`
[http://localhost/healthz](http://localhost/healthz)
[http://localhost/readyz](http://localhost/readyz)
> Healthcheck response body
```json

View File

@@ -179,7 +179,7 @@ func (GatewayServer) Config(configFile string) (*GatewayServer, error) {
c := &GatewayConfig{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return nil, fmt.Errorf("in file %q: %w", configFile, err)
return nil, fmt.Errorf("error parsing yaml %q: %w", configFile, err)
}
return &GatewayServer{
ctx: nil,
@@ -338,11 +338,15 @@ func ToJWTRuler(input interface{}) (JWTRuler, error) {
var bytes []byte
bytes, err := yaml.Marshal(input)
if err != nil {
return JWTRuler{}, fmt.Errorf("error marshalling yaml: %v", err)
return JWTRuler{}, fmt.Errorf("error parsing yaml: %v", err)
}
err = yaml.Unmarshal(bytes, jWTRuler)
if err != nil {
return JWTRuler{}, fmt.Errorf("error unmarshalling yaml: %v", err)
return JWTRuler{}, fmt.Errorf("error parsing yaml: %v", err)
}
if jWTRuler.URL == "" {
return JWTRuler{}, fmt.Errorf("error parsing yaml: empty url in jwt auth middleware")
}
return *jWTRuler, nil
}
@@ -352,11 +356,15 @@ func ToBasicAuth(input interface{}) (BasicRule, error) {
var bytes []byte
bytes, err := yaml.Marshal(input)
if err != nil {
return BasicRule{}, fmt.Errorf("error marshalling yaml: %v", err)
return BasicRule{}, fmt.Errorf("error parsing yaml: %v", err)
}
err = yaml.Unmarshal(bytes, basicAuth)
if err != nil {
return BasicRule{}, fmt.Errorf("error unmarshalling yaml: %v", err)
return BasicRule{}, fmt.Errorf("error parsing yaml: %v", err)
}
if basicAuth.Username == "" || basicAuth.Password == "" {
return BasicRule{}, fmt.Errorf("error parsing yaml: empty username/password in basic auth middleware")
}
return *basicAuth, nil
}

View File

@@ -105,3 +105,15 @@ func (heathRoute HealthCheckRoute) HealthCheckHandler(w http.ResponseWriter, r *
return
}
}
func (heathRoute HealthCheckRoute) HealthReadyHandler(w http.ResponseWriter, r *http.Request) {
response := HealthCheckRouteResponse{
Status: "healthy",
Error: "",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(response)
if err != nil {
return
}
}

View File

@@ -20,5 +20,5 @@ func Intro() {
nameFigure.Print()
fmt.Printf("Version: %s\n", util.FullVersion())
fmt.Println("Copyright (c) 2024 Jonas Kaninda")
fmt.Println("Starting Goma server...")
fmt.Println("Starting Goma Gateway server...")
}

View File

@@ -34,8 +34,8 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
Routes: gateway.Routes,
}
// Define the health check route
r.HandleFunc("/health", heath.HealthCheckHandler).Methods("GET")
r.HandleFunc("/healthz", heath.HealthCheckHandler).Methods("GET")
r.HandleFunc("/readyz", heath.HealthReadyHandler).Methods("GET")
// Apply global Cors middlewares
r.Use(CORSHandler(gateway.Cors)) // Apply CORS middleware
if gateway.RateLimiter != 0 {
@@ -45,15 +45,76 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
r.Use(limiter.RateLimitMiddleware())
}
for _, route := range gateway.Routes {
blM := middleware.BlockListMiddleware{
Path: route.Path,
List: route.Blocklist,
}
// Add block access middleware to all route, if defined
r.Use(blM.BlocklistMiddleware)
//if route.Middlewares != nil {
for _, mid := range route.Middlewares {
secureRouter := r.PathPrefix(util.ParseURLPath(route.Path + mid.Path)).Subrouter()
if route.Path != "" {
blM := middleware.BlockListMiddleware{
Path: route.Path,
List: route.Blocklist,
}
// Add block access middleware to all route, if defined
r.Use(blM.BlocklistMiddleware)
// Apply route middleware
for _, mid := range route.Middlewares {
if mid.Path != "" {
secureRouter := r.PathPrefix(util.ParseURLPath(route.Path + mid.Path)).Subrouter()
proxyRoute := ProxyRoute{
path: route.Path,
rewrite: route.Rewrite,
destination: route.Destination,
disableXForward: route.DisableHeaderXForward,
cors: route.Cors,
}
rMiddleware, err := searchMiddleware(mid.Rules, middlewares)
if err != nil {
logger.Error("Middleware name not found")
} else {
//Check Authentication middleware
switch rMiddleware.Type {
case "basic":
basicAuth, err := ToBasicAuth(rMiddleware.Rule)
if err != nil {
logger.Error("Error: %s", err.Error())
} else {
amw := middleware.AuthBasic{
Username: basicAuth.Username,
Password: basicAuth.Password,
Headers: nil,
Params: nil,
}
// Apply JWT authentication middleware
secureRouter.Use(amw.AuthMiddleware)
secureRouter.Use(CORSHandler(route.Cors))
secureRouter.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) // Proxy handler
secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler
}
case "jwt":
jwt, err := ToJWTRuler(rMiddleware.Rule)
if err != nil {
logger.Error("Error: %s", err.Error())
} else {
amw := middleware.AuthJWT{
AuthURL: jwt.URL,
RequiredHeaders: jwt.RequiredHeaders,
Headers: jwt.Headers,
Params: jwt.Params,
}
// Apply JWT authentication middleware
secureRouter.Use(amw.AuthMiddleware)
secureRouter.Use(CORSHandler(route.Cors))
secureRouter.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) // Proxy handler
secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler
}
default:
logger.Error("Unknown middleware type %s", rMiddleware.Type)
}
}
} else {
logger.Error("Error, middleware path is empty")
logger.Error("Middleware ignored")
}
}
proxyRoute := ProxyRoute{
path: route.Path,
rewrite: route.Rewrite,
@@ -61,63 +122,14 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
disableXForward: route.DisableHeaderXForward,
cors: route.Cors,
}
rMiddleware, err := searchMiddleware(mid.Rules, middlewares)
if err != nil {
logger.Error("Middleware name not found")
} else {
//Check Authentication middleware
switch rMiddleware.Type {
case "basic":
basicAuth, err := ToBasicAuth(rMiddleware.Rule)
if err != nil {
logger.Error("Error: %s", err.Error())
} else {
amw := middleware.AuthBasic{
Username: basicAuth.Username,
Password: basicAuth.Password,
Headers: nil,
Params: nil,
}
// Apply JWT authentication middleware
secureRouter.Use(amw.AuthMiddleware)
}
case "jwt":
jwt, err := ToJWTRuler(rMiddleware.Rule)
if err != nil {
} else {
amw := middleware.AuthJWT{
AuthURL: jwt.URL,
RequiredHeaders: jwt.RequiredHeaders,
Headers: jwt.Headers,
Params: jwt.Params,
}
// Apply JWT authentication middleware
secureRouter.Use(amw.AuthMiddleware)
}
default:
logger.Error("Unknown middleware type %s", rMiddleware.Type)
}
}
secureRouter.Use(CORSHandler(route.Cors))
secureRouter.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) // Proxy handler
secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler
router := r.PathPrefix(route.Path).Subrouter()
router.Use(CORSHandler(route.Cors))
router.PathPrefix("/").Handler(proxyRoute.ProxyHandler())
} else {
logger.Error("Error, path is empty in route %s", route.Name)
logger.Info("Route path ignored: %s", route.Path)
}
proxyRoute := ProxyRoute{
path: route.Path,
rewrite: route.Rewrite,
destination: route.Destination,
disableXForward: route.DisableHeaderXForward,
cors: route.Cors,
}
router := r.PathPrefix(route.Path).Subrouter()
router.Use(CORSHandler(route.Cors))
router.PathPrefix("/").Handler(proxyRoute.ProxyHandler())
}
return r

View File

@@ -66,3 +66,45 @@ func (gatewayServer GatewayServer) Start(ctx context.Context) error {
return nil
}
func waitForReady(
ctx context.Context,
timeout time.Duration,
endpoint string,
) error {
client := http.Client{}
startTime := time.Now()
for {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
endpoint,
nil,
)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Error making request: %s\n", err.Error())
continue
}
if resp.StatusCode == http.StatusOK {
fmt.Println("Endpoint is ready!")
resp.Body.Close()
return nil
}
resp.Body.Close()
select {
case <-ctx.Done():
return ctx.Err()
default:
if time.Since(startTime) >= timeout {
return fmt.Errorf("timeout reached while waiting for endpoint")
}
// wait a little while between checks
time.Sleep(250 * time.Millisecond)
}
}
}