Compare commits

...

5 Commits

Author SHA1 Message Date
bb5796ab8c doc: add layer endpoints
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-04-19 09:28:46 +02:00
83fcb9a39d feat: add limited retry mechanism to prevent startup error if redis is not ready
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-04-05 10:30:34 +02:00
ad907576dc fix: move log message
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-03-29 11:13:05 +01:00
3a894972f1 doc: enable json highlighting in reference examples
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-03-29 09:36:36 +01:00
274bef13d8 feat: match proxy's from on whole targeted url
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-03-29 09:21:01 +01:00
11 changed files with 350 additions and 99 deletions

View File

@ -5,14 +5,16 @@
### Déploiement mono-noeud ### Déploiement mono-noeud
![](../resources/deployment_fr.png) ![](../resources/deployment_fr.png)
## Terminologie ## Terminologie
Voici une liste des termes utilisés dans le lexique Bouncer. Voici une liste des termes utilisés dans le lexique Bouncer.
### Proxy ### Proxy
Un "proxy" est une entité logique définissant le relation suivante: Un "proxy" est une entité logique définissant le relation suivante:
- Un ou plusieurs patrons de filtrage sous la forme `<host>:<port>`. Ceux ci identifient le ou les domaines associés à l'entité; - Un ou plusieurs patrons de filtrage sous la forme d'un patron d'URL avec le caractère `*` comme caractère générique. Ceux ci identifient le ou les domaines/chemins associés à l'entité;
- Une URL cible qui servira de base pour la réécriture des requêtes. - Une URL cible qui servira de base pour la réécriture des requêtes.
Un "proxy" peut avoir zéro ou plusieurs "layers" associés. Un "proxy" peut avoir zéro ou plusieurs "layers" associés.

View File

@ -22,9 +22,8 @@ Où:
- `"<subject>"` est une chaîne de caractère arbitraire ayant pour objectif d'identifier de manière unique l'utilisateur associé au jeton; - `"<subject>"` est une chaîne de caractère arbitraire ayant pour objectif d'identifier de manière unique l'utilisateur associé au jeton;
- `"<role>"` peut prendre une des deux valeurs `reader` ou `writer` correspondant aux droits suivants respectifs: - `"<role>"` peut prendre une des deux valeurs `reader` ou `writer` correspondant aux droits suivants respectifs:
- droit en lecture sur l'ensemble des entités (proxy, layer); - droit en lecture sur l'ensemble des entités (proxy, layer);
- droit en lecture ET en écriture sur l'ensemble des entités. - droit en lecture ET en écriture sur l'ensemble des entités.
## Points d'entrée ## Points d'entrée
@ -34,29 +33,29 @@ Créer un nouveau proxy
#### Exemple de corps de requête #### Exemple de corps de requête
```json5 ```json
{ {
"name": "myproxy", // OBLIGATOIRE - Nom du proxy "name": "myproxy", // OBLIGATOIRE - Nom du proxy
"to": "https://www.cadoles.com", // OBLIGATOIRE - Site distant ciblé par le proxy "to": "https://www.cadoles.com", // OBLIGATOIRE - Site distant ciblé par le proxy
"from": ["*"] // OPTIONNEL - Liste de patrons de filtrage associés au proxy "from": ["*"] // OPTIONNEL - Liste de patrons de filtrage associés au proxy
} }
``` ```
#### Exemple de résultat #### Exemple de résultat
```json5 ```json
{ {
"data": { "data": {
"proxy": { "proxy": {
"name": "myproxy", "name": "myproxy",
"weight": 0, "weight": 0,
"enabled": false, "enabled": false,
"to": "https://www.cadoles.com", "to": "https://www.cadoles.com",
"from": ["*"], "from": ["*"],
"createdAt": "2018-12-10T13:45:00.000Z", "createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z" "updatedAt": "2018-12-10T13:45:00.000Z"
}
} }
}
} }
``` ```
@ -74,19 +73,19 @@ Récupérer les informations complètes sur un proxy
#### Exemple de résultat #### Exemple de résultat
```json5 ```json
{ {
"data": { "data": {
"proxy": { "proxy": {
"name": "myproxy", "name": "myproxy",
"weight": 0, "weight": 0,
"enabled": false, "enabled": false,
"to": "https://www.cadoles.com", "to": "https://www.cadoles.com",
"from": ["*"], "from": ["*"],
"createdAt": "2018-12-10T13:45:00.000Z", "createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z" "updatedAt": "2018-12-10T13:45:00.000Z"
}
} }
}
} }
``` ```
@ -100,30 +99,30 @@ Modifier un proxy
#### Exemple de corps de requête #### Exemple de corps de requête
```json5 ```json
{ {
"to": "https://www.cadoles.com", // OPTIONNEL - Site distant ciblé par le proxy "to": "https://www.cadoles.com", // OPTIONNEL - Site distant ciblé par le proxy
"from": ["mylocalproxydomain:*"], // OPTIONNEL - Liste de patrons de filtrage associés au proxy "from": ["mylocalproxydomain:*"], // OPTIONNEL - Liste de patrons de filtrage associés au proxy
"weight": 100, // OPTIONNEL - Poids à associer au proxy "weight": 100, // OPTIONNEL - Poids à associer au proxy
"enabled": true, // OPTIONNEL - Activer/désactiver le proxy "enabled": true // OPTIONNEL - Activer/désactiver le proxy
} }
``` ```
#### Exemple de résultat #### Exemple de résultat
```json5 ```json
{ {
"data": { "data": {
"proxy": { "proxy": {
"name": "myproxy", "name": "myproxy",
"weight": 100, "weight": 100,
"enabled": true, "enabled": true,
"to": "https://www.cadoles.com", "to": "https://www.cadoles.com",
"from": ["mylocalproxydomain:*"], "from": ["mylocalproxydomain:*"],
"createdAt": "2018-12-10T13:45:00.000Z", "createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2020-10-02T15:09:00.000Z" "updatedAt": "2020-10-02T15:09:00.000Z"
}
} }
}
} }
``` ```
@ -141,17 +140,19 @@ Lister les proxies existants
#### Exemple de résultat #### Exemple de résultat
```json5 ```json
{ {
"data": { "data": {
"proxies": [ "proxies": [
{ {
"name": "myproxy", "name": "myproxy",
"weight": 0, "weight": 0,
"enabled": false, "enabled": false,
} "createdAt": "2018-12-10T13:45:00.000Z",
] "updatedAt": "2018-12-10T13:45:00.000Z"
} }
]
}
} }
``` ```
@ -169,14 +170,180 @@ Supprimer le proxy
#### Exemple de résultat #### Exemple de résultat
```json5 ```json
{ {
"data": { "data": {
"proxyName": "myproxy" "proxyName": "myproxy"
} }
} }
``` ```
#### Source #### Source
Voir [`internal/admin/proxy_route.go#deleteProxy()`](../../../internal/admin/proxy_route.go#deleteProxy) Voir [`internal/admin/proxy_route.go#deleteProxy()`](../../../internal/admin/proxy_route.go#deleteProxy)
### `POST /api/v1/proxies/{proxyName}/layers`
Créer un nouveau layer pour un proxy donné
#### Paramètres
- `{proxyName}` - Nom du proxy sur lequel créer le layer
#### Exemple de corps de requête
```json
{
"name": "mylayer", // OBLIGATOIRE - Nom du layer
"type": "<layer_type>", // OBLIGATOIRE - Type du layer, voir doc/fr/references/layers
"options": {} // OPTIONNEL - Options associées au layer, voir doc/fr/references/layers
}
```
#### Exemple de résultat
```json
{
"data": {
"layer": {
"name": "mylayer",
"type": "<layer_type>",
"enabled": false,
"weight": 0,
"options": {},
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
}
}
```
#### Source
Voir [`internal/admin/layer_route.go#createLayer()`](../../../internal/admin/layer_route.go#createLayer)
### `GET /api/v1/proxies/{proxyName}/layers/{layerName}`
Récupérer les informations complètes sur un layer
#### Paramètres
- `{proxyName}` - Nom du proxy parent
- `{layerName}` - Nom du layer
#### Exemple de résultat
```json
{
"data": {
"layer": {
"name": "mylayer",
"type": "<layer_type>",
"enabled": false,
"weight": 0,
"options": {},
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
}
}
```
#### Source
Voir [`internal/admin/layer_route.go#getLayer()`](../../../internal/admin/layer_route.go#getLayer)
### `PUT /api/v1/proxies/{proxyName}/layers/{layerName}`
Modifier un layer
#### Paramètres
- `{proxyName}` - Nom du proxy parent
- `{layerName}` - Nom du layer
#### Exemple de corps de requête
```json
{
"weight": 100, // OPTIONNEL - Poids à associer au layer
"enabled": true, // OPTIONNEL - Activer/désactiver le layer
"options": {} // OPTIONNEL - Modifier les options associées au layer, voir doc/fr/references/layers
}
```
#### Exemple de résultat
```json
{
"data": {
"layer": {
"name": "mylayer",
"type": "<layer_type>",
"enabled": false,
"weight": 0,
"options": {},
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
}
}
```
#### Source
Voir [`internal/admin/layer_route.go#updateLayer()`](../../../internal/admin/layer_route.go#updateLayer)
### `GET /api/v1/proxies/{proxyName}/layers?names={name1,name2,...}`
Lister les layers existants
#### Paramètres
- `{proxyName}` - Nom du proxy parent
- `{names}` - Optionnel - Liste des noms de proxy à appliquer en tant que filtre
#### Exemple de résultat
```json
{
"data": {
"layers": [
{
"name": "mylayer",
"weight": 0,
"enabled": false,
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
]
}
}
```
#### Source
Voir [`internal/admin/layer_route.go#queryLayers()`](../../../internal/admin/layer_route.go#queryLayers)
## `DELETE /api/v1/proxies/{proxyName}/layers/{layerName}`
Supprimer le layer
#### Paramètres
- `{proxyName}` - Nom du proxy parent
- `{layerName}` - Nom du layer
#### Exemple de résultat
```json
{
"data": {
"layerName": "mylayer"
}
}
```
#### Source
Voir [`internal/admin/layer_route.go#deleteLayer()`](../../../internal/admin/layer_route.go#deleteLayer)

View File

@ -22,7 +22,7 @@ func (s *Server) bootstrapProxies(ctx context.Context) error {
layerRepo := s.layerRepository layerRepo := s.layerRepository
lockTimeout := time.Duration(s.bootstrapConfig.LockTimeout) lockTimeout := time.Duration(s.bootstrapConfig.LockTimeout)
locker := redis.NewLocker(s.redisClient) locker := redis.NewLocker(s.redisClient, int(s.bootstrapConfig.MaxConnectionRetries))
err := locker.WithLock(ctx, "bouncer-admin-bootstrap", lockTimeout, func(ctx context.Context) error { err := locker.WithLock(ctx, "bouncer-admin-bootstrap", lockTimeout, func(ctx context.Context) error {
logger.Info(ctx, "bootstrapping proxies") logger.Info(ctx, "bootstrapping proxies")

View File

@ -12,9 +12,10 @@ import (
) )
type BootstrapConfig struct { type BootstrapConfig struct {
Proxies map[store.ProxyName]BootstrapProxyConfig `yaml:"proxies"` Proxies map[store.ProxyName]BootstrapProxyConfig `yaml:"proxies"`
Dir InterpolatedString `yaml:"dir"` Dir InterpolatedString `yaml:"dir"`
LockTimeout InterpolatedDuration `yaml:"lockTimeout"` LockTimeout InterpolatedDuration `yaml:"lockTimeout"`
MaxConnectionRetries InterpolatedInt `yaml:"maxRetries"`
} }
func (c *BootstrapConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { func (c *BootstrapConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
@ -62,8 +63,9 @@ type BootstrapLayerConfig struct {
func NewDefaultBootstrapConfig() BootstrapConfig { func NewDefaultBootstrapConfig() BootstrapConfig {
return BootstrapConfig{ return BootstrapConfig{
Dir: "", Dir: "",
LockTimeout: *NewInterpolatedDuration(30 * time.Second), LockTimeout: *NewInterpolatedDuration(30 * time.Second),
MaxConnectionRetries: 10,
} }
} }

View File

@ -9,19 +9,21 @@ const (
) )
type RedisConfig struct { type RedisConfig struct {
Adresses InterpolatedStringSlice `yaml:"addresses"` Adresses InterpolatedStringSlice `yaml:"addresses"`
Master InterpolatedString `yaml:"master"` Master InterpolatedString `yaml:"master"`
ReadTimeout InterpolatedDuration `yaml:"readTimeout"` ReadTimeout InterpolatedDuration `yaml:"readTimeout"`
WriteTimeout InterpolatedDuration `yaml:"writeTimeout"` WriteTimeout InterpolatedDuration `yaml:"writeTimeout"`
DialTimeout InterpolatedDuration `yaml:"dialTimeout"` DialTimeout InterpolatedDuration `yaml:"dialTimeout"`
LockMaxRetries InterpolatedInt `yaml:"lockMaxRetries"`
} }
func NewDefaultRedisConfig() RedisConfig { func NewDefaultRedisConfig() RedisConfig {
return RedisConfig{ return RedisConfig{
Adresses: InterpolatedStringSlice{"localhost:6379"}, Adresses: InterpolatedStringSlice{"localhost:6379"},
Master: "", Master: "",
ReadTimeout: InterpolatedDuration(30 * time.Second), ReadTimeout: InterpolatedDuration(30 * time.Second),
WriteTimeout: InterpolatedDuration(30 * time.Second), WriteTimeout: InterpolatedDuration(30 * time.Second),
DialTimeout: InterpolatedDuration(30 * time.Second), DialTimeout: InterpolatedDuration(30 * time.Second),
LockMaxRetries: 10,
} }
} }

View File

@ -12,8 +12,8 @@ import (
) )
type Locker struct { type Locker struct {
client redis.UniversalClient client redis.UniversalClient
timeout time.Duration maxRetries int
} }
// WithLock implements lock.Locker. // WithLock implements lock.Locker.
@ -26,33 +26,41 @@ func (l *Locker) WithLock(ctx context.Context, key string, timeout time.Duration
logger.Debug(ctx, "acquiring lock") logger.Debug(ctx, "acquiring lock")
lock, err := locker.Obtain(ctx, key, timeout, &redislock.Options{ err := retryWithBackoff(ctx, l.maxRetries, func(ctx context.Context) error {
RetryStrategy: backoff, lock, err := locker.Obtain(ctx, key, timeout, &redislock.Options{
}) RetryStrategy: backoff,
if err != nil { })
return errors.WithStack(err) if err != nil {
} return errors.WithStack(err)
logger.Debug(ctx, "lock obtained")
defer func() {
if err := lock.Release(ctx); err != nil {
logger.Error(ctx, "could not release lock", logger.E(errors.WithStack(err)))
} }
logger.Debug(ctx, "lock released") logger.Debug(ctx, "lock obtained")
}()
if err := fn(ctx); err != nil { defer func() {
if err := lock.Release(ctx); err != nil {
logger.Error(ctx, "could not release lock", logger.E(errors.WithStack(err)))
}
logger.Debug(ctx, "lock released")
}()
if err := fn(ctx); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil return nil
} }
func NewLocker(client redis.UniversalClient) *Locker { func NewLocker(client redis.UniversalClient, maxRetries int) *Locker {
return &Locker{ return &Locker{
client: client, client: client,
maxRetries: maxRetries,
} }
} }

View File

@ -0,0 +1,42 @@
package redis
import (
"context"
"time"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const (
baseWatchBackoffDelay = time.Millisecond * 500
maxDelay = time.Minute * 10
)
func retryWithBackoff(ctx context.Context, attempts int, fn func(ctx context.Context) error) error {
backoffDelay := baseWatchBackoffDelay
count := 0
for {
err := fn(ctx)
if err == nil {
return nil
}
err = errors.WithStack(err)
count++
if count >= attempts {
return errors.Wrapf(err, "execution failed after %d attempts", attempts)
}
logger.Error(ctx, "error while executing func, retrying with backoff", logger.E(err), logger.F("backoffDelay", backoffDelay), logger.F("remainingAttempts", attempts-count))
time.Sleep(backoffDelay)
backoffDelay *= 2
if backoffDelay > maxDelay {
backoffDelay = maxDelay
}
}
}

View File

@ -3,7 +3,6 @@ package director
import ( import (
"context" "context"
"net/http" "net/http"
"net/url"
"sort" "sort"
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
@ -28,15 +27,27 @@ func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
return r, errors.WithStack(err) return r, errors.WithStack(err)
} }
url := getRequestURL(r)
ctx = logger.With(r.Context(), logger.F("url", url.String()))
var match *store.Proxy var match *store.Proxy
MAIN: MAIN:
for _, p := range proxies { for _, p := range proxies {
for _, from := range p.From { for _, from := range p.From {
if matches := wildcard.Match(r.Host, from); !matches { logger.Debug(
ctx, "matching request with proxy's from",
logger.F("from", from),
)
if matches := wildcard.Match(url.String(), from); !matches {
continue continue
} }
logger.Debug(
ctx, "proxy's from matched",
logger.F("from", from),
)
match = p match = p
break MAIN break MAIN
} }

View File

@ -2,6 +2,7 @@ package director
import ( import (
"net/http" "net/http"
"net/url"
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/util" "forge.cadoles.com/Cadoles/go-proxy/util"
@ -16,3 +17,19 @@ func createMiddlewareChain(handler http.Handler, middlewares []proxy.Middleware)
return handler return handler
} }
func getRequestURL(r *http.Request) *url.URL {
scheme := "http"
if r.URL.Scheme != "" {
scheme = r.URL.Scheme
}
url := url.URL{
Host: r.Host,
Scheme: scheme,
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
}
return &url
}

View File

@ -28,7 +28,7 @@ func SetupIntegrations(ctx context.Context, conf *config.Config) ([]integration.
func setupKubernetesIntegration(ctx context.Context, conf *config.Config) (*kubernetes.Integration, error) { func setupKubernetesIntegration(ctx context.Context, conf *config.Config) (*kubernetes.Integration, error) {
client := newRedisClient(conf.Redis) client := newRedisClient(conf.Redis)
locker := redis.NewLocker(client) locker := redis.NewLocker(client, 10)
integration := kubernetes.NewIntegration( integration := kubernetes.NewIntegration(
kubernetes.WithReaderTokenSecret(string(conf.Integrations.Kubernetes.ReaderTokenSecret)), kubernetes.WithReaderTokenSecret(string(conf.Integrations.Kubernetes.ReaderTokenSecret)),

View File

@ -10,6 +10,6 @@ import (
func SetupLocker(ctx context.Context, conf *config.Config) (lock.Locker, error) { func SetupLocker(ctx context.Context, conf *config.Config) (lock.Locker, error) {
client := newRedisClient(conf.Redis) client := newRedisClient(conf.Redis)
locker := redis.NewLocker(client) locker := redis.NewLocker(client, int(conf.Redis.LockMaxRetries))
return locker, nil return locker, nil
} }