Compare commits

...

12 Commits

Author SHA1 Message Date
b44ff2a68e doc: add proxy http api reference
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-08 12:19:43 -06:00
c719fdca37 feat: add prometheus + grafterm dashboard in local dev environment 2023-07-08 12:18:38 -06:00
2b91c1e167 feat(store,repository): add more integration tests
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-07 12:22:31 -06:00
cebf1daf72 chore: update github.com/lestrrat-go/jwx/v2
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-07 10:20:27 -06:00
6734cf6526 Merge pull request 'Génération de l'image Docker via le pipeline Jenkins' (#7) from ci-docker-release into develop
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
Reviewed-on: #7
2023-07-07 18:11:06 +02:00
368273f1ee chore(ci): release docker image
Some checks are pending
Cadoles/bouncer/pipeline/pr-develop Build started...
2023-07-07 10:10:22 -06:00
553513d647 Merge pull request 'Implémentation du layer "circuitbreaker"' (#6) from circuitbreaker into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #6
2023-07-06 16:46:17 +02:00
60487c11d6 feat: optional real-ip middleware
All checks were successful
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2023-07-06 08:16:17 -06:00
e6f18e7cd8 fix(doc): typo
All checks were successful
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2023-07-06 07:59:20 -06:00
a207291c04 feat: implements circuitbreaker layer 2023-07-06 07:59:20 -06:00
64b5182f8b fix(doc): bad link
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-06 07:41:53 -06:00
ce2c19f9b3 feat(layer,queue): implement matchURLs option
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-05 13:54:01 -06:00
33 changed files with 1116 additions and 42 deletions

27
Jenkinsfile vendored
View File

@ -29,7 +29,7 @@ pipeline {
}
}
stage('Release') {
stage('Release binaries and packages') {
when {
anyOf {
branch 'master'
@ -50,6 +50,31 @@ pipeline {
}
}
}
stage('Build and release Docker image') {
when {
anyOf {
branch 'master'
branch 'develop'
}
}
steps {
script {
withCredentials([
usernamePassword([
credentialsId: 'kipp-credentials',
usernameVariable: 'DOCKER_REGISTRY_USERNAME',
passwordVariable: 'DOCKER_REGISTRY_PASSWORD'
])
]) {
sh """
echo '${env.DOCKER_REGISTRY_PASSWORD}' | docker login --username '${env.DOCKER_REGISTRY_USERNAME}' --password-stdin reg.cadoles.com
make docker-build docker-release
"""
}
}
}
}
}
post {

View File

@ -101,6 +101,12 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
tools/gitea-release/bin/gitea-release.sh
grafterm: tools/grafterm/bin/grafterm
tools/grafterm/bin/grafterm -c ./misc/grafterm/dashboard.json -v job=bouncer-proxy -r 5s
siege:
siege -i -c 100 -f ./misc/siege/urls.txt
tools/gitea-release/bin/gitea-release.sh:
mkdir -p tools/gitea-release/bin
curl --output tools/gitea-release/bin/gitea-release.sh https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/gitea/gitea-release.sh
@ -110,6 +116,10 @@ tools/modd/bin/modd:
mkdir -p tools/modd/bin
GOBIN=$(PWD)/tools/modd/bin go install github.com/cortesi/modd/cmd/modd@latest
tools/grafterm/bin/grafterm:
mkdir -p tools/grafterm/bin
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
full-version:
@echo $(FULL_VERSION)
@ -128,4 +138,12 @@ run-redis:
redis-shell:
docker exec -it \
bouncer-redis \
redis-cli
redis-cli
run-prometheus:
docker kill bouncer-prometheus || exit 0
docker run --rm -t \
--name bouncer-prometheus \
--network host \
-v $(PWD)/misc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
prom/prometheus

View File

@ -9,15 +9,16 @@
## Référence
- [(FR) - Layers](./fr/references/layers/README.md)
- [Fichier de configuration](../misc/packaging/common/config.yml)
- [(FR) - Fichier de configuration](../misc/packaging/common/config.yml)
- [(FR) - API d'administration](./fr/references/admin_api.md)
## Tutoriels
### Utilisation
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
- [(FR) - Ajouter un layer de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
### Développement
- [(FR) - Démarrer avec les sources](./fr/tutorials/getting-start-with-sources.md)
- [(FR) - Démarrer avec les sources](./fr/tutorials/getting-started-with-sources.md)
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)

View File

@ -0,0 +1,182 @@
# API d'administration
## Authentification
L'ensemble des appels aux APIs HTTP du service `bouncer-admin` sont authentifiées via l'utilisation d'un jeton [JWT](https://datatracker.ietf.org/doc/html/rfc7519) signé par la clé privée du serveur.
Le jeton d'accès doit être transmis avec l'ensemble des appels aux points d'entrée via l'entête HTTP `Authorization` en respectant la forme suivante:
```
Authorization: Bearer <jwt>
```
### Génération d'un jeton d'authentification
La génération d'un jeton d'authentification s'effectue via la commande suivante:
```shell
bouncer auth create-token --subject "<subject>" --role "<role>"
```
Où:
- `"<subject>"` est une chaîne de caractère arbitraire ayant pour objectif d'identifier de manière unique l'utilisateur associé au jeton;
- `"<role>"` peut prendre une des deux valeurs `reader` ou `writer` correspondant aux droits suivants respectifs:
- droit en lecture sur l'ensemble des entités (proxy, layer);
- droit en lecture ET en écriture sur l'ensemble des entités.
## Points d'entrée
### `POST /api/v1/proxies`
Créer un nouveau proxy
#### Exemple de corps de requête
```json5
{
"name": "myproxy", // OBLIGATOIRE - Nom du proxy
"to": "https://www.cadoles.com", // OBLIGATOIRE - Site distant ciblé par le proxy
"from": ["*"] // OPTIONNEL - Liste de patrons de filtrage associés au proxy
}
```
#### Exemple de résultat
```json5
{
"data": {
"proxy": {
"name": "myproxy",
"weight": 0,
"enabled": false,
"to": "https://www.cadoles.com",
"from": ["*"],
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
}
}
```
#### Source
Voir [`internal/admin/proxy_route.go#createProxy()`](../../../internal/admin/proxy_route.go#createProxy)
### `GET /api/v1/proxies/{proxyName}`
Récupérer les informations complètes sur un proxy
#### Paramètres
- `{proxyName}` - Nom du proxy
#### Exemple de résultat
```json5
{
"data": {
"proxy": {
"name": "myproxy",
"weight": 0,
"enabled": false,
"to": "https://www.cadoles.com",
"from": ["*"],
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2018-12-10T13:45:00.000Z"
}
}
}
```
#### Source
Voir [`internal/admin/proxy_route.go#getProxy()`](../../../internal/admin/proxy_route.go#getProxy)
### `PUT /api/v1/proxies/{proxyName}`
Modifier un proxy
#### Exemple de corps de requête
```json5
{
"to": "https://www.cadoles.com", // OPTIONNEL - Site distant ciblé par le proxy
"from": ["mylocalproxydomain:*"], // OPTIONNEL - Liste de patrons de filtrage associés au proxy
"weight": 100, // OPTIONNEL - Poids à associer au proxy
"enabled": true, // OPTIONNEL - Activer/désactiver le proxy
}
```
#### Exemple de résultat
```json5
{
"data": {
"proxy": {
"name": "myproxy",
"weight": 100,
"enabled": true,
"to": "https://www.cadoles.com",
"from": ["mylocalproxydomain:*"],
"createdAt": "2018-12-10T13:45:00.000Z",
"updatedAt": "2020-10-02T15:09:00.000Z"
}
}
}
```
#### Source
Voir [`internal/admin/proxy_route.go#updateProxy()`](../../../internal/admin/proxy_route.go#updateProxy)
### `GET /api/v1/proxies?names={name1,name2,...}`
Lister les proxies existants
#### Paramètres
- `{names}` - Optionnel - Liste des noms de proxy à appliquer en tant que filtre
#### Exemple de résultat
```json5
{
"data": {
"proxies": [
{
"name": "myproxy",
"weight": 0,
"enabled": false,
}
]
}
}
```
#### Source
Voir [`internal/admin/proxy_route.go#queryProxy()`](../../../internal/admin/proxy_route.go#queryProxy)
## `DELETE /api/v1/proxies/{proxyName}`
Supprimer le proxy
#### Paramètres
- `{proxyName}` - Nom du proxy
#### Exemple de résultat
```json5
{
"data": {
"proxyName": "myproxy"
}
}
```
#### Source
Voir [`internal/admin/proxy_route.go#deleteProxy()`](../../../internal/admin/proxy_route.go#deleteProxy)

View File

@ -2,4 +2,5 @@
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
- [Queue](./queue.md) - File d'attente dynamique
- [Queue](./queue.md) - File d'attente dynamique
- [Circuit Breaker](./circuitbreaker.md) - Coupure d'accès à un site ou une sous section de celui ci

View File

@ -0,0 +1,37 @@
# Layer "Circuit Breaker"
## Description
Ce layer permet de bloquer l'accès à un site (ou une section de celui ci) ciblé par un proxy.
## Type
`circuitbreaker`
## Options
### `authorizedCIDRs`
- **Type:** `[]string`
- **Valeur par défaut:** `[]`
- **Description:** Autoriser les adresses distantes contenues dans un des masques réseau (en notation ["CIDR"](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation) définis à contourner la restriction d'accès.
### `matchURLs`
- **Type:** `[]string`
- **Valeur par défaut:** `["*"]`
- **Description:** Limiter l'action du layer à cette liste de patrons d'URLs.
Par exemple, si vous souhaitez limiter votre restriction d'accès à l'ensemble d'une section "`/blog`" d'un site, vous pouvez déclarer la valeur `["*/blog*"]`. Les autres URLs du site ne seront pas affectées par la restriction.
### `templateBlock`
- **Type:** `string`
- **Valeur par défaut:** `"default"`
- **Description:** Bloc du template HTML pour effectuer le rendu de la page indiquant la restriction d'accès.
Voir le [fichier de configuration de référence](../../../../misc/packaging/common/config.yml), section `layers.circuitbreaker` pour voir les options permettant de personnaliser le chemin du répertoire contenant les templates.
### Schéma
Voir le [schéma JSON](../../../../internal/proxy/director/layer/circuitbreaker/layer-options.json).

View File

@ -22,6 +22,14 @@ Ce layer permet d'ajouter un mécanisme de file d'attente dynamique au proxy ass
- **Valeur par défaut:** `1m`
- **Description:** Durée de vie d'une session dans la file d'attente sans activité avant expiration.
### `matchURLs`
- **Type:** `[]string`
- **Valeur par défaut:** `["*"]`
- **Description:** Limiter l'action de la file d'attente à cette liste de patrons d'URLs.
Par exemple, si vous souhaitez limiter votre file à l'ensemble d'une section "`/blog`" d'un site, vous pouvez déclarer la valeur `["*/blog*"]`. Les autres URLs du site ne seront pas affectées par cette file d'attente.
### Schéma
Voir le [schéma JSON](../../../../internal/proxy/director/layer/queue/schema/layer-options.json).

View File

@ -70,15 +70,15 @@ docker run --rm -t \
Surveiller les sources, compiler celles ci en cas de modifications et lancer les services `bouncer-proxy` et `bouncer-admin`.
#### `make test`
### `make test`
Exécuter les tests unitaires/d'intégration du projet.
#### `make build`
### `make build`
Compiler une version de développement du binaire `bouncer`.
#### `make docker-build`
### `make docker-build`
Construire une image Docker pour Bouncer.
@ -92,6 +92,13 @@ docker run \
bouncer server proxy run
```
### `make grafterm`
Afficher un tableau de bord [`grafterm`](https://github.com/slok/grafterm) branché sur l'instance Prometheus locale.
### `make siege`
Lancer une session de test [`siege`](https://github.com/JoeDog/siege) sur l'instance `bouncer-proxy` locale.
## Arborescence du projet
```bash

11
go.mod
View File

@ -52,6 +52,7 @@ require (
github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
@ -68,7 +69,7 @@ require (
cdr.dev/slog v1.4.2 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dlclark/regexp2 v1.9.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-chi/cors v1.2.1
@ -82,7 +83,7 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.9
github.com/lestrrat-go/jwx/v2 v2.0.11
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lib/pq v1.10.0 // indirect
github.com/lithammer/shortuuid/v4 v4.0.0
@ -94,10 +95,10 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect

34
go.sum
View File

@ -143,9 +143,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@ -330,8 +331,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8=
github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM=
github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ=
github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
@ -421,6 +422,8 @@ 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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
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/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
@ -446,8 +449,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@ -493,9 +496,8 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
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.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.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -578,8 +580,8 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx
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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -674,6 +676,7 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -682,15 +685,15 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.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-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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -702,7 +705,6 @@ 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.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.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/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -9,6 +9,7 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/auth"
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"forge.cadoles.com/cadoles/bouncer/internal/store"
@ -91,7 +92,11 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
router := chi.NewRouter()
router.Use(middleware.Logger)
if s.serverConfig.HTTP.UseRealIP {
router.Use(middleware.RealIP)
}
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
if s.serverConfig.Sentry.DSN != "" {
logger.Info(ctx, "enabling sentry http middleware")

View File

@ -1,13 +1,15 @@
package config
type HTTPConfig struct {
Host InterpolatedString `yaml:"host"`
Port InterpolatedInt `yaml:"port"`
Host InterpolatedString `yaml:"host"`
Port InterpolatedInt `yaml:"port"`
UseRealIP InterpolatedBool `yaml:"useRealIP"`
}
func NewHTTPConfig(host string, port int) HTTPConfig {
return HTTPConfig{
Host: InterpolatedString(host),
Port: InterpolatedInt(port),
Host: InterpolatedString(host),
Port: InterpolatedInt(port),
UseRealIP: true,
}
}

View File

@ -3,7 +3,8 @@ package config
import "time"
type LayersConfig struct {
Queue QueueLayerConfig `yaml:"queue"`
Queue QueueLayerConfig `yaml:"queue"`
CircuitBreaker CircuitBreakerLayerConfig `yaml:"circuitbreaker"`
}
func NewDefaultLayersConfig() LayersConfig {
@ -12,6 +13,9 @@ func NewDefaultLayersConfig() LayersConfig {
TemplateDir: "./layers/queue/templates",
DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
},
CircuitBreaker: CircuitBreakerLayerConfig{
TemplateDir: "./layers/circuitbreaker/templates",
},
}
}
@ -19,3 +23,7 @@ type QueueLayerConfig struct {
TemplateDir InterpolatedString `yaml:"templateDir"`
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
}
type CircuitBreakerLayerConfig struct {
TemplateDir InterpolatedString `yaml:"templateDir"`
}

View File

@ -0,0 +1,23 @@
{
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/circuitbreaker-layer-options",
"title": "Circuit breaker layer options",
"type": "object",
"properties": {
"matchURLs": {
"type": "array",
"items": {
"type": "string"
}
},
"authorizedCIDRs": {
"type": "array",
"items": {
"type": "string"
}
},
"templateBlock": {
"type": "string"
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,151 @@
package circuitbreaker
import (
"context"
"html/template"
"net"
"net/http"
"path/filepath"
"sync"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const LayerType store.LayerType = "circuitbreaker"
type Layer struct {
templateDir string
loadOnce sync.Once
tmpl *template.Template
}
// LayerType implements director.MiddlewareLayer
func (l *Layer) LayerType() store.LayerType {
return LayerType
}
// Middleware implements director.MiddlewareLayer
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
options, err := fromStoreOptions(layer.Options)
if err != nil {
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
matches, err := l.matchAnyAuthorizedCIDRs(ctx, r.RemoteAddr, options.AuthorizedCIDRs)
if err != nil {
logger.Error(ctx, "could not match authorized cidrs", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if matches {
h.ServeHTTP(w, r)
return
}
matches = wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
if !matches {
h.ServeHTTP(w, r)
return
}
l.renderCircuitBreakerPage(w, r, layer, options)
}
return http.HandlerFunc(fn)
}
}
func (l *Layer) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) {
remoteHost, _, err := net.SplitHostPort(remoteHostPort)
if err != nil {
return false, errors.WithStack(err)
}
remoteAddr := net.ParseIP(remoteHost)
if remoteAddr == nil {
return false, errors.Errorf("remote host '%s' is not a valid ip address", remoteHost)
}
for _, rawCIDR := range CIDRs {
_, net, err := net.ParseCIDR(rawCIDR)
if err != nil {
return false, errors.WithStack(err)
}
match := net.Contains(remoteAddr)
if !match {
continue
}
return true, nil
}
logger.Debug(ctx, "comparing remote host with authorized cidrs", logger.F("remoteAddr", remoteAddr))
return false, nil
}
func (l *Layer) renderCircuitBreakerPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions) {
ctx := r.Context()
pattern := filepath.Join(l.templateDir, "*.gohtml")
logger.Info(ctx, "loading circuit breaker page templates", logger.F("pattern", pattern))
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
if err != nil {
logger.Error(ctx, "could not load circuit breaker templates", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
templateData := struct {
Layer *store.Layer
LayerOptions *LayerOptions
}{
Layer: layer,
LayerOptions: options,
}
w.Header().Add("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
if err := tmpl.ExecuteTemplate(w, options.TemplateBlock, templateData); err != nil {
logger.Error(ctx, "could not render circuit breaker page", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
func New(funcs ...OptionFunc) *Layer {
opts := defaultOptions()
for _, fn := range funcs {
fn(opts)
}
return &Layer{
templateDir: opts.TemplateDir,
}
}
var _ director.MiddlewareLayer = &Layer{}

View File

@ -0,0 +1,36 @@
package circuitbreaker
import (
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
type LayerOptions struct {
MatchURLs []string `mapstructure:"matchURLs"`
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
TemplateBlock string `mapstructure:"templateBlock"`
}
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
layerOptions := LayerOptions{
MatchURLs: []string{"*"},
AuthorizedCIDRs: []string{},
TemplateBlock: "default",
}
config := mapstructure.DecoderConfig{
Result: &layerOptions,
}
decoder, err := mapstructure.NewDecoder(&config)
if err != nil {
return nil, err
}
if err := decoder.Decode(storeOptions); err != nil {
return nil, errors.WithStack(err)
}
return &layerOptions, nil
}

View File

@ -0,0 +1,19 @@
package circuitbreaker
type Options struct {
TemplateDir string
}
type OptionFunc func(*Options)
func defaultOptions() *Options {
return &Options{
TemplateDir: "./templates",
}
}
func WithTemplateDir(templateDir string) OptionFunc {
return func(o *Options) {
o.TemplateDir = templateDir
}
}

View File

@ -0,0 +1,8 @@
package circuitbreaker
import (
_ "embed"
)
//go:embed layer-options.json
var RawLayerOptionsSchema []byte

View File

@ -11,15 +11,15 @@ import (
type LayerOptions struct {
Capacity int64 `mapstructure:"capacity"`
Matchers []string `mapstructure:"matchers"`
KeepAlive time.Duration `mapstructure:"keepAlive"`
MatchURLs []string `mapstructure:"matchURLs"`
}
func fromStoreOptions(storeOptions store.LayerOptions, defaultKeepAlive time.Duration) (*LayerOptions, error) {
layerOptions := LayerOptions{
Capacity: 1000,
Matchers: []string{"*"},
KeepAlive: defaultKeepAlive,
MatchURLs: []string{"*"},
}
config := mapstructure.DecoderConfig{

View File

@ -1,6 +1,8 @@
package queue
import "time"
import (
"time"
)
type Options struct {
TemplateDir string

View File

@ -13,6 +13,7 @@ import (
"time"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/Masterminds/sprig/v3"
@ -57,6 +58,13 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
return
}
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
if !matches {
h.ServeHTTP(w, r)
return
}
defer q.updateMetrics(ctx, layer.Proxy, layer.Name, options)
cookieName := q.getCookieName(layer.Name)

View File

@ -9,6 +9,12 @@
},
"keepAlive": {
"type": "string"
},
"matchURLs": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false

View File

@ -89,6 +89,10 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
s.directorLayers...,
)
if s.serverConfig.HTTP.UseRealIP {
router.Use(middleware.RealIP)
}
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
if s.serverConfig.Sentry.DSN != "" {

View File

@ -0,0 +1,21 @@
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/circuitbreaker"
)
func init() {
RegisterLayer(circuitbreaker.LayerType, setupCircuitBreakerLayer, circuitbreaker.RawLayerOptionsSchema)
}
func setupCircuitBreakerLayer(conf *config.Config) (director.Layer, error) {
options := []circuitbreaker.OptionFunc{
circuitbreaker.WithTemplateDir(string(conf.Layers.CircuitBreaker.TemplateDir)),
}
return circuitbreaker.New(
options...,
), nil
}

View File

@ -2,6 +2,7 @@ package testsuite
import (
"context"
"reflect"
"testing"
"forge.cadoles.com/cadoles/bouncer/internal/store"
@ -48,6 +49,187 @@ var layerRepositoryTestCases = []layerRepositoryTestCase{
return errors.Errorf("layer.UpdatedAt should not be zero value")
}
return nil
},
},
{
Name: "Create then get layer",
Do: func(repo store.LayerRepository) error {
ctx := context.Background()
var proxyName store.ProxyName = "create_then_get_layer_proxy"
var layerName store.LayerName = "create_then_get_layer"
var layerType store.LayerType = "dummy"
var layerOptions store.LayerOptions = store.LayerOptions{
"foo": "bar",
"test": struct {
Items []int `json:"items"`
}{
Items: []int{1, 2, 3},
},
}
createdLayer, err := repo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions)
if err != nil {
return errors.WithStack(err)
}
foundLayer, err := repo.GetLayer(ctx, proxyName, layerName)
if err != nil {
return errors.WithStack(err)
}
if e, g := createdLayer.Name, foundLayer.Name; e != g {
return errors.Errorf("foundLayer.Name: expected '%v', got '%v'", createdLayer.Name, foundLayer.Name)
}
if e, g := createdLayer.CreatedAt, foundLayer.CreatedAt; !reflect.DeepEqual(e, g) {
return errors.Errorf("foundLayer.CreatedAt: expected '%v', got '%v'", createdLayer.CreatedAt, foundLayer.CreatedAt)
}
if e, g := createdLayer.UpdatedAt, foundLayer.UpdatedAt; !reflect.DeepEqual(e, g) {
return errors.Errorf("foundLayer.UpdatedAt: expected '%v', got '%v'", createdLayer.UpdatedAt, foundLayer.UpdatedAt)
}
if e, g := createdLayer.Enabled, foundLayer.Enabled; !reflect.DeepEqual(e, g) {
return errors.Errorf("foundLayer.Enabled: expected '%v', got '%v'", createdLayer.Enabled, foundLayer.Enabled)
}
if e, g := createdLayer.Weight, foundLayer.Weight; !reflect.DeepEqual(e, g) {
return errors.Errorf("foundLayer.Weight: expected '%v', got '%v'", createdLayer.Weight, foundLayer.Weight)
}
if e, g := createdLayer.Proxy, foundLayer.Proxy; !reflect.DeepEqual(e, g) {
return errors.Errorf("foundLayer.Proxy: expected '%v', got '%v'", createdLayer.Proxy, foundLayer.Proxy)
}
if e, g := createdLayer.Options, foundLayer.Options; !reflect.DeepEqual(e, g) {
return errors.Errorf("foundLayer.Options: expected '%v', got '%v'", createdLayer.Options, foundLayer.Options)
}
return nil
},
},
{
Name: "Create then delete layer",
Do: func(repo store.LayerRepository) error {
ctx := context.Background()
var layerName store.LayerName = "create_then_delete_layer"
var proxyName store.ProxyName = store.ProxyName(string(layerName) + "_proxy")
var layerType store.LayerType = "dummy"
var layerOptions store.LayerOptions = store.LayerOptions{
"foo": "bar",
"test": struct {
Items []int `json:"items"`
}{
Items: []int{1, 2, 3},
},
}
createdLayer, err := repo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions)
if err != nil {
return errors.WithStack(err)
}
if err := repo.DeleteLayer(ctx, createdLayer.Proxy, createdLayer.Name); err != nil {
return errors.WithStack(err)
}
foundLayer, err := repo.GetLayer(ctx, createdLayer.Proxy, createdLayer.Name)
if err == nil {
return errors.New("err should not be nil")
}
if !errors.Is(err, store.ErrNotFound) {
return errors.Errorf("err should be store.ErrNotFound, got '%+v'", err)
}
if foundLayer != nil {
return errors.Errorf("foundLayer should be nil, got '%v'", foundLayer)
}
return nil
},
},
{
Name: "Create already existing layer",
Do: func(repo store.LayerRepository) error {
ctx := context.Background()
var layerName store.LayerName = "create_already_existing_layer"
var proxyName store.ProxyName = store.ProxyName(string(layerName) + "_proxy")
var layerType store.LayerType = "dummy"
var layerOptions store.LayerOptions = store.LayerOptions{
"foo": "bar",
"test": struct {
Items []int `json:"items"`
}{
Items: []int{1, 2, 3},
},
}
_, err := repo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions)
if err != nil {
return errors.WithStack(err)
}
_, err = repo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions)
if err == nil {
return errors.New("err should not be nil")
}
if !errors.Is(err, store.ErrAlreadyExist) {
return errors.Errorf("err: expected store.ErrAlreadyExists, got '%+v'", err)
}
return nil
},
},
{
Name: "Create then query layer",
Do: func(repo store.LayerRepository) error {
ctx := context.Background()
var layerName store.LayerName = "create_then_query_layer"
var proxyName store.ProxyName = store.ProxyName(string(layerName) + "_proxy")
var layerType store.LayerType = "dummy"
var layerOptions store.LayerOptions = store.LayerOptions{
"foo": "bar",
"test": struct {
Items []int `json:"items"`
}{
Items: []int{1, 2, 3},
},
}
createdLayer, err := repo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions)
if err != nil {
return errors.WithStack(err)
}
headers, err := repo.QueryLayers(ctx, createdLayer.Proxy)
if err != nil {
return errors.WithStack(err)
}
if len(headers) < 1 {
return errors.Errorf("len(headers): expected value > 1, got '%v'", len(headers))
}
found := false
for _, h := range headers {
if h.Name == createdLayer.Name {
found = true
break
}
}
if !found {
return errors.New("could not find created layer in query results")
}
return nil
},
},

View File

@ -83,6 +83,14 @@ var proxyRepositoryTestCases = []proxyRepositoryTestCase{
return errors.Errorf("foundProxy.To: expected '%v', got '%v'", createdProxy.To, foundProxy.To)
}
if e, g := createdProxy.Enabled, foundProxy.Enabled; e != g {
return errors.Errorf("foundProxy.Enabled: expected '%v', got '%v'", createdProxy.Enabled, foundProxy.Enabled)
}
if e, g := createdProxy.Weight, foundProxy.Weight; e != g {
return errors.Errorf("foundProxy.Weight: expected '%v', got '%v'", createdProxy.Weight, foundProxy.Weight)
}
if e, g := createdProxy.CreatedAt, foundProxy.CreatedAt; e != g {
return errors.Errorf("foundProxy.CreatedAt: expected '%v', got '%v'", createdProxy.CreatedAt, foundProxy.CreatedAt)
}
@ -127,7 +135,7 @@ var proxyRepositoryTestCases = []proxyRepositoryTestCase{
},
},
{
Name: "Create then query",
Name: "Create then query layer",
Do: func(repo store.ProxyRepository) error {
ctx := context.Background()

View File

@ -0,0 +1,73 @@
{{ define "default" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Accès bloqué - {{ .Layer.Name }}</title>
<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;
}
#card {
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;
}
.footer {
font-size: 0.7em;
margin-top: 2em;
text-align: right;
}
</style>
</head>
<body>
<div id="container">
<div id="card">
<h2 class="title">Page indisponible</h2>
<p>La page à laquelle vous souhaitez accéder est actuellement indisponible.</p>
<p class="footer">Propulsé par <a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.</p>
</div>
</div>
</body>
</html>
{{ end }}

View File

@ -0,0 +1,196 @@
{
"version": "v1",
"datasources": {
"prometheus": {
"prometheus": {
"address": "http://127.0.0.1:9090"
}
}
},
"dashboard": {
"variables": {
"job": {
"constant": { "value": "bouncer-proxy" }
},
"interval": {
"interval": { "steps": 50 }
}
},
"widgets": [
{
"title": "Bouncer - Total queue sessions",
"gridPos": { "w": 20 },
"singlestat": {
"thresholds": [{ "color": "#47D038" }],
"query": {
"datasourceID": "prometheus",
"expr": "sum(bouncer_layer_queue_sessions{job=\"{{.job}}\"})"
}
}
},
{
"title": "Bouncer Traffic",
"gridPos": {
"w": 80
},
"graph": {
"queries": [
{
"datasourceID": "prometheus",
"expr": "sum(rate(bouncer_proxy_director_proxy_requests_total{job=\"{{.job}}\"}[{{.interval}}]))",
"legend": "req/s"
}
]
}
},
{
"title": "Goroutines",
"gridPos": { "w": 20 },
"singlestat": {
"thresholds": [{ "color": "#47D038" }],
"query": {
"datasourceID": "prometheus",
"expr": "sum(go_goroutines{job=\"{{.job}}\"})"
}
}
},
{
"title": "GC duration",
"gridPos": { "w": 20 },
"singlestat": {
"unit": "second",
"query": {
"datasourceID": "prometheus",
"expr": "max(go_gc_duration_seconds{job=\"{{.job}}\"})"
}
}
},
{
"title": "Stack",
"gridPos": { "w": 20 },
"singlestat": {
"unit": "bytes",
"thresholds": [{ "color": "#22F1F1" }],
"query": {
"datasourceID": "prometheus",
"expr": "sum(go_memstats_stack_inuse_bytes{job=\"{{.job}}\"})"
}
}
},
{
"title": "Heap",
"gridPos": { "w": 20 },
"singlestat": {
"unit": "bytes",
"thresholds": [{ "color": "#22F1F1" }],
"query": {
"datasourceID": "prometheus",
"expr": "sum(go_memstats_heap_inuse_bytes{job=\"{{.job}}\"})"
}
}
},
{
"title": "Alloc",
"gridPos": { "w": 20 },
"singlestat": {
"unit": "bytes",
"thresholds": [{ "color": "#22F1F1" }],
"query": {
"datasourceID": "prometheus",
"expr": "sum(go_memstats_alloc_bytes{job=\"{{.job}}\"})"
}
}
},
{
"title": "Goroutines",
"gridPos": { "w": 50 },
"graph": {
"visualization": {
"legend": { "disable": true },
"yAxis": { "unit": "", "decimals": 2 }
},
"queries": [
{
"datasourceID": "prometheus",
"expr": "sum(go_goroutines{job=\"{{.job}}\"})"
}
]
}
},
{
"title": "GC duration",
"gridPos": { "w": 50 },
"graph": {
"queries": [
{
"datasourceID": "prometheus",
"expr": "max(go_gc_duration_seconds{job=\"{{.job}}\"}) by (quantile)",
"legend": "Q{{.quantile}}"
}
],
"visualization": {
"yAxis": { "unit": "second" },
"seriesOverride": [
{ "regex": "^Q0$", "color": "#F9E2D2" },
{ "regex": "^Q0.25$", "color": "#F2C96D" },
{ "regex": "^Q0.5(0)?$", "color": "#EAB839" },
{ "regex": "^Q0.75$", "color": "#EF843C" },
{ "regex": "^Q1(.0)?$", "color": "#E24D42" }
]
}
}
},
{
"title": "Memory",
"gridPos": { "w": 50 },
"graph": {
"visualization": {
"yAxis": { "unit": "byte", "decimals": 0 }
},
"queries": [
{
"datasourceID": "prometheus",
"expr": "sum(go_memstats_stack_inuse_bytes{job=\"{{.job}}\"})",
"legend": "stack inuse"
},
{
"datasourceID": "prometheus",
"expr": "sum(go_memstats_heap_inuse_bytes{job=\"{{.job}}\"})",
"legend": "heap inuse"
},
{
"datasourceID": "prometheus",
"expr": "sum(go_memstats_alloc_bytes{job=\"{{.job}}\"})",
"legend": "alloc"
}
]
}
},
{
"title": "Memory ops rate",
"gridPos": {
"w": 50
},
"graph": {
"queries": [
{
"datasourceID": "prometheus",
"expr": "sum(rate(go_memstats_frees_total{job=\"{{.job}}\"}[{{.interval}}]))",
"legend": "frees/s"
},
{
"datasourceID": "prometheus",
"expr": "sum(rate(go_memstats_mallocs_total{job=\"{{.job}}\"}[{{.interval}}]))",
"legend": "mallocs/s"
},
{
"datasourceID": "prometheus",
"expr": "sum(rate(go_memstats_lookups_total{job=\"{{.job}}\"}[{{.interval}}]))",
"legend": "lookups/s"
}
]
}
}
]
}
}

View File

@ -7,12 +7,22 @@ ARG https_proxy=
# Install dev environment dependencies
RUN export DEBIAN_FRONTEND=noninteractive &&\
apt clean &&\
apt-get update -y &&\
apt-get install -y --no-install-recommends curl ca-certificates build-essential wget unzip tar git jq
apt-get install -y --no-install-recommends curl ca-certificates build-essential wget unzip tar git jq gnupg
# Add LetsEncrypt certificates
RUN curl -k https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/common/add-letsencrypt-ca.sh | bash
RUN install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& chmod a+r /etc/apt/keyrings/docker.gpg \
&& echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt-get update \
&& apt-get install -y docker-ce-cli
ARG GO_VERSION=1.20.4
# Install Go

View File

@ -6,6 +6,9 @@ admin:
host: 127.0.0.1
# Port d'écoute du service
port: 8081
# Utiliser les entêtes HTTP True-Client-IP, X-Real-IP ou X-Forwarded-For
# pour le calcul de l'adresse distante à l'origine des requêtes
useRealIP: true
# Configuration CORS du service
# Uniquement nécessaire si un frontend web
@ -73,6 +76,9 @@ proxy:
host: 0.0.0.0
# Port d'écoute du service
port: 8080
# Utiliser les entêtes HTTP True-Client-IP, X-Real-IP ou X-Forwarded-For
# pour le calcul de l'adresse distante à l'origine des requêtes
useRealIP: true
# Métriques Prometheus
metrics:
@ -161,4 +167,10 @@ layers:
# Répertoire contenant les templates
templateDir: "/etc/bouncer/layers/queue/templates"
# Temps de vie par défaut d'une session
defaultKeepAlive: 1m
defaultKeepAlive: 1m
# Configuration du layer "circuitbreaker"
circuitbreaker:
# Répertoire contenant les templates
templateDir: "/etc/bouncer/layers/circuitbreaker/templates"

View File

@ -0,0 +1,7 @@
scrape_configs:
- job_name: bouncer-proxy
metrics_path: /.bouncer/metrics
static_configs:
- targets:
- "localhost:8080"
scrape_interval: 5s

6
misc/siege/urls.txt Normal file
View File

@ -0,0 +1,6 @@
http://localhost:8080/blog/
http://localhost:8080/services/
http://localhost:8080/
http://localhost:8080/recrutement/
http://localhost:8080/faq/
http://localhost:8080/societe/histoire/

View File

@ -14,4 +14,9 @@ layers/**
{
daemon +sigint: make run-redis
}
misc/prometheus/prometheus.yml
{
daemon +sigint: make run-prometheus
}