From c887d6d01084dcae63d69e0f04d9dbdf06e57a80 Mon Sep 17 00:00:00 2001 From: Angel Barrera Sanchez Date: Fri, 7 Feb 2020 20:40:57 +0100 Subject: [PATCH] Initial commit --- .gitignore | 15 ++++ README.md | 0 build/package/Dockerfile | 24 +++++ cmd/prometheus-multi-tenant-proxy/main.go | 45 ++++++++++ configs/bad.yaml | 4 + configs/multiple.user.yaml | 7 ++ configs/sample.yaml | 7 ++ deployment.yaml | 74 ++++++++++++++++ go.mod | 8 ++ go.sum | 6 ++ .../app/prometheus-multi-tenant-proxy/auth.go | 46 ++++++++++ .../auth_test.go | 66 ++++++++++++++ .../prometheus-multi-tenant-proxy/logging.go | 14 +++ .../prometheus-multi-tenant-proxy/reverse.go | 28 ++++++ .../prometheus-multi-tenant-proxy/server.go | 32 +++++++ internal/pkg/config.go | 33 +++++++ internal/pkg/config_test.go | 87 +++++++++++++++++++ 17 files changed, 496 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build/package/Dockerfile create mode 100644 cmd/prometheus-multi-tenant-proxy/main.go create mode 100644 configs/bad.yaml create mode 100644 configs/multiple.user.yaml create mode 100644 configs/sample.yaml create mode 100644 deployment.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/prometheus-multi-tenant-proxy/auth.go create mode 100644 internal/app/prometheus-multi-tenant-proxy/auth_test.go create mode 100644 internal/app/prometheus-multi-tenant-proxy/logging.go create mode 100644 internal/app/prometheus-multi-tenant-proxy/reverse.go create mode 100644 internal/app/prometheus-multi-tenant-proxy/server.go create mode 100644 internal/pkg/config.go create mode 100644 internal/pkg/config_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1db6f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Binary +cmd/prometheus-multi-tenant-proxy/prometheus-multi-tenant-proxy \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build/package/Dockerfile b/build/package/Dockerfile new file mode 100644 index 0000000..f0da9b7 --- /dev/null +++ b/build/package/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.12-alpine as builder + +ENV GO111MODULE=on +ENV CGO_ENABLED=0 + +RUN apk add -U --no-cache git ca-certificates && \ + mkdir -p src/github.com/angelbarrera92/prometheus-multi-tenant-proxy + +WORKDIR /go/src/github.com/angelbarrera92/prometheus-multi-tenant-proxy + +COPY go.mod go.mod +COPY go.sum go.sum +COPY cmd cmd +COPY internal internal + +RUN cd cmd/prometheus-multi-tenant-proxy && \ + go build . + +FROM scratch + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /go/src/github.com/angelbarrera92/prometheus-multi-tenant-proxy/cmd/prometheus-multi-tenant-proxy/prometheus-multi-tenant-proxy /prometheus-multi-tenant-proxy + +ENTRYPOINT [ "/prometheus-multi-tenant-proxy" ] \ No newline at end of file diff --git a/cmd/prometheus-multi-tenant-proxy/main.go b/cmd/prometheus-multi-tenant-proxy/main.go new file mode 100644 index 0000000..910d36f --- /dev/null +++ b/cmd/prometheus-multi-tenant-proxy/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "os" + + "github.com/angelbarrera92/prometheus-multi-tenant-proxy/internal/app/prometheus-multi-tenant-proxy" + "github.com/urfave/cli" +) + +var ( + version = "dev" + commit = "none" + date = "unknown" +) + +func main() { + app := cli.NewApp() + app.Name = "Prometheus Multitenant Proxy" + app.Usage = "Makes your Prometheus server multi tenant" + app.Version = version + app.Author = "Ángel Barrera - @angelbarrera92" + app.Commands = []cli.Command{ + { + Name: "run", + Usage: "Runs the Prometheus multi tenant proxy", + Action: proxy.Serve, + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "port", + Usage: "Port to expose this prometheus proxy", + Value: 9092, + }, cli.StringFlag{ + Name: "prometheus-label-proxy-endpoint", + Usage: "Prometheus Label Proxy", + Value: "http://localhost:9091", + }, cli.StringFlag{ + Name: "auth-config", + Usage: "AuthN yaml configuration file path", + Value: "authn.yaml", + }, + }, + }, + } + app.Run(os.Args) +} diff --git a/configs/bad.yaml b/configs/bad.yaml new file mode 100644 index 0000000..b9aa378 --- /dev/null +++ b/configs/bad.yaml @@ -0,0 +1,4 @@ +users + - username: User + password: Prom + namespace: tenant-1 diff --git a/configs/multiple.user.yaml b/configs/multiple.user.yaml new file mode 100644 index 0000000..89607f9 --- /dev/null +++ b/configs/multiple.user.yaml @@ -0,0 +1,7 @@ +users: + - username: User-a + password: pass-a + namespace: tenant-a + - username: User-b + password: pass-b + namespace: tenant-b \ No newline at end of file diff --git a/configs/sample.yaml b/configs/sample.yaml new file mode 100644 index 0000000..1a5d614 --- /dev/null +++ b/configs/sample.yaml @@ -0,0 +1,7 @@ +users: + - username: Happy + password: Prometheus + namespace: default + - username: Sad + password: Prometheus + namespace: kube-system diff --git a/deployment.yaml b/deployment.yaml new file mode 100644 index 0000000..4286fec --- /dev/null +++ b/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: v1 +data: + authn.yaml: dXNlcnM6CiAgLSB1c2VybmFtZTogSGFwcHkKICAgIHBhc3N3b3JkOiBQcm9tZXRoZXVzCiAgICBuYW1lc3BhY2U6IGRlZmF1bHQKICAtIHVzZXJuYW1lOiBTYWQKICAgIHBhc3N3b3JkOiBQcm9tZXRoZXVzCiAgICBuYW1lc3BhY2U6IGt1YmUtc3lzdGVtCg== +kind: Secret +metadata: + labels: + application: prometheus-multitenant-proxy + name: prometheus-auth-config + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + application: prometheus-multitenant-proxy + name: prometheus-multitenant-proxy + namespace: default +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + application: prometheus-multitenant-proxy + template: + metadata: + labels: + application: prometheus-multitenant-proxy + spec: + volumes: + - name: prometheus-auth-config + secret: + secretName: prometheus-auth-config + containers: + - name: prometheus-multitenant-proxy + image: angelbarrera92/prometheus-multi-tenant-proxy:dev + imagePullPolicy: Always + args: + - "run" + - "--port=9092" + - "--prometheus-label-proxy-endpoint=http://127.0.0.1:9091" + - "--auth-config=/etc/prometheus-auth-config/authn.yaml" + ports: + - name: http + containerPort: 9092 + protocol: TCP + volumeMounts: + - name: prometheus-auth-config + mountPath: /etc/prometheus-auth-config + - name: prometheus-label-proxy + image: angelbarrera92/prom-label-proxy:dev + imagePullPolicy: Always + args: + - "--insecure-listen-address=127.0.0.1:9091" + - "--label=namespace" + - "--upstream=http://prometheus-operated.default.svc.cluster.local:9090" +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus-multitenant-proxy + namespace: default + labels: + application: prometheus-multitenant-proxy +spec: + type: ClusterIP + ports: + - name: http + port: 9092 + protocol: TCP + targetPort: http + selector: + application: prometheus-multitenant-proxy + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..53cdbed --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/angelbarrera92/prometheus-multi-tenant-proxy + +go 1.12 + +require ( + github.com/urfave/cli v1.21.0 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..49037c5 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE= +github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/app/prometheus-multi-tenant-proxy/auth.go b/internal/app/prometheus-multi-tenant-proxy/auth.go new file mode 100644 index 0000000..76770fa --- /dev/null +++ b/internal/app/prometheus-multi-tenant-proxy/auth.go @@ -0,0 +1,46 @@ +package proxy + +import ( + "context" + "crypto/subtle" + "net/http" + + "github.com/angelbarrera92/prometheus-multi-tenant-proxy/internal/pkg" +) + +type key int + +const ( + //Namespace Key used to pass prometheus tenant id though the middleware context + Namespace key = iota + realm = "Prometheus multi-tenant proxy" +) + +// 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 { + return func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + authorized, namespace := isAuthorized(user, pass, authConfig) + if !ok || !authorized { + writeUnauthorisedResponse(w) + return + } + ctx := context.WithValue(r.Context(), Namespace, namespace) + handler(w, r.WithContext(ctx)) + } +} + +func isAuthorized(user string, pass string, authConfig *pkg.Authn) (bool, string) { + 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 + } + } + return false, "" +} + +func writeUnauthorisedResponse(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + w.WriteHeader(401) + w.Write([]byte("Unauthorised\n")) +} diff --git a/internal/app/prometheus-multi-tenant-proxy/auth_test.go b/internal/app/prometheus-multi-tenant-proxy/auth_test.go new file mode 100644 index 0000000..9d5e82d --- /dev/null +++ b/internal/app/prometheus-multi-tenant-proxy/auth_test.go @@ -0,0 +1,66 @@ +package proxy + +import ( + "testing" + + "github.com/angelbarrera92/prometheus-multi-tenant-proxy/internal/pkg" +) + +func Test_isAuthorized(t *testing.T) { + authConfig := pkg.Authn{ + []pkg.User{ + pkg.User{ + "User-a", + "pass-a", + "tenant-a", + }, + pkg.User{ + "User-b", + "pass-b", + "tenant-b", + }, + }, + } + type args struct { + user string + pass string + authConfig *pkg.Authn + } + tests := []struct { + name string + args args + want bool + want1 string + }{ + { + "Valid User", + args{ + "User-a", + "pass-a", + &authConfig, + }, + true, + "tenant-a", + }, { + "Invalid User", + args{ + "invalid", + "pass-a", + &authConfig, + }, + false, + "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := isAuthorized(tt.args.user, tt.args.pass, tt.args.authConfig) + if got != tt.want { + t.Errorf("isAuthorized() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("isAuthorized() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/internal/app/prometheus-multi-tenant-proxy/logging.go b/internal/app/prometheus-multi-tenant-proxy/logging.go new file mode 100644 index 0000000..b293e8c --- /dev/null +++ b/internal/app/prometheus-multi-tenant-proxy/logging.go @@ -0,0 +1,14 @@ +package proxy + +import ( + "log" + "net/http" +) + +// LogRequest can be used as a middleware chain to log every request before proxying the request +func LogRequest(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + handler(w, r) + } +} diff --git a/internal/app/prometheus-multi-tenant-proxy/reverse.go b/internal/app/prometheus-multi-tenant-proxy/reverse.go new file mode 100644 index 0000000..90926a4 --- /dev/null +++ b/internal/app/prometheus-multi-tenant-proxy/reverse.go @@ -0,0 +1,28 @@ +package proxy + +import ( + "net/http" + "net/http/httputil" + "net/url" + "log" +) + +// ReversePrometheus a +func ReversePrometheus(reverseProxy *httputil.ReverseProxy, prometheusLabelProxyServerURL *url.URL) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + modifyRequest(r, prometheusLabelProxyServerURL) + reverseProxy.ServeHTTP(w, r) + log.Printf("[PROXY]\t%+v\n", r.URL) + } +} + +func modifyRequest(r *http.Request, prometheusLabelProxyServerURL *url.URL) { + r.URL.Scheme = prometheusLabelProxyServerURL.Scheme + r.URL.Host = prometheusLabelProxyServerURL.Host + r.Host = prometheusLabelProxyServerURL.Host + namespace := r.Context().Value(Namespace) + r.Header.Set("X-Forwarded-Host", r.Host) + q := r.URL.Query() + q.Add("namespace", namespace.(string)) + r.URL.RawQuery = q.Encode() +} diff --git a/internal/app/prometheus-multi-tenant-proxy/server.go b/internal/app/prometheus-multi-tenant-proxy/server.go new file mode 100644 index 0000000..6a25063 --- /dev/null +++ b/internal/app/prometheus-multi-tenant-proxy/server.go @@ -0,0 +1,32 @@ +package proxy + +import ( + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/angelbarrera92/prometheus-multi-tenant-proxy/internal/pkg" + "github.com/urfave/cli" +) + +// Serve serves +func Serve(c *cli.Context) error { + prometheusLabelProxyServerURL, _ := url.Parse(c.String("prometheus-label-proxy-endpoint")) + serveAt := fmt.Sprintf(":%d", c.Int("port")) + authConfigLocation := c.String("auth-config") + authConfig, _ := pkg.ParseConfig(&authConfigLocation) + + http.HandleFunc("/", createHandler(prometheusLabelProxyServerURL, authConfig)) + if err := http.ListenAndServe(serveAt, nil); err != nil { + log.Fatalf("Prometheus multi tenant proxy can not start %v", err) + return err + } + return nil +} + +func createHandler(prometheusLabelProxyServerURL *url.URL, authConfig *pkg.Authn) http.HandlerFunc { + reverseProxy := httputil.NewSingleHostReverseProxy(prometheusLabelProxyServerURL) + return LogRequest(BasicAuth(ReversePrometheus(reverseProxy, prometheusLabelProxyServerURL), authConfig)) +} diff --git a/internal/pkg/config.go b/internal/pkg/config.go new file mode 100644 index 0000000..9cab5fd --- /dev/null +++ b/internal/pkg/config.go @@ -0,0 +1,33 @@ +package pkg + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// Authn Contains a list of users +type Authn struct { + Users []User `yaml:"users"` +} + +// User Identifies a user including the tenant +type User struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + Namespace string `yaml:"namespace"` +} + +// ParseConfig read a configuration file in the path `location` and returns an Authn object +func ParseConfig(location *string) (*Authn, error) { + data, err := ioutil.ReadFile(*location) + if err != nil { + return nil, err + } + authn := Authn{} + err = yaml.Unmarshal([]byte(data), &authn) + if err != nil { + return nil, err + } + return &authn, nil +} diff --git a/internal/pkg/config_test.go b/internal/pkg/config_test.go new file mode 100644 index 0000000..eec19c1 --- /dev/null +++ b/internal/pkg/config_test.go @@ -0,0 +1,87 @@ +package pkg + +import ( + "reflect" + "testing" +) + +func TestParseConfig(t *testing.T) { + configInvalidLocation := "../../configs/no.config.yaml" + configInvalidConfigFileLocation := "../../configs/bad.yaml" + configSampleLocation := "../../configs/sample.yaml" + configMultipleUserLocation := "../../configs/multiple.user.yaml" + expectedSampleAuth := Authn{ + []User{ + User{ + "Happy", + "Prometheus", + "tenant-1", + }, + }, + } + expectedMultipleUserAuth := Authn{ + []User{ + User{ + "User-a", + "pass-a", + "tenant-a", + }, + User{ + "User-b", + "pass-b", + "tenant-b", + }, + }, + } + type args struct { + location *string + } + tests := []struct { + name string + args args + want *Authn + wantErr bool + }{ + { + "Basic", + args{ + &configSampleLocation, + }, + &expectedSampleAuth, + false, + }, { + "Multiples users", + args{ + &configMultipleUserLocation, + }, + &expectedMultipleUserAuth, + false, + }, { + "Invalid location", + args{ + &configInvalidLocation, + }, + nil, + true, + }, { + "Invalid yaml file", + args{ + &configInvalidConfigFileLocation, + }, + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseConfig(tt.args.location) + if (err != nil) != tt.wantErr { + t.Errorf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseConfig() = %v, want %v", got, tt.want) + } + }) + } +}