From 1c96fbb9928fcac94b01b08373dc3567fae8b154 Mon Sep 17 00:00:00 2001 From: Max Leonard Inden Date: Thu, 11 May 2017 17:09:24 +0200 Subject: [PATCH] Expose current Prometheus config via /status/config This PR adds the `/status/config` endpoint which exposes the currently loaded Prometheus config. This is the same config that is displayed on `/config` in the UI in YAML format. The response payload looks like such: ``` { "status": "success", "data": { "yaml": } } ``` --- config/config_test.go | 24 +++++++++++++++----- web/api/v1/api.go | 22 +++++++++++++++--- web/api/v1/api_test.go | 28 ++++++++++++++++++++--- web/federate.go | 2 +- web/federate_test.go | 6 ++++- web/web.go | 51 +++++++++++++++++++++++++----------------- 6 files changed, 98 insertions(+), 35 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 506d5a8d0..7e43c447f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -540,15 +540,27 @@ func TestLoadConfig(t *testing.T) { if !reflect.DeepEqual(c, expectedConf) { t.Fatalf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.good.yml", bgot, bexp) } +} - // String method must not reveal authentication credentials. - s := c.String() - secretRe := regexp.MustCompile("") - matches := secretRe.FindAllStringIndex(s, -1) - if len(matches) != 6 || strings.Contains(s, "mysecret") { - t.Fatalf("config's String method reveals authentication credentials.") +// YAML marshalling must not reveal authentication credentials. +func TestElideSecrets(t *testing.T) { + c, err := LoadFile("testdata/conf.good.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.good.yml", err) } + secretRe := regexp.MustCompile(`\\u003csecret\\u003e|`) + + config, err := yaml.Marshal(c) + if err != nil { + t.Fatal(err) + } + yamlConfig := string(config) + + matches := secretRe.FindAllStringIndex(yamlConfig, -1) + if len(matches) != 6 || strings.Contains(yamlConfig, "mysecret") { + t.Fatalf("yaml marshal reveals authentication credentials.") + } } func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) { diff --git a/web/api/v1/api.go b/web/api/v1/api.go index e4f493154..af80b9f66 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -29,6 +29,7 @@ import ( "github.com/prometheus/common/route" "golang.org/x/net/context" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/retrieval" "github.com/prometheus/prometheus/storage/local" @@ -103,17 +104,19 @@ type API struct { targetRetriever targetRetriever alertmanagerRetriever alertmanagerRetriever - now func() model.Time + now func() model.Time + config func() config.Config } // NewAPI returns an initialized API type. -func NewAPI(qe *promql.Engine, st local.Storage, tr targetRetriever, ar alertmanagerRetriever) *API { +func NewAPI(qe *promql.Engine, st local.Storage, tr targetRetriever, ar alertmanagerRetriever, configFunc func() config.Config) *API { return &API{ QueryEngine: qe, Storage: st, targetRetriever: tr, alertmanagerRetriever: ar, - now: model.Now, + now: model.Now, + config: configFunc, } } @@ -147,6 +150,8 @@ func (api *API) Register(r *route.Router) { r.Get("/targets", instr("targets", api.targets)) r.Get("/alertmanagers", instr("alertmanagers", api.alertmanagers)) + + r.Get("/status/config", instr("config", api.serveConfig)) } type queryData struct { @@ -436,6 +441,17 @@ func (api *API) alertmanagers(r *http.Request) (interface{}, *apiError) { return ams, nil } +type prometheusConfig struct { + YAML string `json:"yaml"` +} + +func (api *API) serveConfig(r *http.Request) (interface{}, *apiError) { + cfg := &prometheusConfig{ + YAML: api.config().String(), + } + return cfg, nil +} + func respond(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 1062f2794..370795d26 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -29,6 +29,7 @@ import ( "github.com/prometheus/common/route" "golang.org/x/net/context" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/retrieval" ) @@ -45,6 +46,15 @@ func (f alertmanagerRetrieverFunc) Alertmanagers() []*url.URL { return f() } +var samplePrometheusCfg = config.Config{ + GlobalConfig: config.GlobalConfig{}, + AlertingConfig: config.AlertingConfig{}, + RuleFiles: []string{}, + ScrapeConfigs: []*config.ScrapeConfig{}, + RemoteWriteConfigs: []*config.RemoteWriteConfig{}, + RemoteReadConfigs: []*config.RemoteReadConfig{}, +} + func TestEndpoints(t *testing.T) { suite, err := promql.NewTest(t, ` load 1m @@ -91,6 +101,9 @@ func TestEndpoints(t *testing.T) { targetRetriever: tr, alertmanagerRetriever: ar, now: func() model.Time { return now }, + config: func() config.Config { + return samplePrometheusCfg + }, } start := model.Time(0) @@ -445,7 +458,8 @@ func TestEndpoints(t *testing.T) { "foo": "bar", }, }, - }, { + }, + { endpoint: api.dropSeries, query: url.Values{ "match[]": []string{`{__name__=~".+"}`}, @@ -453,7 +467,8 @@ func TestEndpoints(t *testing.T) { response: struct { NumDeleted int `json:"numDeleted"` }{2}, - }, { + }, + { endpoint: api.targets, response: &TargetDiscovery{ ActiveTargets: []*Target{ @@ -465,7 +480,8 @@ func TestEndpoints(t *testing.T) { }, }, }, - }, { + }, + { endpoint: api.alertmanagers, response: &AlertmanagerDiscovery{ ActiveAlertmanagers: []*AlertmanagerTarget{ @@ -475,6 +491,12 @@ func TestEndpoints(t *testing.T) { }, }, }, + { + endpoint: api.serveConfig, + response: &prometheusConfig{ + YAML: samplePrometheusCfg.String(), + }, + }, } for _, test := range tests { diff --git a/web/federate.go b/web/federate.go index 1c5771bdf..2be9d3baf 100644 --- a/web/federate.go +++ b/web/federate.go @@ -74,7 +74,7 @@ func (h *Handler) federation(w http.ResponseWriter, req *http.Request) { } sort.Sort(byName(vector)) - externalLabels := h.externalLabels.Clone() + externalLabels := h.config.GlobalConfig.ExternalLabels.Clone() if _, ok := externalLabels[model.InstanceLabel]; !ok { externalLabels[model.InstanceLabel] = "" } diff --git a/web/federate_test.go b/web/federate_test.go index 0b0204309..df9d9e21d 100644 --- a/web/federate_test.go +++ b/web/federate_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/promql" ) @@ -178,10 +179,13 @@ func TestFederation(t *testing.T) { storage: suite.Storage(), queryEngine: suite.QueryEngine(), now: func() model.Time { return 101 * 60 * 1000 }, // 101min after epoch. + config: &config.Config{ + GlobalConfig: config.GlobalConfig{}, + }, } for name, scenario := range scenarios { - h.externalLabels = scenario.externalLabels + h.config.GlobalConfig.ExternalLabels = scenario.externalLabels req, err := http.ReadRequest(bufio.NewReader(strings.NewReader( "GET http://example.org/federate?" + scenario.params + " HTTP/1.0\r\n\r\n", ))) diff --git a/web/web.go b/web/web.go index 70663dadb..eeae72ba8 100644 --- a/web/web.go +++ b/web/web.go @@ -68,31 +68,29 @@ type Handler struct { apiV1 *api_v1.API - router *route.Router - listenErrCh chan error - quitCh chan struct{} - reloadCh chan chan error - options *Options - configString string - versionInfo *PrometheusVersion - birth time.Time - cwd string - flagsMap map[string]string + router *route.Router + listenErrCh chan error + quitCh chan struct{} + reloadCh chan chan error + options *Options + config *config.Config + versionInfo *PrometheusVersion + birth time.Time + cwd string + flagsMap map[string]string - externalLabels model.LabelSet - mtx sync.RWMutex - now func() model.Time + mtx sync.RWMutex + now func() model.Time ready uint32 // ready is uint32 rather than boolean to be able to use atomic functions. } -// ApplyConfig updates the status state as the new config requires. +// ApplyConfig updates the config field of the Handler struct func (h *Handler) ApplyConfig(conf *config.Config) error { h.mtx.Lock() defer h.mtx.Unlock() - h.externalLabels = conf.GlobalConfig.ExternalLabels - h.configString = conf.String() + h.config = conf return nil } @@ -158,12 +156,23 @@ func New(o *Options) *Handler { storage: o.Storage, notifier: o.Notifier, - apiV1: api_v1.NewAPI(o.QueryEngine, o.Storage, o.TargetManager, o.Notifier), - now: model.Now, + now: model.Now, ready: 0, } + h.apiV1 = api_v1.NewAPI( + o.QueryEngine, + o.Storage, + o.TargetManager, + o.Notifier, + func() config.Config { + h.mtx.RLock() + defer h.mtx.RUnlock() + return *h.config + }, + ) + if o.RoutePrefix != "/" { // If the prefix is missing for the root path, prepend it. router.Get("/", func(w http.ResponseWriter, r *http.Request) { @@ -184,7 +193,7 @@ func New(o *Options) *Handler { router.Get("/graph", readyf(instrf("graph", h.graph))) router.Get("/status", readyf(instrf("status", h.status))) router.Get("/flags", readyf(instrf("flags", h.flags))) - router.Get("/config", readyf(instrf("config", h.config))) + router.Get("/config", readyf(instrf("config", h.serveConfig))) router.Get("/rules", readyf(instrf("rules", h.rules))) router.Get("/targets", readyf(instrf("targets", h.targets))) router.Get("/version", readyf(instrf("version", h.version))) @@ -404,11 +413,11 @@ func (h *Handler) flags(w http.ResponseWriter, r *http.Request) { h.executeTemplate(w, "flags.html", h.flagsMap) } -func (h *Handler) config(w http.ResponseWriter, r *http.Request) { +func (h *Handler) serveConfig(w http.ResponseWriter, r *http.Request) { h.mtx.RLock() defer h.mtx.RUnlock() - h.executeTemplate(w, "config.html", h.configString) + h.executeTemplate(w, "config.html", h.config.String()) } func (h *Handler) rules(w http.ResponseWriter, r *http.Request) {