From 294b733cba92c30a77cf9a9e8c5dc65794424a0f Mon Sep 17 00:00:00 2001 From: hainenber Date: Mon, 20 Nov 2023 23:24:50 +0700 Subject: [PATCH] feat(exporter/elasticsearch): add Basic Auth support Signed-off-by: hainenber --- CHANGELOG.md | 4 ++ .../exporter/elasticsearch/elasticsearch.go | 37 +++++++++-------- .../elasticsearch/elasticsearch_test.go | 18 +++++++++ .../elasticsearch_exporter.go | 40 +++++++++++++++++-- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c54181de8e..5618f5c36146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ internal API changes are not present. Main (unreleased) ----------------- +### Features + +- Add support for Basic Auth-secured connection with Elasticsearch cluster using `prometheus.exporter.elasticsearch`. (@hainenber) + v0.38.0-rc.0 (2023-11-16) ------------------------- diff --git a/component/prometheus/exporter/elasticsearch/elasticsearch.go b/component/prometheus/exporter/elasticsearch/elasticsearch.go index 84904e6e3fee..efe3a3b41897 100644 --- a/component/prometheus/exporter/elasticsearch/elasticsearch.go +++ b/component/prometheus/exporter/elasticsearch/elasticsearch.go @@ -4,6 +4,7 @@ import ( "time" "github.com/grafana/agent/component" + commoncfg "github.com/grafana/agent/component/common/config" "github.com/grafana/agent/component/prometheus/exporter" "github.com/grafana/agent/pkg/integrations" "github.com/grafana/agent/pkg/integrations/elasticsearch_exporter" @@ -35,23 +36,24 @@ var DefaultArguments = Arguments{ } type Arguments struct { - Address string `river:"address,attr,optional"` - Timeout time.Duration `river:"timeout,attr,optional"` - AllNodes bool `river:"all,attr,optional"` - Node string `river:"node,attr,optional"` - ExportIndices bool `river:"indices,attr,optional"` - ExportIndicesSettings bool `river:"indices_settings,attr,optional"` - ExportClusterSettings bool `river:"cluster_settings,attr,optional"` - ExportShards bool `river:"shards,attr,optional"` - IncludeAliases bool `river:"aliases,attr,optional"` - ExportSnapshots bool `river:"snapshots,attr,optional"` - ExportClusterInfoInterval time.Duration `river:"clusterinfo_interval,attr,optional"` - CA string `river:"ca,attr,optional"` - ClientPrivateKey string `river:"client_private_key,attr,optional"` - ClientCert string `river:"client_cert,attr,optional"` - InsecureSkipVerify bool `river:"ssl_skip_verify,attr,optional"` - ExportDataStreams bool `river:"data_stream,attr,optional"` - ExportSLM bool `river:"slm,attr,optional"` + Address string `river:"address,attr,optional"` + Timeout time.Duration `river:"timeout,attr,optional"` + AllNodes bool `river:"all,attr,optional"` + Node string `river:"node,attr,optional"` + ExportIndices bool `river:"indices,attr,optional"` + ExportIndicesSettings bool `river:"indices_settings,attr,optional"` + ExportClusterSettings bool `river:"cluster_settings,attr,optional"` + ExportShards bool `river:"shards,attr,optional"` + IncludeAliases bool `river:"aliases,attr,optional"` + ExportSnapshots bool `river:"snapshots,attr,optional"` + ExportClusterInfoInterval time.Duration `river:"clusterinfo_interval,attr,optional"` + CA string `river:"ca,attr,optional"` + ClientPrivateKey string `river:"client_private_key,attr,optional"` + ClientCert string `river:"client_cert,attr,optional"` + InsecureSkipVerify bool `river:"ssl_skip_verify,attr,optional"` + ExportDataStreams bool `river:"data_stream,attr,optional"` + ExportSLM bool `river:"slm,attr,optional"` + BasicAuth *commoncfg.BasicAuth `river:"basic_auth,block,optional"` } // SetToDefault implements river.Defaulter. @@ -78,5 +80,6 @@ func (a *Arguments) Convert() *elasticsearch_exporter.Config { InsecureSkipVerify: a.InsecureSkipVerify, ExportDataStreams: a.ExportDataStreams, ExportSLM: a.ExportSLM, + BasicAuth: a.BasicAuth, } } diff --git a/component/prometheus/exporter/elasticsearch/elasticsearch_test.go b/component/prometheus/exporter/elasticsearch/elasticsearch_test.go index 3e87a5a98dc6..ae90dc9d3cd8 100644 --- a/component/prometheus/exporter/elasticsearch/elasticsearch_test.go +++ b/component/prometheus/exporter/elasticsearch/elasticsearch_test.go @@ -4,8 +4,10 @@ import ( "testing" "time" + commoncfg "github.com/grafana/agent/component/common/config" "github.com/grafana/agent/pkg/integrations/elasticsearch_exporter" "github.com/grafana/river" + "github.com/grafana/river/rivertypes" "github.com/stretchr/testify/require" ) @@ -27,6 +29,10 @@ func TestRiverUnmarshal(t *testing.T) { ssl_skip_verify = true data_stream = true slm = true + basic_auth { + username = "username" + password = "pass" + } ` var args Arguments @@ -50,6 +56,10 @@ func TestRiverUnmarshal(t *testing.T) { InsecureSkipVerify: true, ExportDataStreams: true, ExportSLM: true, + BasicAuth: &commoncfg.BasicAuth{ + Username: "username", + Password: rivertypes.Secret("pass"), + }, } require.Equal(t, expected, args) @@ -73,6 +83,10 @@ func TestConvert(t *testing.T) { ssl_skip_verify = true data_stream = true slm = true + basic_auth { + username = "username" + password = "pass" + } ` var args Arguments err := river.Unmarshal([]byte(riverConfig), &args) @@ -97,6 +111,10 @@ func TestConvert(t *testing.T) { InsecureSkipVerify: true, ExportDataStreams: true, ExportSLM: true, + BasicAuth: &commoncfg.BasicAuth{ + Username: "username", + Password: rivertypes.Secret("pass"), + }, } require.Equal(t, expected, *res) } diff --git a/pkg/integrations/elasticsearch_exporter/elasticsearch_exporter.go b/pkg/integrations/elasticsearch_exporter/elasticsearch_exporter.go index d22fd2c618d8..a3ee001b5010 100644 --- a/pkg/integrations/elasticsearch_exporter/elasticsearch_exporter.go +++ b/pkg/integrations/elasticsearch_exporter/elasticsearch_exporter.go @@ -4,13 +4,17 @@ package elasticsearch_exporter //nolint:golint import ( "context" + "encoding/base64" "fmt" "net/http" "net/url" + "os" + "strings" "time" "github.com/go-kit/log" "github.com/go-kit/log/level" + commoncfg "github.com/grafana/agent/component/common/config" "github.com/grafana/agent/pkg/integrations" integrations_v2 "github.com/grafana/agent/pkg/integrations/v2" "github.com/grafana/agent/pkg/integrations/v2/metricsutils" @@ -66,6 +70,19 @@ type Config struct { ExportDataStreams bool `yaml:"data_stream,omitempty"` // Export stats for Snapshot Lifecycle Management ExportSLM bool `yaml:"slm,omitempty"` + // BasicAuth block allows secure connection with Elasticsearch cluster via Basic-Auth + BasicAuth *commoncfg.BasicAuth `yaml:"basic_auth,omitempty"` +} + +// Custom http.Transport struct for Basic Auth-secured communication with ES cluster +type BasicAuthHTTPTransport struct { + http.Transport + authHeader string +} + +func (b *BasicAuthHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Add("authorization", b.authHeader) + return b.Transport.RoundTrip(req) } // UnmarshalYAML implements yaml.Unmarshaler for Config @@ -115,14 +132,31 @@ func New(logger log.Logger, c *Config) (integrations.Integration, error) { // returns nil if not provided and falls back to simple TCP. tlsConfig := createTLSConfig(c.CA, c.ClientCert, c.ClientPrivateKey, c.InsecureSkipVerify) - httpClient := &http.Client{ - Timeout: c.Timeout, - Transport: &http.Transport{ + esHttpTransport := &BasicAuthHTTPTransport{ + Transport: http.Transport{ TLSClientConfig: tlsConfig, Proxy: http.ProxyFromEnvironment, }, } + if c.BasicAuth != nil { + password := string(c.BasicAuth.Password) + if len(c.BasicAuth.PasswordFile) > 0 { + buff, err := os.ReadFile(c.BasicAuth.PasswordFile) + if err != nil { + return nil, fmt.Errorf("unable to load password file %s: %w", c.BasicAuth.PasswordFile, err) + } + password = strings.TrimSpace(string(buff)) + } + encodedAuth := base64.StdEncoding.EncodeToString([]byte(c.BasicAuth.Username + ":" + password)) + esHttpTransport.authHeader = "Basic " + encodedAuth + } + + httpClient := &http.Client{ + Timeout: c.Timeout, + Transport: esHttpTransport, + } + clusterInfoRetriever := clusterinfo.New(logger, httpClient, esURL, c.ExportClusterInfoInterval) collectors := []prometheus.Collector{