Compare commits
5 Commits
v2024.5.28
...
v2024.5.29
Author | SHA1 | Date | |
---|---|---|---|
2952f68720 | |||
3e98901931 | |||
d667bb03f5 | |||
3a9fde9bc9 | |||
42dab5797a |
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,3 +9,5 @@
|
|||||||
/data
|
/data
|
||||||
/out
|
/out
|
||||||
.dockerconfigjson
|
.dockerconfigjson
|
||||||
|
*.prof
|
||||||
|
proxy.test
|
@ -50,10 +50,12 @@ EXPOSE 8080
|
|||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|
||||||
RUN adduser -D -H bouncer
|
RUN adduser -D -s /bin/sh bouncer
|
||||||
|
|
||||||
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
|
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
|
||||||
|
|
||||||
USER bouncer
|
USER bouncer
|
||||||
|
|
||||||
|
WORKDIR /home/bouncer
|
||||||
|
|
||||||
CMD ["bouncer"]
|
CMD ["bouncer"]
|
7
Makefile
7
Makefile
@ -130,6 +130,13 @@ tools/grafterm/bin/grafterm:
|
|||||||
mkdir -p tools/grafterm/bin
|
mkdir -p tools/grafterm/bin
|
||||||
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
|
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
|
||||||
|
|
||||||
|
bench:
|
||||||
|
go test -bench=. -run '^$$' -count=10 ./...
|
||||||
|
|
||||||
|
tools/benchstat/bin/benchstat:
|
||||||
|
mkdir -p tools/benchstat/bin
|
||||||
|
GOBIN=$(PWD)/tools/benchstat/bin go install golang.org/x/perf/cmd/benchstat@latest
|
||||||
|
|
||||||
full-version:
|
full-version:
|
||||||
@echo $(FULL_VERSION)
|
@echo $(FULL_VERSION)
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ Par défaut ce serveur écoute sur le port 8082. Il est possible de modifier l'a
|
|||||||
bouncer admin proxy update --proxy-name my-proxy --proxy-enabled
|
bouncer admin proxy update --proxy-name my-proxy --proxy-enabled
|
||||||
```
|
```
|
||||||
|
|
||||||
3. À ce stade, vous devriez pouvoir afficher la page du serveur `dummy` en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut
|
3. À ce stade, vous devriez pouvoir afficher la page du serveur `dummy` en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous travaillez avec une instance Bouncer locale avec la configuration par défaut
|
||||||
|
|
||||||
4. Créer un layer de type `authn-oidc` pour notre nouveau proxy
|
4. Créer un layer de type `authn-oidc` pour notre nouveau proxy
|
||||||
|
|
||||||
|
31
doc/fr/tutorials/profiling.md
Normal file
31
doc/fr/tutorials/profiling.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Analyser les performances de Bouncer
|
||||||
|
|
||||||
|
1. Lancer un benchmark du proxy
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go test -bench=. -run '^$' -count=5 -cpuprofile bench_proxy.prof ./internal/proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Visualiser les temps d'exécution
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go tool pprof -web bench_proxy.prof
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Comparer les performances d'une exécution à l'autre
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Lancer un premier benchmark
|
||||||
|
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_before.txt
|
||||||
|
|
||||||
|
# Faire des modifications sur les sources
|
||||||
|
|
||||||
|
# Lancer un second benchmark
|
||||||
|
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_after.txt
|
||||||
|
|
||||||
|
# Installer l'outil benchstat
|
||||||
|
make tools/benchstat/bin/benchstat
|
||||||
|
|
||||||
|
# Comparer les rapports
|
||||||
|
tools/benchstat/bin/benchstat bench_before.txt bench_after.txt
|
||||||
|
```
|
1
go.mod
1
go.mod
@ -93,6 +93,7 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.21.0 // indirect
|
go.opentelemetry.io/otel v1.21.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -405,8 +405,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
6
internal/cache/cache.go
vendored
Normal file
6
internal/cache/cache.go
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
type Cache[K comparable, V any] interface {
|
||||||
|
Get(key K) (V, bool)
|
||||||
|
Set(key K, value V)
|
||||||
|
}
|
34
internal/cache/memory/cache.go
vendored
Normal file
34
internal/cache/memory/cache.go
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
cache "forge.cadoles.com/cadoles/bouncer/internal/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache[K comparable, V any] struct {
|
||||||
|
store *sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements cache.Cache.
|
||||||
|
func (c *Cache[K, V]) Get(key K) (V, bool) {
|
||||||
|
raw, exists := c.store.Load(key)
|
||||||
|
if !exists {
|
||||||
|
return *new(V), false
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw.(V), exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set implements cache.Cache.
|
||||||
|
func (c *Cache[K, V]) Set(key K, value V) {
|
||||||
|
c.store.Store(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache[K comparable, V any]() *Cache[K, V] {
|
||||||
|
return &Cache[K, V]{
|
||||||
|
store: new(sync.Map),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cache.Cache[string, bool] = &Cache[string, bool]{}
|
39
internal/cache/ttl/cache.go
vendored
Normal file
39
internal/cache/ttl/cache.go
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package ttl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cache "forge.cadoles.com/cadoles/bouncer/internal/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache[K comparable, V any] struct {
|
||||||
|
timestamps cache.Cache[K, time.Time]
|
||||||
|
values cache.Cache[K, V]
|
||||||
|
ttl time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements cache.Cache.
|
||||||
|
func (c *Cache[K, V]) Get(key K) (V, bool) {
|
||||||
|
timestamp, exists := c.timestamps.Get(key)
|
||||||
|
if !exists || timestamp.Add(c.ttl).Before(time.Now()) {
|
||||||
|
return *new(V), false
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.values.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set implements cache.Cache.
|
||||||
|
func (c *Cache[K, V]) Set(key K, value V) {
|
||||||
|
c.timestamps.Set(key, time.Now())
|
||||||
|
c.values.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache[K comparable, V any](values cache.Cache[K, V], timestamps cache.Cache[K, time.Time], ttl time.Duration) *Cache[K, V] {
|
||||||
|
return &Cache[K, V]{
|
||||||
|
values: values,
|
||||||
|
timestamps: timestamps,
|
||||||
|
ttl: ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cache.Cache[string, bool] = &Cache[string, bool]{}
|
39
internal/cache/ttl/cache_test.go
vendored
Normal file
39
internal/cache/ttl/cache_test.go
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package ttl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCache(t *testing.T) {
|
||||||
|
cache := NewCache(
|
||||||
|
memory.NewCache[string, int](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
time.Second,
|
||||||
|
)
|
||||||
|
|
||||||
|
key := "foo"
|
||||||
|
|
||||||
|
if _, exists := cache.Get(key); exists {
|
||||||
|
t.Errorf("cache.Get(\"%s\"): should not exists", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.Set(key, 1)
|
||||||
|
|
||||||
|
value, exists := cache.Get(key)
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("cache.Get(\"%s\"): should exists", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, value; e != g {
|
||||||
|
t.Errorf("cache.Get(\"%s\"): expected '%v', got '%v'", key, e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
if _, exists := cache.Get("foo"); exists {
|
||||||
|
t.Errorf("cache.Get(\"%s\"): should not exists", key)
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package proxy
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy"
|
||||||
@ -45,6 +46,7 @@ func RunCommand() *cli.Command {
|
|||||||
proxy.WithServerConfig(conf.Proxy),
|
proxy.WithServerConfig(conf.Proxy),
|
||||||
proxy.WithRedisConfig(conf.Redis),
|
proxy.WithRedisConfig(conf.Redis),
|
||||||
proxy.WithDirectorLayers(layers...),
|
proxy.WithDirectorLayers(layers...),
|
||||||
|
proxy.WithDirectorCacheTTL(time.Duration(conf.Proxy.Cache.TTL)),
|
||||||
)
|
)
|
||||||
|
|
||||||
addrs, srvErrs := srv.Start(ctx.Context)
|
addrs, srvErrs := srv.Start(ctx.Context)
|
||||||
|
@ -206,7 +206,12 @@ func (id *InterpolatedDuration) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
|
|
||||||
duration, err := time.ParseDuration(str)
|
duration, err := time.ParseDuration(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "could not parse duration '%v', line '%d'", str, value.Line)
|
nanoseconds, err := strconv.ParseInt(str, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not parse duration '%v', line '%d'", str, value.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration = time.Duration(nanoseconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
*id = InterpolatedDuration(duration)
|
*id = InterpolatedDuration(duration)
|
||||||
|
@ -12,6 +12,18 @@ type ProxyServerConfig struct {
|
|||||||
Transport TransportConfig `yaml:"transport"`
|
Transport TransportConfig `yaml:"transport"`
|
||||||
Dial DialConfig `yaml:"dial"`
|
Dial DialConfig `yaml:"dial"`
|
||||||
Sentry SentryConfig `yaml:"sentry"`
|
Sentry SentryConfig `yaml:"sentry"`
|
||||||
|
Cache CacheConfig `yaml:"cache"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultProxyServerConfig() ProxyServerConfig {
|
||||||
|
return ProxyServerConfig{
|
||||||
|
HTTP: NewHTTPConfig("0.0.0.0", 8080),
|
||||||
|
Metrics: NewDefaultMetricsConfig(),
|
||||||
|
Transport: NewDefaultTransportConfig(),
|
||||||
|
Dial: NewDefaultDialConfig(),
|
||||||
|
Sentry: NewDefaultSentryConfig(),
|
||||||
|
Cache: NewDefaultCacheConfig(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://pkg.go.dev/net/http#Transport
|
// See https://pkg.go.dev/net/http#Transport
|
||||||
@ -58,13 +70,22 @@ func (c TransportConfig) AsTransport() *http.Transport {
|
|||||||
return httpTransport
|
return httpTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultProxyServerConfig() ProxyServerConfig {
|
func NewDefaultTransportConfig() TransportConfig {
|
||||||
return ProxyServerConfig{
|
return TransportConfig{
|
||||||
HTTP: NewHTTPConfig("0.0.0.0", 8080),
|
ForceAttemptHTTP2: true,
|
||||||
Metrics: NewDefaultMetricsConfig(),
|
MaxIdleConns: 100,
|
||||||
Transport: NewDefaultTransportConfig(),
|
MaxIdleConnsPerHost: 100,
|
||||||
Dial: NewDefaultDialConfig(),
|
MaxConnsPerHost: 100,
|
||||||
Sentry: NewDefaultSentryConfig(),
|
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second),
|
||||||
|
TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
|
||||||
|
ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
|
||||||
|
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
|
||||||
|
DisableCompression: false,
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
ReadBufferSize: 4096,
|
||||||
|
WriteBufferSize: 4096,
|
||||||
|
MaxResponseHeaderBytes: 0,
|
||||||
|
InsecureSkipVerify: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,21 +106,12 @@ func NewDefaultDialConfig() DialConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultTransportConfig() TransportConfig {
|
type CacheConfig struct {
|
||||||
return TransportConfig{
|
TTL InterpolatedDuration `yaml:"ttl"`
|
||||||
ForceAttemptHTTP2: true,
|
}
|
||||||
MaxIdleConns: 100,
|
|
||||||
MaxIdleConnsPerHost: 100,
|
func NewDefaultCacheConfig() CacheConfig {
|
||||||
MaxConnsPerHost: 100,
|
return CacheConfig{
|
||||||
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second),
|
TTL: *NewInterpolatedDuration(time.Second * 30),
|
||||||
TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
|
|
||||||
ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
|
|
||||||
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
|
|
||||||
DisableCompression: false,
|
|
||||||
DisableKeepAlives: false,
|
|
||||||
ReadBufferSize: 4096,
|
|
||||||
WriteBufferSize: 4096,
|
|
||||||
MaxResponseHeaderBytes: 0,
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"forge.cadoles.com/Cadoles/go-proxy"
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@ -17,6 +18,9 @@ type Director struct {
|
|||||||
proxyRepository store.ProxyRepository
|
proxyRepository store.ProxyRepository
|
||||||
layerRepository store.LayerRepository
|
layerRepository store.LayerRepository
|
||||||
layerRegistry *LayerRegistry
|
layerRegistry *LayerRegistry
|
||||||
|
|
||||||
|
proxyCache cache.Cache[string, []*store.Proxy]
|
||||||
|
layerCache cache.Cache[string, []*store.Layer]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
|
func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
|
||||||
@ -88,7 +92,14 @@ MAIN:
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proxiesCacheKey = "proxies"
|
||||||
|
|
||||||
func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
|
func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
|
||||||
|
proxies, exists := d.proxyCache.Get(proxiesCacheKey)
|
||||||
|
if exists {
|
||||||
|
return proxies, nil
|
||||||
|
}
|
||||||
|
|
||||||
headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true))
|
headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
@ -96,7 +107,7 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
|
|||||||
|
|
||||||
sort.Sort(store.ByProxyWeight(headers))
|
sort.Sort(store.ByProxyWeight(headers))
|
||||||
|
|
||||||
proxies := make([]*store.Proxy, 0, len(headers))
|
proxies = make([]*store.Proxy, 0, len(headers))
|
||||||
|
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
if !h.Enabled {
|
if !h.Enabled {
|
||||||
@ -111,10 +122,19 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
|
|||||||
proxies = append(proxies, proxy)
|
proxies = append(proxies, proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.proxyCache.Set(proxiesCacheKey, proxies)
|
||||||
|
|
||||||
return proxies, nil
|
return proxies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) {
|
func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) {
|
||||||
|
cacheKey := "layers-" + string(proxyName)
|
||||||
|
|
||||||
|
layers, exists := d.layerCache.Get(cacheKey)
|
||||||
|
if exists {
|
||||||
|
return layers, nil
|
||||||
|
}
|
||||||
|
|
||||||
headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true))
|
headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
@ -122,7 +142,7 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
|
|||||||
|
|
||||||
sort.Sort(store.ByLayerWeight(headers))
|
sort.Sort(store.ByLayerWeight(headers))
|
||||||
|
|
||||||
layers := make([]*store.Layer, 0, len(headers))
|
layers = make([]*store.Layer, 0, len(headers))
|
||||||
|
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
if !h.Enabled {
|
if !h.Enabled {
|
||||||
@ -137,6 +157,8 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
|
|||||||
layers = append(layers, layer)
|
layers = append(layers, layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.layerCache.Set(cacheKey, layers)
|
||||||
|
|
||||||
return layers, nil
|
return layers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,8 +262,16 @@ func (d *Director) Middleware() proxy.Middleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, layers ...Layer) *Director {
|
func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, funcs ...OptionFunc) *Director {
|
||||||
registry := NewLayerRegistry(layers...)
|
opts := NewOptions(funcs...)
|
||||||
|
|
||||||
return &Director{proxyRepository, layerRepository, registry}
|
registry := NewLayerRegistry(opts.Layers...)
|
||||||
|
|
||||||
|
return &Director{
|
||||||
|
proxyRepository: proxyRepository,
|
||||||
|
layerRepository: layerRepository,
|
||||||
|
layerRegistry: registry,
|
||||||
|
proxyCache: opts.ProxyCache,
|
||||||
|
layerCache: opts.LayerCache,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
58
internal/proxy/director/options.go
Normal file
58
internal/proxy/director/options.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package director
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Layers []Layer
|
||||||
|
ProxyCache cache.Cache[string, []*store.Proxy]
|
||||||
|
LayerCache cache.Cache[string, []*store.Layer]
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(opts *Options)
|
||||||
|
|
||||||
|
func NewOptions(funcs ...OptionFunc) *Options {
|
||||||
|
opts := &Options{
|
||||||
|
Layers: make([]Layer, 0),
|
||||||
|
ProxyCache: ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Proxy](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
30*time.Second,
|
||||||
|
),
|
||||||
|
LayerCache: ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Layer](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
30*time.Second,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLayers(layers ...Layer) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.Layers = layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithProxyCache(cache cache.Cache[string, []*store.Proxy]) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.ProxyCache = cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLayerCache(cache cache.Cache[string, []*store.Layer]) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.LayerCache = cache
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,27 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
ServerConfig config.ProxyServerConfig
|
ServerConfig config.ProxyServerConfig
|
||||||
RedisConfig config.RedisConfig
|
RedisConfig config.RedisConfig
|
||||||
DirectorLayers []director.Layer
|
DirectorLayers []director.Layer
|
||||||
|
DirectorCacheTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(*Option)
|
type OptionFunc func(*Option)
|
||||||
|
|
||||||
func defaultOption() *Option {
|
func defaultOption() *Option {
|
||||||
return &Option{
|
return &Option{
|
||||||
ServerConfig: config.NewDefaultProxyServerConfig(),
|
ServerConfig: config.NewDefaultProxyServerConfig(),
|
||||||
RedisConfig: config.NewDefaultRedisConfig(),
|
RedisConfig: config.NewDefaultRedisConfig(),
|
||||||
DirectorLayers: make([]director.Layer, 0),
|
DirectorLayers: make([]director.Layer, 0),
|
||||||
|
DirectorCacheTTL: 30 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,3 +42,9 @@ func WithDirectorLayers(layers ...director.Layer) OptionFunc {
|
|||||||
opt.DirectorLayers = layers
|
opt.DirectorLayers = layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithDirectorCacheTTL(ttl time.Duration) OptionFunc {
|
||||||
|
return func(opt *Option) {
|
||||||
|
opt.DirectorCacheTTL = ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
156
internal/proxy/proxy_test.go
Normal file
156
internal/proxy/proxy_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
package proxy_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkProxy(b *testing.B) {
|
||||||
|
redisEndpoint := os.Getenv("BOUNCER_BENCH_REDIS_ADDR")
|
||||||
|
if redisEndpoint == "" {
|
||||||
|
redisEndpoint = "127.0.0.1:6379"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||||
|
Addrs: []string{redisEndpoint},
|
||||||
|
})
|
||||||
|
|
||||||
|
proxyRepository := redisStore.NewProxyRepository(client)
|
||||||
|
layerRepository := redisStore.NewLayerRepository(client)
|
||||||
|
|
||||||
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
if _, err := w.Write([]byte("Hello, world.")); err != nil {
|
||||||
|
b.Logf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer backend.Close()
|
||||||
|
|
||||||
|
if err := waitFor(backend.URL, 5*time.Second); err != nil {
|
||||||
|
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Logf("started backend '%s'", backend.URL)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
proxyName := store.ProxyName(b.Name())
|
||||||
|
|
||||||
|
b.Logf("creating proxy '%s'", proxyName)
|
||||||
|
|
||||||
|
if err := proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
|
||||||
|
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := proxyRepository.CreateProxy(ctx, proxyName, backend.URL, "*"); err != nil {
|
||||||
|
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := proxyRepository.UpdateProxy(ctx, proxyName, store.WithProxyUpdateEnabled(true)); err != nil {
|
||||||
|
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
director := director.New(
|
||||||
|
proxyRepository, layerRepository,
|
||||||
|
director.WithLayerCache(
|
||||||
|
ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Layer](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
30*time.Second,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
director.WithProxyCache(
|
||||||
|
ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Proxy](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
30*time.Second,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
directorMiddleware := director.Middleware()
|
||||||
|
|
||||||
|
handler := proxy.New(
|
||||||
|
proxy.WithRequestTransformers(
|
||||||
|
director.RequestTransformer(),
|
||||||
|
),
|
||||||
|
proxy.WithResponseTransformers(
|
||||||
|
director.ResponseTransformer(),
|
||||||
|
),
|
||||||
|
proxy.WithReverseProxyFactory(func(ctx context.Context, target *url.URL) *httputil.ReverseProxy {
|
||||||
|
reverse := httputil.NewSingleHostReverseProxy(target)
|
||||||
|
reverse.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
b.Logf("[ERROR] %s", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
return reverse
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
server := httptest.NewServer(directorMiddleware(handler))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
b.Logf("started proxy '%s'", server.URL)
|
||||||
|
|
||||||
|
httpClient := server.Client()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
res, err := httpClient.Get(server.URL)
|
||||||
|
if err != nil {
|
||||||
|
b.Errorf("could not fetch server url: %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
b.Errorf("could not read response body: %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Logf("%s - %v", res.Status, string(body))
|
||||||
|
|
||||||
|
if err := res.Body.Close(); err != nil {
|
||||||
|
b.Errorf("could not close response body: %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitFor(url string, ttl time.Duration) error {
|
||||||
|
var lastErr error
|
||||||
|
timeout := time.After(ttl)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
if lastErr != nil {
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("wait timed out")
|
||||||
|
default:
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = errors.WithStack(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 200 && res.StatusCode < 400 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/go-proxy"
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
|
||||||
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
|
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
@ -25,11 +27,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
serverConfig config.ProxyServerConfig
|
serverConfig config.ProxyServerConfig
|
||||||
redisConfig config.RedisConfig
|
redisConfig config.RedisConfig
|
||||||
directorLayers []director.Layer
|
directorLayers []director.Layer
|
||||||
proxyRepository store.ProxyRepository
|
directorCacheTTL time.Duration
|
||||||
layerRepository store.LayerRepository
|
proxyRepository store.ProxyRepository
|
||||||
|
layerRepository store.LayerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||||
@ -86,7 +89,21 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
|||||||
director := director.New(
|
director := director.New(
|
||||||
s.proxyRepository,
|
s.proxyRepository,
|
||||||
s.layerRepository,
|
s.layerRepository,
|
||||||
s.directorLayers...,
|
director.WithLayers(s.directorLayers...),
|
||||||
|
director.WithLayerCache(
|
||||||
|
ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Layer](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
s.directorCacheTTL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
director.WithProxyCache(
|
||||||
|
ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Proxy](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
s.directorCacheTTL,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if s.serverConfig.HTTP.UseRealIP {
|
if s.serverConfig.HTTP.UseRealIP {
|
||||||
@ -184,8 +201,9 @@ func NewServer(funcs ...OptionFunc) *Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
serverConfig: opt.ServerConfig,
|
serverConfig: opt.ServerConfig,
|
||||||
redisConfig: opt.RedisConfig,
|
redisConfig: opt.RedisConfig,
|
||||||
directorLayers: opt.DirectorLayers,
|
directorLayers: opt.DirectorLayers,
|
||||||
|
directorCacheTTL: opt.DirectorCacheTTL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,25 +13,31 @@ Le répertoire [`misc/docker-compose`](./) contient un exemple de déploiement d
|
|||||||
|
|
||||||
## Étapes
|
## Étapes
|
||||||
|
|
||||||
1. Se positionner dans le répertoire puis lancer l'environnement avec la commande `docker-compose`:
|
1. Se positionner dans le répertoire puis lancer l'environnement avec la commande `docker compose`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd misc/docker-compose
|
cd misc/docker-compose
|
||||||
docker-compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Entrer dans le conteneur `bouncer-admin` puis créer un jeton d'accès:
|
2. Entrer dans le conteneur `bouncer-admin` puis créer un jeton d'accès:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose exec bouncer-admin /bin/sh
|
docker compose exec bouncer-admin /bin/sh
|
||||||
bouncer auth create-token --role writer > .bouncer-token
|
bouncer auth create-token --role writer > .bouncer-token
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Créer un proxy via le CLI:
|
3. Créer un proxy via le CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bouncer admin proxy create --proxy-name myproxy --proxy-to "https://www.cadoles.com/"
|
bouncer admin proxy create --proxy-name myproxy --proxy-to "https://www.cadoles.com/"
|
||||||
bouncer admin proxy update --proxy-name myproxy --proxy-enabled=true
|
bouncer admin proxy update --proxy-name myproxy --proxy-enabled=true
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Via votre navigateur, accéder à l'URL http://127.0.0.1:8080. La page du site Cadoles devrait s'afficher. Dans le log de la commande `docker-compose up` vous devriez voir que les requêtes sont routées à tour de rôle sur les 3 instances de Bouncer en exécution.
|
4. Via votre navigateur, accéder à l'URL http://127.0.0.1:8080. La page du site Cadoles devrait s'afficher. Dans le log de la commande `docker-compose up` vous devriez voir que les requêtes sont routées à tour de rôle sur les 3 instances de Bouncer en exécution.
|
||||||
|
|
||||||
|
5. Stopper l'environnement:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose down -v
|
||||||
|
```
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
version: "2"
|
|
||||||
services:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
image: reg.cadoles.com/proxy_cache/library/haproxy:2.7-alpine
|
image: reg.cadoles.com/proxy_cache/library/haproxy:2.7-alpine
|
||||||
@ -31,7 +30,7 @@ services:
|
|||||||
|
|
||||||
bouncer-proxy-2: *bouncer-proxy
|
bouncer-proxy-2: *bouncer-proxy
|
||||||
bouncer-proxy-3: *bouncer-proxy
|
bouncer-proxy-3: *bouncer-proxy
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: reg.cadoles.com/proxy_cache/library/redis:7-alpine
|
image: reg.cadoles.com/proxy_cache/library/redis:7-alpine
|
||||||
command: redis-server --save 60 1 --loglevel verbose
|
command: redis-server --save 60 1 --loglevel verbose
|
||||||
@ -39,4 +38,4 @@ services:
|
|||||||
- redis-data:/data
|
- redis-data:/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis-data:
|
redis-data:
|
||||||
|
@ -93,6 +93,14 @@ proxy:
|
|||||||
credentials:
|
credentials:
|
||||||
prom: etheus
|
prom: etheus
|
||||||
|
|
||||||
|
# Configuration de la mise en cache
|
||||||
|
# locale des données proxy/layers
|
||||||
|
cache:
|
||||||
|
# Les proxys/layers sont mis en cache local pour une durée de 30s
|
||||||
|
# par défaut. Si les modifications sont rares, vous pouvez augmenter
|
||||||
|
# cette valeur pour réduire la "pression" sur le serveur Redis.
|
||||||
|
ttl: 30s
|
||||||
|
|
||||||
# Configuration du transport HTTP(S)
|
# Configuration du transport HTTP(S)
|
||||||
# Voir https://pkg.go.dev/net/http#Transport
|
# Voir https://pkg.go.dev/net/http#Transport
|
||||||
transport:
|
transport:
|
||||||
|
Reference in New Issue
Block a user