Skip to content

Commit 33b34a0

Browse files
hainenberrfrattoerikbaranowski
authored andcommitted
feat(exporter/elasticsearch): add Basic Auth support (grafana#5814)
* feat(exporter/elasticsearch): add Basic Auth support Signed-off-by: hainenber <[email protected]> * misc: fix changelog entry (grafana#5812) PR grafana#5802 accidentally added the changelog entry to a published release. * fix(exporter/elasticsearch): only add Auth header when needed Signed-off-by: hainenber <[email protected]> * doc(exporter/elasticsearch): add Basic Auth block desc Signed-off-by: hainenber <[email protected]> * feat(exporter/elasticsearch): add Basic Auth support Signed-off-by: hainenber <[email protected]> * feat(exporter/elasticsearch): support Basic Auth for static config Signed-off-by: hainenber <[email protected]> * chore(CHANGELOG): remove merge conflict artifact Signed-off-by: hainenber <[email protected]> * misc: fix changelog entry Signed-off-by: hainenber <[email protected]> * feat(converter): map ES exporter's BasicAuth flow from static to flow Signed-off-by: hainenber <[email protected]> * fix(converter): nil check for ES exporter's Basic Auth Signed-off-by: hainenber <[email protected]> * lint Signed-off-by: erikbaranowski <[email protected]> --------- Signed-off-by: hainenber <[email protected]> Signed-off-by: erikbaranowski <[email protected]> Co-authored-by: Robert Fratto <[email protected]> Co-authored-by: erikbaranowski <[email protected]>
1 parent 04244c3 commit 33b34a0

File tree

7 files changed

+128
-21
lines changed

7 files changed

+128
-21
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Main (unreleased)
4545
Previously, only `remote.*` and `local.*` components could be referenced
4646
without a circular dependency. (@rfratto)
4747

48+
- Add support for Basic Auth-secured connection with Elasticsearch cluster using `prometheus.exporter.elasticsearch`. (@hainenber)
49+
4850
- Add a `resource_to_telemetry_conversion` argument to `otelcol.exporter.prometheus`
4951
for converting resource attributes to Prometheus labels. (@hainenber)
5052

component/prometheus/exporter/elasticsearch/elasticsearch.go

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"time"
55

66
"github.com/grafana/agent/component"
7+
commonCfg "github.com/grafana/agent/component/common/config"
78
"github.com/grafana/agent/component/prometheus/exporter"
89
"github.com/grafana/agent/pkg/integrations"
910
"github.com/grafana/agent/pkg/integrations/elasticsearch_exporter"
@@ -35,23 +36,24 @@ var DefaultArguments = Arguments{
3536
}
3637

3738
type Arguments struct {
38-
Address string `river:"address,attr,optional"`
39-
Timeout time.Duration `river:"timeout,attr,optional"`
40-
AllNodes bool `river:"all,attr,optional"`
41-
Node string `river:"node,attr,optional"`
42-
ExportIndices bool `river:"indices,attr,optional"`
43-
ExportIndicesSettings bool `river:"indices_settings,attr,optional"`
44-
ExportClusterSettings bool `river:"cluster_settings,attr,optional"`
45-
ExportShards bool `river:"shards,attr,optional"`
46-
IncludeAliases bool `river:"aliases,attr,optional"`
47-
ExportSnapshots bool `river:"snapshots,attr,optional"`
48-
ExportClusterInfoInterval time.Duration `river:"clusterinfo_interval,attr,optional"`
49-
CA string `river:"ca,attr,optional"`
50-
ClientPrivateKey string `river:"client_private_key,attr,optional"`
51-
ClientCert string `river:"client_cert,attr,optional"`
52-
InsecureSkipVerify bool `river:"ssl_skip_verify,attr,optional"`
53-
ExportDataStreams bool `river:"data_stream,attr,optional"`
54-
ExportSLM bool `river:"slm,attr,optional"`
39+
Address string `river:"address,attr,optional"`
40+
Timeout time.Duration `river:"timeout,attr,optional"`
41+
AllNodes bool `river:"all,attr,optional"`
42+
Node string `river:"node,attr,optional"`
43+
ExportIndices bool `river:"indices,attr,optional"`
44+
ExportIndicesSettings bool `river:"indices_settings,attr,optional"`
45+
ExportClusterSettings bool `river:"cluster_settings,attr,optional"`
46+
ExportShards bool `river:"shards,attr,optional"`
47+
IncludeAliases bool `river:"aliases,attr,optional"`
48+
ExportSnapshots bool `river:"snapshots,attr,optional"`
49+
ExportClusterInfoInterval time.Duration `river:"clusterinfo_interval,attr,optional"`
50+
CA string `river:"ca,attr,optional"`
51+
ClientPrivateKey string `river:"client_private_key,attr,optional"`
52+
ClientCert string `river:"client_cert,attr,optional"`
53+
InsecureSkipVerify bool `river:"ssl_skip_verify,attr,optional"`
54+
ExportDataStreams bool `river:"data_stream,attr,optional"`
55+
ExportSLM bool `river:"slm,attr,optional"`
56+
BasicAuth *commonCfg.BasicAuth `river:"basic_auth,block,optional"`
5557
}
5658

5759
// SetToDefault implements river.Defaulter.
@@ -78,5 +80,6 @@ func (a *Arguments) Convert() *elasticsearch_exporter.Config {
7880
InsecureSkipVerify: a.InsecureSkipVerify,
7981
ExportDataStreams: a.ExportDataStreams,
8082
ExportSLM: a.ExportSLM,
83+
BasicAuth: a.BasicAuth.Convert(),
8184
}
8285
}

component/prometheus/exporter/elasticsearch/elasticsearch_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import (
44
"testing"
55
"time"
66

7+
commonCfg "github.com/grafana/agent/component/common/config"
78
"github.com/grafana/agent/pkg/integrations/elasticsearch_exporter"
89
"github.com/grafana/river"
10+
"github.com/grafana/river/rivertypes"
11+
promCfg "github.com/prometheus/common/config"
912
"github.com/stretchr/testify/require"
1013
)
1114

@@ -27,6 +30,10 @@ func TestRiverUnmarshal(t *testing.T) {
2730
ssl_skip_verify = true
2831
data_stream = true
2932
slm = true
33+
basic_auth {
34+
username = "username"
35+
password = "pass"
36+
}
3037
`
3138

3239
var args Arguments
@@ -50,6 +57,10 @@ func TestRiverUnmarshal(t *testing.T) {
5057
InsecureSkipVerify: true,
5158
ExportDataStreams: true,
5259
ExportSLM: true,
60+
BasicAuth: &commonCfg.BasicAuth{
61+
Username: "username",
62+
Password: rivertypes.Secret("pass"),
63+
},
5364
}
5465

5566
require.Equal(t, expected, args)
@@ -73,6 +84,10 @@ func TestConvert(t *testing.T) {
7384
ssl_skip_verify = true
7485
data_stream = true
7586
slm = true
87+
basic_auth {
88+
username = "username"
89+
password = "pass"
90+
}
7691
`
7792
var args Arguments
7893
err := river.Unmarshal([]byte(riverConfig), &args)
@@ -97,6 +112,10 @@ func TestConvert(t *testing.T) {
97112
InsecureSkipVerify: true,
98113
ExportDataStreams: true,
99114
ExportSLM: true,
115+
BasicAuth: &promCfg.BasicAuth{
116+
Username: "username",
117+
Password: promCfg.Secret("pass"),
118+
},
100119
}
101120
require.Equal(t, expected, *res)
102121
}

converter/internal/staticconvert/internal/build/elasticsearch_exporter.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package build
22

33
import (
4+
commonCfg "github.com/grafana/agent/component/common/config"
45
"github.com/grafana/agent/component/discovery"
56
"github.com/grafana/agent/component/prometheus/exporter/elasticsearch"
67
"github.com/grafana/agent/pkg/integrations/elasticsearch_exporter"
8+
"github.com/grafana/river/rivertypes"
79
)
810

911
func (b *IntegrationsConfigBuilder) appendElasticsearchExporter(config *elasticsearch_exporter.Config, instanceKey *string) discovery.Exports {
@@ -12,7 +14,7 @@ func (b *IntegrationsConfigBuilder) appendElasticsearchExporter(config *elastics
1214
}
1315

1416
func toElasticsearchExporter(config *elasticsearch_exporter.Config) *elasticsearch.Arguments {
15-
return &elasticsearch.Arguments{
17+
arg := &elasticsearch.Arguments{
1618
Address: config.Address,
1719
Timeout: config.Timeout,
1820
AllNodes: config.AllNodes,
@@ -31,4 +33,14 @@ func toElasticsearchExporter(config *elasticsearch_exporter.Config) *elasticsear
3133
ExportDataStreams: config.ExportDataStreams,
3234
ExportSLM: config.ExportSLM,
3335
}
36+
37+
if config.BasicAuth != nil {
38+
arg.BasicAuth = &commonCfg.BasicAuth{
39+
Username: config.BasicAuth.Username,
40+
Password: rivertypes.Secret(config.BasicAuth.Password),
41+
PasswordFile: config.BasicAuth.PasswordFile,
42+
}
43+
}
44+
45+
return arg
3446
}

docs/sources/flow/reference/components/prometheus.exporter.elasticsearch.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ Omitted fields take their default values.
5656
| `data_streams` | `bool` | Export stats for Data Streams. | | no |
5757
| `slm` | `bool` | Export stats for SLM (Snapshot Lifecycle Management). | | no |
5858

59+
## Blocks
60+
61+
The following blocks are supported inside the definition of
62+
`prometheus.exporter.elasticsearch`:
63+
64+
| Hierarchy | Block | Description | Required |
65+
| ------------------- | ----------------- | -------------------------------------------------------- | -------- |
66+
| basic_auth | [basic_auth][] | Configure basic_auth for authenticating to the endpoint. | no |
67+
68+
[basic_auth]: #basic_auth-block
69+
70+
### basic_auth block
71+
72+
{{< docs/shared lookup="flow/reference/components/basic-auth-block.md" source="agent" version="<AGENT VERSION>" >}}
73+
5974
## Exported fields
6075

6176
{{< docs/shared lookup="flow/reference/components/exporter-component-exports.md" source="agent" version="<AGENT_VERSION>" >}}
@@ -84,6 +99,10 @@ from `prometheus.exporter.elasticsearch`:
8499
```river
85100
prometheus.exporter.elasticsearch "example" {
86101
address = "http://localhost:9200"
102+
basic_auth {
103+
username = USERNAME
104+
password = PASSWORD
105+
}
87106
}
88107
89108
// Configure a prometheus.scrape component to collect Elasticsearch metrics.

docs/sources/static/configuration/integrations/elasticsearch-exporter-config.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,12 @@ Full reference of options:
116116

117117
# Export stats for SLM (Snapshot Lifecycle Management).
118118
[ slm: <boolean> ]
119+
120+
# Sets the `Authorization` header on every ES probe with the
121+
# configured username and password.
122+
# password and password_file are mutually exclusive.
123+
basic_auth:
124+
[ username: <string> ]
125+
[ password: <secret> ]
126+
[ password_file: <string> ]
119127
```

pkg/integrations/elasticsearch_exporter/elasticsearch_exporter.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ package elasticsearch_exporter //nolint:golint
44

55
import (
66
"context"
7+
"encoding/base64"
78
"fmt"
89
"net/http"
910
"net/url"
11+
"os"
12+
"strings"
1013
"time"
1114

1215
"github.com/go-kit/log"
@@ -15,6 +18,7 @@ import (
1518
integrations_v2 "github.com/grafana/agent/pkg/integrations/v2"
1619
"github.com/grafana/agent/pkg/integrations/v2/metricsutils"
1720
"github.com/prometheus/client_golang/prometheus"
21+
promCfg "github.com/prometheus/common/config"
1822

1923
"github.com/prometheus-community/elasticsearch_exporter/collector"
2024
"github.com/prometheus-community/elasticsearch_exporter/pkg/clusterinfo"
@@ -66,6 +70,21 @@ type Config struct {
6670
ExportDataStreams bool `yaml:"data_stream,omitempty"`
6771
// Export stats for Snapshot Lifecycle Management
6872
ExportSLM bool `yaml:"slm,omitempty"`
73+
// BasicAuth block allows secure connection with Elasticsearch cluster via Basic-Auth
74+
BasicAuth *promCfg.BasicAuth `yaml:"basic_auth,omitempty"`
75+
}
76+
77+
// Custom http.Transport struct for Basic Auth-secured communication with ES cluster
78+
type BasicAuthHTTPTransport struct {
79+
http.Transport
80+
authHeader string
81+
}
82+
83+
func (b *BasicAuthHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
84+
if b.authHeader != "" {
85+
req.Header.Add("authorization", b.authHeader)
86+
}
87+
return b.Transport.RoundTrip(req)
6988
}
7089

7190
// UnmarshalYAML implements yaml.Unmarshaler for Config
@@ -115,14 +134,39 @@ func New(logger log.Logger, c *Config) (integrations.Integration, error) {
115134
// returns nil if not provided and falls back to simple TCP.
116135
tlsConfig := createTLSConfig(c.CA, c.ClientCert, c.ClientPrivateKey, c.InsecureSkipVerify)
117136

118-
httpClient := &http.Client{
119-
Timeout: c.Timeout,
120-
Transport: &http.Transport{
137+
esHttpTransport := &BasicAuthHTTPTransport{
138+
Transport: http.Transport{
121139
TLSClientConfig: tlsConfig,
122140
Proxy: http.ProxyFromEnvironment,
123141
},
124142
}
125143

144+
if c.BasicAuth != nil {
145+
password := string(c.BasicAuth.Password)
146+
if len(c.BasicAuth.PasswordFile) > 0 {
147+
buff, err := os.ReadFile(c.BasicAuth.PasswordFile)
148+
if err != nil {
149+
return nil, fmt.Errorf("unable to load password file %s: %w", c.BasicAuth.PasswordFile, err)
150+
}
151+
password = strings.TrimSpace(string(buff))
152+
}
153+
username := c.BasicAuth.Username
154+
if len(c.BasicAuth.UsernameFile) > 0 {
155+
buff, err := os.ReadFile(c.BasicAuth.UsernameFile)
156+
if err != nil {
157+
return nil, fmt.Errorf("unable to load username file %s: %w", c.BasicAuth.UsernameFile, err)
158+
}
159+
username = strings.TrimSpace(string(buff))
160+
}
161+
encodedAuth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
162+
esHttpTransport.authHeader = "Basic " + encodedAuth
163+
}
164+
165+
httpClient := &http.Client{
166+
Timeout: c.Timeout,
167+
Transport: esHttpTransport,
168+
}
169+
126170
clusterInfoRetriever := clusterinfo.New(logger, httpClient, esURL, c.ExportClusterInfoInterval)
127171

128172
collectors := []prometheus.Collector{

0 commit comments

Comments
 (0)