gateway

package module
v0.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 9, 2024 License: MIT Imports: 27 Imported by: 0

README

API Gateway

Lightweight API Gateway written in Go.

Config

The main way of configuring the Gateway is done by the config.json file. You can always find the latest state of the config in config.json.example file, even if this document is not updated.

A basic config looks something like this:

{
  "address": 3100,
  "productionLevel": 0,
  "middlewaresEnabled": 1,
  "healthCheckInterval": "1m",
  "secretKey": "",
	"loggerConfig": {
    "disabledLoggers": [
      "error"
		]
  },
  "services": [
    {
      "protocol": "http",
      "name": "testService",
      "host": "localhost",
      "port": "3001",
      "prefix": "/api/test",
      "timeOutSec": 5
    }
  ]
}

Features

Creating a new instance

There are two main ways to create a new instance. The first one is the more general, that reads in the config file, then initiates the instance by given details. The only parameter it requires is the relative path for the config file. It returns a pointer to the new instance and an error, if there is any.

gw, err := gateway.NewFromConfig("./example.config.json")
if err != nil {
	fmt.Println("gateway create err: %v\n", err)
	os.Exit(1)
}

The other way is the more traditional one, where we can initiate a new instance programatically with the decoractor pattern. It does not return any error, only the pointer to the new Gateway instance.

func main() {
	gw := gateway.New(
		gateway.WithAddress(8000),
		gateway.WithHealthCheckFrequency(5*time.Second),
	)

	gw.Start()
}
Custom endpoints

It is possible for the Gateway to acts a router itself, by registering routes with handlers.

The router is REST compatible, which means, is supports selection based upon HTTP methods, and wildcard path params. Eg.: GET /api/foo/{id} and DELETE /api/foo/{id}

Example:

gw.Get("/api/foo/{id}", func(ctx *gateway.Context) {
	id := ctx.GetParam(id)

	type response struct {
		param string
	}

	res := &response{
		param: id,
	}

	ctx.SendJson(res)
})

Endpoint middlewares

As it is mentioned earlier, it is possible to register custom HTTP endpoints. Also there is a way to attach middlewares to each one. Every given middleware function is attached to the endpoint as a pre runner, which means, the middleware functions run before the execution of the handler itself. The sequence of the middleware chain is the same as the registrations order.

Example:

mw1 := func(ctx *gateway.Context, next gateway.HandlerFunc) {
	// Some work or auth to do.
	// ...

	// Then we call the next in the sequence.
	next(ctx)
}

mw2 := func(ctx *gateway.Context, next gateway.HandlerFunc) {
	// Some other work or auth to do.
	// ...

	// Then we call the next in the sequence.
	next(ctx)
}

gw.Get("/api/foo/bar", func(ctx *gateway.Context) {
	ctx.SendOk()
}).RegisterMiddlewares(mw1, mw2)
Global middlewares

Besides the middlewares that are attached to specific endpoints, we can register global middlewares, which takes a normal middlewarefunc – as mentioned before – and a matcherfunc as a necessity. The matcher takes the Context as a parameter and returns a boolean, that indicates for a match. Eg.: it could be a match for URL.

Example – the MW is only called if the url contains foo:

var matcher = func(ctx *gateway.Context) bool {
	return strings.Contains(ctx.GetFullUrl(), "foo")
}

gw.RegisterMiddleware(func(ctx *gateway.Context, next gateway.HandlerFunc) {
	// Some other work or auth to do.
	// ...

	// Then we call the next in the sequence.
	next(ctx)
}, matcher)
Logging to file

As well as normal logging to stdout and stderr, it is enabled by deafult to write the same logs to persistent files, which date stamps.

Service registry

The main feature of any API Gataway is the abilitiy to handle traffic to and between different services. In this gateway the routing is based upon prefixing.

Every service's config must be follow this rule in the config.json file:

{
  "protocol": "http",
  "host": "localhost",
  "name": "exampleService",
  "port": "3001",
  "prefix": "/api/test",
  "timeOutSec": 5
}

After the Gateway is up and running, it will make requests to the registered services – as a heartbeat – periodically. Each registered services must have a public REST endpoint: GET /api/status/health-check. It should only respond with HTTP 200. Any other status code or timeout will be acknowledged as the given service is down.

There is another way to signal the Gateway that one service is up, is by making a POST request as the following. The url be: /api/system/services/update.

The request body :

{
	"serviceName": "exampleService"
}

To ensure that this the request is done by an authorized service, the following header must be present, or the request is not proccessed:

'X-GATEWAY-KEY': $HASHVALUE

where the $HASHVALUE is calculated by the following method. Lets take body of the request, stringify it, and compact it – remove all unnecessary whitespaces and newline characters. Then append the common secret key to the and. eq: {"serviceName":"exampleService"}exampleSecretKey. Then, you must make a hash with SHA256 algorithm and you are done.

If a service is down you are trying to access it, the Gateway would return an HTTP 503 error, as expected.

There is way to get some information about the inner state of the Gateway and service. You have to make a POST request to: /api/system/services/info. The body must be an empty object: {}, and the it should include the appended secret key and also the header aswell.

gRPC proxy

With version v0.4.0 a gRPC proxy is introduced int the Gateway. In order to start it, simply have to add this into the main configuration file of the Gateway:

"grpcProxy": {
  "address": 3000
}

This addition to the config, will start a gRPC sever listening at the given port. This will proxy all the gRPC calls between the services in the cluster.

For now, this gRPC proxy only supports interservice communication, so from the outside only REST calls are supported.

To mark a service as gRPC compatible service, only have to modify the config of the given service as below:

"services": [
  {
    "serviceType": 1,
    "protocol": "http",
    "name": "exampleService",
    "host": "localhost",
    "port": "3001",
    "prefix": "/example.ExampleService"
  }
]

Where the serviceType must take the value 1, and the prefix should be a unique part of the gRPC service FullMethodName.

To identify this, you have to look inside the generated *._grpc.pb.go file. There you would find something like this:

const (
	ExampleService_GetMessage_FullMethodName = "/example.ExampleService/GetMessage"
)

Probably, there is more than one FullMethodName in your file, but the package description will be the same.

const (
	ExampleService_GetMessage_FullMethodName 	= "/example.ExampleService/GetMessage"
	OtherService_GetMessage_FullMethodName 		= "/example.OtherService/GetMessage"
)

In the case of the latter example, the prefix should be /example. Every gRCP proxy call will make a lookup inside the Service registry, and find the best fit, due to the longest match in the given prefix.

Documentation

Overview

This lightweight gRPC proxy was based on the work: https://github.com/mwitkow/grpc-proxy I am grateful for publishing that package, helped me a lot! Thanks for the authors!

Index

Constants

View Source
const (
	IncomingDecodedKey ContextKey = "incomingDecoded"
	X_GW_HEADER_KEY    string     = "X-GATEWAY-KEY"
)
View Source
const (
	JsonContentType     = "application/json"
	JsonContentTypeUTF8 = JsonContentType + "; charset=UTF-8"
	TextHtmlContentType = "text/html"
	XmlContentType      = "application/xml"
)
View Source
const (
	StateRegistered serviceState = iota
	StateUnknown
	StateRefused
	StateAvailable
)
View Source
const Version = "v0.5.3"

Variables

View Source
var (
	ErrServiceNotExists = errors.New("[registry]: service not exists")
)

Functions

This section is empty.

Types

type Context added in v0.3.0

type Context = gorouter.Context

type ContextKey added in v0.5.0

type ContextKey = gorouter.ContextKey

type Gateway

type Gateway struct {
	// contains filtered or unexported fields
}

func New

func New(opts ...GatewayOptionFunc) *Gateway

New returns a new instance of the gateway decorated with the given opts.

func NewFromConfig added in v0.3.0

func NewFromConfig(path ...string) (*Gateway, error)

NewFromConfig creates and returns a new Gateway based on the given config file path. In case of any errors – due to IO reading or marshal error – it returns the error also.

func (*Gateway) Delete added in v0.3.0

func (gw *Gateway) Delete(url string, handler HandlerFunc) Route

Delete registers a custom route with method @DELETE.

func (*Gateway) Get

func (gw *Gateway) Get(url string, handler HandlerFunc) Route

Get registers a custom route with method @GET.

func (*Gateway) GetService

func (gw *Gateway) GetService(name string) (Service, error)

GetService searches for a service by its name. Returns error if, there is no service by the given name.

func (*Gateway) Head added in v0.3.0

func (gw *Gateway) Head(url string, handler HandlerFunc) Route

Head registers a custom route with method @HEAD.

func (*Gateway) ListenForMocks

func (gw *Gateway) ListenForMocks(_ *[]any)

Listener for mocks.

func (*Gateway) Post

func (gw *Gateway) Post(url string, handler HandlerFunc) Route

Post registers a custom route with method @POST.

func (*Gateway) Put added in v0.3.0

func (gw *Gateway) Put(url string, handler HandlerFunc) Route

Put registers a custom route with method @PUT.

func (*Gateway) RegisterMiddleware

func (gw *Gateway) RegisterMiddleware(mw ...Middleware) error

RegisterMiddleware registers a middleware instance to the gateway.

func (*Gateway) RegisterService added in v0.3.0

func (g *Gateway) RegisterService(conf *ServiceConfig) error

RegisterService creates and registers a new Service to the registry based on the given config. In case of validation error or duplicate service, it returns error.

func (*Gateway) Start

func (gw *Gateway) Start()

Start the main process for the Gateway. It listens until it receives the signal to close it. This method sutable for graceful shutdown.

type GatewayConfig added in v0.3.0

type GatewayConfig struct {
	Address             int              `json:"address"`
	MiddlewaresEnabled  *runLevel        `json:"middlewaresEnabled"`
	ProductionLevel     *runLevel        `json:"productionLevel"`
	SecretKey           string           `json:"secretKey"`
	HealthCheckInterval string           `json:"healthCheckInterval"`
	TimeOutSec          int              `json:"timeOutSec"`
	LoggerConfig        *LoggerConfig    `json:"loggerConfig"`
	GrpcProxy           *GrpcProxyConfig `json:"grpcProxy"`

	Services []*ServiceConfig `json:"services"`
}

type GatewayInfo added in v0.3.0

type GatewayInfo struct {
	// contains filtered or unexported fields
}

type GatewayOptionFunc added in v0.3.0

type GatewayOptionFunc = func(*Gateway)

func ReadConfig added in v0.3.0

func ReadConfig(path string) ([]GatewayOptionFunc, error)

ReadConfig reads the JSON config from given path, then returns it as a slice of GatewayOptionFunc, which can be passed into the New factory. In case of unexpected behaviour, it returns error.

func WithAddress added in v0.3.0

func WithAddress(address int) GatewayOptionFunc

func WithDisabledLoggers added in v0.3.4

func WithDisabledLoggers(disabled logTypeValue) GatewayOptionFunc

func WithGrpcProxy added in v0.4.0

func WithGrpcProxy(addr int) GatewayOptionFunc

func WithHealthCheckFrequency added in v0.3.0

func WithHealthCheckFrequency(t time.Duration) GatewayOptionFunc

func WithMiddlewaresEnabled added in v0.3.0

func WithMiddlewaresEnabled(val runLevel) GatewayOptionFunc

func WithProductionLevel added in v0.3.0

func WithProductionLevel(val runLevel) GatewayOptionFunc

func WithSecretKey added in v0.3.0

func WithSecretKey(key string) GatewayOptionFunc

func WithService added in v0.3.0

func WithService(conf *ServiceConfig) GatewayOptionFunc

type GrpcProxyConfig added in v0.4.0

type GrpcProxyConfig struct {
	Address int `json:"address"`
}

type HandlerFunc added in v0.3.0

type HandlerFunc = gorouter.HandlerFunc

type LoggerConfig added in v0.3.4

type LoggerConfig struct {
	DisabledLoggers []logTypeName `json:"disabledLoggers"`
}

type Middleware added in v0.3.0

type Middleware = gorouter.Middleware

type MiddlewareFunc added in v0.3.0

type MiddlewareFunc = gorouter.MiddlewareFunc

type Route added in v0.3.0

type Route = gorouter.Route

type Service added in v0.3.0

type Service interface {
	Handle(Context)
	Get(string, ...http.Header) (*http.Response, error)
	Post(string, []byte, ...http.Header) (*http.Response, error)
	Put(string, []byte, ...http.Header) (*http.Response, error)
	Delete(string, ...http.Header) (*http.Response, error)
	GetConfig() *ServiceConfig
	GetAddressWithProtocol() string
	GetAddress() string
}

type ServiceConfig added in v0.3.0

type ServiceConfig struct {
	// See the type def.
	ServiceType serviceType `json:"serviceType"`

	// The name of the service, must be unique.
	Name string `json:"name"`
	// The unique prefix which identified a URL
	Prefix string `json:"prefix"`

	// Which protocol is used to call the service.
	// Only http and https are supported.
	Protocol string `json:"protocol"`

	// The basic host:port format, where each service listens at.
	Host string `json:"host"`
	Port string `json:"port"`

	// How many seconds it should wait before timeout.
	TimeOutSec int `json:"timeOutSec"`

	// The url to call for healtcheck.
	StatusPath string `json:"statusPath"`
}

type ServiceInfo added in v0.3.0

type ServiceInfo struct {
	*ServiceConfig
	State string `json:"state"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL