diff --git a/metricbeat/module/elasticsearch/metricset.go b/metricbeat/module/elasticsearch/metricset.go index ae36cf991367..fcef81970e0b 100644 --- a/metricbeat/module/elasticsearch/metricset.go +++ b/metricbeat/module/elasticsearch/metricset.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/elastic/beats/v7/libbeat/common/productorigin" "github.com/elastic/beats/v7/metricbeat/helper" @@ -34,8 +35,8 @@ var ( // HostParser parses host urls for RabbitMQ management plugin HostParser = parse.URLHostParserBuilder{ DefaultScheme: "http", - DefaultUsername: os.Getenv("ELASTICSEARCH_READ_USERNAME"), - DefaultPassword: os.Getenv("ELASTICSEARCH_READ_PASSWORD"), + DefaultUsername: getEnv("AUTOOPS_ES_USERNAME", "ELASTICSEARCH_READ_USERNAME"), + DefaultPassword: getEnv("AUTOOPS_ES_PASSWORD", "ELASTICSEARCH_READ_PASSWORD"), PathConfigKey: "path", }.Build() ) @@ -52,6 +53,16 @@ const ( ScopeCluster ) +// Get an environment variable set via the `key` and, if unset, return the value of the environment variable +// defined by `backupKey`. If that's not set, it will ultimately return "". +func getEnv(key string, backupKey string) string { + if value := os.Getenv(key); value != "" { + return value + } + + return os.Getenv(backupKey) +} + func (h *Scope) Unpack(str string) error { switch str { case "node": @@ -96,7 +107,7 @@ func NewMetricSet(base mb.BaseMetricSet, servicePath string) (*MetricSet, error) }{ Scope: ScopeNode, XPackEnabled: false, - ApiKey: os.Getenv("ELASTICSEARCH_READ_API_KEY"), + ApiKey: getEnv("AUTOOPS_ES_API_KEY", "ELASTICSEARCH_READ_API_KEY"), } if err := base.Module().UnpackConfig(&config); err != nil { return nil, err @@ -109,7 +120,15 @@ func NewMetricSet(base mb.BaseMetricSet, servicePath string) (*MetricSet, error) if hostData.User != "" || hostData.Password != "" { return nil, fmt.Errorf("cannot set both api_key and username/password") } - http.SetHeader("Authorization", "ApiKey "+base64.StdEncoding.EncodeToString([]byte(config.ApiKey))) + + apiKey := config.ApiKey + + // Base64 encode the API Key if necessary + if strings.Contains(config.ApiKey, ":") { + apiKey = base64.StdEncoding.EncodeToString([]byte(apiKey)) + } + + http.SetHeader("Authorization", "ApiKey "+apiKey) } ms := &MetricSet{ diff --git a/metricbeat/module/elasticsearch/metricset_test.go b/metricbeat/module/elasticsearch/metricset_test.go new file mode 100644 index 000000000000..a7c0fe9b8aac --- /dev/null +++ b/metricbeat/module/elasticsearch/metricset_test.go @@ -0,0 +1,55 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +//go:build !integration + +package elasticsearch + +import ( + "testing" +) + +func TestGetEnv(t *testing.T) { + tests := []struct { + name string + key string + backupKey string + expected string + useBackup bool + }{ + {"both set", "TEST_KEY", "BACKUP_KEY", "test_value", false}, + {"only key set", "TEST_KEY", "BACKUP_KEY", "test_value", false}, + {"only backup key set", "NOT_SET", "BACKUP_KEY", "backup_value", true}, + {"neither set", "NOT_SET", "BACKUP_KEY", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.useBackup { + t.Setenv(tt.backupKey, tt.expected) + } else if tt.expected != "" { + t.Setenv(tt.key, tt.expected) + } + + result := getEnv(tt.key, tt.backupKey) + + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} diff --git a/metricbeat/module/elasticsearch/node/node_test.go b/metricbeat/module/elasticsearch/node/node_test.go index 32f91bc8c609..28827dc074f3 100644 --- a/metricbeat/module/elasticsearch/node/node_test.go +++ b/metricbeat/module/elasticsearch/node/node_test.go @@ -81,46 +81,81 @@ func TestFetch(t *testing.T) { } t.Run("with api key", func(t *testing.T) { - apiKey := "foo:bar" - expectedHeader := "ApiKey " + base64.StdEncoding.EncodeToString([]byte(apiKey)) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.RequestURI { - case "/_nodes/_local": - apiKey := r.Header.Get("Authorization") - if apiKey != expectedHeader { - t.Errorf("expected api key to be %s but got %s", expectedHeader, apiKey) - } - w.WriteHeader(200) - w.Header().Set("Content-Type", "application/json;") - w.Write([]byte([]byte("{}"))) - - case "/": - apiKey := r.Header.Get("Authorization") - if apiKey != expectedHeader { - t.Errorf("expected api key to be %s but got %s", expectedHeader, apiKey) + tests := []struct { + name string + envVar string + apiKey string + base64Encoded bool + expectedHeader string + }{ + {"from config without base64 encoding", "", "foo:bar1", false, "ApiKey " + base64.StdEncoding.EncodeToString([]byte("foo:bar1"))}, + {"from config with base64 encoding", "", "foo:bar2", true, "ApiKey " + base64.StdEncoding.EncodeToString([]byte("foo:bar2"))}, + {"from env [AUTOOPS_ES_API_KEY] without base64 encoding", "AUTOOPS_ES_API_KEY", "foo:bar3", false, "ApiKey " + base64.StdEncoding.EncodeToString([]byte("foo:bar3"))}, + {"from env [AUTOOPS_ES_API_KEY] with base64 encoding", "AUTOOPS_ES_API_KEY", "bar:foo4", true, "ApiKey " + base64.StdEncoding.EncodeToString([]byte("bar:foo4"))}, + {"from env [ELASTICSEARCH_READ_API_KEY] without base64 encoding", "ELASTICSEARCH_READ_API_KEY", "foo:bar5", false, "ApiKey " + base64.StdEncoding.EncodeToString([]byte("foo:bar5"))}, + {"from env [ELASTICSEARCH_READ_API_KEY] with base64 encoding", "ELASTICSEARCH_READ_API_KEY", "bar:foo6", true, "ApiKey " + base64.StdEncoding.EncodeToString([]byte("bar:foo6"))}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expectedHeader := tt.expectedHeader + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/_nodes/_local": + apiKey := r.Header.Get("Authorization") + if apiKey != expectedHeader { + t.Errorf("expected api key to be %s but got %s", expectedHeader, apiKey) + } + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json;") + w.Write([]byte([]byte("{}"))) + + case "/": + apiKey := r.Header.Get("Authorization") + if apiKey != expectedHeader { + t.Errorf("expected api key to be %s but got %s", expectedHeader, apiKey) + } + + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) + + default: + t.FailNow() + } + + })) + defer server.Close() + + config := map[string]any{ + "module": "elasticsearch", + "metricsets": []string{"node"}, + "hosts": []string{server.URL}, } - w.WriteHeader(200) - w.Header().Set("Content-Type", "application/json") - w.Write([]byte("{}")) + apiKey := base64.StdEncoding.EncodeToString([]byte(tt.apiKey)) - default: - t.FailNow() - } + if tt.envVar != "" { + if tt.envVar != "ELASTICSEARCH_READ_API_KEY" { + // ensure that this is not the preferred value + t.Setenv("ELASTICSEARCH_READ_API_KEY", "_ignored_") + } + t.Setenv(tt.envVar, apiKey) + } else { + // config beats env + t.Setenv("AUTOOPS_ES_API_KEY", "_ignored_") + t.Setenv("ELASTICSEARCH_READ_API_KEY", "_ignored_") + + config["api_key"] = apiKey + } - })) - defer server.Close() + reporter := &mbtest.CapturingReporterV2{} - config := map[string]interface{}{ - "module": "elasticsearch", - "metricsets": []string{"node"}, - "hosts": []string{server.URL}, - "api_key": "foo:bar", + metricSet := mbtest.NewReportingMetricSetV2Error(t, config) + metricSet.Fetch(reporter) + }) } - reporter := &mbtest.CapturingReporterV2{} - metricSet := mbtest.NewReportingMetricSetV2Error(t, config) - metricSet.Fetch(reporter) }) }