Compare commits

...

6 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
f548c8c8e7 feat: add host to access log fields
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-03-28 19:47:32 +01:00
12 changed files with 353 additions and 99 deletions

View File

@ -5,14 +5,16 @@
### Déploiement mono-noeud
![](../resources/deployment_fr.png)
## Terminologie
Voici une liste des termes utilisés dans le lexique Bouncer.
### Proxy
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.
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;
- `"<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 ET en écriture sur l'ensemble des entités.
- droit en lecture sur l'ensemble des entités (proxy, layer);
- droit en lecture ET en écriture sur l'ensemble des entités.
## Points d'entrée
@ -34,29 +33,29 @@ Créer un nouveau proxy
#### Exemple de corps de requête
```json5
```json
{
"name": "myproxy", // OBLIGATOIRE - Nom du proxy
"to": "https://www.cadoles.com", // OBLIGATOIRE - Site distant ciblé par le proxy
"from": ["*"] // OPTIONNEL - Liste de patrons de filtrage associés au proxy
"name": "myproxy", // OBLIGATOIRE - Nom du proxy
"to": "https://www.cadoles.com", // OBLIGATOIRE - Site distant ciblé par le proxy
"from": ["*"] // OPTIONNEL - Liste de patrons de filtrage associés au proxy
}
```
#### Exemple de résultat
```json5
```json
{
"data": {
"proxy": {
"name": "myproxy",
"weight": 0,
"enabled": false,
"to": "https://www.cadoles.com",
"from": ["*"],
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
"data": {
"proxy": {
"name": "myproxy",
"weight": 0,
"enabled": false,
"to": "https://www.cadoles.com",
"from": ["*"],
"createdAt": "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
```json5
```json
{
"data": {
"proxy": {
"name": "myproxy",
"weight": 0,
"enabled": false,
"to": "https://www.cadoles.com",
"from": ["*"],
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
"data": {
"proxy": {
"name": "myproxy",
"weight": 0,
"enabled": false,
"to": "https://www.cadoles.com",
"from": ["*"],
"createdAt": "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
```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
"weight": 100, // OPTIONNEL - Poids à associer au proxy
"enabled": true, // OPTIONNEL - Activer/désactiver le proxy
"weight": 100, // OPTIONNEL - Poids à associer au proxy
"enabled": true // OPTIONNEL - Activer/désactiver le proxy
}
```
#### Exemple de résultat
```json5
```json
{
"data": {
"proxy": {
"name": "myproxy",
"weight": 100,
"enabled": true,
"to": "https://www.cadoles.com",
"from": ["mylocalproxydomain:*"],
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2020-10-02T15:09:00.000Z"
}
"data": {
"proxy": {
"name": "myproxy",
"weight": 100,
"enabled": true,
"to": "https://www.cadoles.com",
"from": ["mylocalproxydomain:*"],
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2020-10-02T15:09:00.000Z"
}
}
}
```
@ -141,17 +140,19 @@ Lister les proxies existants
#### Exemple de résultat
```json5
```json
{
"data": {
"proxies": [
{
"name": "myproxy",
"weight": 0,
"enabled": false,
}
]
}
"data": {
"proxies": [
{
"name": "myproxy",
"weight": 0,
"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
```json5
```json
{
"data": {
"proxyName": "myproxy"
}
"data": {
"proxyName": "myproxy"
}
}
```
#### Source
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
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 {
logger.Info(ctx, "bootstrapping proxies")

View File

@ -16,6 +16,7 @@ type LogFormatter struct{}
func (*LogFormatter) NewLogEntry(r *http.Request) middleware.LogEntry {
return &LogEntry{
method: r.Method,
host: r.Host,
path: r.URL.Path,
ctx: r.Context(),
}
@ -29,6 +30,7 @@ var _ middleware.LogFormatter = &LogFormatter{}
type LogEntry struct {
method string
host string
path string
ctx context.Context
}
@ -41,6 +43,7 @@ func (e *LogEntry) Panic(v interface{}, stack []byte) {
// Write implements middleware.LogEntry
func (e *LogEntry) Write(status int, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
logger.Info(e.ctx, fmt.Sprintf("%s %s - %d", e.method, e.path, status),
logger.F("host", e.host),
logger.F("status", status),
logger.F("bytes", bytes),
logger.F("elapsed", elapsed),

View File

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

View File

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

View File

@ -12,8 +12,8 @@ import (
)
type Locker struct {
client redis.UniversalClient
timeout time.Duration
client redis.UniversalClient
maxRetries int
}
// 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")
lock, err := locker.Obtain(ctx, key, timeout, &redislock.Options{
RetryStrategy: backoff,
})
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)))
err := retryWithBackoff(ctx, l.maxRetries, func(ctx context.Context) error {
lock, err := locker.Obtain(ctx, key, timeout, &redislock.Options{
RetryStrategy: backoff,
})
if err != nil {
return 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 nil
}
func NewLocker(client redis.UniversalClient) *Locker {
func NewLocker(client redis.UniversalClient, maxRetries int) *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 (
"context"
"net/http"
"net/url"
"sort"
"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)
}
url := getRequestURL(r)
ctx = logger.With(r.Context(), logger.F("url", url.String()))
var match *store.Proxy
MAIN:
for _, p := range proxies {
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
}
logger.Debug(
ctx, "proxy's from matched",
logger.F("from", from),
)
match = p
break MAIN
}

View File

@ -2,6 +2,7 @@ package director
import (
"net/http"
"net/url"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/util"
@ -16,3 +17,19 @@ func createMiddlewareChain(handler http.Handler, middlewares []proxy.Middleware)
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) {
client := newRedisClient(conf.Redis)
locker := redis.NewLocker(client)
locker := redis.NewLocker(client, 10)
integration := kubernetes.NewIntegration(
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) {
client := newRedisClient(conf.Redis)
locker := redis.NewLocker(client)
locker := redis.NewLocker(client, int(conf.Redis.LockMaxRetries))
return locker, nil
}