Compare commits
13 Commits
v2023.5.21
...
v2023.6.26
Author | SHA1 | Date | |
---|---|---|---|
440d467938 | |||
f8d33299b9 | |||
6fed6358b2 | |||
ef869a02ea | |||
6559d1f594 | |||
8d91f646c2 | |||
e32c72e030 | |||
8d21e9083c | |||
2a8849493d | |||
830d4d3904 | |||
7a45a5ba3b | |||
1cfe07a343 | |||
8f0501b445 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@
|
|||||||
/admin-key.json
|
/admin-key.json
|
||||||
/.bouncer-token
|
/.bouncer-token
|
||||||
/data
|
/data
|
||||||
|
/out
|
@ -60,6 +60,9 @@ nfpms:
|
|||||||
- src: misc/packaging/common/config.yml
|
- src: misc/packaging/common/config.yml
|
||||||
dst: /etc/bouncer/config.yml
|
dst: /etc/bouncer/config.yml
|
||||||
type: config
|
type: config
|
||||||
|
- src: layers
|
||||||
|
dst: /etc/bouncer/layers
|
||||||
|
type: config
|
||||||
- id: bouncer-admin
|
- id: bouncer-admin
|
||||||
meta: true
|
meta: true
|
||||||
package_name: bouncer-admin
|
package_name: bouncer-admin
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
## Guide d'utilisation
|
- [(FR) - Premiers pas](./fr/getting-started.md)
|
||||||
|
- [(FR) - Architecture générale](./fr/general-architecture.md)
|
||||||
> TODO
|
|
||||||
|
|
||||||
## Référence
|
## Référence
|
||||||
|
|
||||||
> TODO
|
- [(FR) - Layers](./fr/references/layers/README.md)
|
||||||
|
- [Fichier de configuration](../misc/packaging/common/config.yml)
|
||||||
|
|
||||||
## Tutoriels
|
## Tutoriels
|
||||||
|
|
||||||
> TODO
|
### Utilisation
|
||||||
|
|
||||||
|
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
||||||
|
|
||||||
|
### Développement
|
||||||
|
|
||||||
|
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)
|
30
doc/fr/general-architecture.md
Normal file
30
doc/fr/general-architecture.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Architecture générale
|
||||||
|
|
||||||
|
## 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).
|
95
doc/fr/getting-started.md
Normal file
95
doc/fr/getting-started.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# Premiers pas
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Une machine Ubuntu 22.04
|
||||||
|
|
||||||
|
## Étapes
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Installer le serveur Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt update
|
||||||
|
apt install redis
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Sur votre machine Ubuntu, télécharger les dernières versions disponibles des paquets Debian correspondant à votre architecture sur la page ["Versions"](https://forge.cadoles.com/Cadoles/bouncer/releases) du projet:
|
||||||
|
- `bouncer-bin_<version>_linux_<arch>.deb`
|
||||||
|
- `bouncer-proxy_<version>_linux_<arch>.deb`
|
||||||
|
- `bouncer-admin_<version>_linux_<arch>.deb`
|
||||||
|
|
||||||
|
|
||||||
|
3. Installer les paquets Debian
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Commencer par le paquet bouncer-bin
|
||||||
|
dpkg -i "bouncer-bin_<version>_linux_<arch>.deb"
|
||||||
|
|
||||||
|
# Puis installer les paquets de services
|
||||||
|
dpkg -i "bouncer-proxy_<version>_linux_<arch>.deb"
|
||||||
|
dpkg -i "bouncer-admin_<version>_linux_<arch>.deb"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Générer un jeton d'authentification pour le CLI d'administration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bouncer --config /etc/bouncer/config.yml auth create-token --role writer --subject $(whoami) > .bouncer-token
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Tester que le CLI est en capacité d'interroger l'API d'administration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bouncer admin query proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Un message équivalent à celui ci devrait s'afficher:
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+---------+--------+
|
||||||
|
| NAME | ENABLED | WEIGHT |
|
||||||
|
+------+---------+--------+
|
||||||
|
+------+---------+--------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Créer un premier proxy
|
||||||
|
|
||||||
|
1. Créer un proxy vers https://www.cadoles.com via le CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Création du proxy nommé 'cadoles' vers https://www.cadoles.com
|
||||||
|
bouncer admin proxy create --proxy-to https://www.cadoles.com --proxy-name cadoles
|
||||||
|
```
|
||||||
|
|
||||||
|
Un message équivalent à celui ci devrait s'afficher:
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------+-------+-------------------------+---------+--------+-------------------------+-------------------------+
|
||||||
|
| NAME | FROM | TO | ENABLED | WEIGHT | CREATEDAT | UPDATEDAT |
|
||||||
|
+---------+-------+-------------------------+---------+--------+-------------------------+-------------------------+
|
||||||
|
| cadoles | ["*"] | https://www.cadoles.com | false | 0 | "2023-05-28T14:28:46... | "2023-05-28T14:28:46... |
|
||||||
|
+---------+-------+-------------------------+---------+--------+-------------------------+-------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
2. À ce stade, le proxy est créé mais encore inactif. Activer le proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Activation du proxy
|
||||||
|
bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Un message équivalent à celui ci devrait s'afficher:
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------+-------+-------------------------+---------+--------+-------------------------+-------------------------+
|
||||||
|
| NAME | FROM | TO | ENABLED | WEIGHT | CREATEDAT | UPDATEDAT |
|
||||||
|
+---------+-------+-------------------------+---------+--------+-------------------------+-------------------------+
|
||||||
|
| cadoles | ["*"] | https://www.cadoles.com | true | 0 | "2023-05-28T14:28:46... | "2023-05-28T14:28:55... |
|
||||||
|
+---------+-------+-------------------------+---------+--------+-------------------------+-------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ouvrir la page `https://<ip_serveur>:8080/` dans un navigateur. Le site Cadoles s'affiche !
|
||||||
|
|
||||||
|
**Bravo, vous avez créé votre premier proxy avec Bouncer !**
|
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).
|
48
doc/fr/tutorials/add-queue-layer.md
Normal file
48
doc/fr/tutorials/add-queue-layer.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# 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).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Création d'un calque nommé 'my-queue' pour le proxy 'cadoles' de type 'queue'
|
||||||
|
bouncer admin layer create --proxy-name cadoles --layer-name my-queue --layer-type queue
|
||||||
|
```
|
||||||
|
|
||||||
|
Un message équivalent à celui ci devrait s'afficher:
|
||||||
|
|
||||||
|
```
|
||||||
|
+----------+-------+---------+--------+---------+-------------------------+-------------------------+
|
||||||
|
| NAME | TYPE | ENABLED | WEIGHT | OPTIONS | CREATEDAT | UPDATEDAT |
|
||||||
|
+----------+-------+---------+--------+---------+-------------------------+-------------------------+
|
||||||
|
| my-queue | queue | false | 0 | {} | "2023-05-28T14:40:25... | "2023-05-28T14:40:25... |
|
||||||
|
+----------+-------+---------+--------+---------+-------------------------+-------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
+----------+-------+---------+--------+----------------+-------------------------+-------------------------+
|
||||||
|
| NAME | TYPE | ENABLED | WEIGHT | OPTIONS | CREATEDAT | UPDATEDAT |
|
||||||
|
+----------+-------+---------+--------+----------------+-------------------------+-------------------------+
|
||||||
|
| my-queue | queue | true | 0 | {"capacity":1} | "2023-05-28T14:51:45... | "2023-05-28T14:52:21... |
|
||||||
|
+----------+-------+---------+--------+----------------+-------------------------+-------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Astuce**
|
||||||
|
>
|
||||||
|
> Les options de chaque type de calque répondent à un schéma spécifique, défini au format [JSON Schema](https://json-schema.org/).
|
||||||
|
>
|
||||||
|
> Par exemple, le schéma du calque type 'queue' est consultable [ici](../../../internal/queue/schema/layer-options.json).
|
||||||
|
|
||||||
|
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 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.
|
499
doc/fr/tutorials/create-custom-layer.md
Normal file
499
doc/fr/tutorials/create-custom-layer.md
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
# Créer son propre layer
|
||||||
|
|
||||||
|
Dans ce tutoriel, nous allons voir comme 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
|
||||||
|
|
||||||
|
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/)
|
||||||
|
|
||||||
|
## Étapes
|
||||||
|
|
||||||
|
### Préparer son environnement de développement
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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.
|
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 |
13
go.mod
13
go.mod
@ -4,17 +4,21 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333
|
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||||
github.com/davecgh/go-spew v1.1.1
|
|
||||||
github.com/go-chi/chi/v5 v5.0.8
|
github.com/go-chi/chi/v5 v5.0.8
|
||||||
github.com/jedib0t/go-pretty/v6 v6.4.6
|
github.com/jedib0t/go-pretty/v6 v6.4.6
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1
|
||||||
github.com/ory/dockertest/v3 v3.10.0
|
github.com/ory/dockertest/v3 v3.10.0
|
||||||
|
github.com/qri-io/jsonschema v0.2.1
|
||||||
github.com/redis/go-redis/v9 v9.0.4
|
github.com/redis/go-redis/v9 v9.0.4
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.99.0 // indirect
|
cloud.google.com/go v0.99.0 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||||
@ -28,19 +32,22 @@ require (
|
|||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/huandu/xstrings v1.3.3 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/kr/pretty v0.3.0 // indirect
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||||
github.com/opencontainers/runc v1.1.5 // indirect
|
github.com/opencontainers/runc v1.1.5 // indirect
|
||||||
github.com/qri-io/jsonpointer v0.1.1 // indirect
|
github.com/qri-io/jsonpointer v0.1.1 // indirect
|
||||||
github.com/qri-io/jsonschema v0.2.1 // indirect
|
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.2.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
|
github.com/spf13/cast v1.3.1 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
|
23
go.sum
23
go.sum
@ -57,6 +57,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||||
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
@ -282,8 +288,11 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
||||||
|
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
|
github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
|
||||||
@ -338,9 +347,13 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
|
|||||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||||
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
|
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
|
||||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
||||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
||||||
@ -391,11 +404,16 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||||
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||||
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||||
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
@ -456,6 +474,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
@ -539,6 +558,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
|
|||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||||
@ -640,12 +660,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||||
@ -659,6 +681,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -155,13 +156,22 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
layerName, err := store.ValidateName(createLayerReq.Name)
|
layerName, err := store.ValidateName(createLayerReq.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
|
logger.Error(r.Context(), "invalid 'name' parameter", logger.E(errors.WithStack(err)))
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||||
|
|
||||||
return
|
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 err != nil {
|
||||||
if errors.Is(err, store.ErrAlreadyExist) {
|
if errors.Is(err, store.ErrAlreadyExist) {
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil)
|
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 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)))
|
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
var invalidDataErr *schema.InvalidDataError
|
var invalidDataErr *schema.InvalidDataError
|
||||||
|
@ -2,27 +2,22 @@ package flag
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
proxyFlag "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/setup"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FlagLayerName = "layer-name"
|
FlagKeyLayerType = "layer-type"
|
||||||
FlagLayerType = "layer-type"
|
|
||||||
FlagLayerOptions = "layer-options"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithLayerFlags(flags ...cli.Flag) []cli.Flag {
|
func WithLayerFlags(flags ...cli.Flag) []cli.Flag {
|
||||||
baseFlags := proxyFlag.WithProxyFlags(
|
baseFlags := proxyFlag.WithProxyFlags(
|
||||||
&cli.StringFlag{
|
LayerName(),
|
||||||
Name: FlagLayerName,
|
|
||||||
Usage: "use `LAYER_NAME` as targeted layer",
|
|
||||||
Value: "",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
flags = append(flags, baseFlags...)
|
flags = append(flags, baseFlags...)
|
||||||
@ -32,22 +27,63 @@ func WithLayerFlags(flags ...cli.Flag) []cli.Flag {
|
|||||||
|
|
||||||
func WithLayerCreateFlags(flags ...cli.Flag) []cli.Flag {
|
func WithLayerCreateFlags(flags ...cli.Flag) []cli.Flag {
|
||||||
return WithLayerFlags(
|
return WithLayerFlags(
|
||||||
&cli.StringFlag{
|
LayerType(),
|
||||||
Name: FlagLayerType,
|
LayerOptions(),
|
||||||
Usage: "Set `LAYER_TYPE` as layer's type",
|
|
||||||
Value: "",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: FlagLayerOptions,
|
|
||||||
Usage: "Set `LAYER_OPTIONS` as layer's options",
|
|
||||||
Value: "{}",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func AssertLayerName(ctx *cli.Context) (store.LayerName, error) {
|
||||||
rawLayerName := ctx.String(FlagLayerName)
|
rawLayerName := ctx.String(KeyLayerName)
|
||||||
|
|
||||||
name, err := store.ValidateName(rawLayerName)
|
name, err := store.ValidateName(rawLayerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -58,13 +94,18 @@ func AssertLayerName(ctx *cli.Context) (store.LayerName, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AssertLayerType(ctx *cli.Context) (store.LayerType, 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) {
|
func AssertLayerOptions(ctx *cli.Context) (store.LayerOptions, error) {
|
||||||
rawLayerOptions := ctx.String(FlagLayerOptions)
|
rawLayerOptions := ctx.String(KeyLayerOptions)
|
||||||
|
|
||||||
layerOptions := store.LayerOptions{}
|
layerOptions := store.LayerOptions{}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
||||||
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
|
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"
|
layerFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag"
|
||||||
proxyFlag "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"
|
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
||||||
@ -20,18 +21,9 @@ func UpdateCommand() *cli.Command {
|
|||||||
Name: "update",
|
Name: "update",
|
||||||
Usage: "Update layer",
|
Usage: "Update layer",
|
||||||
Flags: layerFlag.WithLayerFlags(
|
Flags: layerFlag.WithLayerFlags(
|
||||||
&cli.BoolFlag{
|
flag.LayerEnabled(),
|
||||||
Name: "enabled",
|
flag.LayerWeight(),
|
||||||
Usage: "Enable or disable proxy",
|
flag.LayerOptions(),
|
||||||
},
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "weight",
|
|
||||||
Usage: "Set `WEIGHT` as proxy's weight",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "options",
|
|
||||||
Usage: "Set `OPTIONS` as proxy's options",
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
@ -53,22 +45,22 @@ func UpdateCommand() *cli.Command {
|
|||||||
|
|
||||||
opts := &client.UpdateLayerOptions{}
|
opts := &client.UpdateLayerOptions{}
|
||||||
|
|
||||||
if ctx.IsSet("options") {
|
if ctx.IsSet(flag.KeyLayerOptions) {
|
||||||
var options store.LayerOptions
|
var options store.LayerOptions
|
||||||
if err := json.Unmarshal([]byte(ctx.String("options")), &options); err != nil {
|
if err := json.Unmarshal([]byte(ctx.String(flag.KeyLayerOptions)), &options); err != nil {
|
||||||
return errors.Wrap(err, "could not parse options")
|
return errors.Wrap(err, "could not parse layer's options")
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.Options = &options
|
opts.Options = &options
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsSet("weight") {
|
if ctx.IsSet(flag.KeyLayerWeight) {
|
||||||
weight := ctx.Int("weight")
|
weight := ctx.Int(flag.KeyLayerWeight)
|
||||||
opts.Weight = &weight
|
opts.Weight = &weight
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsSet("enabled") {
|
if ctx.IsSet(flag.KeyLayerEnabled) {
|
||||||
enabled := ctx.Bool("enabled")
|
enabled := ctx.Bool(flag.KeyLayerEnabled)
|
||||||
opts.Enabled = &enabled
|
opts.Enabled = &enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
||||||
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
|
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"
|
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -18,17 +19,8 @@ func CreateCommand() *cli.Command {
|
|||||||
Name: "create",
|
Name: "create",
|
||||||
Usage: "Create proxy",
|
Usage: "Create proxy",
|
||||||
Flags: proxyFlag.WithProxyFlags(
|
Flags: proxyFlag.WithProxyFlags(
|
||||||
&cli.StringFlag{
|
flag.ProxyTo(true),
|
||||||
Name: "to",
|
flag.ProxyFrom(),
|
||||||
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("*"),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
@ -43,12 +35,12 @@ func CreateCommand() *cli.Command {
|
|||||||
return errors.Wrap(err, "'to' parameter should be a valid url")
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "'to' parameter should be a valid url")
|
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))
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
|
@ -7,16 +7,9 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FlagProxyName = "proxy-name"
|
|
||||||
|
|
||||||
func WithProxyFlags(flags ...cli.Flag) []cli.Flag {
|
func WithProxyFlags(flags ...cli.Flag) []cli.Flag {
|
||||||
baseFlags := clientFlag.ComposeFlags(
|
baseFlags := clientFlag.ComposeFlags(
|
||||||
&cli.StringFlag{
|
ProxyName(),
|
||||||
Name: FlagProxyName,
|
|
||||||
Usage: "use `PROXY_NAME` as targeted proxy",
|
|
||||||
Value: "",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
flags = append(flags, baseFlags...)
|
flags = append(flags, baseFlags...)
|
||||||
@ -24,8 +17,58 @@ func WithProxyFlags(flags ...cli.Flag) []cli.Flag {
|
|||||||
return flags
|
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) {
|
func AssertProxyName(ctx *cli.Context) (store.ProxyName, error) {
|
||||||
rawProxyName := ctx.String(FlagProxyName)
|
rawProxyName := ctx.String(KeyProxyName)
|
||||||
|
|
||||||
name, err := store.ValidateName(rawProxyName)
|
name, err := store.ValidateName(rawProxyName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
"forge.cadoles.com/cadoles/bouncer/internal/client"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
|
||||||
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
|
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"
|
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
"forge.cadoles.com/cadoles/bouncer/internal/format"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -18,22 +19,10 @@ func UpdateCommand() *cli.Command {
|
|||||||
Name: "update",
|
Name: "update",
|
||||||
Usage: "Update proxy",
|
Usage: "Update proxy",
|
||||||
Flags: proxyFlag.WithProxyFlags(
|
Flags: proxyFlag.WithProxyFlags(
|
||||||
&cli.StringFlag{
|
flag.ProxyTo(false),
|
||||||
Name: "to",
|
flag.ProxyFrom(),
|
||||||
Usage: "Set `TO` as proxy's destination url",
|
flag.ProxyEnabled(),
|
||||||
},
|
flag.ProxyWeight(),
|
||||||
&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",
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
@ -50,27 +39,29 @@ func UpdateCommand() *cli.Command {
|
|||||||
|
|
||||||
opts := &client.UpdateProxyOptions{}
|
opts := &client.UpdateProxyOptions{}
|
||||||
|
|
||||||
if ctx.IsSet("to") {
|
if ctx.IsSet(flag.KeyProxyTo) {
|
||||||
to := ctx.String("to")
|
to := ctx.String(flag.KeyProxyTo)
|
||||||
if _, err := url.Parse(to); err != nil {
|
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
|
opts.To = &to
|
||||||
}
|
}
|
||||||
|
|
||||||
from := ctx.StringSlice("from")
|
if ctx.IsSet(flag.KeyProxyFrom) {
|
||||||
|
from := ctx.StringSlice(flag.KeyProxyFrom)
|
||||||
if from != nil {
|
if from != nil {
|
||||||
opts.From = from
|
opts.From = from
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.IsSet("weight") {
|
if ctx.IsSet(flag.KeyProxyWeight) {
|
||||||
weight := ctx.Int("weight")
|
weight := ctx.Int(flag.KeyProxyWeight)
|
||||||
opts.Weight = &weight
|
opts.Weight = &weight
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsSet("enabled") {
|
if ctx.IsSet(flag.KeyProxyEnabled) {
|
||||||
enabled := ctx.Bool("enabled")
|
enabled := ctx.Bool(flag.KeyProxyEnabled)
|
||||||
opts.Enabled = &enabled
|
opts.Enabled = &enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
"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"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/queue"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@ -32,7 +28,7 @@ func RunCommand() *cli.Command {
|
|||||||
logger.SetFormat(logger.Format(conf.Logger.Format))
|
logger.SetFormat(logger.Format(conf.Logger.Format))
|
||||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||||
|
|
||||||
layers, err := initDirectorLayers(ctx.Context, conf)
|
layers, err := setup.GetLayers(ctx.Context, conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not initialize director layers")
|
return errors.Wrap(err, "could not initialize director layers")
|
||||||
}
|
}
|
||||||
@ -63,25 +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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return queue.New(adapter), nil
|
|
||||||
}
|
|
||||||
|
@ -14,6 +14,7 @@ type Config struct {
|
|||||||
Proxy ProxyServerConfig `yaml:"proxy"`
|
Proxy ProxyServerConfig `yaml:"proxy"`
|
||||||
Redis RedisConfig `yaml:"redis"`
|
Redis RedisConfig `yaml:"redis"`
|
||||||
Logger LoggerConfig `yaml:"logger"`
|
Logger LoggerConfig `yaml:"logger"`
|
||||||
|
Layers LayersConfig `yaml:"layers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFromFile retrieves the configuration from the given file
|
// NewFromFile retrieves the configuration from the given file
|
||||||
@ -46,6 +47,7 @@ func NewDefault() *Config {
|
|||||||
Proxy: NewDefaultProxyServerConfig(),
|
Proxy: NewDefaultProxyServerConfig(),
|
||||||
Logger: NewDefaultLoggerConfig(),
|
Logger: NewDefaultLoggerConfig(),
|
||||||
Redis: NewDefaultRedisConfig(),
|
Redis: NewDefaultRedisConfig(),
|
||||||
|
Layers: NewDefaultLayersConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -123,3 +124,37 @@ func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InterpolatedDuration time.Duration
|
||||||
|
|
||||||
|
func (id *InterpolatedDuration) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
var str string
|
||||||
|
|
||||||
|
if err := value.Decode(&str); err != nil {
|
||||||
|
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := reVar.FindStringSubmatch(str); len(match) > 0 {
|
||||||
|
str = os.Getenv(match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(str)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not parse duration '%v', line '%d'", str, value.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
*id = InterpolatedDuration(duration)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *InterpolatedDuration) MarshalYAML() (interface{}, error) {
|
||||||
|
duration := time.Duration(*id)
|
||||||
|
|
||||||
|
return duration.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterpolatedDuration(d time.Duration) *InterpolatedDuration {
|
||||||
|
id := InterpolatedDuration(d)
|
||||||
|
return &id
|
||||||
|
}
|
||||||
|
21
internal/config/layers.go
Normal file
21
internal/config/layers.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type LayersConfig struct {
|
||||||
|
Queue QueueLayerConfig `yaml:"queue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultLayersConfig() LayersConfig {
|
||||||
|
return LayersConfig{
|
||||||
|
Queue: QueueLayerConfig{
|
||||||
|
TemplateDir: "./layers/queue/templates",
|
||||||
|
DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueueLayerConfig struct {
|
||||||
|
TemplateDir InterpolatedString `yaml:"templateDir"`
|
||||||
|
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
|
||||||
|
}
|
@ -15,11 +15,11 @@ type LayerOptions struct {
|
|||||||
KeepAlive time.Duration `mapstructure:"keepAlive"`
|
KeepAlive time.Duration `mapstructure:"keepAlive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
func fromStoreOptions(storeOptions store.LayerOptions, defaultKeepAlive time.Duration) (*LayerOptions, error) {
|
||||||
layerOptions := LayerOptions{
|
layerOptions := LayerOptions{
|
||||||
Capacity: 1000,
|
Capacity: 1000,
|
||||||
Matchers: []string{"*"},
|
Matchers: []string{"*"},
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: defaultKeepAlive,
|
||||||
}
|
}
|
||||||
|
|
||||||
config := mapstructure.DecoderConfig{
|
config := mapstructure.DecoderConfig{
|
29
internal/proxy/director/layer/queue/options.go
Normal file
29
internal/proxy/director/layer/queue/options.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package queue
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
TemplateDir string
|
||||||
|
DefaultKeepAlive time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(*Options)
|
||||||
|
|
||||||
|
func defaultOptions() *Options {
|
||||||
|
return &Options{
|
||||||
|
TemplateDir: "./templates",
|
||||||
|
DefaultKeepAlive: time.Minute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTemplateDir(templateDir string) OptionFunc {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.TemplateDir = templateDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDefaultKeepAlive(keepAlive time.Duration) OptionFunc {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.DefaultKeepAlive = keepAlive
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,17 @@ package queue
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/go-proxy"
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/Masterminds/sprig/v3"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
@ -19,6 +23,13 @@ const LayerType store.LayerType = "queue"
|
|||||||
|
|
||||||
type Queue struct {
|
type Queue struct {
|
||||||
adapter Adapter
|
adapter Adapter
|
||||||
|
|
||||||
|
defaultKeepAlive time.Duration
|
||||||
|
|
||||||
|
templateDir string
|
||||||
|
loadOnce sync.Once
|
||||||
|
tmpl *template.Template
|
||||||
|
|
||||||
refreshJobRunning uint32
|
refreshJobRunning uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +44,7 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
|
|||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
options, err := fromStoreOptions(layer.Options)
|
options, err := fromStoreOptions(layer.Options, q.defaultKeepAlive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
|
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
@ -102,7 +113,52 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Error(w, fmt.Sprintf("queued (rank: %d, status: %d/%d)", rank+1, status.Sessions, options.Capacity), http.StatusServiceUnavailable)
|
q.loadOnce.Do(func() {
|
||||||
|
pattern := filepath.Join(q.templateDir, "*.gohtml")
|
||||||
|
|
||||||
|
logger.Info(ctx, "loading queue page templates", logger.F("pattern", pattern))
|
||||||
|
|
||||||
|
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not load queue templates", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q.tmpl = tmpl
|
||||||
|
})
|
||||||
|
|
||||||
|
if q.tmpl == nil {
|
||||||
|
logger.Error(ctx, "queue page templates not loaded", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateData := struct {
|
||||||
|
QueueName string
|
||||||
|
LayerOptions *LayerOptions
|
||||||
|
Rank int64
|
||||||
|
CurrentSessions int64
|
||||||
|
MaxSessions int64
|
||||||
|
RefreshRate int64
|
||||||
|
}{
|
||||||
|
QueueName: queueName,
|
||||||
|
LayerOptions: options,
|
||||||
|
Rank: rank + 1,
|
||||||
|
CurrentSessions: status.Sessions,
|
||||||
|
MaxSessions: options.Capacity,
|
||||||
|
RefreshRate: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
|
||||||
|
if err := q.tmpl.ExecuteTemplate(w, "queue", templateData); err != nil {
|
||||||
|
logger.Error(ctx, "could not render queue page", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queue) refreshQueue(queueName string, keepAlive time.Duration) {
|
func (q *Queue) refreshQueue(queueName string, keepAlive time.Duration) {
|
||||||
@ -137,6 +193,8 @@ func New(adapter Adapter, funcs ...OptionFunc) *Queue {
|
|||||||
|
|
||||||
return &Queue{
|
return &Queue{
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
|
templateDir: opts.TemplateDir,
|
||||||
|
defaultKeepAlive: opts.DefaultKeepAlive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/queue"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/redis/go-redis/v9"
|
"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",
|
"type": "number",
|
||||||
"minimum": 0
|
"minimum": 0
|
||||||
},
|
},
|
||||||
"matchers": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"keepAlive": {
|
"keepAlive": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
@ -1,9 +0,0 @@
|
|||||||
package queue
|
|
||||||
|
|
||||||
type Options struct{}
|
|
||||||
|
|
||||||
type OptionFunc func(*Options)
|
|
||||||
|
|
||||||
func defaultOptions() *Options {
|
|
||||||
return &Options{}
|
|
||||||
}
|
|
@ -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"
|
"github.com/qri-io/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Parse(data []byte) (*jsonschema.Schema, error) {
|
type Schema = jsonschema.Schema
|
||||||
var schema jsonschema.Schema
|
|
||||||
|
func Parse(data []byte) (*Schema, error) {
|
||||||
|
var schema Schema
|
||||||
if err := json.Unmarshal(data, &schema); err != nil {
|
if err := json.Unmarshal(data, &schema); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
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, conf config.RedisConfig) (queue.Adapter, error) {
|
|
||||||
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
|
|
||||||
Addrs: conf.Adresses,
|
|
||||||
MasterName: string(conf.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),
|
||||||
|
}
|
||||||
|
}
|
84
layers/queue/templates/queue.gohtml
Normal file
84
layers/queue/templates/queue.gohtml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{{ define "queue" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Veuillez patienter - {{ .QueueName }}</title>
|
||||||
|
<meta http-equiv="refresh" content="{{ .RefreshRate }}">
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *:before, *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#queue {
|
||||||
|
padding: 1.5em 1em;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 2px 2px #cccccc1c;
|
||||||
|
color: #333333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-top: 2em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<div id="queue">
|
||||||
|
<h2 class="title">Un instant s'il vous plaît...</h2>
|
||||||
|
<p>Le site auquel vous souhaitez accéder est actuellement surchargé.</p>
|
||||||
|
{{ $rank := sub .Rank .MaxSessions }}
|
||||||
|
{{ $waiting := sub .CurrentSessions .MaxSessions }}
|
||||||
|
<p class="text-centered">
|
||||||
|
<b>Position: {{ $rank }}</b> <em>(sur {{ $waiting }} en attente)</em>
|
||||||
|
</p>
|
||||||
|
<p class="text-centered"><em>Cette page se rafraîchira automatiquement lorsqu'une place sera disponible.</em></p>
|
||||||
|
<p class="footer">Propulsé par <a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
@ -1,10 +1,18 @@
|
|||||||
|
# Configuration du service "admin"
|
||||||
admin:
|
admin:
|
||||||
http:
|
http:
|
||||||
|
# Hôte d'écoute du service,
|
||||||
|
# 0.0.0.0 pour écouter sur toutes les interfaces
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
|
# Port d'écoute du service
|
||||||
port: 8081
|
port: 8081
|
||||||
|
|
||||||
|
# Configuration CORS du service
|
||||||
|
# Uniquement nécessaire si un frontend web
|
||||||
|
# est branché sur l'API d'administration.
|
||||||
cors:
|
cors:
|
||||||
allowedOrigins:
|
allowedOrigins:
|
||||||
- http://localhost:3001
|
- http://localhost:8081
|
||||||
allowCredentials: true
|
allowCredentials: true
|
||||||
allowMethods:
|
allowMethods:
|
||||||
- POST
|
- POST
|
||||||
@ -20,14 +28,45 @@ admin:
|
|||||||
debug: false
|
debug: false
|
||||||
auth:
|
auth:
|
||||||
issuer: http://127.0.0.1:8081
|
issuer: http://127.0.0.1:8081
|
||||||
privateKey: admin-key.json
|
privateKey: /etc/bouncer/admin-key.json
|
||||||
|
|
||||||
|
# Configuration du service "proxy"
|
||||||
proxy:
|
proxy:
|
||||||
http:
|
http:
|
||||||
|
# Hôte d'écoute du service,
|
||||||
|
# 0.0.0.0 pour écouter sur toutes les interfaces
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
|
# Port d'écoute du service
|
||||||
port: 8080
|
port: 8080
|
||||||
database:
|
|
||||||
driver: sqlite
|
# Configuration du client Redis
|
||||||
dsn: sqlite://bouncer.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=10000
|
#
|
||||||
|
# Les modes "standalone", "sentinel" et "cluster" de Redis sont supportés:
|
||||||
|
# - Mode "standalone": renseigner une seule entrée dans redis.addresses;
|
||||||
|
# - Mode "sentinel": renseigner une adresse dans redis.master et une ou plusieurs adresses dans redis.addresses;
|
||||||
|
# - Mode "cluster": renseigner plusieurs adresses dans redis.addresses et laisser redis.master vide.
|
||||||
|
redis:
|
||||||
|
addresses:
|
||||||
|
- localhost:6379
|
||||||
|
master: ""
|
||||||
|
|
||||||
|
# Configuration des logs
|
||||||
logger:
|
logger:
|
||||||
|
# Niveau de verbosité
|
||||||
|
# 0 - DEBUG
|
||||||
|
# 1 - INFO
|
||||||
|
# 2 - WARNING
|
||||||
|
# 3 - ERROR
|
||||||
|
# 4 - FATAL
|
||||||
level: 1
|
level: 1
|
||||||
|
# Format des logs, "human" ou "json"
|
||||||
format: human
|
format: human
|
||||||
|
|
||||||
|
# Configuration des différents layers
|
||||||
|
layers:
|
||||||
|
# Configuration du layer "queue"
|
||||||
|
queue:
|
||||||
|
# Répertoire contenant les templates
|
||||||
|
templateDir: "/etc/bouncer/layers/queue/templates"
|
||||||
|
# Temps de vie par défaut d'une session
|
||||||
|
defaultKeepAlive: 1m
|
@ -28,7 +28,7 @@ cleanInstall() {
|
|||||||
|
|
||||||
service ${service_name} restart || :
|
service ${service_name} restart || :
|
||||||
else
|
else
|
||||||
if [[ "${systemd_version}" -lt 231 ]]; then
|
if [ "${systemd_version}" -lt 231 ]; then
|
||||||
printf "\033[31m systemd version %s is less then 231, fixing the service file \033[0m\n" "${systemd_version}"
|
printf "\033[31m systemd version %s is less then 231, fixing the service file \033[0m\n" "${systemd_version}"
|
||||||
sed -i "s/=+/=/g" /usr/lib/systemd/system/${service_name}.service
|
sed -i "s/=+/=/g" /usr/lib/systemd/system/${service_name}.service
|
||||||
fi
|
fi
|
||||||
|
@ -28,7 +28,7 @@ cleanInstall() {
|
|||||||
|
|
||||||
service ${service_name} restart || :
|
service ${service_name} restart || :
|
||||||
else
|
else
|
||||||
if [[ "${systemd_version}" -lt 231 ]]; then
|
if [ "${systemd_version}" -lt 231 ]; then
|
||||||
printf "\033[31m systemd version %s is less then 231, fixing the service file \033[0m\n" "${systemd_version}"
|
printf "\033[31m systemd version %s is less then 231, fixing the service file \033[0m\n" "${systemd_version}"
|
||||||
sed -i "s/=+/=/g" /usr/lib/systemd/system/${service_name}.service
|
sed -i "s/=+/=/g" /usr/lib/systemd/system/${service_name}.service
|
||||||
fi
|
fi
|
||||||
|
@ -6,10 +6,6 @@ supervisor=supervise-daemon
|
|||||||
output_log="/var/log/bouncer/admin.log"
|
output_log="/var/log/bouncer/admin.log"
|
||||||
error_log="$output_log"
|
error_log="$output_log"
|
||||||
|
|
||||||
start_pre() {
|
|
||||||
/usr/bin/bouncer --workdir /usr/share/bouncer --config /etc/bouncer/config.yml database migrate
|
|
||||||
}
|
|
||||||
|
|
||||||
depend() {
|
depend() {
|
||||||
need net
|
need net
|
||||||
}
|
}
|
@ -6,10 +6,6 @@ supervisor=supervise-daemon
|
|||||||
output_log="/var/log/bouncer/proxy.log"
|
output_log="/var/log/bouncer/proxy.log"
|
||||||
error_log="$output_log"
|
error_log="$output_log"
|
||||||
|
|
||||||
start_pre() {
|
|
||||||
/usr/bin/bouncer --workdir /usr/share/bouncer --config /etc/bouncer/config.yml database migrate
|
|
||||||
}
|
|
||||||
|
|
||||||
depend() {
|
depend() {
|
||||||
need net
|
need net
|
||||||
}
|
}
|
@ -2,6 +2,7 @@
|
|||||||
internal/**/*.json
|
internal/**/*.json
|
||||||
modd.conf
|
modd.conf
|
||||||
config.yml
|
config.yml
|
||||||
|
layers/**
|
||||||
.env {
|
.env {
|
||||||
prep: make RUN_INSTALL_TESTS=no GOTEST_ARGS="-short" test
|
prep: make RUN_INSTALL_TESTS=no GOTEST_ARGS="-short" test
|
||||||
prep: make build-bouncer
|
prep: make build-bouncer
|
||||||
|
Reference in New Issue
Block a user