diff --git a/CHANGELOG.md b/CHANGELOG.md index bac51f189c7..dc6cf0a6a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Here is an overview of all new **experimental** features: ### Improvements +- **General**: Add parameter queryParameters to prometheus-scaler ([#4962](https://github.com/kedacore/keda/issues/4962)) - **General**: TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX)) - **Kafka Scaler**: Add support for Kerberos authentication (SASL / GSSAPI) ([#4836](https://github.com/kedacore/keda/issues/4836)) diff --git a/pkg/scalers/prometheus_scaler.go b/pkg/scalers/prometheus_scaler.go index 86171ce61b7..685bae55a59 100644 --- a/pkg/scalers/prometheus_scaler.go +++ b/pkg/scalers/prometheus_scaler.go @@ -25,6 +25,7 @@ import ( const ( promServerAddress = "serverAddress" promQuery = "query" + promQueryParameters = "queryParameters" promThreshold = "threshold" promActivationThreshold = "activationThreshold" promNamespace = "namespace" @@ -48,6 +49,7 @@ type prometheusScaler struct { type prometheusMetadata struct { serverAddress string query string + queryParameters map[string]string threshold float64 activationThreshold float64 prometheusAuth *authentication.AuthMeta @@ -151,6 +153,15 @@ func parsePrometheusMetadata(config *ScalerConfig) (meta *prometheusMetadata, er return nil, fmt.Errorf("no %s given", promQuery) } + if val, ok := config.TriggerMetadata[promQueryParameters]; ok && val != "" { + queryParameters, err := kedautil.ParseStringList(val) + if err != nil { + return nil, fmt.Errorf("error parsing %s: %w", promQueryParameters, err) + } + + meta.queryParameters = queryParameters + } + if val, ok := config.TriggerMetadata[promThreshold]; ok && val != "" { t, err := strconv.ParseFloat(val, 64) if err != nil { @@ -266,6 +277,12 @@ func (s *prometheusScaler) ExecutePromQuery(ctx context.Context) (float64, error url = fmt.Sprintf("%s&namespace=%s", url, s.metadata.namespace) } + for queryParameterKey, queryParameterValue := range s.metadata.queryParameters { + queryParameterKeyEscaped := url_pkg.QueryEscape(queryParameterKey) + queryParameterValueEscaped := url_pkg.QueryEscape(queryParameterValue) + url = fmt.Sprintf("%s&%s=%s", url, queryParameterKeyEscaped, queryParameterValueEscaped) + } + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return -1, err diff --git a/pkg/scalers/prometheus_scaler_test.go b/pkg/scalers/prometheus_scaler_test.go index a8d71dc4759..befce3d3b91 100644 --- a/pkg/scalers/prometheus_scaler_test.go +++ b/pkg/scalers/prometheus_scaler_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" @@ -60,6 +61,10 @@ var testPromMetadata = []parsePrometheusMetadataTestData{ {map[string]string{"serverAddress": "http://localhost:9090", "metricName": "http_requests_total", "threshold": "100", "query": "up", "customHeaders": "key1=value1,key2"}, true}, // deprecated cortexOrgID {map[string]string{"serverAddress": "http://localhost:9090", "metricName": "http_requests_total", "threshold": "100", "query": "up", "cortexOrgID": "my-org"}, true}, + // queryParameters + {map[string]string{"serverAddress": "http://localhost:9090", "metricName": "http_requests_total", "threshold": "100", "query": "up", "queryParameters": "key1=value1,key2=value2"}, false}, + // queryParameters with wrong format + {map[string]string{"serverAddress": "http://localhost:9090", "metricName": "http_requests_total", "threshold": "100", "query": "up", "queryParameters": "key1=value1,key2"}, true}, } var prometheusMetricIdentifiers = []prometheusMetricIdentifier{ @@ -372,6 +377,49 @@ func TestPrometheusScalerCustomHeaders(t *testing.T) { assert.NoError(t, err) } +func TestPrometheusScalerExecutePromQueryParameters(t *testing.T) { + testData := prometheusQromQueryResultTestData{ + name: "no values", + bodyStr: `{"data":{"result":[]}}`, + responseStatus: http.StatusOK, + expectedValue: 0, + isError: false, + ignoreNullValues: true, + } + queryParametersValue := map[string]string{ + "first": "foo", + "second": "bar", + } + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + time := time.Now().UTC().Format(time.RFC3339) + expectedPath := fmt.Sprintf("/api/v1/query?query=&time=%s&first=foo&second=bar", time) + for queryParameterName, queryParameterValue := range queryParametersValue { + queryParameter := request.URL.Query() + queryParameter.Add(queryParameterName, queryParameterValue) + } + + if request.RequestURI != expectedPath { + t.Error("Expect request path to =", expectedPath, "but it is", request.RequestURI) + } + + writer.WriteHeader(testData.responseStatus) + if _, err := writer.Write([]byte(testData.bodyStr)); err != nil { + t.Fatal(err) + } + })) + scaler := prometheusScaler{ + metadata: &prometheusMetadata{ + serverAddress: server.URL, + queryParameters: queryParametersValue, + ignoreNullValues: testData.ignoreNullValues, + }, + httpClient: http.DefaultClient, + } + _, err := scaler.ExecutePromQuery(context.TODO()) + + assert.NoError(t, err) +} + func TestPrometheusScaler_ExecutePromQuery_WithGCPNativeAuthentication(t *testing.T) { fakeGoogleOAuthServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"token_type": "Bearer", "access_token": "fake_access_token"}`)