feat: kubernetes basic integration
Some checks failed
Cadoles/bouncer/pipeline/head There was a failure building this commit
Cadoles/bouncer/pipeline/pr-develop There was a failure building this commit

This commit is contained in:
wpetit 2024-03-27 17:47:39 +01:00
parent d8b78ad277
commit e76a82668d
29 changed files with 936 additions and 172 deletions

View File

@ -20,6 +20,7 @@
- [(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

View 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.

42
go.mod
View File

@ -1,6 +1,8 @@
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-20230701194111-c6b3d482cca6
@ -16,6 +18,9 @@ require (
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 (
@ -29,22 +34,35 @@ require (
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/golang/protobuf v1.5.3 // 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/text v0.2.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
@ -53,7 +71,6 @@ require (
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.9.0 // indirect
@ -61,10 +78,21 @@ require (
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
google.golang.org/protobuf v1.30.0 // 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 (
@ -98,10 +126,10 @@ require (
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/mod v0.9.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.7.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

99
go.sum
View File

@ -83,7 +83,9 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
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=
@ -166,6 +168,8 @@ 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=
@ -191,9 +195,18 @@ github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL
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-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/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
@ -201,6 +214,9 @@ github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEK
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=
@ -240,12 +256,14 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
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/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/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=
@ -259,7 +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=
@ -277,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=
@ -310,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=
@ -319,6 +347,7 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6
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.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=
@ -342,6 +371,8 @@ 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=
@ -369,17 +400,29 @@ 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/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=
@ -392,6 +435,7 @@ github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4a
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/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=
@ -438,6 +482,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
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=
@ -452,6 +498,7 @@ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
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.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=
@ -535,8 +582,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.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=
@ -579,7 +626,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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
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=
@ -596,6 +644,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=
@ -608,7 +658,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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
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=
@ -702,6 +753,8 @@ 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=
@ -757,8 +810,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.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=
@ -801,6 +854,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=
@ -903,22 +957,26 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
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=
@ -927,6 +985,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=
@ -934,6 +993,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
View 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
}

View File

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

View File

@ -2,12 +2,14 @@ package admin
import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/integration"
)
type Option struct {
BootstrapConfig config.BootstrapConfig
ServerConfig config.AdminServerConfig
RedisConfig config.RedisConfig
Integrations []integration.Integration
}
type OptionFunc func(*Option)
@ -16,6 +18,7 @@ func defaultOption() *Option {
return &Option{
ServerConfig: config.NewDefaultAdminServerConfig(),
RedisConfig: config.NewDefaultRedisConfig(),
Integrations: make([]integration.Integration, 0),
}
}
@ -36,3 +39,9 @@ func WithBootstrapConfig(conf config.BootstrapConfig) OptionFunc {
opt.BootstrapConfig = conf
}
}
func WithIntegrations(integrations ...integration.Integration) OptionFunc {
return func(opt *Option) {
opt.Integrations = integrations
}
}

View File

@ -11,6 +11,7 @@ import (
"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"
@ -24,9 +25,13 @@ import (
)
type Server struct {
serverConfig config.AdminServerConfig
redisConfig config.RedisConfig
redisClient redis.UniversalClient
serverConfig config.AdminServerConfig
redisConfig config.RedisConfig
redisClient redis.UniversalClient
integrations []integration.Integration
bootstrapConfig config.BootstrapConfig
proxyRepository store.ProxyRepository
layerRepository store.LayerRepository
@ -62,6 +67,12 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
return
}
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)
@ -187,5 +198,6 @@ func NewServer(funcs ...OptionFunc) *Server {
serverConfig: opt.ServerConfig,
redisConfig: opt.RedisConfig,
bootstrapConfig: opt.BootstrapConfig,
integrations: opt.Integrations,
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -65,10 +65,16 @@ func RunCommand() *cli.Command {
logger.SetLevel(logger.Level(conf.Logger.Level))
}
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)

View File

@ -10,12 +10,13 @@ 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"`
Bootstrap BootstrapConfig `yaml:"bootstrap"`
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
@ -44,12 +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(),
Bootstrap: NewDefaultBootstrapConfig(),
Admin: NewDefaultAdminServerConfig(),
Proxy: NewDefaultProxyServerConfig(),
Logger: NewDefaultLoggerConfig(),
Redis: NewDefaultRedisConfig(),
Layers: NewDefaultLayersConfig(),
Bootstrap: NewDefaultBootstrapConfig(),
Integrations: NewDefaultIntegrationsConfig(),
}
}

View File

@ -0,0 +1,27 @@
package config
import "time"
type IntegrationsConfig struct {
Kubernetes KubernetesConfig `yaml:"kubernetes"`
}
func NewDefaultIntegrationsConfig() IntegrationsConfig {
return IntegrationsConfig{
Kubernetes: KubernetesConfig{
Enabled: false,
WriterTokenSecret: "",
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"`
LockTimeout InterpolatedDuration `yaml:"lockTimeout"`
}

View File

@ -0,0 +1,31 @@
package integration
import (
"context"
"github.com/pkg/errors"
)
type Integration interface {
Integration()
}
type OnStartup interface {
Integration
OnStartup(ctx context.Context) 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
}

View File

@ -0,0 +1,194 @@
package kubernetes
import (
"context"
"crypto"
"fmt"
"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
}
// 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 {
config, err := rest.InClusterConfig()
if err != nil {
return errors.WithStack(err)
}
client, err := kubernetes.NewForConfig(config)
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
}
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")
key, err := jwk.LoadOrGenerate(i.Options.PrivateKey, jwk.DefaultKeySize)
if err != nil {
return errors.WithStack(err)
}
publicKey, err := key.PublicKey()
if err != nil {
return errors.WithStack(err)
}
publicKeyThumbprint, err := publicKey.Thumbprint(crypto.SHA256)
if err != nil {
return errors.WithStack(err)
}
publicKeyHash := fmt.Sprintf("%x", publicKeyThumbprint)
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)
}
}
if !alreadyExists {
token, err := jwt.GenerateToken(ctx, key, 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: publicKeyHash,
},
},
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 || publicKeyHash != existingPublicKeyHash {
token, err := jwt.GenerateToken(ctx, key, 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] = publicKeyHash
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) 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{}
)

View File

@ -0,0 +1,87 @@
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
PrivateKey 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: "",
PrivateKey: "",
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 WithPrivateKey(privateKeyFile string) OptionFunc {
return func(opts *Options) {
opts.PrivateKey = privateKeyFile
}
}
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
}
}

10
internal/lock/locker.go Normal file
View 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
}

View 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{}

View 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{}

View File

@ -0,0 +1,45 @@
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.WithIssuer(string(conf.Admin.Auth.Issuer)),
kubernetes.WithPrivateKey(string(conf.Admin.Auth.PrivateKey)),
kubernetes.WithLocker(locker),
kubernetes.WithLockTimeout(time.Duration(conf.Integrations.Kubernetes.LockTimeout)),
)
return integration, nil
}

15
internal/setup/lock.go Normal file
View 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
}

View File

@ -38,3 +38,9 @@ logger:
bootstrap:
dir: /etc/bouncer/bootstrap.d
lockTimeout: 30s
integrations:
kubernetes:
enabled: true
writerTokenSecret: ${BOUNCER_WRITER_TOKEN_SECRET}
readerTokenSecret: ${BOUNCER_READER_TOKEN_SECRET}

View File

@ -4,6 +4,7 @@ kind: Kustomization
resources:
- ./resources/service.yaml
- ./resources/deployment.yaml
- ./resources/serviceaccount.yaml
configMapGenerator:
- name: bouncer-admin-config
@ -14,3 +15,5 @@ configMapGenerator:
- name: bouncer-admin-env
literals:
- BOUNCER_LOG_LEVEL=2
- BOUNCER_WRITER_TOKEN_SECRET=bouncer-admin-writer-token
- BOUNCER_READER_TOKEN_SECRET=bouncer-admin-reader-token

View File

@ -4,7 +4,8 @@ metadata:
name: bouncer-admin
labels:
app: bouncer-admin
io.kompose.service: bouncer-admin
app.kubernetes.io/name: bouncer-admin
app.kubernetes.io/part-of: bouncer
spec:
replicas: 3
selector:
@ -14,8 +15,11 @@ spec:
metadata:
labels:
app: bouncer-admin
io.kompose.service: bouncer-admin
app.kubernetes.io/name: bouncer-admin
app.kubernetes.io/part-of: bouncer
spec:
restartPolicy: Always
serviceAccountName: bouncer-admin
containers:
- name: bouncer-admin
image: bouncer

View File

@ -2,13 +2,14 @@ apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: bouncer-admin
app.kubernetes.io/name: bouncer-admin
app.kubernetes.io/part-of: bouncer
name: bouncer-admin
spec:
type: ClusterIP
ports:
- name: bouncer-admin
port: 8081
targetPort: bouncer-admin
- name: bouncer-admin
port: 8081
targetPort: bouncer-admin
selector:
io.kompose.service: bouncer-admin
app.kubernetes.io/name: bouncer-admin

View File

@ -0,0 +1,32 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: bouncer-admin
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: bouncer-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: bouncer-admin
subjects:
- kind: ServiceAccount
name: bouncer-admin
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: bouncer-admin
rules:
- apiGroups:
- ""
- v1
resources:
- secrets
verbs:
- create
- get
- update

View File

@ -4,7 +4,8 @@ metadata:
name: bouncer-server
labels:
app: bouncer-server
io.kompose.service: bouncer-server
app.kubernetes.io/name: bouncer-server
app.kubernetes.io/part-of: bouncer
spec:
replicas: 3
selector:
@ -14,7 +15,8 @@ spec:
metadata:
labels:
app: bouncer-server
io.kompose.service: bouncer-server
app.kubernetes.io/name: bouncer-server
app.kubernetes.io/part-of: bouncer
spec:
containers:
- name: bouncer-server

View File

@ -2,13 +2,14 @@ apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: bouncer-server
app.kubernetes.io/name: bouncer-server
app.kubernetes.io/part-of: bouncer
name: bouncer-server
spec:
type: ClusterIP
ports:
- name: bouncer-server
port: 8080
targetPort: bouncer-server
- name: bouncer-server
port: 8080
targetPort: bouncer-server
selector:
io.kompose.service: bouncer-server
app.kubernetes.io/name: bouncer-server

View File

@ -23,7 +23,7 @@ build:
push: true
tagPolicy:
sha256: {}
inputDigest: {}
artifacts:
- image: bouncer