diff --git a/README.md b/README.md index f5dc95a..061d6ec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Prometheus — Multi-tenant proxy -![Build Status](https://action-badges.now.sh/k8spin/k8spin-operator) +![Build Status](https://action-badges.now.sh/k8spin/prometheus-multi-tenant-proxy) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/k8spin.svg?style=social&label=Follow%20%40k8spin)](https://twitter.com/k8spin) @@ -31,7 +31,7 @@ instance, configure the auth proxy configuration and run it. ### Run it ```bash -$ prometheus-multi-tenant-proxy run --prometheus-endpoint http://localhost:9090 --port 9091 --auth-config ./my-auth-config.yaml +$ prometheus-multi-tenant-proxy run --prometheus-endpoint http://localhost:9090 --port 9091 --auth-config ./my-auth-config.yaml --reload-interval=5 ``` Where: @@ -39,6 +39,7 @@ Where: - `--port`: Port used to expose this proxy. - `--prometheus-endpoint`: URL of your Prometheus instance. - `--auth-config`: Authentication configuration file path. +- `--reload-interval`: Interval in minutes to reload the auth config file. #### Configure the proxy @@ -77,54 +78,64 @@ A tenant can contain multiple users. But a user is tied to a simple tenant. If you want to build it from this repository, follow the instructions bellow: ```bash -$ docker run -it --entrypoint /bin/bash --rm golang:1.15.8-buster +$ docker run -it --entrypoint /bin/bash --rm golang:1.17-buster root@6985c5523ed0:/go# git clone https://github.com/k8spin/prometheus-multi-tenant-proxy.git Cloning into 'prometheus-multi-tenant-proxy'... -remote: Enumerating objects: 96, done. -remote: Counting objects: 100% (96/96), done. -remote: Compressing objects: 100% (54/54), done. -remote: Total 96 (delta 31), reused 87 (delta 22), pack-reused 0 -Unpacking objects: 100% (96/96), done. +remote: Enumerating objects: 297, done. +remote: Counting objects: 100% (85/85), done. +remote: Compressing objects: 100% (42/42), done. +remote: Total 297 (delta 42), reused 57 (delta 37), pack-reused 212 +Receiving objects: 100% (297/297), 209.10 KiB | 376.00 KiB/s, done. +Resolving deltas: 100% (120/120), done. root@6985c5523ed0:/go# cd prometheus-multi-tenant-proxy/cmd/prometheus-multi-tenant-proxy/ root@6985c5523ed0:/go# go build -go: downloading github.com/urfave/cli/v2 v2.3.0 -go: downloading github.com/prometheus-community/prom-label-proxy v0.3.0 -go: downloading github.com/prometheus/prometheus v1.8.2-0.20210811141203-dcb07e8eac34 +go: downloading github.com/urfave/cli/v2 v2.11.1 +go: downloading github.com/prometheus/prometheus v0.35.0 +go: downloading github.com/prometheus-community/prom-label-proxy v0.5.0 go: downloading gopkg.in/yaml.v2 v2.4.0 -go: downloading github.com/efficientgo/tools/core v0.0.0-20210201224146-3d78f4d30648 -go: downloading github.com/go-openapi/runtime v0.19.28 -go: downloading github.com/go-openapi/strfmt v0.20.1 go: downloading github.com/pkg/errors v0.9.1 -go: downloading github.com/prometheus/alertmanager v0.22.2 -go: downloading github.com/cpuguy83/go-md2man/v2 v2.0.0 -go: downloading github.com/cespare/xxhash/v2 v2.1.1 -go: downloading github.com/prometheus/common v0.30.0 -go: downloading github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef -go: downloading github.com/go-openapi/errors v0.20.0 -go: downloading github.com/mitchellh/mapstructure v1.4.1 +go: downloading github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b +go: downloading github.com/go-openapi/runtime v0.24.1 +go: downloading github.com/go-openapi/strfmt v0.21.3 +go: downloading github.com/prometheus/alertmanager v0.24.0 +go: downloading github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d +go: downloading github.com/go-openapi/errors v0.20.2 +go: downloading github.com/mitchellh/mapstructure v1.5.0 go: downloading github.com/oklog/ulid v1.3.1 -go: downloading go.mongodb.org/mongo-driver v1.5.1 +go: downloading go.mongodb.org/mongo-driver v1.10.0 go: downloading github.com/opentracing/opentracing-go v1.2.0 -go: downloading github.com/go-openapi/swag v0.19.15 -go: downloading github.com/go-openapi/validate v0.20.2 -go: downloading github.com/russross/blackfriday/v2 v2.0.1 -go: downloading github.com/go-kit/log v0.1.0 -go: downloading github.com/go-openapi/analysis v0.20.0 -go: downloading github.com/go-openapi/loads v0.20.2 -go: downloading github.com/go-openapi/spec v0.20.3 -go: downloading github.com/mailru/easyjson v0.7.6 -go: downloading github.com/shurcooL/sanitized_anchor_name v1.0.0 -go: downloading github.com/go-logfmt/logfmt v0.5.0 -go: downloading go.uber.org/atomic v1.9.0 +go: downloading github.com/cpuguy83/go-md2man/v2 v2.0.2 +go: downloading github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 +go: downloading github.com/prometheus/common v0.37.0 +go: downloading github.com/go-openapi/swag v0.21.1 +go: downloading github.com/go-openapi/validate v0.22.0 +go: downloading github.com/go-openapi/analysis v0.21.4 +go: downloading github.com/go-openapi/loads v0.21.1 +go: downloading github.com/go-openapi/spec v0.20.6 +go: downloading github.com/cespare/xxhash/v2 v2.1.2 +go: downloading github.com/grafana/regexp v0.0.0-20220304095617-2e8d9baf4ac2 +go: downloading github.com/russross/blackfriday/v2 v2.1.0 +go: downloading github.com/mailru/easyjson v0.7.7 go: downloading github.com/go-openapi/jsonpointer v0.19.5 -go: downloading github.com/go-stack/stack v1.8.0 +go: downloading github.com/go-kit/log v0.2.1 go: downloading github.com/josharian/intern v1.0.0 -go: downloading golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c -go: downloading github.com/go-openapi/jsonreference v0.19.5 -go: downloading github.com/PuerkitoBio/purell v1.1.1 -go: downloading github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 -go: downloading golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 -go: downloading golang.org/x/text v0.3.6 +go: downloading github.com/go-openapi/jsonreference v0.20.0 +go: downloading github.com/dennwc/varint v1.0.0 +go: downloading github.com/prometheus/client_golang v1.12.2 +go: downloading go.uber.org/atomic v1.9.0 +go: downloading github.com/stretchr/testify v1.8.0 +go: downloading github.com/go-logfmt/logfmt v0.5.1 +go: downloading golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f +go: downloading go.uber.org/goleak v1.1.12 +go: downloading github.com/davecgh/go-spew v1.1.1 +go: downloading github.com/pmezard/go-difflib v1.0.0 +go: downloading gopkg.in/yaml.v3 v3.0.1 +go: downloading github.com/prometheus/client_model v0.2.0 +go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 +go: downloading github.com/golang/protobuf v1.5.2 +go: downloading github.com/beorn7/perks v1.0.1 +go: downloading github.com/prometheus/procfs v0.7.3 +go: downloading google.golang.org/protobuf v1.28.0 root@6985c5523ed0:/go# ./prometheus-multi-tenant-proxy NAME: Prometheus multi-tenant proxy - Makes your Prometheus server multi tenant diff --git a/cmd/prometheus-multi-tenant-proxy/main.go b/cmd/prometheus-multi-tenant-proxy/main.go index 9420cb0..0ab3965 100644 --- a/cmd/prometheus-multi-tenant-proxy/main.go +++ b/cmd/prometheus-multi-tenant-proxy/main.go @@ -40,6 +40,10 @@ func main() { Name: "auth-config", Usage: "AuthN yaml configuration file path", Value: "authn.yaml", + }, &cli.IntFlag{ + Name: "reload-interval", + Usage: "Interval time to reload the authn configuration file (minutes)", + Value: 5, }, }, }, diff --git a/deployments/kubernetes.yaml b/deployments/kubernetes.yaml index 2d646ec..3aa0b5e 100644 --- a/deployments/kubernetes.yaml +++ b/deployments/kubernetes.yaml @@ -13,8 +13,15 @@ metadata: application: prometheus-multi-tenant-proxy name: prometheus-auth-config namespace: default -data: - authn.yaml: dXNlcnM6CiAgLSB1c2VybmFtZTogSGFwcHkKICAgIHBhc3N3b3JkOiBQcm9tZXRoZXVzCiAgICBuYW1lc3BhY2U6IGRlZmF1bHQKICAtIHVzZXJuYW1lOiBTYWQKICAgIHBhc3N3b3JkOiBQcm9tZXRoZXVzCiAgICBuYW1lc3BhY2U6IGt1YmUtc3lzdGVtCg== +stringData: + authn.yaml: | + users: + - username: Happy + password: Prometheus + namespace: default + - username: Sad + password: Prometheus + namespace: kube-system --- apiVersion: apps/v1 kind: Deployment @@ -43,7 +50,7 @@ spec: image: ghcr.io/k8spin/prometheus-multi-tenant-proxy:latest imagePullPolicy: Always command: ["/bin/bash"] - args: ["-c", "/prometheus-multi-tenant-proxy run --port=9092 --prometheus-endpoint=${PROMETHEUS_MULTI_TENANT_PROXY_PROMETHEUS_ENDPOINT} --auth-config=/etc/prometheus-auth-config/authn.yaml"] + args: ["-c", "/prometheus-multi-tenant-proxy run --port=9092 --prometheus-endpoint=${PROMETHEUS_MULTI_TENANT_PROXY_PROMETHEUS_ENDPOINT} --auth-config=/etc/prometheus-auth-config/authn.yaml --reload-interval=5"] ports: - name: http containerPort: 9092 diff --git a/internal/app/prometheus-multi-tenant-proxy/auth.go b/internal/app/prometheus-multi-tenant-proxy/auth.go index 1afd2ff..aa8e441 100644 --- a/internal/app/prometheus-multi-tenant-proxy/auth.go +++ b/internal/app/prometheus-multi-tenant-proxy/auth.go @@ -4,8 +4,6 @@ import ( "context" "crypto/subtle" "net/http" - - "github.com/k8spin/prometheus-multi-tenant-proxy/internal/pkg" ) type key int @@ -17,10 +15,10 @@ const ( ) // BasicAuth can be used as a middleware chain to authenticate users before proxying a request -func BasicAuth(handler http.HandlerFunc, authConfig *pkg.Authn) http.HandlerFunc { +func BasicAuth(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() - authorized, namespace := isAuthorized(user, pass, authConfig) + authorized, namespace := isAuthorized(user, pass) if !ok || !authorized { writeUnauthorisedResponse(w) return @@ -30,7 +28,8 @@ func BasicAuth(handler http.HandlerFunc, authConfig *pkg.Authn) http.HandlerFunc } } -func isAuthorized(user string, pass string, authConfig *pkg.Authn) (bool, string) { +func isAuthorized(user string, pass string) (bool, string) { + authConfig := GetConfig() for _, v := range authConfig.Users { if subtle.ConstantTimeCompare([]byte(user), []byte(v.Username)) == 1 && subtle.ConstantTimeCompare([]byte(pass), []byte(v.Password)) == 1 { return true, v.Namespace diff --git a/internal/app/prometheus-multi-tenant-proxy/auth_test.go b/internal/app/prometheus-multi-tenant-proxy/auth_test.go index c7c7026..867dd53 100644 --- a/internal/app/prometheus-multi-tenant-proxy/auth_test.go +++ b/internal/app/prometheus-multi-tenant-proxy/auth_test.go @@ -1,13 +1,14 @@ package proxy import ( + "sync" "testing" "github.com/k8spin/prometheus-multi-tenant-proxy/internal/pkg" ) -func Test_isAuthorized(t *testing.T) { - authConfig := pkg.Authn{ +func init() { + config = &pkg.Authn{ Users: []pkg.User{ { Username: "User-a", @@ -21,10 +22,13 @@ func Test_isAuthorized(t *testing.T) { }, }, } + configLock = new(sync.RWMutex) +} + +func Test_isAuthorized(t *testing.T) { type args struct { - user string - pass string - authConfig *pkg.Authn + user string + pass string } tests := []struct { name string @@ -37,7 +41,6 @@ func Test_isAuthorized(t *testing.T) { args{ "User-a", "pass-a", - &authConfig, }, true, "tenant-a", @@ -46,7 +49,6 @@ func Test_isAuthorized(t *testing.T) { args{ "invalid", "pass-a", - &authConfig, }, false, "", @@ -54,7 +56,7 @@ func Test_isAuthorized(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1 := isAuthorized(tt.args.user, tt.args.pass, tt.args.authConfig) + got, got1 := isAuthorized(tt.args.user, tt.args.pass) if got != tt.want { t.Errorf("isAuthorized() got = %v, want %v", got, tt.want) } diff --git a/internal/app/prometheus-multi-tenant-proxy/server.go b/internal/app/prometheus-multi-tenant-proxy/server.go index dd76daa..7cf8746 100644 --- a/internal/app/prometheus-multi-tenant-proxy/server.go +++ b/internal/app/prometheus-multi-tenant-proxy/server.go @@ -6,17 +6,41 @@ import ( "net/http" "net/http/httputil" "net/url" + "os" + "sync" + "time" "github.com/k8spin/prometheus-multi-tenant-proxy/internal/pkg" "github.com/urfave/cli/v2" ) +var ( + config *pkg.Authn + configLock = new(sync.RWMutex) +) + // Serve serves func Serve(c *cli.Context) error { prometheusServerURL, _ := url.Parse(c.String("prometheus-endpoint")) serveAt := fmt.Sprintf(":%d", c.Int("port")) authConfigLocation := c.String("auth-config") - authConfig, _ := pkg.ParseConfig(&authConfigLocation) + reloadInterval := c.Int("reload-interval") + + loadConfig(authConfigLocation) + ticker := time.NewTicker(time.Duration(reloadInterval) * time.Minute) + quit := make(chan struct{}) + go func() { + for { + select { + case <-ticker.C: + loadConfig(authConfigLocation) + log.Printf("Reloaded config file %s", authConfigLocation) + case <-quit: + ticker.Stop() + return + } + } + }() rprt := ReversePrometheusRoundTripper{ prometheusServerURL: prometheusServerURL, @@ -29,10 +53,27 @@ func Serve(c *cli.Context) error { http.HandleFunc("/-/healthy", LogRequest(reverseProxy.ServeHTTP)) http.HandleFunc("/-/ready", LogRequest(reverseProxy.ServeHTTP)) - http.HandleFunc("/", LogRequest(BasicAuth(reverseProxy.ServeHTTP, authConfig))) + http.HandleFunc("/", LogRequest(BasicAuth(reverseProxy.ServeHTTP))) if err := http.ListenAndServe(serveAt, nil); err != nil { log.Fatalf("Prometheus multi tenant proxy can not start %v", err) return err } return nil } + +func loadConfig(location string) { + temp, err := pkg.ParseConfig(&location) + if err != nil { + log.Fatalf("Could not parse config file %s: %v", location, err) + os.Exit(1) + } + configLock.Lock() + config = temp + configLock.Unlock() +} + +func GetConfig() *pkg.Authn { + configLock.RLock() + defer configLock.RUnlock() + return config +}