Compare commits

...

28 Commits

Author SHA1 Message Date
d8b78ad277 feat(docker): run as non-root user
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-03-27 09:19:08 +01:00
61012b07cd Merge pull request 'feat: bootstrap default proxy and layers from configuration' (#17) from proxy-bootstrap into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #17
2024-03-26 17:30:20 +01:00
d12ebfc642 feat: proxy bootstrapping from configuration
All checks were successful
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2024-03-26 17:28:38 +01:00
441d3a623e Merge pull request 'feat(k8s): adding kubernetes support' (#12) from feat/issue-10/add-k8s-kustomize into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #12
2024-03-26 14:49:24 +01:00
e1d9acb980 fix[README]: Added --role writer in token authentication creation
Some checks are pending
Cadoles/bouncer/pipeline/pr-develop Build started...
2024-03-26 14:04:07 +01:00
f8be2c08d6 fix[README]: add identifier generation step 2024-03-26 14:04:07 +01:00
bc7422a50c feat: add configurable redis timeouts 2024-03-26 14:04:07 +01:00
9d32551ec5 feat: generalize siege task 2024-03-26 14:04:07 +01:00
ded6d179c1 fix(k8s): redis configuration 2024-03-26 14:04:07 +01:00
6f4ee0ebd1 fix(skaffold): adding port-forward for testing 2024-03-26 14:04:07 +01:00
1375c9b317 fup 2024-03-26 14:04:05 +01:00
53a0d26a47 feat(pkg): adding archlinux package to gorelease 2024-03-26 13:49:58 +01:00
87354ef0d4 fix(doc): test command was incorrect 2024-03-26 13:49:58 +01:00
8560041598 fix(kustomization): adding correct labels to deployments 2024-03-26 13:49:58 +01:00
0611cc9f70 feat(k8s): adding kubernetes support
Now we can use skaffold and deploy bouncer in a kubernetes cluster

ref #10
2024-03-26 13:49:58 +01:00
734ed64e8e feat: add basic k6 load testing script
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-03-25 15:40:25 +01:00
c8fc143efa doc: add prometheus metrics documentation
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-02-21 12:29:03 +01:00
f91c14e5d4 feat(admin): print default writer token to logs by default
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-02-21 11:09:34 +01:00
1602626e8c Merge pull request 'fix(depends): update go.mod library versions' (#15) from fix/go-lib-versions into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #15
2024-02-05 11:24:02 +01:00
e2e38841f4 fix(depends): update go.mod library versions
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2024-02-05 11:21:27 +01:00
c23d8e3adb Merge pull request 'fix(config): supporting multiple env variables in a value.' (#11) from fix/issue-9/multiple-env-variables into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #11
2024-02-05 11:13:47 +01:00
a3f44cf123 fix(config): supporting multiple env variables in a value.
ref #9
2024-02-05 11:13:47 +01:00
5453988419 Merge pull request 'fix(dockerfile): updating base images versions.' (#14) from fix/dockerfile into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #14
2024-02-05 11:08:50 +01:00
1e392f94a7 fix(dockerfile): updating base images versions.
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Cadoles/bouncer/pipeline/pr-develop This commit looks good
Keep things up to date and security alerts away from trivi.

Using apk package for dumb-init
2024-02-05 11:04:28 +01:00
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
60 changed files with 1904 additions and 282 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@
/.bouncer-token
/data
/out
.dockerconfigjson

View File

@ -33,15 +33,15 @@ archives:
- README.md
- misc/packaging/common/config.yml
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Version }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- "^docs:"
- "^test:"
nfpms:
- id: bouncer-bin
builds:
@ -63,6 +63,10 @@ nfpms:
- src: layers
dst: /etc/bouncer/layers
type: config
- dst: /etc/bouncer/bootstrap.d
type: dir
file_info:
mode: 0700
- id: bouncer-admin
meta: true
package_name: bouncer-admin

View File

@ -1,4 +1,4 @@
FROM golang:1.20 AS BUILD
FROM reg.cadoles.com/proxy_cache/library/golang:1.21.6 AS BUILD
RUN apt-get update \
&& apt-get install -y make
@ -9,29 +9,31 @@ RUN mkdir -p /usr/local/bin \
&& wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64 \
&& chmod +x /usr/local/bin/yq
COPY . /src
WORKDIR /src
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . /src
RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
# Patch config
RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.layers.queue.templateDir = "/usr/share/bouncer/layers/queue/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.admin.auth.privateKey = "/etc/bouncer/admin-key.json"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml
&& yq -i '.redis.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.writeTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.readTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.dialTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml
FROM alpine:3.18 AS RUNTIME
FROM reg.cadoles.com/proxy_cache/library/alpine:3.19.1 AS RUNTIME
ARG DUMB_INIT_VERSION=1.2.5
RUN apk add --no-cache ca-certificates dumb-init
RUN apk add --no-cache ca-certificates
RUN mkdir -p /usr/local/bin \
&& wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64 \
&& chmod +x /usr/local/bin/dumb-init
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer
@ -44,6 +46,10 @@ RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer
EXPOSE 8080
EXPOSE 8081
RUN adduser -D -H bouncer
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
USER bouncer
CMD ["bouncer"]

View File

@ -16,6 +16,9 @@ GOTEST_ARGS ?= -short
OPENWRT_DEVICE ?= 192.168.1.1
SIEGE_URLS_FILE ?= misc/siege/urls.txt
SIEGE_CONCURRENCY ?= 100
watch: tools/modd/bin/modd deps ## Watching updated files - live reload
( set -o allexport && source .env && set +o allexport && tools/modd/bin/modd )
@ -101,6 +104,15 @@ 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:
$(eval TMP := $(shell mktemp))
cat $(SIEGE_URLS_FILE) | envsubst > $(TMP)
siege -i -b -c $(SIEGE_CONCURRENCY) -f $(TMP)
rm -rf $(TMP)
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 +122,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)
@ -129,3 +145,11 @@ redis-shell:
docker exec -it \
bouncer-redis \
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

@ -6,16 +6,20 @@
## Exemples
- [(FR) - Exemple de déploiement multi-noeuds](../misc/docker-compose/README.md)
## Référence
- [(FR) - Layers](./fr/references/layers/README.md)
- [(FR) - Métriques](./fr/references/metrics.md)
- [(FR) - Fichier de configuration](../misc/packaging/common/config.yml)
- [(FR) - API d'administration](./fr/references/admin_api.md)
## Tutoriels
### Utilisation
- [(FR) - Ajouter un layer de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
- [(FR) - Amorçage d'un serveur Bouncer via la configuration](./fr/tutorials/bootstrapping.md)
### Développement

View File

@ -41,7 +41,7 @@
5. Tester que le CLI est en capacité d'interroger l'API d'administration
```bash
bouncer admin query proxy
bouncer admin proxy query
```
Un message équivalent à celui ci devrait s'afficher:

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

@ -32,6 +32,10 @@ Ce layer permet de bloquer l'accès à un site (ou une section de celui ci) cibl
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
## Schéma
Voir le [schéma JSON](../../../../internal/proxy/director/layer/circuitbreaker/layer-options.json).
## Métriques
_Aucune [métrique Prometheus](../metrics.md) n'est exportée par ce layer._

View File

@ -30,6 +30,34 @@ Ce layer permet d'ajouter un mécanisme de file d'attente dynamique au proxy ass
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
## Schéma
Voir le [schéma JSON](../../../../internal/proxy/director/layer/queue/schema/layer-options.json).
## Métriques
Les [métriques Prometheus](../metrics.md) suivantes sont exposées par ce layer.
### `bouncer_layer_queue_capacity{layer=<layerName>,proxy=<proxyName>}`
- **Type:** `gauge`
- **Description**: Capacité maximale de la queue
- **Exemple**
```
# HELP bouncer_layer_queue_capacity Bouncer's queue layer capacity
# TYPE bouncer_layer_queue_capacity gauge
bouncer_layer_queue_capacity{layer="queue",proxy="cadoles"} 2
```
### `bouncer_layer_queue_sessions{layer=<layerName>,proxy=<proxyName>}`
- **Type:** `gauge`
- **Description**: Nombre courant de sessions ouvertes
- **Exemple**
```
# HELP bouncer_layer_queue_sessions Bouncer's queue layer current sessions
# TYPE bouncer_layer_queue_sessions gauge
bouncer_layer_queue_sessions{layer="queue",proxy="cadoles"} 3
```

View File

@ -0,0 +1,29 @@
# Métriques
Bouncer expose un certain nombre de métriques Prometheus sur le serveur proxy ainsi que sur le serveur d'administration. Ces métriques sont par défaut accessibles sur `/.bouncer/metrics`.
Il est possible de configurer le point d'entrée de ces métriques ainsi que d'ajouter une authentification de type `Basic Auth` [via la configuration](../../../misc/packaging/common/config.yml) (voir les clés `admin.metrics` et `proxy.metrics`).
Outre les métriques par défaut fournies par la librairie [Prometheus](https://prometheus.io/docs/guides/go-application/#instrumenting-a-go-application-for-prometheus), les serveurs Bouncer exposent également des métriques propres.
Chaque layer associé à un proxy peut également ses propres métriques spécifiques. [Voir la page de documentation](./layers/README.md) de chaque layer pour plus d'informations.
## Métriques spécifiques
### Serveur proxy
#### `bouncer_proxy_director_proxy_requests_total{proxy=<proxyName>}`
- **Type:** `counter`
- **Description**: Nombre total de requêtes ayant transité par le proxy
- **Exemple**
```
# HELP bouncer_proxy_director_proxy_requests_total Bouncer proxy total requests
# TYPE bouncer_proxy_director_proxy_requests_total counter
bouncer_proxy_director_proxy_requests_total{proxy="cadoles"} 64
```
### Serveur d'administration
_Pas de métrique supplémentaire._

View File

@ -0,0 +1,47 @@
# Amorçage d'un serveur Bouncer via la configuration
Il est possible d'amorcer des données par défaut (i.e. des "proxies" et "layers" associés) via la configuration du serveur d'administration.
> **Attention** Ce mécanisme de modifiera pas des proxies déjà existants dans la base de données du serveur Bouncer. Autrement dit, si un proxy est déjà pré-existant lors du démarrage du serveur Bouncer, il ne sera pas modifié.
La définition des proxies et layers par défaut s'effectue dans la section `bootstrap` du fichier de configuration. Deux possibilités pour définir les proxys à charger par défaut:
- Utiliser un répertoire contenant des fichiers YAML (un par proxy) en définissant le chemin du répertoire via l'attribut `bootstrap.dir`;
- Définir directement la liste des proxies via l'attribut `bootstrap.proxies`.
```yaml
# Configuration d'une série de proxy/layers
# à créer par défaut par le serveur d'administration
bootstrap:
# Répertoire contenant les définitions de proxy à créer
# par défaut. Les fichiers seront récupérés si ils
# correspondent au patron de nommage suivant:
#
# <bootstrap_dir>/<proxy_name>.yml
#
# Voir ci-dessous pour les attributs possibles dans les fichiers.
#
# Si l'attribut est vide ou absent le chargement des fichiers
# est désactivé.
dir: /etc/bouncer/bootstrap.d
# Tableau associatif de définition de proxies à créer par
# défaut par le serveur d'administration.
# Si `proxies` et `dir` sont tous les deux définis, les fichiers
# présents dans le répertoire `dir` surchargeront les valeurs définies
# dans `proxies`.
#
# Par défaut vide.
proxies:
# my-proxy:
# enabled: true # Activer/désactiver le proxy
# from: ["*"] # Filtre d'origine d'activation du proxy
# to: "https://example.net" # Destination du proxy
# weight: 0 # Priorité du proxy
# layers: # Layers associés au proxy
# my-layer:
# type: queue # Type du proxy
# enabled: false # Activer/désactiver le layer
# weight: 0 # Priorité du layer
# options: {"capacity": 100} # Options associées au layer
```

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

17
go.mod
View File

@ -5,7 +5,9 @@ go 1.20
require (
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6
github.com/Masterminds/sprig/v3 v3.2.3
github.com/bsm/redislock v0.9.4
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/drone/envsubst v1.0.3
github.com/getsentry/sentry-go v0.22.0
github.com/go-chi/chi/v5 v5.0.8
github.com/jedib0t/go-pretty/v6 v6.4.6
@ -52,13 +54,14 @@ 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
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@ -68,7 +71,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
@ -78,11 +81,11 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.3.0
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
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.19
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 +97,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.17.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.15.0 // indirect
golang.org/x/term v0.15.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

51
go.sum
View File

@ -84,6 +84,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
@ -144,8 +146,8 @@ 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/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=
@ -162,6 +164,8 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -322,17 +326,16 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
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/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY=
github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU=
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=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
@ -421,6 +424,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 +451,7 @@ 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/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 +497,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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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=
@ -532,7 +535,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -577,9 +579,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
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.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/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=
@ -608,7 +608,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -680,17 +679,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
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.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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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=
@ -701,10 +697,8 @@ 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.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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -763,7 +757,6 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -2,12 +2,22 @@ package admin
import (
"context"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/schema"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/bsm/redislock"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func (s *Server) initRepositories(ctx context.Context) error {
if err := s.initRedisClient(ctx); err != nil {
return errors.WithStack(err)
}
if err := s.initLayerRepository(ctx); err != nil {
return errors.WithStack(err)
}
@ -19,8 +29,16 @@ func (s *Server) initRepositories(ctx context.Context) error {
return nil
}
func (s *Server) initRedisClient(ctx context.Context) error {
client := setup.NewRedisClient(ctx, s.redisConfig)
s.redisClient = client
return nil
}
func (s *Server) initLayerRepository(ctx context.Context) error {
layerRepository, err := setup.NewLayerRepository(ctx, s.redisConfig)
layerRepository, err := setup.NewLayerRepository(ctx, s.redisClient)
if err != nil {
return errors.WithStack(err)
}
@ -31,7 +49,7 @@ func (s *Server) initLayerRepository(ctx context.Context) error {
}
func (s *Server) initProxyRepository(ctx context.Context) error {
proxyRepository, err := setup.NewProxyRepository(ctx, s.redisConfig)
proxyRepository, err := setup.NewProxyRepository(ctx, s.redisClient)
if err != nil {
return errors.WithStack(err)
}
@ -40,3 +58,112 @@ func (s *Server) initProxyRepository(ctx context.Context) error {
return nil
}
const bootstrapLockKey = "bouncer-bootstrap"
func (s *Server) bootstrapProxies(ctx context.Context) error {
if err := s.validateBootstrap(ctx); err != nil {
return errors.Wrap(err, "could not validate bootstrapped proxies")
}
proxyRepo := s.proxyRepository
layerRepo := s.layerRepository
locker := redislock.New(s.redisClient)
backoff := redislock.ExponentialBackoff(time.Second, time.Duration(s.bootstrapConfig.LockTimeout)*2)
logger.Debug(ctx, "acquiring proxies bootstrap lock", logger.F("lockTimeout", s.bootstrapConfig.LockTimeout))
lock, err := locker.Obtain(ctx, bootstrapLockKey, time.Duration(s.bootstrapConfig.LockTimeout), &redislock.Options{
RetryStrategy: backoff,
})
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := lock.Release(ctx); err != nil {
logger.Error(ctx, "could not release lock", logger.E(errors.WithStack(err)))
}
}()
logger.Info(ctx, "bootstrapping proxies")
for proxyName, proxyConfig := range s.bootstrapConfig.Proxies {
_, err := s.proxyRepository.GetProxy(ctx, proxyName)
if !errors.Is(err, store.ErrNotFound) {
if err != nil {
return errors.WithStack(err)
}
logger.Info(ctx, "ignoring existing proxy", logger.F("proxyName", proxyName))
continue
}
logger.Info(ctx, "creating proxy", logger.F("proxyName", proxyName))
if _, err := proxyRepo.CreateProxy(ctx, proxyName, string(proxyConfig.To), proxyConfig.From...); err != nil {
return errors.WithStack(err)
}
_, err = proxyRepo.UpdateProxy(
ctx, proxyName,
store.WithProxyUpdateEnabled(bool(proxyConfig.Enabled)),
store.WithProxyUpdateWeight(int(proxyConfig.Weight)),
)
if err != nil {
return errors.WithStack(err)
}
for layerName, layerConfig := range proxyConfig.Layers {
layerType := store.LayerType(layerConfig.Type)
layerOptions := store.LayerOptions(layerConfig.Options)
if _, err := layerRepo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions); err != nil {
return errors.WithStack(err)
}
_, err := layerRepo.UpdateLayer(
ctx,
proxyName, layerName,
store.WithLayerUpdateEnabled(bool(layerConfig.Enabled)),
store.WithLayerUpdateOptions(layerOptions),
store.WithLayerUpdateWeight(int(layerConfig.Weight)),
)
if err != nil {
return errors.WithStack(err)
}
}
}
return nil
}
const validateErrMessage = "could not validate proxy '%s': could not validate layer '%s'"
func (s *Server) validateBootstrap(ctx context.Context) error {
for proxyName, proxyConf := range s.bootstrapConfig.Proxies {
for layerName, layerConf := range proxyConf.Layers {
layerType := store.LayerType(layerConf.Type)
if !setup.LayerTypeExists(layerType) {
return errors.Errorf(validateErrMessage+": could not find layer type '%s'", proxyName, layerName, layerType)
}
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layerType)
if err != nil {
return errors.Wrapf(err, validateErrMessage, proxyName, layerName)
}
rawOptions := func(opts config.InterpolatedMap) map[string]any {
return opts
}(layerConf.Options)
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
return errors.Wrapf(err, validateErrMessage, proxyName, layerName)
}
}
}
return nil
}

View File

@ -51,15 +51,6 @@ func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) {
})
}
func validateLayerName(v string) (store.LayerName, error) {
name, err := store.ValidateName(v)
if err != nil {
return "", errors.WithStack(err)
}
return store.LayerName(name), nil
}
type GetLayerResponse struct {
Layer *store.Layer `json:"layer"`
}

View File

@ -5,8 +5,9 @@ import (
)
type Option struct {
ServerConfig config.AdminServerConfig
RedisConfig config.RedisConfig
BootstrapConfig config.BootstrapConfig
ServerConfig config.AdminServerConfig
RedisConfig config.RedisConfig
}
type OptionFunc func(*Option)
@ -29,3 +30,9 @@ func WithRedisConfig(conf config.RedisConfig) OptionFunc {
opt.RedisConfig = conf
}
}
func WithBootstrapConfig(conf config.BootstrapConfig) OptionFunc {
return func(opt *Option) {
opt.BootstrapConfig = conf
}
}

View File

@ -114,6 +114,23 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
return
}
layers, err := s.layerRepository.QueryLayers(ctx, proxyName)
if err != nil {
logAndCaptureError(ctx, "could not query proxy's layers", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
for _, layer := range layers {
if err := s.layerRepository.DeleteLayer(ctx, proxyName, layer.Name); err != nil {
logAndCaptureError(ctx, "could not delete layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
}
api.DataResponse(w, http.StatusOK, DeleteProxyResponse{
ProxyName: proxyName,
})

View File

@ -19,12 +19,15 @@ import (
"github.com/go-chi/cors"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/redis/go-redis/v9"
"gitlab.com/wpetit/goweb/logger"
)
type Server struct {
serverConfig config.AdminServerConfig
redisConfig config.RedisConfig
redisClient redis.UniversalClient
bootstrapConfig config.BootstrapConfig
proxyRepository store.ProxyRepository
layerRepository store.LayerRepository
}
@ -53,6 +56,12 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
return
}
if err := s.bootstrapProxies(ctx); err != nil {
errs <- errors.WithStack(err)
return
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.serverConfig.HTTP.Host, s.serverConfig.HTTP.Port))
if err != nil {
errs <- errors.WithStack(err)
@ -175,7 +184,8 @@ func NewServer(funcs ...OptionFunc) *Server {
}
return &Server{
serverConfig: opt.ServerConfig,
redisConfig: opt.RedisConfig,
serverConfig: opt.ServerConfig,
redisConfig: opt.RedisConfig,
bootstrapConfig: opt.BootstrapConfig,
}
}

View File

@ -5,15 +5,28 @@ import (
"strings"
"forge.cadoles.com/cadoles/bouncer/internal/admin"
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)
const (
flagPrintDefaultToken = "print-default-token"
)
func RunCommand() *cli.Command {
flags := common.Flags()
flags := append(
common.Flags(),
&cli.BoolFlag{
Name: flagPrintDefaultToken,
Usage: "Generate and print a default writer token in console at startup",
Value: true,
},
)
return &cli.Command{
Name: "run",
@ -36,9 +49,26 @@ func RunCommand() *cli.Command {
defer flushSentry()
if printDefaultToken := ctx.Bool(flagPrintDefaultToken); printDefaultToken {
key, err := jwk.Generate(jwk.DefaultKeySize)
if err != nil {
return errors.Wrap(err, "could not generate default key")
}
token, err := jwt.GenerateToken(ctx.Context, key, string(conf.Admin.Auth.Issuer), "default-admin", jwt.Role(jwt.RoleWriter))
if err != nil {
return errors.WithStack(err)
}
logger.SetLevel(logger.LevelInfo)
logger.Info(ctx.Context, "default writer token", logger.F("token", token))
logger.SetLevel(logger.Level(conf.Logger.Level))
}
srv := admin.NewServer(
admin.WithServerConfig(conf.Admin),
admin.WithRedisConfig(conf.Redis),
admin.WithBootstrapConfig(conf.Bootstrap),
)
addrs, srvErrs := srv.Start(ctx.Context)

View File

@ -0,0 +1,104 @@
package config
import (
"os"
"path/filepath"
"strings"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
type BootstrapConfig struct {
Proxies map[store.ProxyName]BootstrapProxyConfig `yaml:"proxies"`
Dir InterpolatedString `yaml:"dir"`
LockTimeout InterpolatedDuration `yaml:"lockTimeout"`
}
func (c *BootstrapConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
src := struct {
Proxies map[store.ProxyName]BootstrapProxyConfig `yaml:"proxies"`
Dir InterpolatedString `yaml:"dir"`
}{
Proxies: make(map[store.ProxyName]BootstrapProxyConfig),
Dir: "",
}
if err := unmarshal(&src); err != nil {
return errors.WithStack(err)
}
c.Proxies = src.Proxies
c.Dir = src.Dir
if src.Dir != "" {
proxies, err := loadBootstrapDir(string(src.Dir))
if err != nil {
return errors.Wrapf(err, "could not load bootstrap dir '%s'", src.Dir)
}
c.Proxies = overrideProxies(c.Proxies, proxies)
}
return nil
}
type BootstrapProxyConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
Weight InterpolatedInt `yaml:"weight"`
To InterpolatedString `yaml:"to"`
From InterpolatedStringSlice `yaml:"from"`
Layers map[store.LayerName]BootstrapLayerConfig `yaml:"layers"`
}
type BootstrapLayerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
Type InterpolatedString `yaml:"type"`
Weight InterpolatedInt `yaml:"weight"`
Options InterpolatedMap `yaml:"options"`
}
func NewDefaultBootstrapConfig() BootstrapConfig {
return BootstrapConfig{
Dir: "",
LockTimeout: *NewInterpolatedDuration(30 * time.Second),
}
}
func loadBootstrapDir(dir string) (map[store.ProxyName]BootstrapProxyConfig, error) {
pattern := filepath.Join(dir, "*.yml")
files, err := filepath.Glob(pattern)
if err != nil {
return nil, errors.WithStack(err)
}
proxies := make(map[store.ProxyName]BootstrapProxyConfig)
for _, f := range files {
data, err := os.ReadFile(f)
if err != nil {
return nil, errors.Wrapf(err, "could not read file '%s'", f)
}
proxy := BootstrapProxyConfig{}
if err := yaml.Unmarshal(data, &proxy); err != nil {
return nil, errors.Wrapf(err, "could not unmarshal proxy")
}
name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
proxies[name] = proxy
}
return proxies, nil
}
func overrideProxies(base map[store.ProxyName]BootstrapProxyConfig, proxies map[store.ProxyName]BootstrapProxyConfig) map[store.ProxyName]BootstrapProxyConfig {
for name, proxy := range proxies {
base[name] = proxy
}
return base
}

View File

@ -2,7 +2,7 @@ package config
import (
"io"
"io/ioutil"
"os"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
@ -10,18 +10,19 @@ import (
// Config definition
type Config struct {
Admin AdminServerConfig `yaml:"admin"`
Proxy ProxyServerConfig `yaml:"proxy"`
Redis RedisConfig `yaml:"redis"`
Logger LoggerConfig `yaml:"logger"`
Layers LayersConfig `yaml:"layers"`
Admin AdminServerConfig `yaml:"admin"`
Proxy ProxyServerConfig `yaml:"proxy"`
Redis RedisConfig `yaml:"redis"`
Logger LoggerConfig `yaml:"logger"`
Layers LayersConfig `yaml:"layers"`
Bootstrap BootstrapConfig `yaml:"bootstrap"`
}
// NewFromFile retrieves the configuration from the given file
func NewFromFile(path string) (*Config, error) {
config := NewDefault()
data, err := ioutil.ReadFile(path)
data, err := os.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "could not read file '%s'", path)
}
@ -43,11 +44,12 @@ func NewDumpDefault() *Config {
// NewDefault return new default configuration
func NewDefault() *Config {
return &Config{
Admin: NewDefaultAdminServerConfig(),
Proxy: NewDefaultProxyServerConfig(),
Logger: NewDefaultLoggerConfig(),
Redis: NewDefaultRedisConfig(),
Layers: NewDefaultLayersConfig(),
Admin: NewDefaultAdminServerConfig(),
Proxy: NewDefaultProxyServerConfig(),
Logger: NewDefaultLoggerConfig(),
Redis: NewDefaultRedisConfig(),
Layers: NewDefaultLayersConfig(),
Bootstrap: NewDefaultBootstrapConfig(),
}
}

View File

@ -6,11 +6,13 @@ import (
"strconv"
"time"
"github.com/drone/envsubst"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
var reVar = regexp.MustCompile(`^\${(\w+)}$`)
// var reVar = regexp.MustCompile(`^\${(\w+)}$`)
var reVar = regexp.MustCompile(`\${(.*?)}`)
type InterpolatedString string
@ -130,14 +132,22 @@ type InterpolatedStringSlice []string
func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error {
var data []string
var evErr error
if err := value.Decode(&data); err != nil {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line)
}
for index, value := range data {
if match := reVar.FindStringSubmatch(value); len(match) > 0 {
value = os.Getenv(match[1])
//match := reVar.FindStringSubmatch(value)
re := regexp.MustCompile(`\${(.*?)}`)
res := re.FindAllStringSubmatch(value, 10)
if len(res) > 0 {
value, evErr = envsubst.EvalEnv(value)
if evErr != nil {
return evErr
}
}
data[index] = value

View File

@ -1,5 +1,7 @@
package config
import "time"
const (
RedisModeSimple = "simple"
RedisModeSentinel = "sentinel"
@ -7,13 +9,19 @@ const (
)
type RedisConfig struct {
Adresses InterpolatedStringSlice `yaml:"addresses"`
Master InterpolatedString `yaml:"master"`
Adresses InterpolatedStringSlice `yaml:"addresses"`
Master InterpolatedString `yaml:"master"`
ReadTimeout InterpolatedDuration `yaml:"readTimeout"`
WriteTimeout InterpolatedDuration `yaml:"writeTimeout"`
DialTimeout InterpolatedDuration `yaml:"dialTimeout"`
}
func NewDefaultRedisConfig() RedisConfig {
return RedisConfig{
Adresses: InterpolatedStringSlice{"localhost:6379"},
Master: "",
Adresses: InterpolatedStringSlice{"localhost:6379"},
Master: "",
ReadTimeout: InterpolatedDuration(30 * time.Second),
WriteTimeout: InterpolatedDuration(30 * time.Second),
DialTimeout: InterpolatedDuration(30 * time.Second),
}
}

View File

@ -2,5 +2,5 @@ logger:
level: 0
format: human
http:
host: "0.0.0.0"
host: "${LISTEN_ADDR}"
port: 3000

View File

@ -4,7 +4,6 @@ import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"io/ioutil"
"os"
"github.com/btcsuite/btcd/btcutil/base58"
@ -56,7 +55,7 @@ func PublicKeySet(keys ...jwk.Key) (jwk.Set, error) {
}
func LoadOrGenerate(path string, size int) (jwk.Key, error) {
data, err := ioutil.ReadFile(path)
data, err := os.ReadFile(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
@ -72,7 +71,7 @@ func LoadOrGenerate(path string, size int) (jwk.Key, error) {
return nil, errors.WithStack(err)
}
if err := ioutil.WriteFile(path, data, 0o640); err != nil {
if err := os.WriteFile(path, data, 0o640); err != nil {
return nil, errors.WithStack(err)
}
}

View File

@ -5,22 +5,25 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
func (s *Server) initRepositories(ctx context.Context) error {
if err := s.initProxyRepository(ctx); err != nil {
client := setup.NewRedisClient(ctx, s.redisConfig)
if err := s.initProxyRepository(ctx, client); err != nil {
return errors.WithStack(err)
}
if err := s.initLayerRepository(ctx); err != nil {
if err := s.initLayerRepository(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Server) initProxyRepository(ctx context.Context) error {
proxyRepository, err := setup.NewProxyRepository(ctx, s.redisConfig)
func (s *Server) initProxyRepository(ctx context.Context, client redis.UniversalClient) error {
proxyRepository, err := setup.NewProxyRepository(ctx, client)
if err != nil {
return errors.WithStack(err)
}
@ -30,8 +33,8 @@ func (s *Server) initProxyRepository(ctx context.Context) error {
return nil
}
func (s *Server) initLayerRepository(ctx context.Context) error {
layerRepository, err := setup.NewLayerRepository(ctx, s.redisConfig)
func (s *Server) initLayerRepository(ctx context.Context, client redis.UniversalClient) error {
layerRepository, err := setup.NewLayerRepository(ctx, client)
if err != nil {
return errors.WithStack(err)
}

View File

@ -9,20 +9,17 @@ import (
"github.com/redis/go-redis/v9"
)
func NewProxyRepository(ctx context.Context, conf config.RedisConfig) (store.ProxyRepository, error) {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
func NewRedisClient(ctx context.Context, conf config.RedisConfig) redis.UniversalClient {
return redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: conf.Adresses,
MasterName: string(conf.Master),
})
return redisStore.NewProxyRepository(rdb), nil
}
func NewLayerRepository(ctx context.Context, conf config.RedisConfig) (store.LayerRepository, error) {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: conf.Adresses,
MasterName: string(conf.Master),
})
return redisStore.NewLayerRepository(rdb), nil
func NewProxyRepository(ctx context.Context, client redis.UniversalClient) (store.ProxyRepository, error) {
return redisStore.NewProxyRepository(client), nil
}
func NewLayerRepository(ctx context.Context, client redis.UniversalClient) (store.LayerRepository, error) {
return redisStore.NewLayerRepository(client), nil
}

View File

@ -8,7 +8,6 @@ import (
"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() {
@ -36,10 +35,6 @@ func setupQueueLayer(conf *config.Config) (director.Layer, error) {
}
func newQueueAdapter(redisConf config.RedisConfig) (queue.Adapter, error) {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: redisConf.Adresses,
MasterName: string(redisConf.Master),
})
rdb := newRedisClient(redisConf)
return queueRedis.NewAdapter(rdb, 2), nil
}

20
internal/setup/redis.go Normal file
View File

@ -0,0 +1,20 @@
package setup
import (
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"github.com/redis/go-redis/v9"
)
func newRedisClient(conf config.RedisConfig) redis.UniversalClient {
return redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: conf.Adresses,
MasterName: string(conf.Master),
ReadTimeout: time.Duration(conf.ReadTimeout),
WriteTimeout: time.Duration(conf.WriteTimeout),
DialTimeout: time.Duration(conf.DialTimeout),
RouteByLatency: true,
ContextTimeoutEnabled: true,
})
}

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,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

@ -0,0 +1,49 @@
FROM golang:1.20 AS BUILD
RUN apt-get update \
&& apt-get install -y make
ARG YQ_VERSION=4.34.1
RUN mkdir -p /usr/local/bin \
&& wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64 \
&& chmod +x /usr/local/bin/yq
COPY . /src
WORKDIR /src
RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
# Patch config
RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.layers.queue.templateDir = "/usr/share/bouncer/layers/queue/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.admin.auth.privateKey = "/etc/bouncer/admin-key.json"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml
FROM alpine:3.18 AS RUNTIME
ARG DUMB_INIT_VERSION=1.2.5
RUN apk add --no-cache ca-certificates
RUN mkdir -p /usr/local/bin \
&& wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64 \
&& chmod +x /usr/local/bin/dumb-init
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer
COPY --from=BUILD /src/layers /usr/share/bouncer/layers
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/config.yml /etc/bouncer/config.yml
RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer
EXPOSE 8080
EXPOSE 8081
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
CMD ["bouncer"]

9
misc/k6/README.md Normal file
View File

@ -0,0 +1,9 @@
# K6 - Load Test
Very basic load testing script for [k6](https://k6.io/).
## How to run
```shell
k6 run cadoles-loadtest.js
```

View File

@ -0,0 +1,29 @@
import { check } from 'k6';
import { browser } from 'k6/experimental/browser';
export const options = {
scenarios: {
browser: {
vus: 10,
iterations: 100,
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
},
}
};
export default async function () {
const page = browser.newPage();
try {
await page.goto('https://www.cadoles.com');
check(page, {
'Homepage loaded': p => p.locator('h1').textContent().trim() == 'La liberté est un choix',
});
} finally {
page.close();
}
}

68
misc/k8s/README.md Normal file
View File

@ -0,0 +1,68 @@
# Kubernetes
## Initialize your project
1. Generate the Docker configuration to enable image builds with Kaniko and communicate with reg.cadoles.com
```shell
docker login reg.cadoles.com
mkdir -p misc/k8s/kustomization/base/secrets/dockerconfig
docker --config misc/k8s/kustomization/base/secrets/dockerconfig login reg.cadoles.com
mv misc/k8s/kustomization/base/secrets/dockerconfig/config.json misc/k8s/kustomization/base/secrets/dockerconfig/.dockerconfigjson
mkdir -p misc/k8s/kustomization/overlays/dev/secrets/dockerconfig
cp misc/k8s/kustomization/base/secrets/dockerconfig/.dockerconfigjson misc/k8s/kustomization/overlays/dev/secrets/dockerconfig/.dockerconfigjson
```
## Getting started with Kind
1. Create your [Kind](https://kind.sigs.k8s.io/) cluster
```shell
kind create cluster --config misc/k8s/kind/bouncer-cluster.yaml
```
2. Deploy required operators
```shell
kubectl apply -k misc/k8s/kind/cluster --server-side
```
3. Deploy your Bouncer development environment
```shell
skaffold dev -p dev --cleanup=false --default-repo reg.cadoles.com/<YOUR_PERSONNAL_USER_NAME>
```
## Testing
Bouncer will automatically create proxies based on the files present in the `misc/k8s/kustomization/overlays/dev/files/bouncer/bootstrap.d` folder.
By default, with you host web browser, open http://localhost:9000, you should see the Cadoles website.
### Using the admin API
1. Open shell in bouncer-admin pod
```shell
kubectl exec -it -n bouncer-dev bouncer-admin-<suffix> -- /bin/sh
```
2. Create an authentication token
```shell
bouncer --config /etc/bouncer/config.yml auth create-token --role writer --subject $(whoami) > .bouncer-token
```
3. Create a proxy and enable it
```shell
bouncer admin proxy query
```
## Benchmarking
You can use [`siege`](https://github.com/JoeDog/siege) to benchmark your instance with the Cadoles proxy.
```shell
BASE_URL=http://localhost:9000 make siege
```

View File

@ -0,0 +1,3 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: bouncer-dev

View File

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://forge.cadoles.com/CadolesKube/c-kustom//base/redis?ref=develop

View File

@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bouncer
resources:
- ./resources/namespace.yaml
- ./resources/bouncer-server
- ./resources/bouncer-admin
- ./resources/redis

View File

@ -0,0 +1 @@
{"d":"JuBw5OsGv3rPgVczxUgtJ6iUQ41LQu4Xpu-t8IKI_z8r-BZBlbndxidPmRlGZASLGL3rhY4qw6_ScFxakrMpCreO1RMU0kqtz--N48BXFnW5tEgr1voyyKP__bPssQNn6PgkoyAd11es7MEKlBff_DtGrcSkVRgU0zDZB-vIU0aNEIZPNw0icbYqc1u_QQNPpBU9cw6P33WHhzvfCVAkZKRszwznhiPM08n1vjpiA7e1kQ8a6OC4IFZBvohkmpmyOq1g1OLRABQ83YPCjGjCAejO-jEWkbLksp6rAl_YYpCvfBAjFV76JuZq4eh5IU82LsSfi3PGYBkhxWuLY779XQ","dp":"gljHOQowGK7fVn2DJizWtgRIDJuKpKnoX2PWNJUbm2WZwcEPZalAkxn7Y-w_reLVJZuRpfKEUMS-Tn3-CwI1ZjCHPqMPTXcoG0Pe2E-Z88jOs9lW4XSOASiiM980VIvkV1xCxDJkN3NsDFQ9j9kRGnKuMnsucCW3AKaU917hXNU","dq":"mqY19JcEBDnzS70_XkAsOKqPzemOScax66b-4N6zrsgeLVlRjHffY9uCAgBWzlxOidRdQN8q23ZJB4fqsKB2w00Iw7Jxx94IoAKGjKDT5iB48Y_kdKLAwSHRTXsqA9GG3po_H_JpP_EqX4TDBYtqQZuBD_tACP9HbLYMi_V2YU8","e":"AQAB","kty":"RSA","n":"sam0X0BGcuFwX8z3Wde8cv2o_zl6A9ghpkT0tCjw8qH3GNWrbAqzncSWdHBzoChBgAbuTOVs-ixYC0KeUhwFdc8Ul-jmKJWFaS8kIr3y4EH62-vLgMuIKfaxbsyUG6KMkJfnftge1jPO4ccddNej9msxcqTxu37dcgstutwtd6QkS9p5RrNbDBc8-Z7SQ4TuxJfP8msXRnCPJ-I44yszGdQF1Np2DXakJHVn8PBrDh3iSFwORw8jxNS4oS0OlBl5aSc0t5XkkaNcSU2a50SKts290w54fl6MPJ1sLnnznLy4uu37-nrfEUvqRLDZL9B1F82RM1dtLIIiN4gnSrMlpQ","p":"wOmFPhAT_wXWzMuwtEdYIer3-CiOWxFKpFL09eEJkJ29MIUchEaoiJaUAghqPxM48llfOVaUaLbFVxmo5U3fyjNMaP-nHMUBwojutykMK-gC2R3J4bQgFWfKbGSL7M7UsextAvpq9iiOuR0LNE-xTfCgPIxHVdPZskO3yx0DkjM","q":"68OGRb0tLRjb_PpkGctcSjEz_vvcyjzxGL-fn4_h4GCw98Xrj6Y4rZ4lfWWRSeDohSvdd-ICSlxvxkQOIOcA0H7jyJcBC0KDs4hX5BRGJNDri3QX0ry4_F1ptAdbfiFgQGqCfMRCr7L60Tfd_6tLczvny7eEBKQNGdj6dLfhgMc","qi":"DFwixyxUDf0REPLLa8hOKieRL95_AH9rbYWzStBOdSjKWra5l0reD6a4bbvAYvl0e8qCcRI6S8Nzpz0BYm4sJL7poVOnjxqvBY3Q9Ppf4Mq8lW39pOCJcqOHIvvYHsMjTC5uwp7Yg2p0GvxuUibbyNL1PXf6WZ_szVP_oSMrCXA"}

View File

@ -0,0 +1,40 @@
admin:
http:
host: 127.0.0.1
port: 8081
cors:
allowedOrigins:
- http://localhost:3001
allowCredentials: true
allowMethods:
- POST
- GET
- PUT
- DELETE
allowedHeaders:
- Origin
- Accept
- Content-Type
- Authorization
- Sentry-Trace
debug: false
auth:
issuer: http://127.0.0.1:8081
privateKey: /etc/bouncer/admin-key.json
metrics:
enabled: true
endpoint: /.bouncer/metrics
basicAuth: null
redis:
addresses:
- rfs-bouncer-redis:${RFS_BOUNCER_REDIS_SERVICE_PORT}
master: mymaster
logger:
level: ${BOUNCER_LOG_LEVEL}
format: human
bootstrap:
dir: /etc/bouncer/bootstrap.d
lockTimeout: 30s

View File

@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./resources/service.yaml
- ./resources/deployment.yaml
configMapGenerator:
- name: bouncer-admin-config
files:
- ./files/config.yml
- ./files/admin-key.json
- name: bouncer-admin-bootstrap
- name: bouncer-admin-env
literals:
- BOUNCER_LOG_LEVEL=2

View File

@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bouncer-admin
labels:
app: bouncer-admin
io.kompose.service: bouncer-admin
spec:
replicas: 3
selector:
matchLabels:
app: bouncer-admin
template:
metadata:
labels:
app: bouncer-admin
io.kompose.service: bouncer-admin
spec:
containers:
- name: bouncer-admin
image: bouncer
command:
[
"bouncer",
"--debug",
"-c",
"/etc/bouncer/config.yml",
"server",
"admin",
"run",
]
imagePullPolicy: Always
resources: {}
ports:
- name: bouncer-admin
containerPort: 8081
envFrom:
- configMapRef:
name: bouncer-admin-env
volumeMounts:
- mountPath: /etc/bouncer/
name: bouncer-admin-config
- mountPath: /etc/bouncer/bootstrap.d
name: bouncer-admin-bootstrap
volumes:
- name: bouncer-admin-config
configMap:
name: bouncer-admin-config
- name: bouncer-admin-bootstrap
configMap:
name: bouncer-admin-bootstrap

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: bouncer-admin
name: bouncer-admin
spec:
type: ClusterIP
ports:
- name: bouncer-admin
port: 8081
targetPort: bouncer-admin
selector:
io.kompose.service: bouncer-admin

View File

@ -0,0 +1,22 @@
proxy:
http:
host: 0.0.0.0
port: 8080
metrics:
enabled: true
endpoint: /.bouncer/metrics
basicAuth: null
layers:
queue:
templateDir: /usr/share/bouncer/layers/queue/templates
defaultKeepAlive: 1m0s
redis:
addresses:
- rfs-bouncer-redis:${RFS_BOUNCER_REDIS_SERVICE_PORT}
master: mymaster
logger:
level: ${BOUNCER_LOG_LEVEL}
format: human

View File

@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./resources/service.yaml
- ./resources/deployment.yaml
configMapGenerator:
- name: bouncer-server-config
files:
- ./files/config.yml
- name: bouncer-server-env
literals:
- BOUNCER_LOG_LEVEL=2

View File

@ -0,0 +1,45 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bouncer-server
labels:
app: bouncer-server
io.kompose.service: bouncer-server
spec:
replicas: 3
selector:
matchLabels:
app: bouncer-server
template:
metadata:
labels:
app: bouncer-server
io.kompose.service: bouncer-server
spec:
containers:
- name: bouncer-server
image: bouncer
command:
[
"bouncer",
"-c",
"/etc/bouncer/config.yml",
"server",
"proxy",
"run",
]
imagePullPolicy: Always
envFrom:
- configMapRef:
name: bouncer-server-env
resources: {}
ports:
- name: bouncer-server
containerPort: 8080
volumeMounts:
- mountPath: /etc/bouncer/
name: bouncer-server-config
volumes:
- name: bouncer-server-config
configMap:
name: bouncer-server-config

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: bouncer-server
name: bouncer-server
spec:
type: ClusterIP
ports:
- name: bouncer-server
port: 8080
targetPort: bouncer-server
selector:
io.kompose.service: bouncer-server

View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: bouncer

View File

@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./resources/redis-cluster.yaml
vars:
- name: REDIS_SERVICE_NAME
objref:
name: bouncer-redis
apiVersion: databases.spotahome.com/v1
kind: RedisFailover
fieldref:
fieldpath: metadata.name

View File

@ -0,0 +1,21 @@
apiVersion: databases.spotahome.com/v1
kind: RedisFailover
metadata:
name: bouncer-redis
spec:
sentinel:
replicas: 3
resources:
requests:
cpu: 100m
limits:
memory: 100Mi
redis:
replicas: 3
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 400m
memory: 500Mi

View File

@ -0,0 +1,11 @@
from: ["*"]
to: https://www.cadoles.com
enabled: true
weight: 0
layers:
my-queue:
type: queue
enabled: true
weight: 0
options:
capacity: 10

View File

@ -0,0 +1,32 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bouncer-dev
resources:
- ../../base
secretGenerator:
- files:
- secrets/dockerconfig/.dockerconfigjson
name: regcred-dev
type: kubernetes.io/dockerconfigjson
patches:
- path: patches/add-registry-pull-secret.patch.yaml
target:
kind: Deployment
version: v1
configMapGenerator:
- name: bouncer-admin-bootstrap
behavior: merge
files:
- ./files/bouncer/bootstrap.d/cadoles.yml
- name: bouncer-admin-env
behavior: merge
literals:
- BOUNCER_LOG_LEVEL=0
- name: bouncer-server-env
behavior: merge
literals:
- BOUNCER_LOG_LEVEL=0

View File

@ -0,0 +1,4 @@
- op: add
path: "/spec/template/spec/imagePullSecrets"
value:
- name: regcred-dev

View File

@ -1,176 +1,212 @@
# Configuration du service "admin"
admin:
http:
# Hôte d'écoute du service,
# 0.0.0.0 pour écouter sur toutes les interfaces
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
http:
# Hôte d'écoute du service,
# 0.0.0.0 pour écouter sur toutes les interfaces
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
# est branché sur l'API d'administration.
cors:
allowedOrigins:
- http://localhost:8081
allowCredentials: true
allowMethods:
- POST
- GET
- PUT
- DELETE
allowedHeaders:
- Origin
- Accept
- Content-Type
- Authorization
- Sentry-Trace
debug: false
# Configuration CORS du service
# Uniquement nécessaire si un frontend web
# est branché sur l'API d'administration.
cors:
allowedOrigins:
- http://localhost:8081
allowCredentials: true
allowMethods:
- POST
- GET
- PUT
- DELETE
allowedHeaders:
- Origin
- Accept
- Content-Type
- Authorization
- Sentry-Trace
debug: false
# Authentification JWT
auth:
# Origine du jeton JWT
issuer: http://127.0.0.1:8081
# Clé privée permettant de signer les jetons
# JWT générés pour l'usage de l'API d'administration.
privateKey: /etc/bouncer/admin-key.json
# Authentification JWT
auth:
# Origine du jeton JWT
issuer: http://127.0.0.1:8081
# Clé privée permettant de signer les jetons
# JWT générés pour l'usage de l'API d'administration.
privateKey: /etc/bouncer/admin-key.json
# Métriques Prometheus
metrics:
# Activer ou désactiver la publication des métriques
enabled: true
# Route de publication des métriques
endpoint: /.bouncer/metrics
# Authentification "basic auth" sur la page
# de publication
# Mettre à null pour désactiver l'authentification
basicAuth: null
# Métriques Prometheus
metrics:
# Activer ou désactiver la publication des métriques
enabled: true
# Route de publication des métriques
endpoint: /.bouncer/metrics
# Authentification "basic auth" sur la page
# de publication
# Mettre à null pour désactiver l'authentification
basicAuth: null
# Configuration de l'intégration Sentry
# Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
sentry:
dsn: ""
debug: false
flushTimeout: 2s
attachStacktrace: true
sampleRate: 1
enableTracing: true
tracesSampleRate: 0.2
profilesSampleRate: 1
ignoreErrors: []
sendDefaultPII: false
serverName: ""
environment: ""
maxBreadcrumbs: 0
maxSpans: 1000
maxErrorDepth: 10
# Configuration de l'intégration Sentry
# Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
sentry:
dsn: ""
debug: false
flushTimeout: 2s
attachStacktrace: true
sampleRate: 1
enableTracing: true
tracesSampleRate: 0.2
profilesSampleRate: 1
ignoreErrors: []
sendDefaultPII: false
serverName: ""
environment: ""
maxBreadcrumbs: 0
maxSpans: 1000
maxErrorDepth: 10
# Configuration du service "proxy"
proxy:
http:
# Hôte d'écoute du service,
# 0.0.0.0 pour écouter sur toutes les interfaces
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
http:
# Hôte d'écoute du service,
# 0.0.0.0 pour écouter sur toutes les interfaces
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:
# Activer ou désactiver la publication des métriques
enabled: true
# Route de publication des métriques
endpoint: /.bouncer/metrics
# Authentification "basic auth" sur la page
# de publication
# Mettre à null pour désactiver l'authentification
basicAuth:
credentials:
prom: etheus
# Métriques Prometheus
metrics:
# Activer ou désactiver la publication des métriques
enabled: true
# Route de publication des métriques
endpoint: /.bouncer/metrics
# Authentification "basic auth" sur la page
# de publication
# Mettre à null pour désactiver l'authentification
basicAuth:
credentials:
prom: etheus
# Configuration du transport HTTP(S)
# Voir https://pkg.go.dev/net/http#Transport
transport:
forceAttemptHTTP2: true
maxIdleConns: 100
maxIdleConnsPerHost: 100
maxConnsPerHost: 100
idleConnTimeout: 1m30s
tlsHandshakeTimeout: 10s
expectContinueTimeout: 1s
disableKeepAlives: false
disableCompression: false
responseHeaderTimeout: 10s
writeBufferSize: 4096
readBufferSize: 4096
maxResponseHeaderBytes: 0
# Configuration du transport HTTP(S)
# Voir https://pkg.go.dev/net/http#Transport
transport:
forceAttemptHTTP2: true
maxIdleConns: 100
maxIdleConnsPerHost: 100
maxConnsPerHost: 100
idleConnTimeout: 1m30s
tlsHandshakeTimeout: 10s
expectContinueTimeout: 1s
disableKeepAlives: false
disableCompression: false
responseHeaderTimeout: 10s
writeBufferSize: 4096
readBufferSize: 4096
maxResponseHeaderBytes: 0
# Configuration de l'intégration Sentry
# Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
sentry:
dsn: ""
debug: false
flushTimeout: 2s
attachStacktrace: true
sampleRate: 1
enableTracing: true
tracesSampleRate: 0.2
profilesSampleRate: 1
ignoreErrors: []
sendDefaultPII: false
serverName: ""
environment: ""
maxBreadcrumbs: 0
maxSpans: 1000
maxErrorDepth: 10
# Configuration de l'intégration Sentry
# Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
sentry:
dsn: ""
debug: false
flushTimeout: 2s
attachStacktrace: true
sampleRate: 1
enableTracing: true
tracesSampleRate: 0.2
profilesSampleRate: 1
ignoreErrors: []
sendDefaultPII: false
serverName: ""
environment: ""
maxBreadcrumbs: 0
maxSpans: 1000
maxErrorDepth: 10
# Configuration des connexions TCP
# Voir https://pkg.go.dev/net#Dialer
dial:
timeout: 30s
keepAlive: 30s
fallbackDelay: 300ms
dualStack: true
# Configuration des connexions TCP
# Voir https://pkg.go.dev/net#Dialer
dial:
timeout: 30s
keepAlive: 30s
fallbackDelay: 300ms
dualStack: true
# Configuration du client Redis
#
# 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 "sentinel": renseigner le nom du master sentinel 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: ""
addresses:
- localhost:6379
master: ""
writeTimeout: 30s
readTimeout: 30s
dialTimeout: 30s
# Configuration des logs
logger:
# Niveau de verbosité
# 0 - DEBUG
# 1 - INFO
# 2 - WARNING
# 3 - ERROR
# 4 - FATAL
level: 1
# Format des logs, "human" ou "json"
format: human
# Niveau de verbosité
# 0 - DEBUG
# 1 - INFO
# 2 - WARNING
# 3 - ERROR
# 4 - FATAL
level: 2
# Format des logs, "human" ou "json"
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
# 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
# Configuration du layer "circuitbreaker"
circuitbreaker:
# Répertoire contenant les templates
templateDir: "/etc/bouncer/layers/circuitbreaker/templates"
# Configuration du layer "circuitbreaker"
circuitbreaker:
# Répertoire contenant les templates
templateDir: "/etc/bouncer/layers/circuitbreaker/templates"
# Configuration d'une série de proxy/layers
# à créer par défaut par le serveur d'administration
bootstrap:
# Répertoire contenant les définitions de proxy à créer
# par défaut. Les fichiers seront récupérés si ils
# correspondent au patron de nommage suivant:
#
# <bootstrap_dir>/<proxy_name>.yml
#
# Si l'attribut est vide ou absent le chargement des fichiers
# est désactivé.
dir: /etc/bouncer/bootstrap.d
# Délai d'expiration du verrou distribué utilisé lors du chargement
# des définitions de proxy par défaut.
lockTimeout: 30s
# Tableau associatif de définition de proxies à créer par
# défaut par le serveur d'administration.
# Si `proxies` et `dir` sont tous les deux définis, les fichiers
# présents dans le répertoire `dir` surchargeront les valeurs définies
# dans `proxies`.
# Par défault non défini
proxies:
# my-proxy:
# enabled: true
# from: ["*"]
# to: "https://example.net"
# weight: 0
# layers:
# my-layer:
# type: queue
# enabled: false
# weight: 0
# options: {"capacity": 100}

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 @@
${BASE_URL}/blog/
${BASE_URL}/services/
${BASE_URL}
${BASE_URL}/recrutement/
${BASE_URL}/faq/
${BASE_URL}/societe/histoire/

View File

@ -15,3 +15,8 @@ layers/**
{
daemon +sigint: make run-redis
}
misc/prometheus/prometheus.yml
{
daemon +sigint: make run-prometheus
}

53
skaffold.yaml Normal file
View File

@ -0,0 +1,53 @@
apiVersion: skaffold/v3
kind: Config
metadata:
name: bouncer
manifests:
kustomize:
paths:
- misc/k8s/kustomization/base
profiles:
- name: dev
manifests:
kustomize:
paths:
- misc/k8s/kustomization/overlays/dev
activation:
- command: dev
build:
local:
push: true
tagPolicy:
sha256: {}
artifacts:
- image: bouncer
context: .
sync:
infer:
- cmd/**
- internal/**
- layers/**
- misc/**
docker:
dockerfile: Dockerfile
deploy:
statusCheckDeadlineSeconds: 600
portForward:
- resourceType: service
resourceName: bouncer-admin
namespace: bouncer-dev
port: 8081
localPort: 9999
- resourceType: service
resourceName: bouncer-server
namespace: bouncer-dev
port: 8080
localPort: 9000 # *Optional*