From cc751b7966f66a60512609c7497379d32bc2847b Mon Sep 17 00:00:00 2001 From: Joe Adams Date: Fri, 4 Mar 2022 16:39:48 -0500 Subject: [PATCH] Add config module The config module supports adding configuration to the exporter via a config file. This supports adding authentication details in a config file so that /probe requests can specify authentication for endpoints Signed-off-by: Joe Adams --- cmd/postgres_exporter/main.go | 11 ++ cmd/postgres_exporter/probe.go | 34 +++++- config/config.go | 126 ++++++++++++++++++++ config/config_test.go | 58 +++++++++ config/testdata/config-bad-auth-module.yaml | 7 ++ config/testdata/config-bad-extra-field.yaml | 8 ++ config/testdata/config-good.yaml | 8 ++ go.mod | 1 + go.sum | 2 + 9 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/testdata/config-bad-auth-module.yaml create mode 100644 config/testdata/config-bad-extra-field.yaml create mode 100644 config/testdata/config-good.yaml diff --git a/cmd/postgres_exporter/main.go b/cmd/postgres_exporter/main.go index 2ec1bf26..aee32503 100644 --- a/cmd/postgres_exporter/main.go +++ b/cmd/postgres_exporter/main.go @@ -20,6 +20,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus-community/postgres_exporter/collector" + "github.com/prometheus-community/postgres_exporter/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/promlog" @@ -31,6 +32,11 @@ import ( ) var ( + c = config.ConfigHandler{ + Config: &config.Config{}, + } + + configFile = kingpin.Flag("config.file", "Promehteus exporter configuration file.").Default("postres_exporter.yml").String() listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").Envar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String() webConfig = webflag.AddFlags(kingpin.CommandLine) metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").Envar("PG_EXPORTER_WEB_TELEMETRY_PATH").String() @@ -85,6 +91,11 @@ func main() { return } + if err := c.ReloadConfig(*configFile, logger); err != nil { + // This is not fatal, but it means that auth must be provided for every dsn. + level.Error(logger).Log("msg", "Error loading config", "err", err) + } + dsns, err := getDataSources() if err != nil { level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error()) diff --git a/cmd/postgres_exporter/probe.go b/cmd/postgres_exporter/probe.go index c23777b4..813f4ea8 100644 --- a/cmd/postgres_exporter/probe.go +++ b/cmd/postgres_exporter/probe.go @@ -14,11 +14,14 @@ package main import ( + "fmt" "net/http" "time" "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/prometheus-community/postgres_exporter/collector" + "github.com/prometheus-community/postgres_exporter/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -26,15 +29,38 @@ import ( func handleProbe(logger log.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + conf := c.GetConfig() params := r.URL.Query() target := params.Get("target") if target == "" { http.Error(w, "target is required", http.StatusBadRequest) return } + var authModule config.AuthModule + authModuleName := params.Get("auth_module") + if authModuleName == "" { + level.Info(logger).Log("msg", "no auth_module specified, using default") + } else { + var ok bool + authModule, ok = conf.AuthModules[authModuleName] + if !ok { + http.Error(w, fmt.Sprintf("auth_module %s not found", authModuleName), http.StatusBadRequest) + return + } + if authModule.UserPass.Username == "" || authModule.UserPass.Password == "" { + http.Error(w, fmt.Sprintf("auth_module %s has no username or password", authModuleName), http.StatusBadRequest) + return + } + } + + dsn, err := authModule.ConfigureTarget(target) + if err != nil { + level.Error(logger).Log("msg", "failed to configure target", "err", err) + http.Error(w, fmt.Sprintf("could not configure dsn for target: %v", err), http.StatusBadRequest) + return + } // TODO: Timeout - // TODO: Auth Module probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_success", @@ -46,18 +72,14 @@ func handleProbe(logger log.Logger) http.HandlerFunc { }) tl := log.With(logger, "target", target) - _ = tl start := time.Now() registry := prometheus.NewRegistry() registry.MustRegister(probeSuccessGauge) registry.MustRegister(probeDurationGauge) - // TODO(@sysadmind): this is a temp hack until we have a proper auth module - target = "postgres://postgres:test@localhost:5432/circle_test?sslmode=disable" - // Run the probe - pc, err := collector.NewProbeCollector(tl, registry, target) + pc, err := collector.NewProbeCollector(tl, registry, dsn) if err != nil { probeSuccessGauge.Set(0) probeDurationGauge.Set(time.Since(start).Seconds()) diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..49a2dbd6 --- /dev/null +++ b/config/config.go @@ -0,0 +1,126 @@ +// Copyright 2022 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "net/url" + "os" + "strings" + "sync" + + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" + "gopkg.in/yaml.v3" +) + +var ( + configReloadSuccess = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "postgres_exporter", + Name: "config_last_reload_successful", + Help: "Postgres exporter config loaded successfully.", + }) + + configReloadSeconds = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "postgres_exporter", + Name: "config_last_reload_success_timestamp_seconds", + Help: "Timestamp of the last successful configuration reload.", + }) +) + +func init() { + prometheus.MustRegister(configReloadSuccess) + prometheus.MustRegister(configReloadSeconds) +} + +type Config struct { + AuthModules map[string]AuthModule `yaml:"auth_modules"` +} + +type AuthModule struct { + Type string `yaml:"type"` + UserPass UserPass `yaml:"userpass,omitempty"` + // Add alternative auth modules here + Options map[string]string `yaml:"options"` +} + +type UserPass struct { + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +type ConfigHandler struct { + sync.RWMutex + Config *Config +} + +func (ch *ConfigHandler) GetConfig() *Config { + ch.RLock() + defer ch.RUnlock() + return ch.Config +} + +func (ch *ConfigHandler) ReloadConfig(f string, logger log.Logger) error { + config := &Config{} + var err error + defer func() { + if err != nil { + configReloadSuccess.Set(0) + } else { + configReloadSuccess.Set(1) + configReloadSeconds.SetToCurrentTime() + } + }() + + yamlReader, err := os.Open(f) + if err != nil { + return fmt.Errorf("Error opening config file %q: %s", f, err) + } + defer yamlReader.Close() + decoder := yaml.NewDecoder(yamlReader) + decoder.KnownFields(true) + + if err = decoder.Decode(config); err != nil { + return fmt.Errorf("Error parsing config file %q: %s", f, err) + } + + ch.Lock() + ch.Config = config + ch.Unlock() + return nil +} + +func (m AuthModule) ConfigureTarget(target string) (string, error) { + // ip:port urls do not parse properly and that is the typical way users interact with postgres + t := fmt.Sprintf("exporter://%s", target) + u, err := url.Parse(t) + if err != nil { + return "", err + } + + if m.Type == "userpass" { + u.User = url.UserPassword(m.UserPass.Username, m.UserPass.Password) + } + + query := u.Query() + for k, v := range m.Options { + query.Set(k, v) + } + u.RawQuery = query.Encode() + + parsed := u.String() + trim := strings.TrimPrefix(parsed, "exporter://") + + return trim, nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..63b932ad --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,58 @@ +// Copyright 2022 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "testing" +) + +func TestLoadConfig(t *testing.T) { + ch := &ConfigHandler{ + Config: &Config{}, + } + + err := ch.ReloadConfig("testdata/config-good.yaml", nil) + if err != nil { + t.Errorf("Error loading config: %s", err) + } +} + +func TestLoadBadConfigs(t *testing.T) { + ch := &ConfigHandler{ + Config: &Config{}, + } + + tests := []struct { + input string + want string + }{ + { + input: "testdata/config-bad-auth-module.yaml", + want: "Error parsing config file \"testdata/config-bad-auth-module.yaml\": yaml: unmarshal errors:\n line 3: field pretendauth not found in type config.AuthModule", + }, + { + input: "testdata/config-bad-extra-field.yaml", + want: "Error parsing config file \"testdata/config-bad-extra-field.yaml\": yaml: unmarshal errors:\n line 8: field doesNotExist not found in type config.AuthModule", + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + got := ch.ReloadConfig(test.input, nil) + if got == nil || got.Error() != test.want { + t.Fatalf("ReloadConfig(%q) = %v, want %s", test.input, got, test.want) + } + }) + } +} diff --git a/config/testdata/config-bad-auth-module.yaml b/config/testdata/config-bad-auth-module.yaml new file mode 100644 index 00000000..8f718dd5 --- /dev/null +++ b/config/testdata/config-bad-auth-module.yaml @@ -0,0 +1,7 @@ +auth_modules: + foo: + pretendauth: + username: test + password: pass + options: + extra: "1" diff --git a/config/testdata/config-bad-extra-field.yaml b/config/testdata/config-bad-extra-field.yaml new file mode 100644 index 00000000..f6ff6d6c --- /dev/null +++ b/config/testdata/config-bad-extra-field.yaml @@ -0,0 +1,8 @@ +auth_modules: + foo: + userpass: + username: test + password: pass + options: + extra: "1" + doesNotExist: test diff --git a/config/testdata/config-good.yaml b/config/testdata/config-good.yaml new file mode 100644 index 00000000..13453e26 --- /dev/null +++ b/config/testdata/config-good.yaml @@ -0,0 +1,8 @@ +auth_modules: + first: + type: userpass + userpass: + username: first + password: firstpass + options: + sslmode: disable diff --git a/go.mod b/go.mod index f27c896b..f2ccd64e 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) require ( diff --git a/go.sum b/go.sum index 3f9003a2..1ad1ca5b 100644 --- a/go.sum +++ b/go.sum @@ -494,6 +494,8 @@ gopkg.in/yaml.v2 v2.2.5/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= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=