diff --git a/go.mod b/go.mod index 67241b8fe68..b641b9aa34a 100644 --- a/go.mod +++ b/go.mod @@ -348,7 +348,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 k8s.io/apiextensions-apiserver v0.29.0 // indirect k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect k8s.io/kms v0.29.0 // indirect diff --git a/pkg/scalers/metrics_api_scaler.go b/pkg/scalers/metrics_api_scaler.go index 60880fa81c0..0d69cec1e2e 100644 --- a/pkg/scalers/metrics_api_scaler.go +++ b/pkg/scalers/metrics_api_scaler.go @@ -1,17 +1,20 @@ package scalers import ( + "bufio" + "bytes" "context" "encoding/xml" "errors" "fmt" - "gopkg.in/yaml.v3" "io" "net/http" neturl "net/url" "strconv" "strings" + "gopkg.in/yaml.v3" + "github.com/go-logr/logr" "github.com/tidwall/gjson" v2 "k8s.io/api/autoscaling/v2" @@ -73,17 +76,15 @@ type APIFormat string // Enum for APIFormat: const ( - PrometheusFormat APIFormat = "prometheus" - OpenMetricsFormat APIFormat = "openmetrics" - JSONFormat APIFormat = "json" - XMLFormat APIFormat = "xml" - YAMLFormat APIFormat = "yaml" + PrometheusFormat APIFormat = "prometheus" + JSONFormat APIFormat = "json" + XMLFormat APIFormat = "xml" + YAMLFormat APIFormat = "yaml" ) var ( supportedFormats = []APIFormat{ - //PrometheusFormat, - //OpenMetricsFormat, + PrometheusFormat, JSONFormat, XMLFormat, YAMLFormat, @@ -249,10 +250,8 @@ func parseMetricsAPIMetadata(config *scalersconfig.ScalerConfig) (*metricsAPISca // GetValueFromResponse uses provided valueLocation to access the numeric value in provided body using the format specified. func GetValueFromResponse(body []byte, valueLocation string, format APIFormat) (float64, error) { switch format { - //case PrometheusFormat: - // return getValueFromPrometheusResponse(body, valueLocation) - //case OpenMetricsFormat: - // return getValueFromOpenMetricsResponse(body, valueLocation) + case PrometheusFormat: + return getValueFromPrometheusResponse(body, valueLocation) case JSONFormat: return getValueFromJSONResponse(body, valueLocation) case XMLFormat: @@ -264,6 +263,31 @@ func GetValueFromResponse(body []byte, valueLocation string, format APIFormat) ( return 0, fmt.Errorf("format %s not supported", format) } +// getValueFromPrometheusResponse uses provided valueLocation to access the numeric value in provided body +func getValueFromPrometheusResponse(body []byte, valueLocation string) (float64, error) { + scanner := bufio.NewScanner(bytes.NewReader(body)) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) == 0 || strings.HasPrefix(fields[0], "#") { + continue + } + if len(fields) == 2 && strings.HasPrefix(fields[0], valueLocation) { + value, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return 0, err + } + return value, nil + } + } + + if err := scanner.Err(); err != nil { + return 0, err + } + + return 0, fmt.Errorf("Value %s not found", valueLocation) +} + // getValueFromJSONResponse uses provided valueLocation to access the numeric value in provided body using GSON func getValueFromJSONResponse(body []byte, valueLocation string) (float64, error) { r := gjson.GetBytes(body, valueLocation) diff --git a/pkg/scalers/metrics_api_scaler_test.go b/pkg/scalers/metrics_api_scaler_test.go index 8133cf4e545..aa827241134 100644 --- a/pkg/scalers/metrics_api_scaler_test.go +++ b/pkg/scalers/metrics_api_scaler_test.go @@ -123,45 +123,40 @@ func TestMetricsAPIGetMetricSpecForScaling(t *testing.T) { } func TestGetValueFromResponse(t *testing.T) { - t.Run("JSON", func(t *testing.T) { - d := []byte(`{"components":[{"id": "82328e93e", "tasks": 32, "str": "64", "k":"1k","wrong":"NaN"}],"count":2.43}`) - v, err := GetValueFromResponse(d, "components.0.tasks", JSONFormat) - if err != nil { - t.Error("Expected success but got error", err) - } - if v != 32 { - t.Errorf("Expected %d got %f", 32, v) - } - v, err = GetValueFromResponse(d, "count", JSONFormat) - if err != nil { - t.Error("Expected success but got error", err) - } - if v != 2.43 { - t.Errorf("Expected %d got %f", 2, v) - } + testCases := []struct { + name string + input []byte + key string + format APIFormat + expectVal interface{} + expectErr bool + }{ + {name: "integer", input: []byte(`{"components":[{"id": "82328e93e", "tasks": 32, "str": "64", "k":"1k","wrong":"NaN"}],"count":2.43}`), key: "count", format: JSONFormat, expectVal: 2.43}, + {name: "string", input: []byte(`{"components":[{"id": "82328e93e", "tasks": 32, "str": "64", "k":"1k","wrong":"NaN"}],"count":2.43}`), key: "components.0.str", format: JSONFormat, expectVal: 64}, + {name: "{}.[].{}", input: []byte(`{"components":[{"id": "82328e93e", "tasks": 32, "str": "64", "k":"1k","wrong":"NaN"}],"count":2.43}`), key: "components.0.tasks", format: JSONFormat, expectVal: 32}, + {name: "invalid data", input: []byte(`{"components":[{"id": "82328e93e", "tasks": 32, "str": "64", "k":"1k","wrong":"NaN"}],"count":2.43}`), key: "components.0.wrong", format: JSONFormat, expectErr: true}, + {name: "integer", input: []byte(`components: [{id: "82328e93e", tasks: 32, str: "64", k: "1k", wrong: "NaN"}] count: 2.43`), key: "count", format: YAMLFormat, expectVal: 2.43}, + {name: "string", input: []byte(`components: [{id: "82328e93e", tasks: 32, str: "64", k: "1k", wrong: "NaN"}] count: 2.43`), key: "components.0.str", format: YAMLFormat, expectVal: 64}, + {name: "{}.[].{}", input: []byte(`components: [{id: "82328e93e", tasks: 32, str: "64", k: "1k", wrong: "NaN"}] count: 2.43`), key: "components.0.tasks", format: YAMLFormat, expectVal: 32}, + {name: "invalid data", input: []byte(`components: [{id: "82328e93e", tasks: 32, str: "64", k: "1k", wrong: "NaN"}] count: 2.43`), key: "components.0.wrong", format: YAMLFormat, expectErr: true}, + } - v, err = GetValueFromResponse(d, "components.0.str", JSONFormat) - if err != nil { - t.Error("Expected success but got error", err) - } - if v != 64 { - t.Errorf("Expected %d got %f", 64, v) - } + for _, tc := range testCases { + t.Run(string(tc.format)+": "+tc.name, func(t *testing.T) { + v, err := GetValueFromResponse(tc.input, tc.key, tc.format) - v, err = GetValueFromResponse(d, "components.0.k", JSONFormat) - if err != nil { - t.Error("Expected success but got error", err) - } - if v != 1000 { - t.Errorf("Expected %d got %f", 1000, v) - } + if tc.expectErr && err == nil { + t.Error("Expected error but got success") + } else if !tc.expectErr && err != nil { + t.Error("Expected success but got error:", err) + } - _, err = GetValueFromResponse(d, "components.0.wrong", JSONFormat) - if err == nil { - t.Error("Expected error but got success", err) - } - }) + if !tc.expectErr && v != tc.expectVal { + t.Errorf("Expected %v, got %v", tc.expectVal, v) + } + }) + } } func TestMetricAPIScalerAuthParams(t *testing.T) {