Compare commits
13 Commits
v2023.6.13
...
v2023.6.30
Author | SHA1 | Date | |
---|---|---|---|
5bf391b6bf | |||
74928fe413 | |||
ff1d01828d | |||
851f5d64cc | |||
e0d81c061b | |||
440d467938 | |||
f8d33299b9 | |||
6fed6358b2 | |||
ef869a02ea | |||
6559d1f594 | |||
8d91f646c2 | |||
e32c72e030 | |||
8d21e9083c |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@ -0,0 +1,9 @@
|
||||
/admin-key.json
|
||||
/config.yml
|
||||
/tools
|
||||
/out
|
||||
/dist
|
||||
/data
|
||||
/bin
|
||||
/.bouncer-token
|
||||
/.env
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@
|
||||
/admin-key.json
|
||||
/.bouncer-token
|
||||
/data
|
||||
/out
|
16
Dockerfile
16
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM golang:1.19 AS BUILD
|
||||
FROM golang:1.20 AS BUILD
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y make
|
||||
@ -19,12 +19,18 @@ RUN mkdir -p /usr/local/bin \
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1 /app
|
||||
COPY --from=BUILD /src/config.yml /etc/bouncer/config.yml
|
||||
RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer
|
||||
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer
|
||||
COPY --from=BUILD /src/layers /usr/share/bouncer/
|
||||
|
||||
RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer \
|
||||
&& /usr/share/bouncer/bin/bouncer -c '' config dump > /etc/bouncer/config.yml
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
ENTRYPOINT ["/app/bouncer"]
|
||||
ENV BOUNCER_WORKDIR=/usr/share/bouncer
|
||||
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
|
||||
|
||||
CMD ["bouncer", "run", "-c", "/etc/bouncer/config.yml"]
|
||||
CMD ["bouncer"]
|
21
Makefile
21
Makefile
@ -5,11 +5,11 @@ GITCHLOG_ARGS ?=
|
||||
SHELL := /bin/bash
|
||||
|
||||
BOUNCER_VERSION ?=
|
||||
GIT_VERSION := $(shell git describe --always)
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD)
|
||||
DATE_VERSION := $(shell date +%Y.%-m.%-d)
|
||||
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
|
||||
FULL_VERSION := v$(DATE_VERSION)-$(GIT_COMMIT)$(if $(shell git diff --stat),-dirty,)
|
||||
|
||||
DOCKER_IMAGE_NAME ?= cadoles/bouncer
|
||||
DOCKER_IMAGE_NAME ?= reg.cadoles.com/cadoles/bouncer
|
||||
DOCKER_IMAGE_TAG ?= $(FULL_VERSION)
|
||||
|
||||
GOTEST_ARGS ?= -short
|
||||
@ -25,16 +25,6 @@ test: test-go ## Executing tests
|
||||
test-go: deps
|
||||
( set -o allexport && source .env && set +o allexport && go test -v -count=1 $(GOTEST_ARGS) ./... )
|
||||
|
||||
test-install-script: tools/bin/bash_unit
|
||||
tools/bin/bash_unit ./misc/script/test_install.sh
|
||||
|
||||
tools/bin/bash_unit:
|
||||
mkdir -p tools/bin
|
||||
cd tools/bin && bash <(curl -s https://raw.githubusercontent.com/pgrange/bash_unit/master/install.sh)
|
||||
|
||||
lint: ## Lint sources code
|
||||
golangci-lint run --enable-all $(LINT_ARGS)
|
||||
|
||||
build: build-bouncer ## Build artefacts
|
||||
|
||||
build-bouncer: deps ## Build executable
|
||||
@ -83,9 +73,6 @@ finish-release:
|
||||
git push --all
|
||||
git push --tags
|
||||
|
||||
install-git-hooks:
|
||||
git config core.hooksPath .githooks
|
||||
|
||||
docker-build:
|
||||
docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) .
|
||||
|
||||
@ -106,7 +93,7 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
||||
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
|
||||
GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_COMMIT)" \
|
||||
GITEA_RELEASE_IS_DRAFT="false" \
|
||||
GITEA_RELEASE_BODY="" \
|
||||
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
||||
|
@ -5,8 +5,16 @@
|
||||
|
||||
## Référence
|
||||
|
||||
- [(FR) - Layers](./fr/references/layers/README.md)
|
||||
- [Fichier de configuration](../misc/packaging/common/config.yml)
|
||||
|
||||
## Tutoriels
|
||||
|
||||
### Utilisation
|
||||
|
||||
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
||||
|
||||
### Développement
|
||||
|
||||
- [(FR) - Démarrer avec les sources](./fr/tutorials/getting-start-with-sources.md)
|
||||
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)
|
@ -1,3 +1,30 @@
|
||||
## Architecture générale
|
||||
# Architecture générale
|
||||
|
||||
> TODO
|
||||
## Modèles de déploiement
|
||||
|
||||
### Déploiement mono-noeud
|
||||
|
||||

|
||||
## 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é;
|
||||
- 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 être activé ou désactivé.
|
||||
|
||||
Un "proxy" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire).
|
||||
|
||||
### Layer
|
||||
|
||||
Un "layer" (calque) est une entité logique définissant un traitement à appliquer aux requêtes et/ou aux réponses transitant par un proxy.
|
||||
|
||||
Un "layer" peut être activé ou désactivé.
|
||||
|
||||
Un "layer" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire).
|
@ -59,7 +59,7 @@
|
||||
|
||||
```bash
|
||||
# Création du proxy nommé 'cadoles' vers https://www.cadoles.com
|
||||
bouncer admin proxy create --to https://www.cadoles.com --proxy-name cadoles
|
||||
bouncer admin proxy create --proxy-to https://www.cadoles.com --proxy-name cadoles
|
||||
```
|
||||
|
||||
Un message équivalent à celui ci devrait s'afficher:
|
||||
@ -77,7 +77,7 @@
|
||||
|
||||
```bash
|
||||
# Activation du proxy
|
||||
bouncer admin proxy update --proxy-name cadoles --enabled=true
|
||||
bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
|
||||
```
|
||||
|
||||
Un message équivalent à celui ci devrait s'afficher:
|
||||
|
5
doc/fr/references/layers/README.md
Normal file
5
doc/fr/references/layers/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Layers
|
||||
|
||||
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
|
||||
|
||||
- [Queue](./queue.md) - File d'attente dynamique
|
27
doc/fr/references/layers/queue.md
Normal file
27
doc/fr/references/layers/queue.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Layer "Queue"
|
||||
|
||||
## Description
|
||||
|
||||
Ce layer permet d'ajouter un mécanisme de file d'attente dynamique au proxy associé.
|
||||
|
||||
## Type
|
||||
|
||||
`queue`
|
||||
|
||||
## Options
|
||||
|
||||
### `capacity`
|
||||
|
||||
- **Type:** `number`
|
||||
- **Valeur par défaut:** `1000`
|
||||
- **Description:** Capacité maximum de la file d'attente.
|
||||
|
||||
### `keepAlive`
|
||||
|
||||
- **Type:** `string` (Voir [`time.ParseDuration()`](https://pkg.go.dev/time#ParseDuration) pour plus d'informations sur le format)
|
||||
- **Valeur par défaut:** `1m`
|
||||
- **Description:** Durée de vie d'une session dans la file d'attente sans activité avant expiration.
|
||||
|
||||
### Schéma
|
||||
|
||||
Voir le [schéma JSON](../../../../internal/proxy/director/layer/queue/schema/layer-options.json).
|
@ -1,8 +1,8 @@
|
||||
# Ajouter un calque de type "file d'attente"
|
||||
# Ajouter un layer de type "file d'attente"
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau calque ("layer") pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
|
||||
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau layer pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
|
||||
|
||||
```bash
|
||||
# Création d'un calque nommé 'my-queue' pour le proxy 'cadoles' de type 'queue'
|
||||
@ -19,10 +19,10 @@
|
||||
+----------+-------+---------+--------+---------+-------------------------+-------------------------+
|
||||
```
|
||||
|
||||
2. À ce stade, le calque est encore inactif. Définir la capacité de la file d'attente à 1 et activer le calque en utilisant le CLI
|
||||
2. À ce stade, le layer est encore inactif. Définir la capacité de la file d'attente à 1 et activer le layer en utilisant le CLI:
|
||||
|
||||
```bash
|
||||
bouncer admin layer update --proxy-name cadoles --layer-name my-queue --enabled=true --options '{"capacity": 1}'
|
||||
bouncer admin layer update --proxy-name cadoles --layer-name my-queue --layer-enabled=true --layer-options '{"capacity": 1}'
|
||||
```
|
||||
|
||||
Un message équivalent à celui ci devrait s'afficher:
|
||||
@ -43,6 +43,6 @@
|
||||
|
||||
3. Le proxy `cadoles` a désormais une file d'attente avec une capacité d'un seul utilisateur. Vous pouvez effectuer le test en ouvrant votre navigateur sur l'adresse `http://<ip_serveur>:8080/` puis en ouvrant une fenêtre de navigation privée sur la même adresse:
|
||||
- La première fenêtre devrait afficher le site Cadoles;
|
||||
- La seconde fenêtre devrait afficher le message suivant: `queued (rank: 2, status: 2/1)`.
|
||||
- La seconde fenêtre devrait afficher une page indiquant qu'on est en file d'attente.
|
||||
|
||||
Si vous laissez expirer la "session" de la première fenêtre (environ 1 minute par défaut) et que vous rafraîchissez la seconde, vous devriez avoir une inversion des états.
|
446
doc/fr/tutorials/create-custom-layer.md
Normal file
446
doc/fr/tutorials/create-custom-layer.md
Normal file
@ -0,0 +1,446 @@
|
||||
# Créer son propre layer
|
||||
|
||||
Dans ce tutoriel, nous verrons comment implémenter un layer personnalisé qui permettra d'ajouter une authentification de type [`Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) à un proxy.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Avoir un environnement de développement local fonctionnel. Voir tutoriel ["Démarrer avec les sources"](./getting-started-with-sources.md).
|
||||
## Étapes
|
||||
### Préparer la structure de base du nouveau layer
|
||||
|
||||
Une implémetation d'un layer se compose majoritairement de 3 éléments:
|
||||
|
||||
- Une structure qui implémente une ou plusieurs interfaces (`director.MiddlewareLayer`, `director.RequestTransformerLayer` et/ou `director.ResponseTransformerLayer`);
|
||||
- Un schéma au format [JSON Schema](http://json-schema.org/) qui permettra de valider les "options" de notre layer;
|
||||
- Un fichier d'amorçage qui permettra à Bouncer de référencer notre nouveau layer.
|
||||
|
||||
1. Créer le répertoire du `package` Go qui contiendra le code de votre layer. Celui ci s'appelera `basicauth`:
|
||||
|
||||
```
|
||||
mkdir -p internal/proxy/director/layer/basicauth
|
||||
```
|
||||
|
||||
2. Créer la structure de base du layer:
|
||||
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
)
|
||||
|
||||
// On définit le "type" (la chaîne de caractères) qui
|
||||
// sera associé à notre layer
|
||||
const LayerType store.LayerType = "basicauth"
|
||||
|
||||
// On déclare la structure qui
|
||||
// servira de socle pour notre layer
|
||||
type BasicAuth struct{}
|
||||
|
||||
// Les deux méthodes suivantes, attachées à
|
||||
// notre structure BasicAuth, permettent de remplir
|
||||
// le contrat définit par l'interface
|
||||
// director.MiddlewareLayer
|
||||
|
||||
// LayerType implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
|
||||
// C'est dans la méthode "Middleware" qu'on pourra implémenter la
|
||||
// logique appliquée par notre layer.
|
||||
// En l'état actuel l'exécution de la méthode provoquera un panic().
|
||||
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func New() *BasicAuth {
|
||||
return &BasicAuth{}
|
||||
}
|
||||
|
||||
// Cette déclaration permet de profiter
|
||||
// des capacités du compilateur pour s'assurer
|
||||
// que la structure BasicAuth remplie toujours le
|
||||
// contrat imposé par l'interface director.MiddlewareLayer
|
||||
var _ director.MiddlewareLayer = &BasicAuth{}
|
||||
```
|
||||
|
||||
3. Créer le schéma JSON qui sera associé aux options possibles pour notre layer:
|
||||
|
||||
```json
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
||||
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
|
||||
"title": "BasicAuth layer options",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
|
||||
4. Puis créer le fichier Go qui embarquera ces données dans notre binaire via le package [`embed`](https://pkg.go.dev/embed):
|
||||
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer_options.go
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
||||
```
|
||||
|
||||
5. Enfin, créer le fichier d'amorçage pour référencer notre nouveau layer avec Bouncer:
|
||||
|
||||
```go
|
||||
// Fichier internal/setup/basicauth_layer.go
|
||||
|
||||
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/basicauth"
|
||||
)
|
||||
|
||||
// On créait une function d'initialisation qui enregistra les éléments suivants auprès de Bouncer:
|
||||
// - Notre nouveau type de layer
|
||||
// - Une fonction capable de créer une instance de notre layer
|
||||
// - Le schéma associé aux options de notre layer
|
||||
func init() {
|
||||
RegisterLayer(basicauth.LayerType, setupBasicAuthLayer, basicauth.RawLayerOptionsSchema)
|
||||
}
|
||||
|
||||
// La fonction de création de notre layer
|
||||
// reçoit automatiquement la configuration actuelle de Bouncer.
|
||||
|
||||
// Une layer plus avancé pourrait être configurable de cette manière
|
||||
// en créant une nouvelle section de configuration dédiée.
|
||||
func setupBasicAuthLayer(conf *config.Config) (director.Layer, error) {
|
||||
return &basicauth.BasicAuth{}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Tester l'intégration de notre nouveau layer
|
||||
|
||||
À ce stade, notre nouveau layer est normalement référencé et donc "utilisable" dans Bouncer (si on omet le fait qu'il déclenchera une `panic()`).
|
||||
|
||||
1. Vérifier que notre layer est bien référencé en exécutant la commande:
|
||||
|
||||
```
|
||||
./bin/bouncer admin layer create --help
|
||||
```
|
||||
|
||||
La sortie devrait ressembler à:
|
||||
|
||||
```
|
||||
NAME:
|
||||
bouncer admin layer create - Create layer
|
||||
|
||||
USAGE:
|
||||
bouncer admin layer create [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--layer-type LAYER_TYPE Set LAYER_TYPE as layer's type (available: [basicauth queue])
|
||||
[...]
|
||||
```
|
||||
|
||||
Comme vous devriez le voir nous pouvons désormais créer des layers de type `basicauth`.
|
||||
|
||||
2. Créer un proxy puis une instance de notre nouveau layer associée à celui ci:
|
||||
|
||||
```bash
|
||||
# Créer un proxy
|
||||
./bin/bouncer admin proxy create --proxy-name cadoles --proxy-to https://www.cadoles.com
|
||||
|
||||
# Activer celui-ci
|
||||
./bin/bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
|
||||
|
||||
# Ajouter un layer de notre nouveau type à notre proxy
|
||||
./bin/bouncer admin layer create --proxy-name cadoles --layer-name mybasicauth --layer-type basicauth
|
||||
|
||||
# Activer notre nouveau layer
|
||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-enabled=true
|
||||
```
|
||||
|
||||
**Notre layer est actif** ! Cependant il est loin d'être fonctionnel. En effet, si vous faites un:
|
||||
|
||||
```
|
||||
curl -v http://localhost:8080
|
||||
```
|
||||
|
||||
Vous n'aurez guère en retour qu'un:
|
||||
|
||||
```
|
||||
* Trying 127.0.0.1:8080...
|
||||
* Connected to localhost (127.0.0.1) port 8080 (#0)
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> User-Agent: curl/8.1.2
|
||||
> Accept: */*
|
||||
>
|
||||
* Empty reply from server
|
||||
* Closing connection 0
|
||||
curl: (52) Empty reply from server
|
||||
```
|
||||
|
||||
Et également dans la console où s'exécute le service `bouncer-proxy`, vous aurez le message:
|
||||
|
||||
```
|
||||
2023/06/23 18:39:59 http: panic serving 127.0.0.1:59868: unimplemented
|
||||
```
|
||||
|
||||
**Il est temps d'implémenter réellement la logique associée à notre layer !**
|
||||
|
||||
> **Note** Vous pouvez désactiver votre layer via le drapeau `--layer-enabled=false` et voir le site Cadoles s'afficher à nouveau !
|
||||
|
||||
## Implémenter l'authentification sur notre nouveau layer
|
||||
|
||||
Nous allons modifier la méthode `Middleware(layer *store.Layer) proxy.Middleware` attachée à notre structure `BasicAuth`.
|
||||
|
||||
1. Modifier le fichier contenant la structure de notre layer de la manière suivante:
|
||||
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
||||
|
||||
// [...]
|
||||
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
// La méthode doit retourner un "Middleware" qui est un alias
|
||||
// pour les fonctions généralement utilisées
|
||||
// dans les librairies http en Go pour créer
|
||||
// une fonction d'interception/transformation de requête.
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// On récupère les identifiants "basic auth" transmis (ou non)
|
||||
// avec la requête
|
||||
username, password, ok := r.BasicAuth()
|
||||
|
||||
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
|
||||
unauthorized := func() {
|
||||
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
|
||||
// de la popup d'authentification dans le navigateur web de l'utilisateur.
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
|
||||
// On retoure un code d'erreur HTTP 401 (Unauthorized)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// L'entête Authorization est absente ou ne correspondant
|
||||
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
|
||||
// on interrompt le traitement de la requête ici
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// On vérifie les identifiants associés à la requête
|
||||
isAuthenticated := authenticate(username, password)
|
||||
|
||||
// Si les identifiants sont non reconnus alors
|
||||
// on interrompt le traitement de la requête
|
||||
if !isAuthenticated {
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// La méthode authenticate() prend un couple d'identifiants
|
||||
// est vérifie en temps constant si ceux ci correspondent à un couple
|
||||
// d'identifiants attendus.
|
||||
func authenticate(username, password string) bool {
|
||||
// On génère une empreinte au format sha256 pour nos identifiants
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
|
||||
// On effectue de même avec les identifiants attendus.
|
||||
// Pour l'instant, on utilise un couple d'identifiants en "dur".
|
||||
expectedUsernameHash := sha256.Sum256([]byte("foo"))
|
||||
expectedPasswordHash := sha256.Sum256([]byte("baz"))
|
||||
|
||||
// On utilise la méthode subtle.ConstantTimeCompare()
|
||||
// pour faire la comparaison des identifiants en temps constant
|
||||
// et ainsi éviter les attaques par timing.
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
|
||||
|
||||
// L'utilisateur est authentifié si son nom et son mot de passe
|
||||
// correspondent avec ceux attendus.
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
```
|
||||
|
||||
2. Dans votre navigateur, essayez d'ouvrir l'URL http://127.0.0.1:8080. La popup d'authentification devrait s'afficher et vous devriez pouvoir utiliser les identifiants définis dans la fonction `authenticate()` pour vous authentifier et accéder au site de Cadoles !
|
||||
|
||||
> **Note** Essayez de désactiver le layer. L'authentification est automatiquement désactivée également !
|
||||
|
||||
## Déclarer des options pour pouvoir utiliser des identifiants dynamiques
|
||||
|
||||
En l'état actuel notre layer est fonctionnel. Cependant il souffre d'un problème notable: les identifiants attendus sont statiques et embarqués en dur dans le code. Nous allons utiliser le schéma associé à nos options, jusqu'alors vide, pour pouvoir créer une paire d'identifiants attendus dynamique.
|
||||
|
||||
|
||||
1. Modifier le schéma JSON des options de notre layer:
|
||||
|
||||
```json
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
||||
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
|
||||
"title": "BasicAuth layer options",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
|
||||
2. On modifie notre méthode `Middleware()` et la fonction `authenticate()` pour utiliser ces nouvelles options:
|
||||
|
||||
```go
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "basicauth"
|
||||
|
||||
type BasicAuth struct{}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
// La méthode doit retourner un "Middleware" qui est un alias
|
||||
// pour les fonctions généralement utilisées
|
||||
// dans les librairies http en Go pour créer
|
||||
// une fonction d'interception/transformation de requête.
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// On récupère les identifiants "basic auth" transmis (ou non)
|
||||
// avec la requête
|
||||
username, password, ok := r.BasicAuth()
|
||||
|
||||
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
|
||||
unauthorized := func() {
|
||||
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
|
||||
// de la popup d'authentification dans le navigateur web de l'utilisateur.
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
|
||||
// On retoure un code d'erreur HTTP 401 (Unauthorized)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// L'entête Authorization est absente ou ne correspondant
|
||||
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
|
||||
// on interrompt le traitement de la requête ici
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// On extrait les identifiants des options associées à notre layer
|
||||
expectedUsername, usernameExists := layer.Options["username"].(string)
|
||||
expectedPassword, passwordExists := layer.Options["password"].(string)
|
||||
|
||||
// Si le nom d'utilisateur ou le mot de passe attendu n'existe pas
|
||||
// alors on retourne une erreur HTTP 500 à l'utilisateur.
|
||||
if !usernameExists || !passwordExists {
|
||||
logger.Error(r.Context(), "basicauth layer missing password or username option")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// On vérifie les identifiants associés à la requête
|
||||
isAuthenticated := authenticate(username, password, expectedUsername, expectedPassword)
|
||||
|
||||
// Si les identifiants sont non reconnus alors
|
||||
// on interrompt le traitement de la requête
|
||||
if !isAuthenticated {
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(username, password string, expectedUsername, expectedPassword string) bool {
|
||||
// On génère une empreinte au format sha256 pour nos identifiants
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
|
||||
// On effectue de même avec les identifiants attendus.
|
||||
expectedUsernameHash := sha256.Sum256([]byte(expectedUsername))
|
||||
expectedPasswordHash := sha256.Sum256([]byte(expectedPassword))
|
||||
|
||||
// On utilise la méthode subtle.ConstantTimeCompare()
|
||||
// pour faire la comparaison des identifiants en temps constant
|
||||
// et ainsi éviter les attaques par timing.
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
|
||||
|
||||
// L'utilisateur est authentifié si son nom et son mot de passe
|
||||
// correspondent avec ceux attendus.
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
|
||||
func New() *BasicAuth {
|
||||
return &BasicAuth{}
|
||||
}
|
||||
|
||||
var _ director.MiddlewareLayer = &BasicAuth{}
|
||||
```
|
||||
|
||||
3. Modifiez votre layer via la commande d'administration pour déclarer une paire d'identifiants:
|
||||
|
||||
```bash
|
||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-options='{"username":"jdoe","password":"notsosecret"}'
|
||||
```
|
||||
|
||||
4. Essayer d'accéder à l'adresse http://127.0.0.1:8080 avec votre navigateur. La popup d'authentification devrait s'afficher et vous devriez pouvoir vous authentifier avec le nouveau couple d'identifiants définis dans les options de votre layer !
|
||||
|
||||
> **Note** Vous pouvez modifier les identifiants plusieurs fois via la commande et vérifier que la fenêtre s'affiche toujours à nouveau, demandant les nouveaux identifiants.
|
125
doc/fr/tutorials/getting-started-with-sources.md
Normal file
125
doc/fr/tutorials/getting-started-with-sources.md
Normal file
@ -0,0 +1,125 @@
|
||||
# Démarrer avec les sources
|
||||
|
||||
Dans ce tutoriel, nous verrons comment lancer un environnement de développement en local sur notre machine afin de travailler sur les sources de Bouncer.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Les éléments suivants doivent être installés sur votre machine:
|
||||
|
||||
- [Golang > 1.20](https://go.dev/)
|
||||
- [Docker](https://www.docker.com/)
|
||||
- [Git](https://git-scm.com/)
|
||||
- [GNU Make](https://www.gnu.org/software/make/)
|
||||
|
||||
Les ports suivants doivent être disponibles sur votre machine:
|
||||
|
||||
- `8080`
|
||||
- `8081`
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Cloner le dépôt des sources du projet Bouncer
|
||||
|
||||
```
|
||||
git clone https://forge.cadoles.com/Cadoles/bouncer
|
||||
```
|
||||
|
||||
2. Se positionner dans le répertoire du projet
|
||||
|
||||
```
|
||||
cd bouncer
|
||||
```
|
||||
|
||||
3. Lancer le projet en mode "développement"
|
||||
|
||||
```
|
||||
make watch
|
||||
```
|
||||
|
||||
Si toutes les dépendances sont correctement installées et configurées sur votre machine, la console devrait afficher une série de messages pour ensuite s'arrêter sur quelque chose ressemblant à:
|
||||
|
||||
```
|
||||
14:47:06: daemon: make run BOUNCER_CMD="--config config.yml server admin run"
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/command/server/admin/run.go:42> RunCommand.func1 listening {"url": "http://127.0.0.1:8081"}
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/admin/server.go:126> (*Server).run http server listening
|
||||
14:47:06: daemon: make run-redis
|
||||
bouncer-redis
|
||||
docker run --rm -t \
|
||||
--name bouncer-redis \
|
||||
-v /home/wpetit/workspace/bouncer/data/redis:/data \
|
||||
-p 6379:6379 \
|
||||
redis:alpine3.17 \
|
||||
redis-server --save 60 1 --loglevel warning
|
||||
1:C 23 Jun 2023 20:47:06.754 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
|
||||
1:C 23 Jun 2023 20:47:06.754 # Redis version=7.0.11, bits=64, commit=00000000, modified=0, pid=1, just started
|
||||
1:C 23 Jun 2023 20:47:06.754 # Configuration loaded
|
||||
1:M 23 Jun 2023 20:47:06.759 # Warning: Could not create server TCP listening socket ::*:6379: unable to bind socket, errno: 97
|
||||
1:M 23 Jun 2023 20:47:06.760 # Server initialized
|
||||
1:M 23 Jun 2023 20:47:06.760 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect
|
||||
```
|
||||
|
||||
À ce stade, le serveur `bouncer-admin` écoute sur http://127.0.0.1:8081 et le serveur `bouncer-proxy` sur http://127.0.0.1:8080.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> L'outil [`modd`](https://github.com/cortesi/modd) est utilisé pour surveiller les modifications sur les sources et relancer automatiquement la compilation et les services en cas de changement.
|
||||
|
||||
## Commandes `make` utiles
|
||||
|
||||
### `make watch`
|
||||
|
||||
Surveiller les sources, compiler celles ci en cas de modifications et lancer les services `bouncer-proxy` et `bouncer-admin`.
|
||||
|
||||
#### `make test`
|
||||
|
||||
Exécuter les tests unitaires/d'intégration du projet.
|
||||
|
||||
#### `make build`
|
||||
|
||||
Compiler une version de développement du binaire `bouncer`.
|
||||
|
||||
#### `make docker-build`
|
||||
|
||||
Construire une image Docker pour Bouncer.
|
||||
|
||||
Vous pouvez ensuite lancer l'image localement avec la commande:
|
||||
|
||||
```
|
||||
docker run \
|
||||
-it --rm \
|
||||
reg.cadoles.com/cadoles/bouncer:<tag> \
|
||||
-p 8080:8080 \
|
||||
bouncer server proxy run
|
||||
```
|
||||
|
||||
## Arborescence du projet
|
||||
|
||||
```bash
|
||||
.
|
||||
├── bin # Répertoire de destination des binaires Go de développement
|
||||
├── cmd # Package principal (main) du binaire Bouncer
|
||||
├── data # Répertoire des données de développement (Redis)
|
||||
├── dist # Répertoire de destination des archives/paquets pour la publication
|
||||
├── doc # Répertoire de documentation du projet
|
||||
├── internal # Source Go du projet
|
||||
│ ├── admin
|
||||
│ ├── auth
|
||||
│ ├── chi
|
||||
│ ├── client
|
||||
│ ├── command
|
||||
│ ├── config
|
||||
│ ├── format
|
||||
│ ├── imports
|
||||
│ ├── jwk
|
||||
│ ├── proxy
|
||||
│ ├── schema
|
||||
│ ├── setup
|
||||
│ └── store
|
||||
├── layers # Fichiers annexes liés aux layers (templates HTML)
|
||||
│ └── queue
|
||||
├── misc # Fichiers annexes
|
||||
│ ├── jenkins # Fichiers liés au pipeline d'intégration continue Jenkins
|
||||
│ ├── logo # Logo du projet
|
||||
│ └── packaging # Fichiers liés à l'empaquetage des binaires
|
||||
└── tools # Outils utilisés en développement
|
||||
```
|
37
doc/resources/deployment_fr.plantuml
Normal file
37
doc/resources/deployment_fr.plantuml
Normal file
@ -0,0 +1,37 @@
|
||||
@startuml
|
||||
skinparam linetype ortho
|
||||
skinparam ranksep 150
|
||||
skinparam nodesep 50
|
||||
top to bottom direction
|
||||
|
||||
frame "Exemple de déploiement mono-noeud" as ExampleSimpleNode {
|
||||
|
||||
actor "Navigateur Web" as WebNavigator
|
||||
|
||||
node "Serveur Bouncer" as BouncerServer {
|
||||
actor "CLI d'administration" as AdminCLI
|
||||
|
||||
database "Redis" as RedisDatabase
|
||||
|
||||
component "bouncer-proxy" as BouncerProxyService
|
||||
component "bouncer-admin" as BouncerAdminService
|
||||
|
||||
folder "/etc/bouncer" as BouncerConfigFolder
|
||||
}
|
||||
|
||||
node "Serveur distant" as RemoteServer {
|
||||
component "Site Web" as RemoteWebsite
|
||||
}
|
||||
|
||||
WebNavigator --down0)- BouncerProxyService: "TCP/80 (HTTP)"
|
||||
AdminCLI -0)- BouncerAdminService: "TCP/8081 (HTTP)"
|
||||
|
||||
BouncerProxyService -down0)-- RemoteWebsite: "TCP/80 (HTTP)\nTCP/443 (HTTPS)"
|
||||
|
||||
BouncerAdminService .down.> RedisDatabase: reads/writes
|
||||
BouncerProxyService .down.> RedisDatabase: reads
|
||||
|
||||
BouncerAdminService ..> BouncerConfigFolder: uses
|
||||
BouncerProxyService ..> BouncerConfigFolder: uses
|
||||
|
||||
@enduml
|
BIN
doc/resources/deployment_fr.png
Normal file
BIN
doc/resources/deployment_fr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
@ -5,6 +5,7 @@ import (
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
@ -155,13 +156,22 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
layerName, err := store.ValidateName(createLayerReq.Name)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
logger.Error(r.Context(), "invalid 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
layer, err := s.layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), store.LayerType(createLayerReq.Type), createLayerReq.Options)
|
||||
layerType := store.LayerType(createLayerReq.Type)
|
||||
|
||||
if !setup.LayerTypeExists(layerType) {
|
||||
logger.Error(r.Context(), "unknown layer type", logger.E(errors.WithStack(err)), logger.F("layerType", layerType))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
layer, err := s.layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), layerType, createLayerReq.Options)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrAlreadyExist) {
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil)
|
||||
@ -235,7 +245,19 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if updateLayerReq.Options != nil {
|
||||
if err := schema.ValidateLayerOptions(ctx, layer.Type, updateLayerReq.Options); err != nil {
|
||||
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not retrieve layer options schema", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
rawOptions := func(opts *store.LayerOptions) map[string]any {
|
||||
return *opts
|
||||
}(updateLayerReq.Options)
|
||||
|
||||
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
|
||||
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
|
||||
|
||||
var invalidDataErr *schema.InvalidDataError
|
||||
|
@ -2,27 +2,22 @@ package flag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagLayerName = "layer-name"
|
||||
FlagLayerType = "layer-type"
|
||||
FlagLayerOptions = "layer-options"
|
||||
FlagKeyLayerType = "layer-type"
|
||||
)
|
||||
|
||||
func WithLayerFlags(flags ...cli.Flag) []cli.Flag {
|
||||
baseFlags := proxyFlag.WithProxyFlags(
|
||||
&cli.StringFlag{
|
||||
Name: FlagLayerName,
|
||||
Usage: "use `LAYER_NAME` as targeted layer",
|
||||
Value: "",
|
||||
Required: true,
|
||||
},
|
||||
LayerName(),
|
||||
)
|
||||
|
||||
flags = append(flags, baseFlags...)
|
||||
@ -32,22 +27,63 @@ func WithLayerFlags(flags ...cli.Flag) []cli.Flag {
|
||||
|
||||
func WithLayerCreateFlags(flags ...cli.Flag) []cli.Flag {
|
||||
return WithLayerFlags(
|
||||
&cli.StringFlag{
|
||||
Name: FlagLayerType,
|
||||
Usage: "Set `LAYER_TYPE` as layer's type",
|
||||
Value: "",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: FlagLayerOptions,
|
||||
Usage: "Set `LAYER_OPTIONS` as layer's options",
|
||||
Value: "{}",
|
||||
},
|
||||
LayerType(),
|
||||
LayerOptions(),
|
||||
)
|
||||
}
|
||||
|
||||
const KeyLayerName = "layer-name"
|
||||
|
||||
func LayerName() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: KeyLayerName,
|
||||
Usage: "use `LAYER_NAME` as targeted layer",
|
||||
Value: "",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
const KeyLayerType = "layer-type"
|
||||
|
||||
func LayerType() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: KeyLayerType,
|
||||
Usage: fmt.Sprintf("Set `LAYER_TYPE` as layer's type (available: %v)", setup.GetLayerTypes()),
|
||||
Value: "",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
const KeyLayerOptions = "layer-options"
|
||||
|
||||
func LayerOptions() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: KeyLayerOptions,
|
||||
Usage: "Set `LAYER_OPTIONS` as layer's options",
|
||||
Value: "{}",
|
||||
}
|
||||
}
|
||||
|
||||
const KeyLayerWeight = "layer-weight"
|
||||
|
||||
func LayerWeight() cli.Flag {
|
||||
return &cli.IntFlag{
|
||||
Name: KeyLayerWeight,
|
||||
Usage: "Set `LAYER_WEIGHT` as layer's weight",
|
||||
}
|
||||
}
|
||||
|
||||
const KeyLayerEnabled = "layer-enabled"
|
||||
|
||||
func LayerEnabled() cli.Flag {
|
||||
return &cli.BoolFlag{
|
||||
Name: KeyLayerEnabled,
|
||||
Usage: "Enable or disable layer",
|
||||
}
|
||||
}
|
||||
|
||||
func AssertLayerName(ctx *cli.Context) (store.LayerName, error) {
|
||||
rawLayerName := ctx.String(FlagLayerName)
|
||||
rawLayerName := ctx.String(KeyLayerName)
|
||||
|
||||
name, err := store.ValidateName(rawLayerName)
|
||||
if err != nil {
|
||||
@ -58,13 +94,18 @@ func AssertLayerName(ctx *cli.Context) (store.LayerName, error) {
|
||||
}
|
||||
|
||||
func AssertLayerType(ctx *cli.Context) (store.LayerType, error) {
|
||||
rawLayerType := ctx.String(FlagLayerType)
|
||||
rawLayerType := ctx.String(FlagKeyLayerType)
|
||||
|
||||
return store.LayerType(rawLayerType), nil
|
||||
layerType := store.LayerType(rawLayerType)
|
||||
if !setup.LayerTypeExists(layerType) {
|
||||
return "", errors.Errorf("unknown layer type '%s'", layerType)
|
||||
}
|
||||
|
||||
return layerType, nil
|
||||
}
|
||||
|
||||
func AssertLayerOptions(ctx *cli.Context) (store.LayerOptions, error) {
|
||||
rawLayerOptions := ctx.String(FlagLayerOptions)
|
||||
rawLayerOptions := ctx.String(KeyLayerOptions)
|
||||
|
||||
layerOptions := store.LayerOptions{}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
||||
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag"
|
||||
layerFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag"
|
||||
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
||||
@ -20,18 +21,9 @@ func UpdateCommand() *cli.Command {
|
||||
Name: "update",
|
||||
Usage: "Update layer",
|
||||
Flags: layerFlag.WithLayerFlags(
|
||||
&cli.BoolFlag{
|
||||
Name: "enabled",
|
||||
Usage: "Enable or disable proxy",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "weight",
|
||||
Usage: "Set `WEIGHT` as proxy's weight",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "options",
|
||||
Usage: "Set `OPTIONS` as proxy's options",
|
||||
},
|
||||
flag.LayerEnabled(),
|
||||
flag.LayerWeight(),
|
||||
flag.LayerOptions(),
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
@ -53,22 +45,22 @@ func UpdateCommand() *cli.Command {
|
||||
|
||||
opts := &client.UpdateLayerOptions{}
|
||||
|
||||
if ctx.IsSet("options") {
|
||||
if ctx.IsSet(flag.KeyLayerOptions) {
|
||||
var options store.LayerOptions
|
||||
if err := json.Unmarshal([]byte(ctx.String("options")), &options); err != nil {
|
||||
return errors.Wrap(err, "could not parse options")
|
||||
if err := json.Unmarshal([]byte(ctx.String(flag.KeyLayerOptions)), &options); err != nil {
|
||||
return errors.Wrap(err, "could not parse layer's options")
|
||||
}
|
||||
|
||||
opts.Options = &options
|
||||
}
|
||||
|
||||
if ctx.IsSet("weight") {
|
||||
weight := ctx.Int("weight")
|
||||
if ctx.IsSet(flag.KeyLayerWeight) {
|
||||
weight := ctx.Int(flag.KeyLayerWeight)
|
||||
opts.Weight = &weight
|
||||
}
|
||||
|
||||
if ctx.IsSet("enabled") {
|
||||
enabled := ctx.Bool("enabled")
|
||||
if ctx.IsSet(flag.KeyLayerEnabled) {
|
||||
enabled := ctx.Bool(flag.KeyLayerEnabled)
|
||||
opts.Enabled = &enabled
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
||||
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
||||
"github.com/pkg/errors"
|
||||
@ -18,17 +19,8 @@ func CreateCommand() *cli.Command {
|
||||
Name: "create",
|
||||
Usage: "Create proxy",
|
||||
Flags: proxyFlag.WithProxyFlags(
|
||||
&cli.StringFlag{
|
||||
Name: "to",
|
||||
Usage: "Set `TO` as proxy's destination url",
|
||||
Value: "",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "from",
|
||||
Usage: "Set `FROM` as proxy's patterns to match incoming requests",
|
||||
Value: cli.NewStringSlice("*"),
|
||||
},
|
||||
flag.ProxyTo(true),
|
||||
flag.ProxyFrom(),
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
@ -43,12 +35,12 @@ func CreateCommand() *cli.Command {
|
||||
return errors.Wrap(err, "'to' parameter should be a valid url")
|
||||
}
|
||||
|
||||
to, err := url.Parse(ctx.String("to"))
|
||||
to, err := url.Parse(ctx.String(flag.KeyProxyTo))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "'to' parameter should be a valid url")
|
||||
}
|
||||
|
||||
from := ctx.StringSlice("from")
|
||||
from := ctx.StringSlice(flag.KeyProxyFrom)
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
|
@ -7,16 +7,9 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const FlagProxyName = "proxy-name"
|
||||
|
||||
func WithProxyFlags(flags ...cli.Flag) []cli.Flag {
|
||||
baseFlags := clientFlag.ComposeFlags(
|
||||
&cli.StringFlag{
|
||||
Name: FlagProxyName,
|
||||
Usage: "use `PROXY_NAME` as targeted proxy",
|
||||
Value: "",
|
||||
Required: true,
|
||||
},
|
||||
ProxyName(),
|
||||
)
|
||||
|
||||
flags = append(flags, baseFlags...)
|
||||
@ -24,8 +17,58 @@ func WithProxyFlags(flags ...cli.Flag) []cli.Flag {
|
||||
return flags
|
||||
}
|
||||
|
||||
const KeyProxyName = "proxy-name"
|
||||
|
||||
func ProxyName() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: KeyProxyName,
|
||||
Usage: "use `PROXY_NAME` as targeted proxy",
|
||||
Value: "",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
const KeyProxyTo = "proxy-to"
|
||||
|
||||
func ProxyTo(required bool) cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: KeyProxyTo,
|
||||
Usage: "Set `PROXY_TO` as proxy's destination url",
|
||||
Value: "",
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
const KeyProxyFrom = "proxy-from"
|
||||
|
||||
func ProxyFrom() cli.Flag {
|
||||
return &cli.StringSliceFlag{
|
||||
Name: KeyProxyFrom,
|
||||
Usage: "Set `PROXY_FROM` as proxy's patterns to match incoming requests",
|
||||
Value: cli.NewStringSlice("*"),
|
||||
}
|
||||
}
|
||||
|
||||
const KeyProxyWeight = "proxy-weight"
|
||||
|
||||
func ProxyWeight() cli.Flag {
|
||||
return &cli.IntFlag{
|
||||
Name: KeyProxyWeight,
|
||||
Usage: "Set `PROXY_WEIGHT` as proxy's weight",
|
||||
}
|
||||
}
|
||||
|
||||
const KeyProxyEnabled = "proxy-enabled"
|
||||
|
||||
func ProxyEnabled() cli.Flag {
|
||||
return &cli.BoolFlag{
|
||||
Name: KeyProxyEnabled,
|
||||
Usage: "Enable or disable proxy",
|
||||
}
|
||||
}
|
||||
|
||||
func AssertProxyName(ctx *cli.Context) (store.ProxyName, error) {
|
||||
rawProxyName := ctx.String(FlagProxyName)
|
||||
rawProxyName := ctx.String(KeyProxyName)
|
||||
|
||||
name, err := store.ValidateName(rawProxyName)
|
||||
if err != nil {
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
||||
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
||||
"github.com/pkg/errors"
|
||||
@ -18,22 +19,10 @@ func UpdateCommand() *cli.Command {
|
||||
Name: "update",
|
||||
Usage: "Update proxy",
|
||||
Flags: proxyFlag.WithProxyFlags(
|
||||
&cli.StringFlag{
|
||||
Name: "to",
|
||||
Usage: "Set `TO` as proxy's destination url",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "from",
|
||||
Usage: "Set `FROM` as proxy's patterns to match incoming requests",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enabled",
|
||||
Usage: "Enable or disable proxy",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "weight",
|
||||
Usage: "Set `WEIGHT` as proxy's weight",
|
||||
},
|
||||
flag.ProxyTo(false),
|
||||
flag.ProxyFrom(),
|
||||
flag.ProxyEnabled(),
|
||||
flag.ProxyWeight(),
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
@ -50,27 +39,29 @@ func UpdateCommand() *cli.Command {
|
||||
|
||||
opts := &client.UpdateProxyOptions{}
|
||||
|
||||
if ctx.IsSet("to") {
|
||||
to := ctx.String("to")
|
||||
if ctx.IsSet(flag.KeyProxyTo) {
|
||||
to := ctx.String(flag.KeyProxyTo)
|
||||
if _, err := url.Parse(to); err != nil {
|
||||
return errors.Wrap(err, "'to' parameter should be a valid url")
|
||||
return errors.Wrapf(err, "'%s' parameter should be a valid url", flag.KeyProxyTo)
|
||||
}
|
||||
|
||||
opts.To = &to
|
||||
}
|
||||
|
||||
from := ctx.StringSlice("from")
|
||||
if from != nil {
|
||||
opts.From = from
|
||||
if ctx.IsSet(flag.KeyProxyFrom) {
|
||||
from := ctx.StringSlice(flag.KeyProxyFrom)
|
||||
if from != nil {
|
||||
opts.From = from
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.IsSet("weight") {
|
||||
weight := ctx.Int("weight")
|
||||
if ctx.IsSet(flag.KeyProxyWeight) {
|
||||
weight := ctx.Int(flag.KeyProxyWeight)
|
||||
opts.Weight = &weight
|
||||
}
|
||||
|
||||
if ctx.IsSet("enabled") {
|
||||
enabled := ctx.Bool("enabled")
|
||||
if ctx.IsSet(flag.KeyProxyEnabled) {
|
||||
enabled := ctx.Bool(flag.KeyProxyEnabled)
|
||||
opts.Enabled = &enabled
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func LoadConfig(ctx *cli.Context) (*config.Config, error) {
|
||||
@ -15,11 +16,15 @@ func LoadConfig(ctx *cli.Context) (*config.Config, error) {
|
||||
)
|
||||
|
||||
if configFile != "" {
|
||||
logger.Info(ctx.Context, "loading config", logger.F("config", configFile))
|
||||
|
||||
conf, err = config.NewFromFile(configFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Could not load config file '%s'", configFile)
|
||||
}
|
||||
} else {
|
||||
logger.Info(ctx.Context, "using default config")
|
||||
|
||||
conf = config.NewDefault()
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands ...*cli.Command) {
|
||||
@ -29,6 +30,8 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
|
||||
workdir := ctx.String("workdir")
|
||||
// Switch to new working directory if defined
|
||||
if workdir != "" {
|
||||
logger.Info(ctx.Context, "changing working directory", logger.F("workdir", workdir))
|
||||
|
||||
if err := os.Chdir(workdir); err != nil {
|
||||
return errors.Wrap(err, "could not change working directory")
|
||||
}
|
||||
@ -50,9 +53,10 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "workdir",
|
||||
Value: "",
|
||||
Usage: "The working directory",
|
||||
Name: "workdir",
|
||||
Value: "",
|
||||
EnvVars: []string{"BOUNCER_WORKDIR"},
|
||||
Usage: "The working directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "projectVersion",
|
||||
|
@ -1,16 +1,11 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/queue"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -33,7 +28,7 @@ func RunCommand() *cli.Command {
|
||||
logger.SetFormat(logger.Format(conf.Logger.Format))
|
||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||
|
||||
layers, err := initDirectorLayers(ctx.Context, conf)
|
||||
layers, err := setup.GetLayers(ctx.Context, conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize director layers")
|
||||
}
|
||||
@ -64,36 +59,3 @@ func RunCommand() *cli.Command {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func initDirectorLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
|
||||
layers := make([]director.Layer, 0)
|
||||
|
||||
queue, err := initQueueLayer(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not initialize queue layer")
|
||||
}
|
||||
|
||||
layers = append(layers, queue)
|
||||
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func initQueueLayer(ctx context.Context, conf *config.Config) (*queue.Queue, error) {
|
||||
adapter, err := setup.NewQueueAdapter(ctx, conf.Redis)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
options := []queue.OptionFunc{
|
||||
queue.WithTemplateDir(string(conf.Layers.Queue.TemplateDir)),
|
||||
}
|
||||
|
||||
if conf.Layers.Queue.DefaultKeepAlive != nil {
|
||||
options = append(options, queue.WithDefaultKeepAlive(time.Duration(*conf.Layers.Queue.DefaultKeepAlive)))
|
||||
}
|
||||
|
||||
return queue.New(
|
||||
adapter,
|
||||
options...,
|
||||
), nil
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/queue"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
8
internal/proxy/director/layer/queue/schema.go
Normal file
8
internal/proxy/director/layer/queue/schema.go
Normal file
@ -0,0 +1,8 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed schema/layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
@ -7,12 +7,6 @@
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"matchers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"keepAlive": {
|
||||
"type": "string"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//go:embed schema/layer-options.json
|
||||
var rawLayerOptionsSchema []byte
|
||||
|
||||
func init() {
|
||||
layerOptionsSchema, err := schema.Parse(rawLayerOptionsSchema)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not parse queue layer options schema"))
|
||||
}
|
||||
|
||||
schema.RegisterLayerOptionsSchema(LayerType, layerOptionsSchema)
|
||||
}
|
@ -7,8 +7,10 @@ import (
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
func Parse(data []byte) (*jsonschema.Schema, error) {
|
||||
var schema jsonschema.Schema
|
||||
type Schema = jsonschema.Schema
|
||||
|
||||
func Parse(data []byte) (*Schema, error) {
|
||||
var schema Schema
|
||||
if err := json.Unmarshal(data, &schema); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
@ -1,56 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
var defaultRegistry = NewRegistry()
|
||||
|
||||
func RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) {
|
||||
defaultRegistry.RegisterLayerOptionsSchema(layerType, schema)
|
||||
}
|
||||
|
||||
func ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error {
|
||||
if err := defaultRegistry.ValidateLayerOptions(ctx, layerType, options); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
layerOptionSchemas map[store.LayerType]*jsonschema.Schema
|
||||
}
|
||||
|
||||
func (r *Registry) RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) {
|
||||
r.layerOptionSchemas[layerType] = schema
|
||||
}
|
||||
|
||||
func (r *Registry) ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error {
|
||||
schema, exists := r.layerOptionSchemas[layerType]
|
||||
if !exists {
|
||||
return errors.WithStack(ErrSchemaNotFound)
|
||||
}
|
||||
|
||||
rawOptions := func(opts *store.LayerOptions) map[string]any {
|
||||
return *opts
|
||||
}(options)
|
||||
|
||||
state := schema.Validate(ctx, rawOptions)
|
||||
|
||||
if len(*state.Errs) > 0 {
|
||||
return errors.WithStack(NewInvalidDataError(*state.Errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
layerOptionSchemas: make(map[store.LayerType]*jsonschema.Schema),
|
||||
}
|
||||
}
|
17
internal/schema/validate.go
Normal file
17
internal/schema/validate.go
Normal file
@ -0,0 +1,17 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func Validate(ctx context.Context, schema *Schema, data map[string]any) error {
|
||||
state := schema.Validate(ctx, data)
|
||||
|
||||
if len(*state.Errs) > 0 {
|
||||
return errors.WithStack(NewInvalidDataError(*state.Errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
43
internal/setup/default_registry.go
Normal file
43
internal/setup/default_registry.go
Normal file
@ -0,0 +1,43 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
var defaultRegistry = NewRegistry()
|
||||
|
||||
func RegisterLayer(layerType store.LayerType, setupFunc LayerSetupFunc, rawOptionsSchema []byte) {
|
||||
defaultRegistry.RegisterLayer(layerType, setupFunc, rawOptionsSchema)
|
||||
}
|
||||
|
||||
func GetLayerOptionsSchema(layerType store.LayerType) (*jsonschema.Schema, error) {
|
||||
schema, err := defaultRegistry.GetLayerOptionsSchema(layerType)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func GetLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
|
||||
layers, err := defaultRegistry.GetLayers(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func GetLayerTypes() []store.LayerType {
|
||||
return defaultRegistry.GetLayerTypes()
|
||||
}
|
||||
|
||||
func LayerTypeExists(layerType store.LayerType) bool {
|
||||
return defaultRegistry.LayerTypeExists(layerType)
|
||||
}
|
5
internal/setup/error.go
Normal file
5
internal/setup/error.go
Normal file
@ -0,0 +1,5 @@
|
||||
package setup
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
45
internal/setup/queue_layer.go
Normal file
45
internal/setup/queue_layer.go
Normal file
@ -0,0 +1,45 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
|
||||
queueRedis "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue/redis"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterLayer(queue.LayerType, setupQueueLayer, queue.RawLayerOptionsSchema)
|
||||
}
|
||||
|
||||
func setupQueueLayer(conf *config.Config) (director.Layer, error) {
|
||||
adapter, err := newQueueAdapter(conf.Redis)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
options := []queue.OptionFunc{
|
||||
queue.WithTemplateDir(string(conf.Layers.Queue.TemplateDir)),
|
||||
}
|
||||
|
||||
if conf.Layers.Queue.DefaultKeepAlive != nil {
|
||||
options = append(options, queue.WithDefaultKeepAlive(time.Duration(*conf.Layers.Queue.DefaultKeepAlive)))
|
||||
}
|
||||
|
||||
return queue.New(
|
||||
adapter,
|
||||
options...,
|
||||
), nil
|
||||
}
|
||||
|
||||
func newQueueAdapter(redisConf config.RedisConfig) (queue.Adapter, error) {
|
||||
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: redisConf.Adresses,
|
||||
MasterName: string(redisConf.Master),
|
||||
})
|
||||
|
||||
return queueRedis.NewAdapter(rdb, 2), nil
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/queue"
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
queueRedis "forge.cadoles.com/cadoles/bouncer/internal/queue/redis"
|
||||
)
|
||||
|
||||
func NewQueueAdapter(ctx context.Context, redisConf config.RedisConfig) (queue.Adapter, error) {
|
||||
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: redisConf.Adresses,
|
||||
MasterName: string(redisConf.Master),
|
||||
})
|
||||
|
||||
return queueRedis.NewAdapter(rdb, 2), nil
|
||||
}
|
80
internal/setup/registry.go
Normal file
80
internal/setup/registry.go
Normal file
@ -0,0 +1,80 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type layerEntry struct {
|
||||
setup LayerSetupFunc
|
||||
rawOptionsSchema []byte
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
layers map[store.LayerType]layerEntry
|
||||
}
|
||||
|
||||
type LayerSetupFunc func(*config.Config) (director.Layer, error)
|
||||
|
||||
func (r *Registry) RegisterLayer(layerType store.LayerType, layerSetup LayerSetupFunc, rawOptionsSchema []byte) {
|
||||
r.layers[layerType] = layerEntry{
|
||||
setup: layerSetup,
|
||||
rawOptionsSchema: rawOptionsSchema,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) GetLayerOptionsSchema(layerType store.LayerType) (*schema.Schema, error) {
|
||||
layerEntry, exists := r.layers[layerType]
|
||||
if !exists {
|
||||
return nil, errors.WithStack(ErrNotFound)
|
||||
}
|
||||
|
||||
schema, err := schema.Parse(layerEntry.rawOptionsSchema)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func (r *Registry) GetLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
|
||||
layers := make([]director.Layer, 0, len(r.layers))
|
||||
|
||||
for layerType, layerEntry := range r.layers {
|
||||
layer, err := layerEntry.setup(conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not create layer '%s'", layerType)
|
||||
}
|
||||
|
||||
layers = append(layers, layer)
|
||||
}
|
||||
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func (r *Registry) LayerTypeExists(layerType store.LayerType) bool {
|
||||
_, exists := r.layers[layerType]
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
func (r *Registry) GetLayerTypes() []store.LayerType {
|
||||
layerTypes := make([]store.LayerType, 0, len(r.layers))
|
||||
|
||||
for layerType := range r.layers {
|
||||
layerTypes = append(layerTypes, layerType)
|
||||
}
|
||||
|
||||
return layerTypes
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
layers: make(map[store.LayerType]layerEntry),
|
||||
}
|
||||
}
|
BIN
misc/logo/bouncer.png
Normal file
BIN
misc/logo/bouncer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
Reference in New Issue
Block a user