feat: implements circuitbreaker layer
This commit is contained in:
@ -3,7 +3,8 @@ package config
|
||||
import "time"
|
||||
|
||||
type LayersConfig struct {
|
||||
Queue QueueLayerConfig `yaml:"queue"`
|
||||
Queue QueueLayerConfig `yaml:"queue"`
|
||||
CircuitBreaker CircuitBreakerLayerConfig `yaml:"circuitbreaker"`
|
||||
}
|
||||
|
||||
func NewDefaultLayersConfig() LayersConfig {
|
||||
@ -12,6 +13,9 @@ func NewDefaultLayersConfig() LayersConfig {
|
||||
TemplateDir: "./layers/queue/templates",
|
||||
DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
|
||||
},
|
||||
CircuitBreaker: CircuitBreakerLayerConfig{
|
||||
TemplateDir: "./layers/circuitbreaker/templates",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,3 +23,7 @@ type QueueLayerConfig struct {
|
||||
TemplateDir InterpolatedString `yaml:"templateDir"`
|
||||
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
|
||||
}
|
||||
|
||||
type CircuitBreakerLayerConfig struct {
|
||||
TemplateDir InterpolatedString `yaml:"templateDir"`
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/circuitbreaker-layer-options",
|
||||
"title": "Circuit breaker layer options",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchURLs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"authorizedCIDRs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"templateBlock": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
151
internal/proxy/director/layer/circuitbreaker/layer.go
Normal file
151
internal/proxy/director/layer/circuitbreaker/layer.go
Normal file
@ -0,0 +1,151 @@
|
||||
package circuitbreaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "circuitbreaker"
|
||||
|
||||
type Layer struct {
|
||||
templateDir string
|
||||
loadOnce sync.Once
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer
|
||||
func (l *Layer) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
|
||||
// Middleware implements director.MiddlewareLayer
|
||||
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
options, err := fromStoreOptions(layer.Options)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
matches, err := l.matchAnyAuthorizedCIDRs(ctx, r.RemoteAddr, options.AuthorizedCIDRs)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not match authorized cidrs", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if matches {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
matches = wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
|
||||
if !matches {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
l.renderCircuitBreakerPage(w, r, layer, options)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Layer) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) {
|
||||
remoteHost, _, err := net.SplitHostPort(remoteHostPort)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
remoteAddr := net.ParseIP(remoteHost)
|
||||
if remoteAddr == nil {
|
||||
return false, errors.Errorf("remote host '%s' is not a valid ip address", remoteHost)
|
||||
}
|
||||
|
||||
for _, rawCIDR := range CIDRs {
|
||||
_, net, err := net.ParseCIDR(rawCIDR)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
match := net.Contains(remoteAddr)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "comparing remote host with authorized cidrs", logger.F("remoteAddr", remoteAddr))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (l *Layer) renderCircuitBreakerPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions) {
|
||||
ctx := r.Context()
|
||||
|
||||
pattern := filepath.Join(l.templateDir, "*.gohtml")
|
||||
|
||||
logger.Info(ctx, "loading circuit breaker page templates", logger.F("pattern", pattern))
|
||||
|
||||
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not load circuit breaker templates", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
templateData := struct {
|
||||
Layer *store.Layer
|
||||
LayerOptions *LayerOptions
|
||||
}{
|
||||
Layer: layer,
|
||||
LayerOptions: options,
|
||||
}
|
||||
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, options.TemplateBlock, templateData); err != nil {
|
||||
logger.Error(ctx, "could not render circuit breaker page", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func New(funcs ...OptionFunc) *Layer {
|
||||
opts := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return &Layer{
|
||||
templateDir: opts.TemplateDir,
|
||||
}
|
||||
}
|
||||
|
||||
var _ director.MiddlewareLayer = &Layer{}
|
@ -0,0 +1,36 @@
|
||||
package circuitbreaker
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LayerOptions struct {
|
||||
MatchURLs []string `mapstructure:"matchURLs"`
|
||||
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
|
||||
TemplateBlock string `mapstructure:"templateBlock"`
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||
layerOptions := LayerOptions{
|
||||
MatchURLs: []string{"*"},
|
||||
AuthorizedCIDRs: []string{},
|
||||
TemplateBlock: "default",
|
||||
}
|
||||
|
||||
config := mapstructure.DecoderConfig{
|
||||
Result: &layerOptions,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(&config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := decoder.Decode(storeOptions); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &layerOptions, nil
|
||||
}
|
19
internal/proxy/director/layer/circuitbreaker/options.go
Normal file
19
internal/proxy/director/layer/circuitbreaker/options.go
Normal file
@ -0,0 +1,19 @@
|
||||
package circuitbreaker
|
||||
|
||||
type Options struct {
|
||||
TemplateDir string
|
||||
}
|
||||
|
||||
type OptionFunc func(*Options)
|
||||
|
||||
func defaultOptions() *Options {
|
||||
return &Options{
|
||||
TemplateDir: "./templates",
|
||||
}
|
||||
}
|
||||
|
||||
func WithTemplateDir(templateDir string) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.TemplateDir = templateDir
|
||||
}
|
||||
}
|
8
internal/proxy/director/layer/circuitbreaker/schema.go
Normal file
8
internal/proxy/director/layer/circuitbreaker/schema.go
Normal file
@ -0,0 +1,8 @@
|
||||
package circuitbreaker
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
21
internal/setup/circuitbreaker_layer.go
Normal file
21
internal/setup/circuitbreaker_layer.go
Normal file
@ -0,0 +1,21 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/circuitbreaker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterLayer(circuitbreaker.LayerType, setupCircuitBreakerLayer, circuitbreaker.RawLayerOptionsSchema)
|
||||
}
|
||||
|
||||
func setupCircuitBreakerLayer(conf *config.Config) (director.Layer, error) {
|
||||
options := []circuitbreaker.OptionFunc{
|
||||
circuitbreaker.WithTemplateDir(string(conf.Layers.CircuitBreaker.TemplateDir)),
|
||||
}
|
||||
|
||||
return circuitbreaker.New(
|
||||
options...,
|
||||
), nil
|
||||
}
|
Reference in New Issue
Block a user