Compare commits
67 Commits
feat/docke
...
v2024.3.29
Author | SHA1 | Date | |
---|---|---|---|
274bef13d8 | |||
f548c8c8e7 | |||
a82fe46fa3 | |||
cc20bdd289 | |||
7de166765b | |||
35717429a2 | |||
16305469c5 | |||
7515be9583 | |||
e76a82668d | |||
d8b78ad277 | |||
61012b07cd | |||
d12ebfc642 | |||
441d3a623e | |||
e1d9acb980 | |||
f8be2c08d6 | |||
bc7422a50c | |||
9d32551ec5 | |||
ded6d179c1 | |||
6f4ee0ebd1 | |||
1375c9b317 | |||
53a0d26a47 | |||
87354ef0d4 | |||
8560041598 | |||
0611cc9f70 | |||
734ed64e8e | |||
c8fc143efa | |||
f91c14e5d4 | |||
1602626e8c | |||
e2e38841f4 | |||
c23d8e3adb | |||
a3f44cf123 | |||
5453988419 | |||
1e392f94a7 | |||
b44ff2a68e | |||
c719fdca37 | |||
2b91c1e167 | |||
cebf1daf72 | |||
6734cf6526 | |||
368273f1ee | |||
553513d647 | |||
60487c11d6 | |||
e6f18e7cd8 | |||
a207291c04 | |||
64b5182f8b | |||
ce2c19f9b3 | |||
1ffec1f173 | |||
aab5452fa2 | |||
a176b754cd | |||
7b04eb2418 | |||
f8d9ff15b5 | |||
5bd7cbc132 | |||
1b06f07ce8 | |||
82228fd115 | |||
15daddbe13 | |||
5a7062d53e | |||
74409f18e8 | |||
ab7f64a684 | |||
d5cc15de3b | |||
56609ec316 | |||
5bf391b6bf | |||
74928fe413 | |||
ff1d01828d | |||
851f5d64cc | |||
e0d81c061b | |||
440d467938 | |||
f8d33299b9 | |||
6fed6358b2 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
||||
/admin-key.json
|
||||
/config.yml
|
||||
/tools
|
||||
/out
|
||||
/dist
|
||||
/data
|
||||
/bin
|
||||
/.bouncer-token
|
||||
/.env
|
||||
/misc/k8s
|
||||
/misc/k6s
|
||||
/misc/grafterm
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,4 +7,5 @@
|
||||
/admin-key.json
|
||||
/.bouncer-token
|
||||
/data
|
||||
/out
|
||||
/out
|
||||
.dockerconfigjson
|
||||
|
@ -11,7 +11,7 @@ builds:
|
||||
- -s
|
||||
- -w
|
||||
- -X 'main.GitRef={{ .Commit }}'
|
||||
- -X 'main.ProjectVersion={{ .Version }}'
|
||||
- -X 'main.ProjectVersion={{ .Version }}'
|
||||
- -X 'main.BuildDate={{ .Date }}'
|
||||
- -X 'main.DefaultConfigPath=/etc/bouncer/config.yml'
|
||||
gcflags:
|
||||
@ -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
|
||||
|
49
Dockerfile
49
Dockerfile
@ -1,30 +1,55 @@
|
||||
FROM golang:1.19 AS BUILD
|
||||
FROM reg.cadoles.com/proxy_cache/library/golang:1.21.6 AS BUILD
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y make
|
||||
|
||||
COPY . /src
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
FROM busybox:latest AS RUNTIME
|
||||
# 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.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
|
||||
|
||||
ARG DUMB_INIT_VERSION=1.2.5
|
||||
FROM reg.cadoles.com/proxy_cache/library/alpine:3.19.1 AS RUNTIME
|
||||
|
||||
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
|
||||
RUN apk add --no-cache ca-certificates dumb-init
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1 /app
|
||||
COPY --from=BUILD /src/config.yml /etc/bouncer/config.yml
|
||||
RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer
|
||||
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer
|
||||
COPY --from=BUILD /src/layers /usr/share/bouncer/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
|
||||
|
||||
ENTRYPOINT ["/app/bouncer"]
|
||||
RUN adduser -D -H bouncer
|
||||
|
||||
CMD ["bouncer", "run", "-c", "/etc/bouncer/config.yml"]
|
||||
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
|
||||
|
||||
USER bouncer
|
||||
|
||||
CMD ["bouncer"]
|
27
Jenkinsfile
vendored
27
Jenkinsfile
vendored
@ -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 {
|
||||
|
49
Makefile
49
Makefile
@ -5,17 +5,20 @@ GITCHLOG_ARGS ?=
|
||||
SHELL := /bin/bash
|
||||
|
||||
BOUNCER_VERSION ?=
|
||||
GIT_VERSION := $(shell git describe --always)
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD)
|
||||
DATE_VERSION := $(shell date +%Y.%-m.%-d)
|
||||
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
|
||||
FULL_VERSION := v$(DATE_VERSION)-$(GIT_COMMIT)$(if $(shell git diff --stat),-dirty,)
|
||||
|
||||
DOCKER_IMAGE_NAME ?= cadoles/bouncer
|
||||
DOCKER_IMAGE_NAME ?= reg.cadoles.com/cadoles/bouncer
|
||||
DOCKER_IMAGE_TAG ?= $(FULL_VERSION)
|
||||
|
||||
GOTEST_ARGS ?= -short
|
||||
|
||||
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 )
|
||||
|
||||
@ -25,16 +28,6 @@ test: test-go ## Executing tests
|
||||
test-go: deps
|
||||
( set -o allexport && source .env && set +o allexport && go test -v -count=1 $(GOTEST_ARGS) ./... )
|
||||
|
||||
test-install-script: tools/bin/bash_unit
|
||||
tools/bin/bash_unit ./misc/script/test_install.sh
|
||||
|
||||
tools/bin/bash_unit:
|
||||
mkdir -p tools/bin
|
||||
cd tools/bin && bash <(curl -s https://raw.githubusercontent.com/pgrange/bash_unit/master/install.sh)
|
||||
|
||||
lint: ## Lint sources code
|
||||
golangci-lint run --enable-all $(LINT_ARGS)
|
||||
|
||||
build: build-bouncer ## Build artefacts
|
||||
|
||||
build-bouncer: deps ## Build executable
|
||||
@ -83,14 +76,13 @@ finish-release:
|
||||
git push --all
|
||||
git push --tags
|
||||
|
||||
install-git-hooks:
|
||||
git config core.hooksPath .githooks
|
||||
|
||||
docker-build:
|
||||
docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) .
|
||||
docker tag $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) $(DOCKER_IMAGE_NAME):latest
|
||||
|
||||
docker-release:
|
||||
docker push $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)
|
||||
docker push $(DOCKER_IMAGE_NAME):latest
|
||||
|
||||
gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
||||
mkdir -p .gitea-release
|
||||
@ -106,12 +98,21 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
||||
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
|
||||
GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_COMMIT)" \
|
||||
GITEA_RELEASE_IS_DRAFT="false" \
|
||||
GITEA_RELEASE_BODY="" \
|
||||
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
||||
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
|
||||
@ -121,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)
|
||||
|
||||
@ -139,4 +144,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
|
@ -3,17 +3,26 @@
|
||||
- [(FR) - Premiers pas](./fr/getting-started.md)
|
||||
- [(FR) - Architecture générale](./fr/general-architecture.md)
|
||||
|
||||
## Exemples
|
||||
|
||||
- [(FR) - Exemple de déploiement multi-noeuds](../misc/docker-compose/README.md)
|
||||
|
||||
## Référence
|
||||
|
||||
- [(FR) - Layers](./fr/references/layers/README.md)
|
||||
- [Fichier de configuration](../misc/packaging/common/config.yml)
|
||||
- [(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 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)
|
||||
- [(FR) - Amorçage d'un serveur Bouncer via la configuration](./fr/tutorials/bootstrapping.md)
|
||||
- [(FR) - Intégration avec Kubernetes](./fr/tutorials/kubernetes-integration.md)
|
||||
|
||||
### Développement
|
||||
|
||||
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.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)
|
||||
|
@ -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:
|
||||
@ -92,4 +92,4 @@
|
||||
|
||||
3. Ouvrir la page `https://<ip_serveur>:8080/` dans un navigateur. Le site Cadoles s'affiche !
|
||||
|
||||
**Bravo, vous avez créé votre premier proxy avec Bouncer !**
|
||||
**Bravo, vous avez créé votre premier proxy avec Bouncer !**
|
||||
|
182
doc/fr/references/admin_api.md
Normal file
182
doc/fr/references/admin_api.md
Normal 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)
|
@ -2,4 +2,5 @@
|
||||
|
||||
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
|
||||
|
||||
- [Queue](./queue) - 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
|
41
doc/fr/references/layers/circuitbreaker.md
Normal file
41
doc/fr/references/layers/circuitbreaker.md
Normal file
@ -0,0 +1,41 @@
|
||||
# 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).
|
||||
|
||||
## Métriques
|
||||
|
||||
_Aucune [métrique Prometheus](../metrics.md) n'est exportée par ce layer._
|
@ -22,6 +22,42 @@ 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.
|
||||
|
||||
### Schéma
|
||||
### `matchURLs`
|
||||
|
||||
Voir le [schéma JSON](../../../../internal/queue/schema/layer-options.json).
|
||||
- **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).
|
||||
|
||||
## 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
|
||||
```
|
29
doc/fr/references/metrics.md
Normal file
29
doc/fr/references/metrics.md
Normal 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._
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau calque ("layer") pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
|
||||
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau layer pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
|
||||
|
||||
```bash
|
||||
# Création d'un calque nommé 'my-queue' pour le proxy 'cadoles' de type 'queue'
|
||||
@ -19,7 +19,7 @@
|
||||
+----------+-------+---------+--------+---------+-------------------------+-------------------------+
|
||||
```
|
||||
|
||||
2. À ce stade, le calque est encore inactif. Définir la capacité de la file d'attente à 1 et activer le calque en utilisant le CLI
|
||||
2. À ce stade, le layer est encore inactif. Définir la capacité de la file d'attente à 1 et activer le layer en utilisant le CLI:
|
||||
|
||||
```bash
|
||||
bouncer admin layer update --proxy-name cadoles --layer-name my-queue --layer-enabled=true --layer-options '{"capacity": 1}'
|
||||
|
47
doc/fr/tutorials/bootstrapping.md
Normal file
47
doc/fr/tutorials/bootstrapping.md
Normal 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
|
||||
```
|
@ -1,64 +1,11 @@
|
||||
# Créer son propre layer
|
||||
|
||||
Dans ce tutoriel, nous allons voir comme implémenter un layer personnalisé qui permettra d'ajouter une authentification de type [`Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) à un proxy.
|
||||
Dans ce tutoriel, nous verrons comment implémenter un layer personnalisé qui permettra d'ajouter une authentification de type [`Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) à un proxy.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Les éléments suivants doivent être installés sur votre machine:
|
||||
|
||||
- [Golang > 1.20](https://go.dev/)
|
||||
- [Docker](https://www.docker.com/)
|
||||
- [Git](https://git-scm.com/)
|
||||
- [GNU Make](https://www.gnu.org/software/make/)
|
||||
|
||||
Avoir un environnement de développement local fonctionnel. Voir tutoriel ["Démarrer avec les sources"](./getting-started-with-sources.md).
|
||||
## Étapes
|
||||
|
||||
### Préparer son environnement de développement
|
||||
|
||||
1. Cloner le dépôt des sources du projet Bouncer
|
||||
|
||||
```
|
||||
git clone https://forge.cadoles.com/Cadoles/bouncer
|
||||
```
|
||||
|
||||
2. Se positionner dans le répertoire du projet
|
||||
|
||||
```
|
||||
cd bouncer
|
||||
```
|
||||
|
||||
3. Lancer le projet en mode "développement"
|
||||
|
||||
```
|
||||
make watch
|
||||
```
|
||||
|
||||
Si toutes les dépendances sont correctement installées et configurées sur votre machine, la console devrait afficher une série de messages pour ensuite s'arrêter sur quelque chose ressemblant à:
|
||||
|
||||
```
|
||||
14:47:06: daemon: make run BOUNCER_CMD="--config config.yml server admin run"
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/command/server/admin/run.go:42> RunCommand.func1 listening {"url": "http://127.0.0.1:8081"}
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/admin/server.go:126> (*Server).run http server listening
|
||||
14:47:06: daemon: make run-redis
|
||||
bouncer-redis
|
||||
docker run --rm -t \
|
||||
--name bouncer-redis \
|
||||
-v /home/wpetit/workspace/bouncer/data/redis:/data \
|
||||
-p 6379:6379 \
|
||||
redis:alpine3.17 \
|
||||
redis-server --save 60 1 --loglevel warning
|
||||
1:C 23 Jun 2023 20:47:06.754 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
|
||||
1:C 23 Jun 2023 20:47:06.754 # Redis version=7.0.11, bits=64, commit=00000000, modified=0, pid=1, just started
|
||||
1:C 23 Jun 2023 20:47:06.754 # Configuration loaded
|
||||
1:M 23 Jun 2023 20:47:06.759 # Warning: Could not create server TCP listening socket ::*:6379: unable to bind socket, errno: 97
|
||||
1:M 23 Jun 2023 20:47:06.760 # Server initialized
|
||||
1:M 23 Jun 2023 20:47:06.760 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect
|
||||
```
|
||||
|
||||
À ce stade, le serveur `bouncer-admin` écoute sur http://127.0.0.1:8081 et le serveur `bouncer-proxy` sur http://127.0.0.1:8080.
|
||||
|
||||
L'outil [`modd`](https://github.com/cortesi/modd) est utilisé pour surveiller les modifications sur les sources et relancer automatiquement la compilation et les services en cas de changement.
|
||||
|
||||
### Préparer la structure de base du nouveau layer
|
||||
|
||||
Une implémetation d'un layer se compose majoritairement de 3 éléments:
|
||||
|
132
doc/fr/tutorials/getting-started-with-sources.md
Normal file
132
doc/fr/tutorials/getting-started-with-sources.md
Normal file
@ -0,0 +1,132 @@
|
||||
# Démarrer avec les sources
|
||||
|
||||
Dans ce tutoriel, nous verrons comment lancer un environnement de développement en local sur notre machine afin de travailler sur les sources de Bouncer.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Les éléments suivants doivent être installés sur votre machine:
|
||||
|
||||
- [Golang > 1.20](https://go.dev/)
|
||||
- [Docker](https://www.docker.com/)
|
||||
- [Git](https://git-scm.com/)
|
||||
- [GNU Make](https://www.gnu.org/software/make/)
|
||||
|
||||
Les ports suivants doivent être disponibles sur votre machine:
|
||||
|
||||
- `8080`
|
||||
- `8081`
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Cloner le dépôt des sources du projet Bouncer
|
||||
|
||||
```
|
||||
git clone https://forge.cadoles.com/Cadoles/bouncer
|
||||
```
|
||||
|
||||
2. Se positionner dans le répertoire du projet
|
||||
|
||||
```
|
||||
cd bouncer
|
||||
```
|
||||
|
||||
3. Lancer le projet en mode "développement"
|
||||
|
||||
```
|
||||
make watch
|
||||
```
|
||||
|
||||
Si toutes les dépendances sont correctement installées et configurées sur votre machine, la console devrait afficher une série de messages pour ensuite s'arrêter sur quelque chose ressemblant à:
|
||||
|
||||
```
|
||||
14:47:06: daemon: make run BOUNCER_CMD="--config config.yml server admin run"
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/command/server/admin/run.go:42> RunCommand.func1 listening {"url": "http://127.0.0.1:8081"}
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/admin/server.go:126> (*Server).run http server listening
|
||||
14:47:06: daemon: make run-redis
|
||||
bouncer-redis
|
||||
docker run --rm -t \
|
||||
--name bouncer-redis \
|
||||
-v /home/wpetit/workspace/bouncer/data/redis:/data \
|
||||
-p 6379:6379 \
|
||||
redis:alpine3.17 \
|
||||
redis-server --save 60 1 --loglevel warning
|
||||
1:C 23 Jun 2023 20:47:06.754 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
|
||||
1:C 23 Jun 2023 20:47:06.754 # Redis version=7.0.11, bits=64, commit=00000000, modified=0, pid=1, just started
|
||||
1:C 23 Jun 2023 20:47:06.754 # Configuration loaded
|
||||
1:M 23 Jun 2023 20:47:06.759 # Warning: Could not create server TCP listening socket ::*:6379: unable to bind socket, errno: 97
|
||||
1:M 23 Jun 2023 20:47:06.760 # Server initialized
|
||||
1:M 23 Jun 2023 20:47:06.760 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect
|
||||
```
|
||||
|
||||
À ce stade, le serveur `bouncer-admin` écoute sur http://127.0.0.1:8081 et le serveur `bouncer-proxy` sur http://127.0.0.1:8080.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> L'outil [`modd`](https://github.com/cortesi/modd) est utilisé pour surveiller les modifications sur les sources et relancer automatiquement la compilation et les services en cas de changement.
|
||||
|
||||
## Commandes `make` utiles
|
||||
|
||||
### `make watch`
|
||||
|
||||
Surveiller les sources, compiler celles ci en cas de modifications et lancer les services `bouncer-proxy` et `bouncer-admin`.
|
||||
|
||||
### `make test`
|
||||
|
||||
Exécuter les tests unitaires/d'intégration du projet.
|
||||
|
||||
### `make build`
|
||||
|
||||
Compiler une version de développement du binaire `bouncer`.
|
||||
|
||||
### `make docker-build`
|
||||
|
||||
Construire une image Docker pour Bouncer.
|
||||
|
||||
Vous pouvez ensuite lancer l'image localement avec la commande:
|
||||
|
||||
```
|
||||
docker run \
|
||||
-it --rm \
|
||||
reg.cadoles.com/cadoles/bouncer:<tag> \
|
||||
-p 8080:8080 \
|
||||
bouncer server proxy run
|
||||
```
|
||||
|
||||
### `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
|
||||
.
|
||||
├── bin # Répertoire de destination des binaires Go de développement
|
||||
├── cmd # Package principal (main) du binaire Bouncer
|
||||
├── data # Répertoire des données de développement (Redis)
|
||||
├── dist # Répertoire de destination des archives/paquets pour la publication
|
||||
├── doc # Répertoire de documentation du projet
|
||||
├── internal # Source Go du projet
|
||||
│ ├── admin
|
||||
│ ├── auth
|
||||
│ ├── chi
|
||||
│ ├── client
|
||||
│ ├── command
|
||||
│ ├── config
|
||||
│ ├── format
|
||||
│ ├── imports
|
||||
│ ├── jwk
|
||||
│ ├── proxy
|
||||
│ ├── schema
|
||||
│ ├── setup
|
||||
│ └── store
|
||||
├── layers # Fichiers annexes liés aux layers (templates HTML)
|
||||
│ └── queue
|
||||
├── misc # Fichiers annexes
|
||||
│ ├── jenkins # Fichiers liés au pipeline d'intégration continue Jenkins
|
||||
│ ├── logo # Logo du projet
|
||||
│ └── packaging # Fichiers liés à l'empaquetage des binaires
|
||||
└── tools # Outils utilisés en développement
|
||||
```
|
61
doc/fr/tutorials/kubernetes-integration.md
Normal file
61
doc/fr/tutorials/kubernetes-integration.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Intégration avec Kubernetes
|
||||
|
||||
Dans le cadre du déploiement de Bouncer dans un environnement Kubernetes, il est possible d'activer un mode d'intégration permettant à Bouncer d'exposer des jetons d'authentification directement sous forme de [`Secret`](https://kubernetes.io/fr/docs/concepts/configuration/secret/).
|
||||
|
||||
L'activation et configuration de l'intégration Kubernetes s'effectue dans le fichier de configuration du serveur d'administration via la section `integrations.kubernetes`:
|
||||
|
||||
```yaml
|
||||
# Section de configuration des intégrations
|
||||
# avec des produits externes
|
||||
integrations:
|
||||
# Intégration avec Kubernetes
|
||||
kubernetes:
|
||||
# Activer/désactiver l'intégration Kubernetes
|
||||
enabled: true
|
||||
# Créer/mettre à jour un Secret automatiquement avec un jeton d'authentification
|
||||
# avec le rôle "writer".
|
||||
# Désactivé si l'attribut est vide ou absent.
|
||||
writerTokenSecret: my-bouncer-admin-writer-token
|
||||
# Namespace de destination du Secret pour le jeton d'authentification
|
||||
# avec le rôle "reader".
|
||||
# Utilise par défaut le namespace courant si absent ou vide.
|
||||
writerTokenSecretNamespace: "my-namespace"
|
||||
# Créer/mettre à jour un Secret automatiquement avec un jeton d'authentification
|
||||
# avec le rôle "reader".
|
||||
# Désactivé si l'attribut est vide ou absent.
|
||||
readerTokenSecret: my-bouncer-admin-reader-token
|
||||
# Namespace de destination du Secret pour le jeton d'authentification
|
||||
# avec le rôle "reader".
|
||||
# Utilise par défaut le namespace courant si absent ou vide.
|
||||
readerTokenSecretNamespace: "my-namespace"
|
||||
# Délai maximum alloué au verrou distribué pour la mise à jour
|
||||
# des secrets.
|
||||
lockTimeout: 30s
|
||||
```
|
||||
|
||||
Vous devrez également définir un `ServiceAccount` pour votre `Pod` avec un `Role` équivalent au suivant (dans le cas nominal où le `Pod` créait les `Secrets` dans son même namespace):
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: bouncer-admin
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
- v1
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- update
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> La génération des jetons d'authentification s'effectue à chaque démarrage du serveur d'administration. Un verrou partagé permet d'éviter que plusieurs instances fonctionnant en parallèle essayent de mettre à jour les ressources Kubernetes au même moment.
|
||||
>
|
||||
> De plus, les jetons seront laissés en l'état si la clé de génération n'a pas été modifiée pour éviter de changer les jetons à chaque redémarrage d'un `Pod` (voir l'annotation `bouncer.cadoles.com/public-key` sur les `Secrets` créés.).
|
||||
|
||||
Un exemple fonctionnel de déploiement Kubernetes est disponible dans le répertoire `misc/k8s` du projet.
|
76
go.mod
76
go.mod
@ -1,17 +1,27 @@
|
||||
module forge.cadoles.com/cadoles/bouncer
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333
|
||||
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
|
||||
github.com/mitchellh/mapstructure v1.4.1
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/ory/dockertest/v3 v3.10.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/qri-io/jsonschema v0.2.1
|
||||
github.com/redis/go-redis/v9 v9.0.4
|
||||
k8s.io/api v0.29.3
|
||||
k8s.io/apimachinery v0.29.3
|
||||
k8s.io/client-go v0.29.3
|
||||
)
|
||||
|
||||
require (
|
||||
@ -21,60 +31,90 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/cli v20.10.17+incompatible // indirect
|
||||
github.com/docker/docker v20.10.13+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.5 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
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.8.1 // 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/net v0.19.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
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
|
||||
github.com/go-playground/locales v0.12.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.16.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/leodido/go-urn v1.1.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.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
|
||||
@ -86,11 +126,11 @@ 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/mod v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.7.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/term v0.15.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
183
go.sum
183
go.sum
@ -49,8 +49,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333 h1:dAajr9wX8WuFPrwjbKNXRmbF+4AaAT7bUj66G7gdZ+c=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 h1:FTk0ZoaV5N8Tkps5Da5RrDMZZXSHZIuD67Hy1Y4fsos=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
@ -80,8 +80,14 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS
|
||||
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
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/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
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=
|
||||
@ -142,8 +148,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=
|
||||
@ -160,6 +166,10 @@ 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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
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=
|
||||
@ -176,20 +186,37 @@ github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBD
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/getsentry/sentry-go v0.22.0 h1:XNX9zKbv7baSEI65l+H1GEJgSeIC1c7EN5kluWaP6dM=
|
||||
github.com/getsentry/sentry-go v0.22.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@ -228,12 +255,15 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@ -247,8 +277,12 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@ -266,6 +300,7 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
@ -299,7 +334,11 @@ github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1R
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@ -307,31 +346,33 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
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/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.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=
|
||||
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@ -347,6 +388,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@ -357,17 +400,31 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
|
||||
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
@ -379,6 +436,9 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
|
||||
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
|
||||
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@ -386,7 +446,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
|
||||
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
|
||||
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
|
||||
@ -397,24 +465,28 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@ -428,8 +500,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=
|
||||
@ -475,9 +547,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=
|
||||
@ -514,9 +585,8 @@ 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/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -559,9 +629,8 @@ 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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
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=
|
||||
@ -578,6 +647,8 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -590,8 +661,8 @@ 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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -657,21 +728,19 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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-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=
|
||||
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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.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=
|
||||
@ -682,12 +751,13 @@ 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.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=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@ -743,9 +813,8 @@ 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/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -788,6 +857,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@ -889,8 +959,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@ -901,11 +972,14 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
@ -914,6 +988,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
|
||||
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@ -921,6 +996,24 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
|
||||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
||||
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
|
||||
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
|
||||
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
|
||||
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
|
||||
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
|
112
internal/admin/bootstrap.go
Normal file
112
internal/admin/bootstrap.go
Normal file
@ -0,0 +1,112 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock/redis"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
lockTimeout := time.Duration(s.bootstrapConfig.LockTimeout)
|
||||
locker := redis.NewLocker(s.redisClient)
|
||||
|
||||
err := locker.WithLock(ctx, "bouncer-admin-bootstrap", lockTimeout, func(ctx context.Context) error {
|
||||
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
|
||||
})
|
||||
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
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const ErrCodeAlreadyExist api.ErrorCode = "already-exist"
|
||||
@ -29,3 +32,8 @@ func invalidDataErrorResponse(w http.ResponseWriter, r *http.Request, err *schem
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func logAndCaptureError(ctx context.Context, message string, err error) {
|
||||
sentry.CaptureException(err)
|
||||
logger.Error(ctx, message, logger.E(err))
|
||||
}
|
||||
|
@ -3,11 +3,18 @@ package admin
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"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 +26,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 +46,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 +55,34 @@ func (s *Server) initProxyRepository(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) initPrivateKey(ctx context.Context) error {
|
||||
localKey, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
ctx = integration.WithPrivateKey(ctx, localKey)
|
||||
|
||||
key, err := integration.RunOnKeyLoad(ctx, s.integrations)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
s.privateKey = key
|
||||
} else {
|
||||
s.privateKey = localKey
|
||||
}
|
||||
|
||||
logger.Info(ctx, "using private key", logger.F("keyID", s.privateKey.KeyID()))
|
||||
|
||||
publicKeys, err := jwk.PublicKeySet(s.privateKey)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.publicKeys = publicKeys
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
@ -10,7 +11,6 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type QueryLayerResponse struct {
|
||||
@ -38,7 +38,7 @@ func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) {
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not list layers", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not list layers", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -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"`
|
||||
}
|
||||
@ -85,7 +76,7 @@ func (s *Server) getLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -120,7 +111,7 @@ func (s *Server) deleteLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not delete layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not delete layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -156,7 +147,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
layerName, err := store.ValidateName(createLayerReq.Name)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "invalid 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "invalid 'name' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||
|
||||
return
|
||||
@ -165,7 +156,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
layerType := store.LayerType(createLayerReq.Type)
|
||||
|
||||
if !setup.LayerTypeExists(layerType) {
|
||||
logger.Error(r.Context(), "unknown layer type", logger.E(errors.WithStack(err)), logger.F("layerType", layerType))
|
||||
logAndCaptureError(ctx, fmt.Sprintf("unknown layer type '%s'", layerType), errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||
|
||||
return
|
||||
@ -179,7 +170,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not create layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not create layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -223,7 +214,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -247,7 +238,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
if updateLayerReq.Options != nil {
|
||||
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not retrieve layer options schema", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not retrieve layer options schema", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -258,7 +249,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
}(updateLayerReq.Options)
|
||||
|
||||
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
|
||||
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not validate layer options", errors.WithStack(err))
|
||||
|
||||
var invalidDataErr *schema.InvalidDataError
|
||||
if errors.As(err, &invalidDataErr) {
|
||||
@ -286,7 +277,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not update layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not update layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -300,21 +291,7 @@ func getLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool
|
||||
|
||||
name, err := store.ValidateName(rawLayerName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return store.LayerName(name), true
|
||||
}
|
||||
|
||||
func geLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) {
|
||||
rawLayerName := chi.URLParam(r, "layerName")
|
||||
|
||||
name, err := store.ValidateName(rawLayerName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse layer name", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
|
@ -2,11 +2,14 @@ package admin
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
ServerConfig config.AdminServerConfig
|
||||
RedisConfig config.RedisConfig
|
||||
BootstrapConfig config.BootstrapConfig
|
||||
ServerConfig config.AdminServerConfig
|
||||
RedisConfig config.RedisConfig
|
||||
Integrations []integration.Integration
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
@ -15,6 +18,7 @@ func defaultOption() *Option {
|
||||
return &Option{
|
||||
ServerConfig: config.NewDefaultAdminServerConfig(),
|
||||
RedisConfig: config.NewDefaultRedisConfig(),
|
||||
Integrations: make([]integration.Integration, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,3 +33,15 @@ func WithRedisConfig(conf config.RedisConfig) OptionFunc {
|
||||
opt.RedisConfig = conf
|
||||
}
|
||||
}
|
||||
|
||||
func WithBootstrapConfig(conf config.BootstrapConfig) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.BootstrapConfig = conf
|
||||
}
|
||||
}
|
||||
|
||||
func WithIntegrations(integrations ...integration.Integration) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Integrations = integrations
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type QueryProxyResponse struct {
|
||||
@ -37,7 +36,7 @@ func (s *Server) queryProxy(w http.ResponseWriter, r *http.Request) {
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not list proxies", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not list proxies", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -79,7 +78,7 @@ func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not get proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -109,12 +108,29 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not delete proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not delete proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
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,
|
||||
})
|
||||
@ -140,14 +156,14 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
name, err := store.ValidateName(createProxyReq.Name)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not parse 'name' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := url.Parse(createProxyReq.To); err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
@ -161,7 +177,7 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not create proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not create proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -207,7 +223,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
if updateProxyReq.To != nil {
|
||||
_, err := url.Parse(*updateProxyReq.To)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
@ -235,7 +251,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not update proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not update proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -249,7 +265,7 @@ func getProxyName(w http.ResponseWriter, r *http.Request) (store.ProxyName, bool
|
||||
|
||||
name, err := store.ValidateName(rawProxyName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse proxy name", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse proxy name", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
@ -263,7 +279,7 @@ func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defa
|
||||
if rawValue != "" {
|
||||
value, err := strconv.ParseInt(rawValue, 10, 64)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse int param", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
@ -296,7 +312,7 @@ func getStringableSliceValues[T ~string](w http.ResponseWriter, r *http.Request,
|
||||
for _, rv := range rawValues {
|
||||
v, err := validate(rv)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse ids slice param", logger.F("param", param), logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse ids slice param", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return nil, false
|
||||
|
@ -9,21 +9,35 @@ 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/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"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
|
||||
serverConfig config.AdminServerConfig
|
||||
redisConfig config.RedisConfig
|
||||
|
||||
redisClient redis.UniversalClient
|
||||
|
||||
integrations []integration.Integration
|
||||
|
||||
bootstrapConfig config.BootstrapConfig
|
||||
proxyRepository store.ProxyRepository
|
||||
layerRepository store.LayerRepository
|
||||
|
||||
privateKey jwk.Key
|
||||
publicKeys jwk.Set
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||
@ -50,6 +64,27 @@ 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
|
||||
}
|
||||
|
||||
if err := s.initPrivateKey(ctx); err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = integration.WithPrivateKey(ctx, s.privateKey)
|
||||
ctx = integration.WithPublicKeySet(ctx, s.publicKeys)
|
||||
|
||||
if err := integration.RunOnStartup(ctx, s.integrations); 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)
|
||||
@ -73,23 +108,23 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
}
|
||||
}()
|
||||
|
||||
key, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := jwk.PublicKeySet(key)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: true,
|
||||
})
|
||||
|
||||
router.Use(sentryMiddleware.Handle)
|
||||
}
|
||||
|
||||
corsMiddleware := cors.New(cors.Options{
|
||||
AllowedOrigins: s.serverConfig.CORS.AllowedOrigins,
|
||||
@ -101,10 +136,29 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
|
||||
router.Use(corsMiddleware.Handler)
|
||||
|
||||
if s.serverConfig.Metrics.Enabled {
|
||||
metrics := s.serverConfig.Metrics
|
||||
|
||||
logger.Info(ctx, "enabling metrics", logger.F("endpoint", metrics.Endpoint))
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
if metrics.BasicAuth != nil {
|
||||
logger.Info(ctx, "enabling authentication on metrics endpoint")
|
||||
|
||||
r.Use(middleware.BasicAuth(
|
||||
"metrics",
|
||||
metrics.BasicAuth.CredentialsMap(),
|
||||
))
|
||||
}
|
||||
|
||||
r.Handle(string(metrics.Endpoint), promhttp.Handler())
|
||||
})
|
||||
}
|
||||
|
||||
router.Route("/api/v1", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
jwt.NewAuthenticator(keys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew),
|
||||
jwt.NewAuthenticator(s.publicKeys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew),
|
||||
))
|
||||
|
||||
r.Route("/proxies", func(r chi.Router) {
|
||||
@ -139,7 +193,9 @@ func NewServer(funcs ...OptionFunc) *Server {
|
||||
}
|
||||
|
||||
return &Server{
|
||||
serverConfig: opt.ServerConfig,
|
||||
redisConfig: opt.RedisConfig,
|
||||
serverConfig: opt.ServerConfig,
|
||||
redisConfig: opt.RedisConfig,
|
||||
bootstrapConfig: opt.BootstrapConfig,
|
||||
integrations: opt.Integrations,
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ const keyRole = "role"
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string, acceptableSkew time.Duration) (jwt.Token, error) {
|
||||
token, err := jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithContext(ctx),
|
||||
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
||||
jwt.WithIssuer(issuer),
|
||||
jwt.WithValidate(true),
|
||||
@ -60,3 +61,17 @@ func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, rol
|
||||
|
||||
return string(rawToken), nil
|
||||
}
|
||||
|
||||
func GenerateTokenWithPrivateKey(ctx context.Context, privateKeyFile string, issuer string, subject string, role Role) (string, jwk.Key, error) {
|
||||
key, err := jwk.LoadOrGenerate(privateKeyFile, jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
return "", nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := GenerateToken(ctx, key, issuer, subject, role)
|
||||
if err != nil {
|
||||
return "", nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return token, key, nil
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ type LogFormatter struct{}
|
||||
func (*LogFormatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
return &LogEntry{
|
||||
method: r.Method,
|
||||
host: r.Host,
|
||||
path: r.URL.Path,
|
||||
ctx: r.Context(),
|
||||
}
|
||||
@ -29,6 +30,7 @@ var _ middleware.LogFormatter = &LogFormatter{}
|
||||
|
||||
type LogEntry struct {
|
||||
method string
|
||||
host string
|
||||
path string
|
||||
ctx context.Context
|
||||
}
|
||||
@ -41,6 +43,7 @@ func (e *LogEntry) Panic(v interface{}, stack []byte) {
|
||||
// Write implements middleware.LogEntry
|
||||
func (e *LogEntry) Write(status int, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
|
||||
logger.Info(e.ctx, fmt.Sprintf("%s %s - %d", e.method, e.path, status),
|
||||
logger.F("host", e.host),
|
||||
logger.F("status", status),
|
||||
logger.F("bytes", bytes),
|
||||
logger.F("elapsed", elapsed),
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -30,20 +29,15 @@ func CreateTokenCommand() *cli.Command {
|
||||
Action: func(ctx *cli.Context) error {
|
||||
conf, err := common.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
return errors.Wrap(err, "could not load configuration")
|
||||
}
|
||||
|
||||
subject := ctx.String("subject")
|
||||
role := ctx.String("role")
|
||||
|
||||
key, err := jwk.LoadOrGenerate(string(conf.Admin.Auth.PrivateKey), jwk.DefaultKeySize)
|
||||
token, _, err := jwt.GenerateTokenWithPrivateKey(ctx.Context, string(conf.Admin.Auth.PrivateKey), string(conf.Admin.Auth.Issuer), subject, jwt.Role(role))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := jwt.GenerateToken(ctx.Context, key, string(conf.Admin.Auth.Issuer), subject, jwt.Role(role))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return errors.Wrap(err, "could not generate token")
|
||||
}
|
||||
|
||||
fmt.Println(token)
|
||||
|
@ -18,6 +18,8 @@ func Dump() *cli.Command {
|
||||
Usage: "Dump the current configuration",
|
||||
Flags: flags,
|
||||
Action: func(ctx *cli.Context) error {
|
||||
logger.SetLevel(logger.LevelError)
|
||||
|
||||
conf, err := common.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -50,9 +51,10 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "workdir",
|
||||
Value: "",
|
||||
Usage: "The working directory",
|
||||
Name: "workdir",
|
||||
Value: "",
|
||||
EnvVars: []string{"BOUNCER_WORKDIR"},
|
||||
Usage: "The working directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "projectVersion",
|
||||
@ -89,6 +91,8 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
|
||||
return
|
||||
}
|
||||
|
||||
sentry.CaptureException(err)
|
||||
|
||||
debug := ctx.Bool("debug")
|
||||
|
||||
if !debug {
|
||||
|
@ -6,13 +6,20 @@ import (
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/admin"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||
"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(),
|
||||
)
|
||||
|
||||
return &cli.Command{
|
||||
Name: "run",
|
||||
@ -27,9 +34,24 @@ func RunCommand() *cli.Command {
|
||||
logger.SetFormat(logger.Format(conf.Logger.Format))
|
||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||
|
||||
projectVersion := ctx.String("projectVersion")
|
||||
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Admin.Sentry, projectVersion)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize sentry client")
|
||||
}
|
||||
|
||||
defer flushSentry()
|
||||
|
||||
integrations, err := setup.SetupIntegrations(ctx.Context, conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not setup integrations")
|
||||
}
|
||||
|
||||
srv := admin.NewServer(
|
||||
admin.WithServerConfig(conf.Admin),
|
||||
admin.WithRedisConfig(conf.Redis),
|
||||
admin.WithBootstrapConfig(conf.Bootstrap),
|
||||
admin.WithIntegrations(integrations...),
|
||||
)
|
||||
|
||||
addrs, srvErrs := srv.Start(ctx.Context)
|
||||
|
@ -28,6 +28,14 @@ func RunCommand() *cli.Command {
|
||||
logger.SetFormat(logger.Format(conf.Logger.Format))
|
||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||
|
||||
projectVersion := ctx.String("projectVersion")
|
||||
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize sentry client")
|
||||
}
|
||||
|
||||
defer flushSentry()
|
||||
|
||||
layers, err := setup.GetLayers(ctx.Context, conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize director layers")
|
||||
|
@ -1,16 +1,20 @@
|
||||
package config
|
||||
|
||||
type AdminServerConfig struct {
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
Metrics MetricsConfig `yaml:"metrics"`
|
||||
Sentry SentryConfig `yaml:"sentry"`
|
||||
}
|
||||
|
||||
func NewDefaultAdminServerConfig() AdminServerConfig {
|
||||
return AdminServerConfig{
|
||||
HTTP: NewHTTPConfig("127.0.0.1", 8081),
|
||||
CORS: NewDefaultCORSConfig(),
|
||||
Auth: NewDefaultAuthConfig(),
|
||||
HTTP: NewHTTPConfig("127.0.0.1", 8081),
|
||||
CORS: NewDefaultCORSConfig(),
|
||||
Auth: NewDefaultAuthConfig(),
|
||||
Metrics: NewDefaultMetricsConfig(),
|
||||
Sentry: NewDefaultSentryConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
104
internal/config/bootstrap.go
Normal file
104
internal/config/bootstrap.go
Normal 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
|
||||
}
|
@ -2,7 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -10,18 +10,20 @@ 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"`
|
||||
Integrations IntegrationsConfig `yaml:"integrations"`
|
||||
}
|
||||
|
||||
// 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 +45,13 @@ 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(),
|
||||
Integrations: NewDefaultIntegrationsConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
@ -53,6 +55,29 @@ func (ii *InterpolatedInt) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type InterpolatedFloat float64
|
||||
|
||||
func (ifl *InterpolatedFloat) UnmarshalYAML(value *yaml.Node) error {
|
||||
var str string
|
||||
|
||||
if err := value.Decode(&str); err != nil {
|
||||
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
|
||||
}
|
||||
|
||||
if match := reVar.FindStringSubmatch(str); len(match) > 0 {
|
||||
str = os.Getenv(match[1])
|
||||
}
|
||||
|
||||
floatVal, err := strconv.ParseFloat(str, 10)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not parse float '%v', line '%d'", str, value.Line)
|
||||
}
|
||||
|
||||
*ifl = InterpolatedFloat(floatVal)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type InterpolatedBool bool
|
||||
|
||||
func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error {
|
||||
@ -107,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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
33
internal/config/integrations.go
Normal file
33
internal/config/integrations.go
Normal file
@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type IntegrationsConfig struct {
|
||||
Kubernetes KubernetesConfig `yaml:"kubernetes"`
|
||||
}
|
||||
|
||||
func NewDefaultIntegrationsConfig() IntegrationsConfig {
|
||||
return IntegrationsConfig{
|
||||
Kubernetes: KubernetesConfig{
|
||||
Enabled: false,
|
||||
WriterTokenSecret: "",
|
||||
WriterTokenSecretNamespace: "",
|
||||
ReaderTokenSecretNamespace: "",
|
||||
PrivateKeySecret: "",
|
||||
PrivateKeySecretNamespace: "",
|
||||
ReaderTokenSecret: "",
|
||||
LockTimeout: *NewInterpolatedDuration(30 * time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type KubernetesConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
WriterTokenSecret InterpolatedString `yaml:"writerTokenSecret"`
|
||||
WriterTokenSecretNamespace InterpolatedString `yaml:"writerTokenSecretNamespace"`
|
||||
ReaderTokenSecret InterpolatedString `yaml:"readerTokenSecret"`
|
||||
ReaderTokenSecretNamespace InterpolatedString `yaml:"readerTokenSecretNamespace"`
|
||||
PrivateKeySecret InterpolatedString `yaml:"privateKeySecret"`
|
||||
PrivateKeySecretNamespace InterpolatedString `yaml:"privateKeySecretNamespace"`
|
||||
LockTimeout InterpolatedDuration `yaml:"lockTimeout"`
|
||||
}
|
@ -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"`
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ type LoggerConfig struct {
|
||||
|
||||
func NewDefaultLoggerConfig() LoggerConfig {
|
||||
return LoggerConfig{
|
||||
Level: InterpolatedInt(logger.LevelInfo),
|
||||
Level: InterpolatedInt(logger.LevelError),
|
||||
Format: InterpolatedString(logger.FormatHuman),
|
||||
}
|
||||
}
|
||||
|
35
internal/config/metrics.go
Normal file
35
internal/config/metrics.go
Normal file
@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type MetricsConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
Endpoint InterpolatedString `yaml:"endpoint"`
|
||||
BasicAuth *BasicAuthConfig `yaml:"basicAuth"`
|
||||
}
|
||||
|
||||
type BasicAuthConfig struct {
|
||||
Credentials *InterpolatedMap `yaml:"credentials"`
|
||||
}
|
||||
|
||||
func (c *BasicAuthConfig) CredentialsMap() map[string]string {
|
||||
if c.Credentials == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
credentials := make(map[string]string, len(*c.Credentials))
|
||||
|
||||
for k, v := range *c.Credentials {
|
||||
credentials[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
return credentials
|
||||
}
|
||||
|
||||
func NewDefaultMetricsConfig() MetricsConfig {
|
||||
return MetricsConfig{
|
||||
Enabled: true,
|
||||
Endpoint: "/.bouncer/metrics",
|
||||
BasicAuth: nil,
|
||||
}
|
||||
}
|
@ -1,11 +1,73 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type ProxyServerConfig struct {
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
Metrics MetricsConfig `yaml:"metrics"`
|
||||
Transport TransportConfig `yaml:"transport"`
|
||||
Dial DialConfig `yaml:"dial"`
|
||||
Sentry SentryConfig `yaml:"sentry"`
|
||||
}
|
||||
|
||||
// See https://pkg.go.dev/net/http#Transport
|
||||
type TransportConfig struct {
|
||||
ForceAttemptHTTP2 InterpolatedBool `yaml:"forceAttemptHTTP2"`
|
||||
MaxIdleConns InterpolatedInt `yaml:"maxIdleConns"`
|
||||
MaxIdleConnsPerHost InterpolatedInt `yaml:"maxIdleConnsPerHost"`
|
||||
MaxConnsPerHost InterpolatedInt `yaml:"maxConnsPerHost"`
|
||||
IdleConnTimeout *InterpolatedDuration `yaml:"idleConnTimeout"`
|
||||
TLSHandshakeTimeout *InterpolatedDuration `yaml:"tlsHandshakeTimeout"`
|
||||
ExpectContinueTimeout *InterpolatedDuration `yaml:"expectContinueTimeout"`
|
||||
DisableKeepAlives InterpolatedBool `yaml:"disableKeepAlives"`
|
||||
DisableCompression InterpolatedBool `yaml:"disableCompression"`
|
||||
ResponseHeaderTimeout *InterpolatedDuration `yaml:"responseHeaderTimeout"`
|
||||
WriteBufferSize InterpolatedInt `yaml:"writeBufferSize"`
|
||||
ReadBufferSize InterpolatedInt `yaml:"readBufferSize"`
|
||||
MaxResponseHeaderBytes InterpolatedInt `yaml:"maxResponseHeaderBytes"`
|
||||
}
|
||||
|
||||
func NewDefaultProxyServerConfig() ProxyServerConfig {
|
||||
return ProxyServerConfig{
|
||||
HTTP: NewHTTPConfig("0.0.0.0", 8080),
|
||||
HTTP: NewHTTPConfig("0.0.0.0", 8080),
|
||||
Metrics: NewDefaultMetricsConfig(),
|
||||
Transport: NewDefaultTransportConfig(),
|
||||
Dial: NewDefaultDialConfig(),
|
||||
Sentry: NewDefaultSentryConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// See https://pkg.go.dev/net#Dialer
|
||||
type DialConfig struct {
|
||||
Timeout *InterpolatedDuration `yaml:"timeout"`
|
||||
KeepAlive *InterpolatedDuration `yaml:"keepAlive"`
|
||||
FallbackDelay *InterpolatedDuration `yaml:"fallbackDelay"`
|
||||
DualStack InterpolatedBool `yaml:"dualStack"`
|
||||
}
|
||||
|
||||
func NewDefaultDialConfig() DialConfig {
|
||||
return DialConfig{
|
||||
Timeout: NewInterpolatedDuration(30 * time.Second),
|
||||
KeepAlive: NewInterpolatedDuration(30 * time.Second),
|
||||
FallbackDelay: NewInterpolatedDuration(300 * time.Millisecond),
|
||||
DualStack: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultTransportConfig() TransportConfig {
|
||||
return TransportConfig{
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
MaxConnsPerHost: 100,
|
||||
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second),
|
||||
TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
|
||||
ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
|
||||
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
|
||||
DisableCompression: false,
|
||||
DisableKeepAlives: false,
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
MaxResponseHeaderBytes: 0,
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
43
internal/config/sentry.go
Normal file
43
internal/config/sentry.go
Normal file
@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Sentry configuration
|
||||
// See https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
|
||||
type SentryConfig struct {
|
||||
DSN InterpolatedString `yaml:"dsn"`
|
||||
Debug InterpolatedBool `yaml:"debug"`
|
||||
FlushTimeout *InterpolatedDuration `yaml:"flushTimeout"`
|
||||
AttachStacktrace InterpolatedBool `yaml:"attachStacktrace"`
|
||||
SampleRate InterpolatedFloat `yaml:"sampleRate"`
|
||||
EnableTracing InterpolatedBool `yaml:"enableTracing"`
|
||||
TracesSampleRate InterpolatedFloat `yaml:"tracesSampleRate"`
|
||||
ProfilesSampleRate InterpolatedFloat `yaml:"profilesSampleRate"`
|
||||
IgnoreErrors InterpolatedStringSlice `yaml:"ignoreErrors"`
|
||||
SendDefaultPII InterpolatedBool `yaml:"sendDefaultPII"`
|
||||
ServerName InterpolatedString `yaml:"serverName"`
|
||||
Environment InterpolatedString `yaml:"environment"`
|
||||
MaxBreadcrumbs InterpolatedInt `yaml:"maxBreadcrumbs"`
|
||||
MaxSpans InterpolatedInt `yaml:"maxSpans"`
|
||||
MaxErrorDepth InterpolatedInt `yaml:"maxErrorDepth"`
|
||||
}
|
||||
|
||||
func NewDefaultSentryConfig() SentryConfig {
|
||||
return SentryConfig{
|
||||
DSN: "",
|
||||
Debug: false,
|
||||
FlushTimeout: NewInterpolatedDuration(2 * time.Second),
|
||||
AttachStacktrace: true,
|
||||
SampleRate: 1,
|
||||
EnableTracing: true,
|
||||
TracesSampleRate: 0.2,
|
||||
ProfilesSampleRate: 1,
|
||||
IgnoreErrors: []string{},
|
||||
SendDefaultPII: false,
|
||||
ServerName: "",
|
||||
Environment: "",
|
||||
MaxBreadcrumbs: 0,
|
||||
MaxSpans: 1000,
|
||||
MaxErrorDepth: 10,
|
||||
}
|
||||
}
|
4
internal/config/testdata/config.yml
vendored
4
internal/config/testdata/config.yml
vendored
@ -2,5 +2,5 @@ logger:
|
||||
level: 0
|
||||
format: human
|
||||
http:
|
||||
host: "0.0.0.0"
|
||||
port: 3000
|
||||
host: "${LISTEN_ADDR}"
|
||||
port: 3000
|
||||
|
49
internal/integration/context.go
Normal file
49
internal/integration/context.go
Normal file
@ -0,0 +1,49 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
ctxPublicKeySet contextKey = "public-key-set"
|
||||
ctxPrivateKey contextKey = "private-key"
|
||||
)
|
||||
|
||||
func CtxPublicKeySet(ctx context.Context) (jwk.Set, error) {
|
||||
return ctxValue[jwk.Set](ctx, ctxPublicKeySet)
|
||||
}
|
||||
|
||||
func WithPublicKeySet(ctx context.Context, set jwk.Set) context.Context {
|
||||
return context.WithValue(ctx, ctxPublicKeySet, set)
|
||||
}
|
||||
|
||||
func CtxPrivateKey(ctx context.Context) (jwk.Key, error) {
|
||||
return ctxValue[jwk.Key](ctx, ctxPrivateKey)
|
||||
}
|
||||
|
||||
func WithPrivateKey(ctx context.Context, key jwk.Key) context.Context {
|
||||
return context.WithValue(ctx, ctxPrivateKey, key)
|
||||
}
|
||||
|
||||
func ctxValue[T any](ctx context.Context, key contextKey) (T, error) {
|
||||
raw := ctx.Value(key)
|
||||
if raw == nil {
|
||||
return *new(T), errors.WithStack(ErrNotFound)
|
||||
}
|
||||
|
||||
value, ok := raw.(T)
|
||||
if !ok {
|
||||
return *new(T), errors.Errorf("unexpected value type '%T'", raw)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
57
internal/integration/integration.go
Normal file
57
internal/integration/integration.go
Normal file
@ -0,0 +1,57 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Integration interface {
|
||||
Integration()
|
||||
}
|
||||
|
||||
type OnStartup interface {
|
||||
Integration
|
||||
OnStartup(ctx context.Context) error
|
||||
}
|
||||
|
||||
type OnKeyLoad interface {
|
||||
Integration
|
||||
OnKeyLoad(ctx context.Context) (jwk.Key, error)
|
||||
}
|
||||
|
||||
func RunOnStartup(ctx context.Context, integrations []Integration) error {
|
||||
for _, it := range integrations {
|
||||
onStartup, ok := it.(OnStartup)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := onStartup.OnStartup(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunOnKeyLoad(ctx context.Context, integrations []Integration) (jwk.Key, error) {
|
||||
for _, it := range integrations {
|
||||
onKeyLoad, ok := it.(OnKeyLoad)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key, err := onKeyLoad.OnKeyLoad(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
293
internal/integration/kubernetes/integration.go
Normal file
293
internal/integration/kubernetes/integration.go
Normal file
@ -0,0 +1,293 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
|
||||
writerTokenSubject = "bouncer-admin-kubernetes-writer"
|
||||
readerTokenSubject = "bouncer-admin-kubernetes-reader"
|
||||
)
|
||||
|
||||
type Integration struct {
|
||||
Options *Options
|
||||
}
|
||||
|
||||
// OnKeyLoad implements integration.OnKeyLoad.
|
||||
func (i *Integration) OnKeyLoad(ctx context.Context) (jwk.Key, error) {
|
||||
locker := i.Options.Locker
|
||||
timeout := i.Options.LockTimeout
|
||||
|
||||
var key jwk.Key
|
||||
err := locker.WithLock(ctx, "bouncer-kubernetes-onkeyload", timeout, func(ctx context.Context) error {
|
||||
client, err := i.getClient()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if i.Options.PrivateKeySecret != "" {
|
||||
sharedPrivateKey, err := i.getSharedPrivateKey(ctx, client, i.Options.PrivateKeySecretNamespace, i.Options.PrivateKeySecret)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if sharedPrivateKey != nil {
|
||||
key = sharedPrivateKey
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Integration implements integration.OnStartup.
|
||||
func (i *Integration) Integration() {}
|
||||
|
||||
// OnStartup implements integration.OnStartup.
|
||||
func (i *Integration) OnStartup(ctx context.Context) error {
|
||||
locker := i.Options.Locker
|
||||
timeout := i.Options.LockTimeout
|
||||
err := locker.WithLock(ctx, "bouncer-kubernetes-onstartup", timeout, func(ctx context.Context) error {
|
||||
client, err := i.getClient()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if i.Options.WriterTokenSecret != "" {
|
||||
if err := i.upsertTokenSecret(ctx, client, i.Options.WriterTokenSecretNamespace, i.Options.WriterTokenSecret, writerTokenSubject, jwt.RoleWriter); err != nil {
|
||||
return errors.Wrap(err, "could not upsert writer token secret")
|
||||
}
|
||||
}
|
||||
|
||||
if i.Options.ReaderTokenSecret != "" {
|
||||
if err := i.upsertTokenSecret(ctx, client, i.Options.ReaderTokenSecretNamespace, i.Options.ReaderTokenSecret, readerTokenSubject, jwt.RoleReader); err != nil {
|
||||
return errors.Wrap(err, "could not upsert reader token secret")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Integration) getClient() (*kubernetes.Clientset, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const (
|
||||
annotationPublicKey = "bouncer.cadoles.com/public-key"
|
||||
)
|
||||
|
||||
func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.Clientset, namespace string, name string, subject string, role jwt.Role) error {
|
||||
if namespace == "" {
|
||||
defaultNamespace, err := i.getCurrentNamespace()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx,
|
||||
logger.F("secretNamespace", namespace),
|
||||
logger.F("secretName", name),
|
||||
logger.F("tokenRole", role),
|
||||
logger.F("tokenSubject", subject),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "generating new token")
|
||||
|
||||
alreadyExists := true
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serr.IsNotFound(err) {
|
||||
alreadyExists = false
|
||||
} else {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
privateKey, err := integration.CtxPrivateKey(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
keySet, err := integration.CtxPublicKeySet(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
publicKeyThumbprint, err := getKeySetThumbprint(keySet)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !alreadyExists {
|
||||
token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret := &v1.Secret{
|
||||
Type: v1.SecretTypeOpaque,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Annotations: map[string]string{
|
||||
annotationPublicKey: publicKeyThumbprint,
|
||||
},
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"token": token,
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info(ctx, "creating token secret")
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
existingPublicKeyHash, exists := secret.Annotations[annotationPublicKey]
|
||||
if !exists || publicKeyThumbprint != existingPublicKeyHash {
|
||||
token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret.StringData = map[string]string{
|
||||
"token": token,
|
||||
}
|
||||
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
secret.Annotations[annotationPublicKey] = publicKeyThumbprint
|
||||
|
||||
logger.Info(ctx, "updating token secret")
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
logger.Info(ctx, "key did not changed, doing nothing")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Integration) getSharedPrivateKey(ctx context.Context, client *kubernetes.Clientset, namespace string, name string) (jwk.Key, error) {
|
||||
if namespace == "" {
|
||||
defaultNamespace, err := i.getCurrentNamespace()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx,
|
||||
logger.F("secretNamespace", namespace),
|
||||
logger.F("secretName", name),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "searching shared private key from secret")
|
||||
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil && !k8serr.IsNotFound(err) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawPrivateKey, exists := secret.Data["key"]
|
||||
|
||||
if exists && len(rawPrivateKey) != 0 {
|
||||
key, err := jwk.ParseKey(rawPrivateKey)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
localKey, err := integration.CtxPrivateKey(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawLocalKey, err := json.Marshal(localKey)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret = &v1.Secret{
|
||||
Type: v1.SecretTypeOpaque,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": rawLocalKey,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return localKey, nil
|
||||
}
|
||||
|
||||
func (i *Integration) getCurrentNamespace() (string, error) {
|
||||
namespace, err := os.ReadFile(namespaceFile)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not retrieve current namespace")
|
||||
}
|
||||
|
||||
return string(namespace), nil
|
||||
}
|
||||
|
||||
func NewIntegration(funcs ...OptionFunc) *Integration {
|
||||
opts := NewOptions(funcs...)
|
||||
|
||||
return &Integration{
|
||||
Options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ integration.OnStartup = &Integration{}
|
||||
_ integration.OnKeyLoad = &Integration{}
|
||||
)
|
41
internal/integration/kubernetes/keyset.go
Normal file
41
internal/integration/kubernetes/keyset.go
Normal file
@ -0,0 +1,41 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func getKeySetThumbprint(set jwk.Set) (string, error) {
|
||||
data := make([][]byte, 0, set.Len())
|
||||
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
key, exists := set.Key(i)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
thumbprint, err := key.Thumbprint(crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
data = append(data, thumbprint)
|
||||
}
|
||||
|
||||
slices.SortFunc(data, bytes.Compare)
|
||||
|
||||
hash := sha256.New()
|
||||
for _, d := range data {
|
||||
if _, err := hash.Write(d); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
}
|
95
internal/integration/kubernetes/options.go
Normal file
95
internal/integration/kubernetes/options.go
Normal file
@ -0,0 +1,95 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock/memory"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
WriterTokenSecret string
|
||||
WriterTokenSecretNamespace string
|
||||
ReaderTokenSecret string
|
||||
ReaderTokenSecretNamespace string
|
||||
PrivateKeySecret string
|
||||
PrivateKeySecretNamespace string
|
||||
Issuer string
|
||||
Locker lock.Locker
|
||||
LockTimeout time.Duration
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
WriterTokenSecret: "",
|
||||
WriterTokenSecretNamespace: "",
|
||||
ReaderTokenSecret: "",
|
||||
ReaderTokenSecretNamespace: "",
|
||||
PrivateKeySecret: "",
|
||||
PrivateKeySecretNamespace: "",
|
||||
Issuer: "",
|
||||
Locker: memory.NewLocker(),
|
||||
LockTimeout: 30 * time.Second,
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithWriterTokenSecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.WriterTokenSecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithWriterTokenSecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.WriterTokenSecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithReaderTokenSecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.ReaderTokenSecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithReaderTokenSecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.ReaderTokenSecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithPrivateKeySecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.PrivateKeySecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithPrivateKeySecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.PrivateKeySecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithIssuer(issuer string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Issuer = issuer
|
||||
}
|
||||
}
|
||||
|
||||
func WithLocker(locker lock.Locker) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Locker = locker
|
||||
}
|
||||
}
|
||||
|
||||
func WithLockTimeout(timeout time.Duration) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.LockTimeout = timeout
|
||||
}
|
||||
}
|
@ -4,13 +4,13 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/base58"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/oklog/ulid/v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -24,8 +24,9 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
FromRaw = jwk.FromRaw
|
||||
NewSet = jwk.NewSet
|
||||
FromRaw = jwk.FromRaw
|
||||
NewSet = jwk.NewSet
|
||||
ParseKey = jwk.ParseKey
|
||||
)
|
||||
|
||||
const AlgorithmKey = jwk.AlgorithmKey
|
||||
@ -56,7 +57,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 +73,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)
|
||||
}
|
||||
}
|
||||
@ -96,6 +97,12 @@ func Generate(size int) (jwk.Key, error) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
keyID := ulid.Make().String()
|
||||
|
||||
if err := key.Set(jwk.KeyIDKey, keyID); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
|
10
internal/lock/locker.go
Normal file
10
internal/lock/locker.go
Normal file
@ -0,0 +1,10 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Locker interface {
|
||||
WithLock(ctx context.Context, key string, timeout time.Duration, fn func(ctx context.Context) error) error
|
||||
}
|
45
internal/lock/memory/locker.go
Normal file
45
internal/lock/memory/locker.go
Normal file
@ -0,0 +1,45 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTimeout = errors.New("timeout")
|
||||
)
|
||||
|
||||
type Locker struct {
|
||||
lock chan struct{}
|
||||
}
|
||||
|
||||
// WithLock implements lock.Locker.
|
||||
func (l *Locker) WithLock(ctx context.Context, key string, timeout time.Duration, fn func(ctx context.Context) error) error {
|
||||
select {
|
||||
case l.lock <- struct{}{}:
|
||||
defer func() {
|
||||
<-l.lock
|
||||
}()
|
||||
if err := fn(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return errors.WithStack(ctx.Err())
|
||||
|
||||
case <-time.After(timeout):
|
||||
return errors.WithStack(ErrTimeout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLocker() *Locker {
|
||||
return &Locker{
|
||||
lock: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
var _ lock.Locker = &Locker{}
|
59
internal/lock/redis/locker.go
Normal file
59
internal/lock/redis/locker.go
Normal file
@ -0,0 +1,59 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"github.com/bsm/redislock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Locker struct {
|
||||
client redis.UniversalClient
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// WithLock implements lock.Locker.
|
||||
func (l *Locker) WithLock(ctx context.Context, key string, timeout time.Duration, fn func(ctx context.Context) error) error {
|
||||
locker := redislock.New(l.client)
|
||||
|
||||
backoff := redislock.ExponentialBackoff(time.Second, timeout*2)
|
||||
|
||||
ctx = logger.With(ctx, logger.F("lockTimeout", timeout), logger.F("lockKey", key))
|
||||
|
||||
logger.Debug(ctx, "acquiring lock")
|
||||
|
||||
lock, err := locker.Obtain(ctx, key, timeout, &redislock.Options{
|
||||
RetryStrategy: backoff,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "lock obtained")
|
||||
|
||||
defer func() {
|
||||
if err := lock.Release(ctx); err != nil {
|
||||
logger.Error(ctx, "could not release lock", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "lock released")
|
||||
}()
|
||||
|
||||
if err := fn(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLocker(client redis.UniversalClient) *Locker {
|
||||
return &Locker{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
var _ lock.Locker = &Locker{}
|
43
internal/logger/writer.go
Normal file
43
internal/logger/writer.go
Normal file
@ -0,0 +1,43 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
ctx context.Context
|
||||
level logger.Level
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
w.log(string(p))
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *Writer) log(message string) {
|
||||
switch w.level {
|
||||
case logger.LevelDebug:
|
||||
logger.Debug(w.ctx, message)
|
||||
case logger.LevelInfo:
|
||||
logger.Info(w.ctx, message)
|
||||
case logger.LevelWarn:
|
||||
logger.Warn(w.ctx, message)
|
||||
case logger.LevelError:
|
||||
logger.Error(w.ctx, message)
|
||||
case logger.LevelCritical:
|
||||
logger.Critical(w.ctx, message)
|
||||
default:
|
||||
logger.Debug(w.ctx, message)
|
||||
}
|
||||
}
|
||||
|
||||
func NewWriter(ctx context.Context, level logger.Level) *Writer {
|
||||
return &Writer{ctx, level}
|
||||
}
|
||||
|
||||
var _ io.Writer = &Writer{}
|
@ -3,13 +3,13 @@ package director
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
@ -27,12 +27,23 @@ func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
|
||||
return r, errors.WithStack(err)
|
||||
}
|
||||
|
||||
url := getRequestURL(r)
|
||||
ctx = logger.With(r.Context(), logger.F("url", url.String()))
|
||||
|
||||
var match *store.Proxy
|
||||
|
||||
MAIN:
|
||||
for _, p := range proxies {
|
||||
for _, from := range p.From {
|
||||
if matches := wildcard.Match(r.Host, from); !matches {
|
||||
logger.Debug(
|
||||
ctx, "matching request with proxy's from",
|
||||
logger.F("from", from),
|
||||
)
|
||||
if matches := wildcard.Match(url.String(), from); !matches {
|
||||
logger.Debug(
|
||||
ctx, "proxy's from matched",
|
||||
logger.F("from", from),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -59,6 +70,8 @@ MAIN:
|
||||
logger.F("remoteAddr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(match.Name)}).Add(1)
|
||||
|
||||
ctx = withProxy(ctx, match)
|
||||
|
||||
layers, err := d.getLayers(ctx, match.Name)
|
||||
|
@ -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
|
||||
}
|
151
internal/proxy/director/layer/circuitbreaker/layer.go
Normal file
151
internal/proxy/director/layer/circuitbreaker/layer.go
Normal 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{}
|
@ -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
|
||||
}
|
19
internal/proxy/director/layer/circuitbreaker/options.go
Normal file
19
internal/proxy/director/layer/circuitbreaker/options.go
Normal 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
|
||||
}
|
||||
}
|
8
internal/proxy/director/layer/circuitbreaker/schema.go
Normal file
8
internal/proxy/director/layer/circuitbreaker/schema.go
Normal file
@ -0,0 +1,8 @@
|
||||
package circuitbreaker
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
73
internal/proxy/director/layer/queue/debouncer.go
Normal file
73
internal/proxy/director/layer/queue/debouncer.go
Normal file
@ -0,0 +1,73 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DebouncerMap struct {
|
||||
debouncers sync.Map
|
||||
}
|
||||
|
||||
func NewDebouncerMap() *DebouncerMap {
|
||||
return &DebouncerMap{
|
||||
debouncers: sync.Map{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DebouncerMap) Do(key string, after time.Duration, fn func()) {
|
||||
newDebouncer := NewDebouncer(after)
|
||||
rawDebouncer, loaded := m.debouncers.LoadOrStore(key, newDebouncer)
|
||||
|
||||
debouncer, ok := rawDebouncer.(*Debouncer)
|
||||
if !ok {
|
||||
panic(errors.Errorf("unexpected debouncer value, expected '%T', got '%T'", newDebouncer, rawDebouncer))
|
||||
}
|
||||
|
||||
if loaded {
|
||||
debouncer.Update(after)
|
||||
}
|
||||
|
||||
debouncer.Do(fn)
|
||||
}
|
||||
|
||||
func NewDebouncer(after time.Duration) *Debouncer {
|
||||
return &Debouncer{after: after}
|
||||
}
|
||||
|
||||
type Debouncer struct {
|
||||
mu sync.Mutex
|
||||
after time.Duration
|
||||
timer *time.Timer
|
||||
fn func()
|
||||
}
|
||||
|
||||
func (d *Debouncer) Do(fn func()) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.timer != nil {
|
||||
d.timer.Stop()
|
||||
}
|
||||
|
||||
d.fn = fn
|
||||
d.timer = time.AfterFunc(d.after, d.fn)
|
||||
}
|
||||
|
||||
func (d *Debouncer) Update(after time.Duration) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if after == d.after {
|
||||
return
|
||||
}
|
||||
|
||||
if d.timer != nil {
|
||||
d.timer.Stop()
|
||||
}
|
||||
|
||||
d.after = after
|
||||
d.timer = time.AfterFunc(d.after, d.fn)
|
||||
}
|
@ -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{
|
||||
|
31
internal/proxy/director/layer/queue/metrics.go
Normal file
31
internal/proxy/director/layer/queue/metrics.go
Normal file
@ -0,0 +1,31 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const (
|
||||
metricNamespace = "bouncer_layer_queue"
|
||||
metricLabelProxy = "proxy"
|
||||
metricLabelLayer = "layer"
|
||||
)
|
||||
|
||||
var (
|
||||
metricQueueSessions = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "sessions",
|
||||
Help: "Bouncer's queue layer current sessions",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
metricQueueCapacity = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "capacity",
|
||||
Help: "Bouncer's queue layer capacity",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
)
|
@ -1,6 +1,8 @@
|
||||
package queue
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
TemplateDir string
|
||||
|
@ -4,18 +4,22 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"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"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
@ -30,7 +34,9 @@ type Queue struct {
|
||||
loadOnce sync.Once
|
||||
tmpl *template.Template
|
||||
|
||||
refreshJobRunning uint32
|
||||
refreshJobRunning uint32
|
||||
updateMetricsJobRunning uint32
|
||||
postKeepAliveDebouncer *DebouncerMap
|
||||
}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer
|
||||
@ -52,6 +58,15 @@ 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)
|
||||
|
||||
cookie, err := r.Cookie(cookieName)
|
||||
@ -72,8 +87,6 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
sessionID := cookie.Value
|
||||
queueName := string(layer.Name)
|
||||
|
||||
q.refreshQueue(queueName, options.KeepAlive)
|
||||
|
||||
rank, err := q.adapter.Touch(ctx, queueName, sessionID)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve session rank", logger.E(errors.WithStack(err)))
|
||||
@ -102,6 +115,30 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) updateSessionsMetric(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName) {
|
||||
if !atomic.CompareAndSwapUint32(&q.updateMetricsJobRunning, 0, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
defer atomic.StoreUint32(&q.updateMetricsJobRunning, 0)
|
||||
|
||||
queueName := string(layerName)
|
||||
|
||||
status, err := q.adapter.Status(ctx, queueName)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve queue status", logger.E(errors.WithStack(err)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
metricQueueSessions.With(
|
||||
prometheus.Labels{
|
||||
metricLabelLayer: string(layerName),
|
||||
metricLabelProxy: string(proxyName),
|
||||
},
|
||||
).Set(float64(status.Sessions))
|
||||
}
|
||||
|
||||
func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueName string, options *LayerOptions, rank int64) {
|
||||
ctx := r.Context()
|
||||
|
||||
@ -135,22 +172,26 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
|
||||
return
|
||||
}
|
||||
|
||||
refreshRate := time.Duration(int64(options.KeepAlive.Seconds()/2)) * time.Second
|
||||
|
||||
templateData := struct {
|
||||
QueueName string
|
||||
LayerOptions *LayerOptions
|
||||
Rank int64
|
||||
CurrentSessions int64
|
||||
MaxSessions int64
|
||||
RefreshRate int64
|
||||
RefreshRate time.Duration
|
||||
}{
|
||||
QueueName: queueName,
|
||||
LayerOptions: options,
|
||||
Rank: rank + 1,
|
||||
CurrentSessions: status.Sessions,
|
||||
MaxSessions: options.Capacity,
|
||||
RefreshRate: 5,
|
||||
RefreshRate: refreshRate,
|
||||
}
|
||||
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
w.Header().Add("Retry-After", strconv.FormatInt(int64(refreshRate.Seconds()), 10))
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
if err := q.tmpl.ExecuteTemplate(w, "queue", templateData); err != nil {
|
||||
@ -161,24 +202,55 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) refreshQueue(queueName string, keepAlive time.Duration) {
|
||||
func (q *Queue) refreshQueue(ctx context.Context, layerName store.LayerName, keepAlive time.Duration) {
|
||||
if !atomic.CompareAndSwapUint32(&q.refreshJobRunning, 0, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer atomic.StoreUint32(&q.refreshJobRunning, 0)
|
||||
defer atomic.StoreUint32(&q.refreshJobRunning, 0)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), keepAlive*2)
|
||||
defer cancel()
|
||||
if err := q.adapter.Refresh(ctx, string(layerName), keepAlive); err != nil {
|
||||
logger.Error(ctx, "could not refresh queue",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("queue", layerName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.adapter.Refresh(ctx, queueName, keepAlive); err != nil {
|
||||
logger.Error(ctx, "could not refresh queue",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("queue", queueName),
|
||||
)
|
||||
}
|
||||
}()
|
||||
func (q *Queue) updateMetrics(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions) {
|
||||
// Update queue capacity metric
|
||||
metricQueueCapacity.With(
|
||||
prometheus.Labels{
|
||||
metricLabelLayer: string(layerName),
|
||||
metricLabelProxy: string(proxyName),
|
||||
},
|
||||
).Set(float64(options.Capacity))
|
||||
|
||||
// Refresh queue data and metrics
|
||||
q.refreshQueue(ctx, layerName, options.KeepAlive)
|
||||
q.updateSessionsMetric(ctx, proxyName, layerName)
|
||||
|
||||
// (Re)schedule an update job after session ttl + semi-random time padding
|
||||
// to update metrics after last session expiration
|
||||
randDuration := rand.Int63n(int64(options.KeepAlive))
|
||||
timePadding := options.KeepAlive/2 + time.Duration(randDuration)
|
||||
after := options.KeepAlive + timePadding
|
||||
|
||||
debouncingKey := fmt.Sprintf("%s/%s", proxyName, layerName)
|
||||
|
||||
q.postKeepAliveDebouncer.Do(debouncingKey, after, func() {
|
||||
ctx := logger.With(
|
||||
context.Background(),
|
||||
logger.F("proxy", proxyName),
|
||||
logger.F("layer", layerName),
|
||||
logger.F("after", after),
|
||||
)
|
||||
|
||||
logger.Info(ctx, "running post keep alive refresh job")
|
||||
|
||||
q.refreshQueue(ctx, layerName, options.KeepAlive)
|
||||
q.updateSessionsMetric(ctx, proxyName, layerName)
|
||||
})
|
||||
}
|
||||
|
||||
func (q *Queue) getCookieName(layerName store.LayerName) string {
|
||||
@ -192,9 +264,10 @@ func New(adapter Adapter, funcs ...OptionFunc) *Queue {
|
||||
}
|
||||
|
||||
return &Queue{
|
||||
adapter: adapter,
|
||||
templateDir: opts.TemplateDir,
|
||||
defaultKeepAlive: opts.DefaultKeepAlive,
|
||||
adapter: adapter,
|
||||
templateDir: opts.TemplateDir,
|
||||
defaultKeepAlive: opts.DefaultKeepAlive,
|
||||
postKeepAliveDebouncer: NewDebouncerMap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ func (a *Adapter) Refresh(ctx context.Context, queueName string, keepAlive time.
|
||||
|
||||
cmd := tx.ZRangeByScore(ctx, lastSeenKey, &redis.ZRangeBy{
|
||||
Min: "0",
|
||||
Max: strconv.FormatInt(expires.Unix(), 10),
|
||||
Max: strconv.FormatInt(expires.UnixNano(), 10),
|
||||
})
|
||||
|
||||
members, err := cmd.Result()
|
||||
@ -75,7 +75,7 @@ func (a *Adapter) Touch(ctx context.Context, queueName string, sessionId string)
|
||||
|
||||
for retry > 0 {
|
||||
err := withTx(ctx, a.client, func(ctx context.Context, tx *redis.Tx) error {
|
||||
now := time.Now().UTC().Unix()
|
||||
now := time.Now().UTC().UnixNano()
|
||||
|
||||
err := tx.ZAddNX(ctx, rankKey, redis.Z{Score: float64(now), Member: sessionId}).Err()
|
||||
if err != nil {
|
||||
|
12
internal/proxy/director/layer/queue/redis/adapter_test.go
Normal file
12
internal/proxy/director/layer/queue/redis/adapter_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue/testsuite"
|
||||
)
|
||||
|
||||
func TestAdapter(t *testing.T) {
|
||||
adapter := NewAdapter(client, 3)
|
||||
testsuite.TestAdapter(t, adapter)
|
||||
}
|
58
internal/proxy/director/layer/queue/redis/main_test.go
Normal file
58
internal/proxy/director/layer/queue/redis/main_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var client redis.UniversalClient
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
// uses pool to try to connect to Docker
|
||||
err = pool.Client.Ping()
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
// pulls an image, creates a container based on it and runs it
|
||||
resource, err := pool.Run("redis", "alpine3.17", []string{})
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
client = redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: []string{resource.GetHostPort("6379/tcp")},
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if cmd := client.Ping(ctx); cmd.Err() != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(resource); err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
@ -9,6 +9,12 @@
|
||||
},
|
||||
"keepAlive": {
|
||||
"type": "string"
|
||||
},
|
||||
"matchURLs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
99
internal/proxy/director/layer/queue/testsuite/adapter.go
Normal file
99
internal/proxy/director/layer/queue/testsuite/adapter.go
Normal file
@ -0,0 +1,99 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type adapterTestCase struct {
|
||||
Name string
|
||||
Do func(adapter queue.Adapter) error
|
||||
}
|
||||
|
||||
var adapterTestCases = []adapterTestCase{
|
||||
{
|
||||
Name: "Test queue ranking",
|
||||
Do: func(adapter queue.Adapter) error {
|
||||
ctx := context.Background()
|
||||
queueName := "test_queue_ranking"
|
||||
|
||||
sessionIdPattern := "session-%d"
|
||||
totalSessions := int64(100)
|
||||
|
||||
for idx := int64(0); idx < totalSessions; idx++ {
|
||||
sessionId := fmt.Sprintf(sessionIdPattern, idx)
|
||||
rank, err := adapter.Touch(ctx, queueName, sessionId)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not touch session '%s' (index: %d, rank: %d)", sessionId, idx, rank)
|
||||
}
|
||||
|
||||
if e, g := int64(idx), rank; e != g {
|
||||
return errors.Errorf("rank('%s'): expected '%v', got '%v'", sessionId, e, g)
|
||||
}
|
||||
}
|
||||
|
||||
status, err := adapter.Status(ctx, queueName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve queue status")
|
||||
}
|
||||
|
||||
if e, g := totalSessions, status.Sessions; e != g {
|
||||
return errors.Errorf("status.Sessions: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Test session expiration",
|
||||
Do: func(adapter queue.Adapter) error {
|
||||
ctx := context.Background()
|
||||
queueName := "test_session_expiration"
|
||||
|
||||
sessionId := "session-1"
|
||||
|
||||
rank, err := adapter.Touch(ctx, queueName, sessionId)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not touch session '%s'", sessionId)
|
||||
}
|
||||
|
||||
if e, g := int64(0), rank; e != g {
|
||||
return errors.Errorf("rank('%s'): expected '%v', got '%v'", sessionId, e, g)
|
||||
}
|
||||
|
||||
<-time.After(time.Second)
|
||||
|
||||
if err := adapter.Refresh(ctx, queueName, time.Second); err != nil {
|
||||
return errors.Wrap(err, "could not refresh queue")
|
||||
}
|
||||
|
||||
status, err := adapter.Status(ctx, queueName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve queue status")
|
||||
}
|
||||
|
||||
if e, g := int64(0), status.Sessions; e != g {
|
||||
return errors.Errorf("status.Sessions: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestAdapter(t *testing.T, adapter queue.Adapter) {
|
||||
for _, tc := range adapterTestCases {
|
||||
func(tc adapterTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
if err := tc.Do(adapter); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
20
internal/proxy/director/metrics.go
Normal file
20
internal/proxy/director/metrics.go
Normal file
@ -0,0 +1,20 @@
|
||||
package director
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const (
|
||||
metricNamespace = "bouncer_proxy_director"
|
||||
metricLabelProxy = "proxy"
|
||||
)
|
||||
|
||||
var metricProxyRequestsTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "proxy_requests_total",
|
||||
Help: "Bouncer proxy total requests",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy},
|
||||
)
|
@ -2,6 +2,7 @@ package director
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/Cadoles/go-proxy/util"
|
||||
@ -16,3 +17,19 @@ func createMiddlewareChain(handler http.Handler, middlewares []proxy.Middleware)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func getRequestURL(r *http.Request) *url.URL {
|
||||
scheme := "http"
|
||||
if r.URL.Scheme != "" {
|
||||
scheme = r.URL.Scheme
|
||||
}
|
||||
|
||||
url := url.URL{
|
||||
Host: r.Host,
|
||||
Scheme: scheme,
|
||||
Path: r.URL.Path,
|
||||
RawQuery: r.URL.RawQuery,
|
||||
}
|
||||
|
||||
return &url
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -6,15 +6,21 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy"
|
||||
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
@ -83,19 +89,56 @@ 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()))
|
||||
router.Use(director.Middleware())
|
||||
|
||||
handler := proxy.New(
|
||||
proxy.WithRequestTransformers(
|
||||
director.RequestTransformer(),
|
||||
),
|
||||
proxy.WithResponseTransformers(
|
||||
director.ResponseTransformer(),
|
||||
),
|
||||
)
|
||||
if s.serverConfig.Sentry.DSN != "" {
|
||||
logger.Info(ctx, "enabling sentry http middleware")
|
||||
|
||||
router.Handle("/*", handler)
|
||||
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: true,
|
||||
})
|
||||
|
||||
router.Use(sentryMiddleware.Handle)
|
||||
}
|
||||
|
||||
if s.serverConfig.Metrics.Enabled {
|
||||
metrics := s.serverConfig.Metrics
|
||||
|
||||
logger.Info(ctx, "enabling metrics", logger.F("endpoint", metrics.Endpoint))
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
if metrics.BasicAuth != nil {
|
||||
logger.Info(ctx, "enabling authentication on metrics endpoint")
|
||||
|
||||
r.Use(middleware.BasicAuth(
|
||||
"metrics",
|
||||
metrics.BasicAuth.CredentialsMap(),
|
||||
))
|
||||
}
|
||||
|
||||
r.Handle(string(metrics.Endpoint), promhttp.Handler())
|
||||
})
|
||||
}
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
r.Use(director.Middleware())
|
||||
|
||||
handler := proxy.New(
|
||||
proxy.WithRequestTransformers(
|
||||
director.RequestTransformer(),
|
||||
),
|
||||
proxy.WithResponseTransformers(
|
||||
director.ResponseTransformer(),
|
||||
),
|
||||
proxy.WithReverseProxyFactory(s.createReverseProxy),
|
||||
)
|
||||
|
||||
r.Handle("/*", handler)
|
||||
})
|
||||
|
||||
if err := http.Serve(listener, router); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
errs <- errors.WithStack(err)
|
||||
@ -104,6 +147,52 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
logger.Info(ctx, "http server exiting")
|
||||
}
|
||||
|
||||
func (s *Server) createReverseProxy(ctx context.Context, target *url.URL) *httputil.ReverseProxy {
|
||||
reverseProxy := httputil.NewSingleHostReverseProxy(target)
|
||||
|
||||
dialConfig := s.serverConfig.Dial
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Duration(*dialConfig.Timeout),
|
||||
KeepAlive: time.Duration(*dialConfig.KeepAlive),
|
||||
FallbackDelay: time.Duration(*dialConfig.FallbackDelay),
|
||||
DualStack: bool(dialConfig.DualStack),
|
||||
}
|
||||
|
||||
transportConfig := s.serverConfig.Transport
|
||||
|
||||
reverseProxy.Transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: dialer.DialContext,
|
||||
ForceAttemptHTTP2: bool(transportConfig.ForceAttemptHTTP2),
|
||||
MaxIdleConns: int(transportConfig.MaxIdleConns),
|
||||
MaxIdleConnsPerHost: int(transportConfig.MaxIdleConnsPerHost),
|
||||
MaxConnsPerHost: int(transportConfig.MaxConnsPerHost),
|
||||
IdleConnTimeout: time.Duration(*transportConfig.IdleConnTimeout),
|
||||
TLSHandshakeTimeout: time.Duration(*transportConfig.TLSHandshakeTimeout),
|
||||
ExpectContinueTimeout: time.Duration(*transportConfig.ExpectContinueTimeout),
|
||||
DisableKeepAlives: bool(transportConfig.DisableKeepAlives),
|
||||
DisableCompression: bool(transportConfig.DisableCompression),
|
||||
ResponseHeaderTimeout: time.Duration(*transportConfig.ResponseHeaderTimeout),
|
||||
WriteBufferSize: int(transportConfig.WriteBufferSize),
|
||||
ReadBufferSize: int(transportConfig.ReadBufferSize),
|
||||
MaxResponseHeaderBytes: int64(transportConfig.MaxResponseHeaderBytes),
|
||||
}
|
||||
|
||||
reverseProxy.ErrorHandler = s.errorHandler
|
||||
|
||||
return reverseProxy
|
||||
}
|
||||
|
||||
func (s *Server) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
|
||||
err = errors.WithStack(err)
|
||||
|
||||
logger.Error(r.Context(), "proxy error", logger.E(err))
|
||||
sentry.CaptureException(err)
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func NewServer(funcs ...OptionFunc) *Server {
|
||||
opt := defaultOption()
|
||||
for _, fn := range funcs {
|
||||
|
21
internal/setup/circuitbreaker_layer.go
Normal file
21
internal/setup/circuitbreaker_layer.go
Normal 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
|
||||
}
|
46
internal/setup/integrations.go
Normal file
46
internal/setup/integrations.go
Normal file
@ -0,0 +1,46 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration/kubernetes"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func SetupIntegrations(ctx context.Context, conf *config.Config) ([]integration.Integration, error) {
|
||||
integrations := make([]integration.Integration, 0)
|
||||
|
||||
if conf.Integrations.Kubernetes.Enabled {
|
||||
kubernetes, err := setupKubernetesIntegration(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not setup kubernetes integration")
|
||||
}
|
||||
|
||||
integrations = append(integrations, kubernetes)
|
||||
}
|
||||
|
||||
return integrations, nil
|
||||
}
|
||||
|
||||
func setupKubernetesIntegration(ctx context.Context, conf *config.Config) (*kubernetes.Integration, error) {
|
||||
client := newRedisClient(conf.Redis)
|
||||
locker := redis.NewLocker(client)
|
||||
|
||||
integration := kubernetes.NewIntegration(
|
||||
kubernetes.WithReaderTokenSecret(string(conf.Integrations.Kubernetes.ReaderTokenSecret)),
|
||||
kubernetes.WithReaderTokenSecretNamespace(string(conf.Integrations.Kubernetes.ReaderTokenSecretNamespace)),
|
||||
kubernetes.WithWriterTokenSecret(string(conf.Integrations.Kubernetes.WriterTokenSecret)),
|
||||
kubernetes.WithWriterTokenSecretNamespace(string(conf.Integrations.Kubernetes.WriterTokenSecretNamespace)),
|
||||
kubernetes.WithPrivateKeySecret(string(conf.Integrations.Kubernetes.PrivateKeySecret)),
|
||||
kubernetes.WithPrivateKeySecretNamespace(string(conf.Integrations.Kubernetes.PrivateKeySecretNamespace)),
|
||||
kubernetes.WithIssuer(string(conf.Admin.Auth.Issuer)),
|
||||
kubernetes.WithLocker(locker),
|
||||
kubernetes.WithLockTimeout(time.Duration(conf.Integrations.Kubernetes.LockTimeout)),
|
||||
)
|
||||
|
||||
return integration, nil
|
||||
}
|
15
internal/setup/lock.go
Normal file
15
internal/setup/lock.go
Normal file
@ -0,0 +1,15 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock/redis"
|
||||
)
|
||||
|
||||
func SetupLocker(ctx context.Context, conf *config.Config) (lock.Locker, error) {
|
||||
client := newRedisClient(conf.Redis)
|
||||
locker := redis.NewLocker(client)
|
||||
return locker, nil
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
20
internal/setup/redis.go
Normal 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,
|
||||
})
|
||||
}
|
42
internal/setup/sentry.go
Normal file
42
internal/setup/sentry.go
Normal file
@ -0,0 +1,42 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
loggerWriter "forge.cadoles.com/cadoles/bouncer/internal/logger"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func SetupSentry(ctx context.Context, conf config.SentryConfig, release string) (func(), error) {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: string(conf.DSN),
|
||||
Debug: bool(conf.Debug),
|
||||
AttachStacktrace: bool(conf.AttachStacktrace),
|
||||
SampleRate: float64(conf.SampleRate),
|
||||
EnableTracing: bool(conf.EnableTracing),
|
||||
TracesSampleRate: float64(conf.TracesSampleRate),
|
||||
ProfilesSampleRate: float64(conf.ProfilesSampleRate),
|
||||
IgnoreErrors: conf.IgnoreErrors,
|
||||
SendDefaultPII: bool(conf.SendDefaultPII),
|
||||
ServerName: string(conf.ServerName),
|
||||
Release: release,
|
||||
Environment: string(conf.Environment),
|
||||
MaxBreadcrumbs: int(conf.MaxBreadcrumbs),
|
||||
MaxSpans: int(conf.MaxSpans),
|
||||
MaxErrorDepth: int(conf.MaxErrorDepth),
|
||||
DebugWriter: loggerWriter.NewWriter(ctx, logger.LevelDebug),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
flush := func() {
|
||||
sentry.Flush(time.Duration(*conf.FlushTimeout))
|
||||
}
|
||||
|
||||
return flush, nil
|
||||
}
|
@ -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
|
||||
},
|
||||
},
|
||||
|
@ -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()
|
||||
|
||||
|
73
layers/circuitbreaker/templates/default.gohtml
Normal file
73
layers/circuitbreaker/templates/default.gohtml
Normal 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 }}
|
@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Veuillez patienter - {{ .QueueName }}</title>
|
||||
<meta http-equiv="refresh" content="{{ .RefreshRate }}">
|
||||
<meta http-equiv="refresh" content="{{ .RefreshRate.Seconds }}">
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
|
37
misc/docker-compose/README.md
Normal file
37
misc/docker-compose/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Exemple de déploiement multi-noeuds avec Docker-Compose
|
||||
|
||||
Le répertoire [`misc/docker-compose`](./) contient un exemple de déploiement de Bouncer multi-noeuds avec:
|
||||
|
||||
- 3 instances du service `bouncer-proxy`;
|
||||
- 1 instance du service `haproxy` en frontal en charge du load-balancing;
|
||||
- 1 instance du service `bouncer-admin`;
|
||||
- 1 serveur Redis.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- [Docker Compose](https://docs.docker.com/compose/)
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Se positionner dans le répertoire puis lancer l'environnement avec la commande `docker-compose`:
|
||||
|
||||
```bash
|
||||
cd misc/docker-compose
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
2. Entrer dans le conteneur `bouncer-admin` puis créer un jeton d'accès:
|
||||
|
||||
```bash
|
||||
docker-compose exec bouncer-admin /bin/sh
|
||||
bouncer auth create-token --role writer > .bouncer-token
|
||||
```
|
||||
|
||||
3. Créer un proxy via le CLI:
|
||||
|
||||
```bash
|
||||
bouncer admin proxy create --proxy-name myproxy --proxy-to "https://www.cadoles.com/"
|
||||
bouncer admin proxy update --proxy-name myproxy --proxy-enabled=true
|
||||
```
|
||||
|
||||
4. Via votre navigateur, accéder à l'URL http://127.0.0.1:8080. La page du site Cadoles devrait s'afficher. Dans le log de la commande `docker-compose up` vous devriez voir que les requêtes sont routées à tour de rôle sur les 3 instances de Bouncer en exécution.
|
1
misc/docker-compose/bouncer/admin-key.json
Normal file
1
misc/docker-compose/bouncer/admin-key.json
Normal 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"}
|
47
misc/docker-compose/bouncer/config.yml
Normal file
47
misc/docker-compose/bouncer/config.yml
Normal file
@ -0,0 +1,47 @@
|
||||
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
|
||||
proxy:
|
||||
http:
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
metrics:
|
||||
enabled: true
|
||||
endpoint: /.bouncer/metrics
|
||||
basicAuth: null
|
||||
|
||||
redis:
|
||||
addresses:
|
||||
- redis:6379
|
||||
master: ""
|
||||
logger:
|
||||
level: 1
|
||||
format: human
|
||||
layers:
|
||||
queue:
|
||||
templateDir: /usr/share/bouncer/layers/queue/templates
|
||||
defaultKeepAlive: 1m0s
|
42
misc/docker-compose/docker-compose.yml
Normal file
42
misc/docker-compose/docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
||||
version: "2"
|
||||
services:
|
||||
haproxy:
|
||||
image: reg.cadoles.com/proxy_cache/library/haproxy:2.7-alpine
|
||||
ports:
|
||||
- 8080:8080
|
||||
links:
|
||||
- bouncer-proxy-1
|
||||
- bouncer-proxy-2
|
||||
- bouncer-proxy-3
|
||||
volumes:
|
||||
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
|
||||
|
||||
bouncer-admin:
|
||||
image: reg.cadoles.com/cadoles/bouncer:latest
|
||||
command: bouncer -c /etc/bouncer/config.yml server admin run
|
||||
links:
|
||||
- redis
|
||||
volumes:
|
||||
- ./bouncer/config.yml:/etc/bouncer/config.yml
|
||||
- ./bouncer/admin-key.json:/etc/bouncer/admin-key.json
|
||||
|
||||
bouncer-proxy-1: &bouncer-proxy
|
||||
image: reg.cadoles.com/cadoles/bouncer:latest
|
||||
command: bouncer -c /etc/bouncer/config.yml server proxy run
|
||||
links:
|
||||
- redis
|
||||
volumes:
|
||||
- ./bouncer/config.yml:/etc/bouncer/config.yml
|
||||
- ./bouncer/admin-key.json:/etc/bouncer/admin-key.json
|
||||
|
||||
bouncer-proxy-2: *bouncer-proxy
|
||||
bouncer-proxy-3: *bouncer-proxy
|
||||
|
||||
redis:
|
||||
image: reg.cadoles.com/proxy_cache/library/redis:7-alpine
|
||||
command: redis-server --save 60 1 --loglevel verbose
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
volumes:
|
||||
redis-data:
|
21
misc/docker-compose/haproxy/haproxy.cfg
Normal file
21
misc/docker-compose/haproxy/haproxy.cfg
Normal file
@ -0,0 +1,21 @@
|
||||
global
|
||||
maxconn 100
|
||||
log stdout format raw local0 info
|
||||
|
||||
defaults
|
||||
mode http
|
||||
timeout client 30s
|
||||
timeout server 30s
|
||||
timeout connect 30s
|
||||
log global
|
||||
option httplog
|
||||
|
||||
frontend proxy
|
||||
bind *:8080
|
||||
default_backend bouncer-proxy
|
||||
|
||||
backend bouncer-proxy
|
||||
mode http
|
||||
server bouncer-proxy-1 bouncer-proxy-1:8080 check
|
||||
server bouncer-proxy-2 bouncer-proxy-2:8080 check
|
||||
server bouncer-proxy-3 bouncer-proxy-3:8080 check
|
196
misc/grafterm/dashboard.json
Normal file
196
misc/grafterm/dashboard.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
49
misc/images/bouncer/Dockerfile
Normal file
49
misc/images/bouncer/Dockerfile
Normal 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"]
|
@ -7,13 +7,23 @@ 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
|
||||
|
||||
ARG GO_VERSION=1.20.4
|
||||
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.22.0
|
||||
|
||||
# Install Go
|
||||
RUN mkdir -p /tmp \
|
||||
|
9
misc/k6/README.md
Normal file
9
misc/k6/README.md
Normal 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
|
||||
```
|
29
misc/k6/cadoles-loadtest.js
Normal file
29
misc/k6/cadoles-loadtest.js
Normal 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();
|
||||
}
|
||||
}
|
84
misc/k8s/README.md
Normal file
84
misc/k8s/README.md
Normal file
@ -0,0 +1,84 @@
|
||||
# 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
|
||||
|
||||
#### From inside the cluster
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
#### From outside the cluster
|
||||
|
||||
1. Retrieve the authentication token from the generated secret
|
||||
|
||||
```shell
|
||||
TOKEN=$(kubectl get secret -n bouncer-dev -o jsonpath="{.data.token}" bouncer-admin-writer-token | base64 -d)
|
||||
```
|
||||
|
||||
2. Use the `bouncer` admin client to query the admin API
|
||||
|
||||
```shell
|
||||
./bouncer admin proxy query -t "${TOKEN}" --server http://127.0.0.1:9999
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
3
misc/k8s/kind/bouncer-cluster.yaml
Normal file
3
misc/k8s/kind/bouncer-cluster.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
name: bouncer-dev
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user