From 7c81b455ba0e7524bfbc2156f8a644cd531c7f87 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:31:32 +0100 Subject: [PATCH 01/26] Add first scaler version Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 202 ++++++++++++++++++++++++++++++++ pkg/scaling/scalers_builder.go | 2 + 2 files changed, 204 insertions(+) create mode 100644 pkg/scalers/dynatrace_scaler.go diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go new file mode 100644 index 00000000000..2da94f6ff77 --- /dev/null +++ b/pkg/scalers/dynatrace_scaler.go @@ -0,0 +1,202 @@ +package scalers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + + "github.com/go-logr/logr" + v2 "k8s.io/api/autoscaling/v2" + "k8s.io/metrics/pkg/apis/external_metrics" + + "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" + kedautil "github.com/kedacore/keda/v2/pkg/util" +) + +type dynatraceScaler struct { + metricType v2.MetricTargetType + metadata *dynatraceMetadata + httpClient *http.Client + logger logr.Logger +} + +type dynatraceMetadata struct { + apiURL string + apiKey string + metricsSelector string + threshold float64 + activationThreshold float64 + triggerIndex int +} + +// Model of relevant part of Dynatrace's Metric Data Points API Response +// as per https://docs.dynatrace.com/docs/dynatrace-api/environment-api/metric-v2/get-data-points#definition--MetricData +type dynatraceResponse struct { + Result []struct { + Data []struct { + Values []int `json:"values"` + } `json:"data"` + } `json:"response"` +} + +func NewDynatraceScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { + metricType, err := GetMetricTargetType(config) + if err != nil { + return nil, fmt.Errorf("error getting scaler metric type: %w", err) + } + + logger := InitializeLogger(config, "dynatrace_scaler") + + meta, err := parseDynatraceMetadata(config) + if err != nil { + return nil, fmt.Errorf("error parsing dynatrace metadata: %w", err) + } + + httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false) + + logMsg := fmt.Sprintf("Initializing Dynatrace Scaler (API URL: %s)", meta.apiURL) + + logger.Info(logMsg) + + return &dynatraceScaler{ + metricType: metricType, + metadata: meta, + httpClient: httpClient, + logger: logger}, nil +} + +func parseDynatraceMetadata(config *scalersconfig.ScalerConfig) (*dynatraceMetadata, error) { + meta := dynatraceMetadata{} + var err error + + apiURL, err := GetFromAuthOrMeta(config, "apiUrl") + if err != nil { + return nil, err + } + meta.apiURL = apiURL + + apiKey, err := GetFromAuthOrMeta(config, "apiKey") + if err != nil { + return nil, err + } + meta.apiKey = apiKey + + if val, ok := config.TriggerMetadata["metricsSelector"]; ok && val != "" { + meta.metricsSelector = val + } else { + return nil, fmt.Errorf("no metricsSelector given") + } + + if val, ok := config.TriggerMetadata["threshold"]; ok && val != "" { + t, err := strconv.ParseFloat(val, 64) + if err != nil { + return nil, fmt.Errorf("error parsing threshold") + } + meta.threshold = t + } else { + if config.AsMetricSource { + meta.threshold = 0 + } else { + return nil, fmt.Errorf("missing threshold value") + } + } + + meta.activationThreshold = 0 + if val, ok := config.TriggerMetadata["activationThreshold"]; ok { + activationThreshold, err := strconv.ParseFloat(val, 64) + if err != nil { + return nil, fmt.Errorf("queryValue parsing error %w", err) + } + meta.activationThreshold = activationThreshold + } + + meta.triggerIndex = config.TriggerIndex + return &meta, nil +} + +func (s *dynatraceScaler) Close(context.Context) error { + if s.httpClient != nil { + s.httpClient.CloseIdleConnections() + } + return nil +} + +func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { + // BUILD REQUEST + var req *http.Request + var err error + + req, err = http.NewRequestWithContext(ctx, "GET", s.metadata.apiURL, nil) + if err != nil { + return 0, err + } + + // Authentication header as per https://docs.dynatrace.com/docs/dynatrace-api/basics/dynatrace-api-authentication#authenticate + req.Header.Add("Authorization", fmt.Sprintf("Api-Token %s", s.metadata.apiKey)) + + // EXECUTE REQUEST + r, err := s.httpClient.Do(req) + if err != nil { + return 0, err + } + defer r.Body.Close() + + if r.StatusCode != http.StatusOK { + msg := fmt.Sprintf("%s: api returned %d", r.Request.URL.Path, r.StatusCode) + return 0, errors.New(msg) + } + + // PARSE RESPONSE + b, err := io.ReadAll(r.Body) + if err != nil { + return 0, err + } + var dynatraceResponse *dynatraceResponse + err = json.Unmarshal(b, &dynatraceResponse) + if err != nil { + return -1, fmt.Errorf("unable to parse Dynatrace Metric Data Points API response: %w", err) + } + + // VALIDATE RESPONSE CONTENT + + if len(dynatraceResponse.Result) > 1 { + s.logger.Info("Multiple results returned from Dynatrace API. Using only the first one") + } + if len(dynatraceResponse.Result[0].Data) > 1 { + s.logger.Info("Multiple data point collections found in results. Using only the first one") + } + if len(dynatraceResponse.Result[0].Data[0].Values) > 1 { + s.logger.Info("Multiple data point values found in collection. Defaulting to average to aggregate them into a single value") + } + return float64(dynatraceResponse.Result[0].Data[0].Values[0]), nil +} + +func (s *dynatraceScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { + val, err := s.GetMetricValue(ctx) + + if err != nil { + s.logger.Error(err, "error executing Dynatrace query") + return []external_metrics.ExternalMetricValue{}, false, err + } + + metric := GenerateMetricInMili(metricName, val) + + return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.activationThreshold, nil +} + +func (s *dynatraceScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { + externalMetric := &v2.ExternalMetricSource{ + Metric: v2.MetricIdentifier{ + Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString("dynatrace")), + }, + Target: GetMetricTargetMili(s.metricType, s.metadata.threshold), + } + metricSpec := v2.MetricSpec{ + External: externalMetric, Type: externalMetricType, + } + return []v2.MetricSpec{metricSpec} +} diff --git a/pkg/scaling/scalers_builder.go b/pkg/scaling/scalers_builder.go index afcfc574bb1..ada24e813a5 100644 --- a/pkg/scaling/scalers_builder.go +++ b/pkg/scaling/scalers_builder.go @@ -168,6 +168,8 @@ func buildScaler(ctx context.Context, client client.Client, triggerType string, return scalers.NewCronScaler(config) case "datadog": return scalers.NewDatadogScaler(ctx, config) + case "dynatrace": + return scalers.NewDynatraceScaler(config) case "elasticsearch": return scalers.NewElasticsearchScaler(config) case "etcd": From 93e327022f3bf537d27ec5ad96a453ee1c49d435 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:49:49 +0100 Subject: [PATCH 02/26] small refactor for response validation Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 41 ++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index 2da94f6ff77..d9733855c24 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -125,8 +125,25 @@ func (s *dynatraceScaler) Close(context.Context) error { return nil } +// Validate that response object contains the minimum expected structure +// as per https://docs.dynatrace.com/docs/dynatrace-api/environment-api/metric-v2/get-data-points#definition--MetricData +func validateDynatraceResponse(response *dynatraceResponse) error { + if len(response.Result) == 0 { + return errors.New("dynatrace response does not contain any results") + } + if len(response.Result[0].Data) == 0 { + return errors.New("dynatrace response does not contain any metric series") + } + if len(response.Result[0].Data[0].Values) == 0 { + return errors.New("dynatrace response does not contain any values for the metric series") + } + return nil +} + func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { - // BUILD REQUEST + /* + * Build request + */ var req *http.Request var err error @@ -138,7 +155,9 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { // Authentication header as per https://docs.dynatrace.com/docs/dynatrace-api/basics/dynatrace-api-authentication#authenticate req.Header.Add("Authorization", fmt.Sprintf("Api-Token %s", s.metadata.apiKey)) - // EXECUTE REQUEST + /* + * Execute request + */ r, err := s.httpClient.Do(req) if err != nil { return 0, err @@ -150,7 +169,9 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { return 0, errors.New(msg) } - // PARSE RESPONSE + /* + * Parse response + */ b, err := io.ReadAll(r.Body) if err != nil { return 0, err @@ -161,17 +182,11 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { return -1, fmt.Errorf("unable to parse Dynatrace Metric Data Points API response: %w", err) } - // VALIDATE RESPONSE CONTENT - - if len(dynatraceResponse.Result) > 1 { - s.logger.Info("Multiple results returned from Dynatrace API. Using only the first one") - } - if len(dynatraceResponse.Result[0].Data) > 1 { - s.logger.Info("Multiple data point collections found in results. Using only the first one") - } - if len(dynatraceResponse.Result[0].Data[0].Values) > 1 { - s.logger.Info("Multiple data point values found in collection. Defaulting to average to aggregate them into a single value") + err = validateDynatraceResponse(dynatraceResponse) + if err != nil { + return 0, err } + return float64(dynatraceResponse.Result[0].Data[0].Values[0]), nil } From 5a77a723bb0aaf352d94bf97ebc3c56a1297d76a Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:31:51 +0100 Subject: [PATCH 03/26] Add 'from' property, rename host/token Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 47 +++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index d9733855c24..9f9d708f092 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -7,7 +7,9 @@ import ( "fmt" "io" "net/http" + neturl "net/url" "strconv" + "strings" "github.com/go-logr/logr" v2 "k8s.io/api/autoscaling/v2" @@ -17,6 +19,10 @@ import ( kedautil "github.com/kedacore/keda/v2/pkg/util" ) +const ( + dynatraceMetricDataPointsAPI = "api/v2/metrics/query" +) + type dynatraceScaler struct { metricType v2.MetricTargetType metadata *dynatraceMetadata @@ -25,9 +31,10 @@ type dynatraceScaler struct { } type dynatraceMetadata struct { - apiURL string - apiKey string + host string + token string metricsSelector string + fromTimestamp string threshold float64 activationThreshold float64 triggerIndex int @@ -51,14 +58,14 @@ func NewDynatraceScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { logger := InitializeLogger(config, "dynatrace_scaler") - meta, err := parseDynatraceMetadata(config) + meta, err := parseDynatraceMetadata(config, logger) if err != nil { return nil, fmt.Errorf("error parsing dynatrace metadata: %w", err) } httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false) - logMsg := fmt.Sprintf("Initializing Dynatrace Scaler (API URL: %s)", meta.apiURL) + logMsg := fmt.Sprintf("Initializing Dynatrace Scaler (API URL: %s)", meta.host) logger.Info(logMsg) @@ -69,21 +76,21 @@ func NewDynatraceScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { logger: logger}, nil } -func parseDynatraceMetadata(config *scalersconfig.ScalerConfig) (*dynatraceMetadata, error) { +func parseDynatraceMetadata(config *scalersconfig.ScalerConfig, logger logr.Logger) (*dynatraceMetadata, error) { meta := dynatraceMetadata{} var err error - apiURL, err := GetFromAuthOrMeta(config, "apiUrl") + host, err := GetFromAuthOrMeta(config, "host") if err != nil { return nil, err } - meta.apiURL = apiURL + meta.host = host - apiKey, err := GetFromAuthOrMeta(config, "apiKey") + token, err := GetFromAuthOrMeta(config, "token") if err != nil { return nil, err } - meta.apiKey = apiKey + meta.token = token if val, ok := config.TriggerMetadata["metricsSelector"]; ok && val != "" { meta.metricsSelector = val @@ -91,6 +98,13 @@ func parseDynatraceMetadata(config *scalersconfig.ScalerConfig) (*dynatraceMetad return nil, fmt.Errorf("no metricsSelector given") } + if val, ok := config.TriggerMetadata["from"]; ok && val != "" { + meta.fromTimestamp = val + } else { + logger.Info("no 'from' timestamp provided, using default value (last 2 hours)") + meta.fromTimestamp = "now-2h" + } + if val, ok := config.TriggerMetadata["threshold"]; ok && val != "" { t, err := strconv.ParseFloat(val, 64) if err != nil { @@ -147,13 +161,24 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { var req *http.Request var err error - req, err = http.NewRequestWithContext(ctx, "GET", s.metadata.apiURL, nil) + // Append host information to appropriate API endpoint + // Trailing slashes are removed from provided host information to avoid double slashes in the URL + dynatraceAPIURL := fmt.Sprintf("%s/%s", strings.TrimRight(s.metadata.host, "/"), dynatraceMetricDataPointsAPI) + + // Add query parameters to the URL + url, _ := neturl.Parse(dynatraceAPIURL) + queryString := url.Query() + queryString.Set("metricSelector", s.metadata.metricsSelector) + queryString.Set("from", s.metadata.fromTimestamp) + url.RawQuery = queryString.Encode() + + req, err = http.NewRequestWithContext(ctx, "GET", url.String(), nil) if err != nil { return 0, err } // Authentication header as per https://docs.dynatrace.com/docs/dynatrace-api/basics/dynatrace-api-authentication#authenticate - req.Header.Add("Authorization", fmt.Sprintf("Api-Token %s", s.metadata.apiKey)) + req.Header.Add("Authorization", fmt.Sprintf("Api-Token %s", s.metadata.token)) /* * Execute request From 0456bc7e4820ef8e31061c46ed5a6b2458155bc2 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:49:06 +0100 Subject: [PATCH 04/26] Add parsing tests Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 10 ++-- pkg/scalers/dynatrace_scaler_test.go | 76 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 pkg/scalers/dynatrace_scaler_test.go diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index 9f9d708f092..ad981b5ab44 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -33,7 +33,7 @@ type dynatraceScaler struct { type dynatraceMetadata struct { host string token string - metricsSelector string + metricSelector string fromTimestamp string threshold float64 activationThreshold float64 @@ -92,10 +92,10 @@ func parseDynatraceMetadata(config *scalersconfig.ScalerConfig, logger logr.Logg } meta.token = token - if val, ok := config.TriggerMetadata["metricsSelector"]; ok && val != "" { - meta.metricsSelector = val + if val, ok := config.TriggerMetadata["metricSelector"]; ok && val != "" { + meta.metricSelector = val } else { - return nil, fmt.Errorf("no metricsSelector given") + return nil, fmt.Errorf("no metricSelector given") } if val, ok := config.TriggerMetadata["from"]; ok && val != "" { @@ -168,7 +168,7 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { // Add query parameters to the URL url, _ := neturl.Parse(dynatraceAPIURL) queryString := url.Query() - queryString.Set("metricSelector", s.metadata.metricsSelector) + queryString.Set("metricSelector", s.metadata.metricSelector) queryString.Set("from", s.metadata.fromTimestamp) url.RawQuery = queryString.Encode() diff --git a/pkg/scalers/dynatrace_scaler_test.go b/pkg/scalers/dynatrace_scaler_test.go new file mode 100644 index 00000000000..c87d998a31e --- /dev/null +++ b/pkg/scalers/dynatrace_scaler_test.go @@ -0,0 +1,76 @@ +package scalers + +import ( + "context" + "fmt" + "testing" + + "github.com/go-logr/logr" + + "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" +) + +type dynatraceMetadataTestData struct { + metadata map[string]string + authParams map[string]string + errorCase bool +} + +type dynatraceMetricIdentifier struct { + metadataTestData *dynatraceMetadataTestData + triggerIndex int + name string +} + +var testDynatraceMetadata = []dynatraceMetadataTestData{ + {map[string]string{}, map[string]string{}, true}, + // all properly formed + {map[string]string{"host": "http://dummy:1234", "token": "dummy", "threshold": "100", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{}, false}, + // host & token passed via auth params + {map[string]string{"threshold": "100", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, false}, + // malformed threshold + {map[string]string{"threshold": "abc", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, true}, + // malformed activationThreshold + {map[string]string{"activationThreshold": "abc", "threshold": "100", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, true}, + // missing threshold + {map[string]string{"metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, true}, + // missing metricsSelector + {map[string]string{"threshold": "100"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, true}, +} + +var dynatraceMetricIdentifiers = []dynatraceMetricIdentifier{ + {&testDynatraceMetadata[1], 0, "s0-dynatrace"}, + {&testDynatraceMetadata[1], 1, "s1-dynatrace"}, +} + +func TestDynatraceParseMetadata(t *testing.T) { + for _, testData := range testDynatraceMetadata { + _, err := parseDynatraceMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams}, logr.Discard()) + if err != nil && !testData.errorCase { + fmt.Printf("X: %s", testData.metadata) + t.Error("Expected success but got error", err) + } + if testData.errorCase && err == nil { + fmt.Printf("X: %s", testData.metadata) + t.Error("Expected error but got success") + } + } +} +func TestDynatraceGetMetricSpecForScaling(t *testing.T) { + for _, testData := range dynatraceMetricIdentifiers { + meta, err := parseDynatraceMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}, logr.Discard()) + if err != nil { + t.Fatal("Could not parse metadata:", err) + } + mockNewRelicScaler := dynatraceScaler{ + metadata: meta, + httpClient: nil, + } + + metricSpec := mockNewRelicScaler.GetMetricSpecForScaling(context.Background()) + metricName := metricSpec[0].External.Metric.Name + if metricName != testData.name { + t.Error("Wrong External metric source name:", metricName) + } + } +} From 402e2e8782a41fc1505049546782120e4890906f Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:05:32 +0100 Subject: [PATCH 05/26] update changelog Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f1dc1c5fc..d5521a39d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio ### New -- **General**: TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX)) +- Add Dynatrace Scaler ([#2387](https://github.com/kedacore/keda/pull/5685)) #### Experimental From 2583560adc1e6c74bbfe706cf9e6f3ea1d284b45 Mon Sep 17 00:00:00 2001 From: damas <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 22:15:43 +0100 Subject: [PATCH 06/26] Update CHANGELOG.md Signed-off-by: damas <19289022+cyrilico@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dae64e0ba93..bcd3f02bea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio ### New -- **General**: Add Dynatrace Scaler ([#5865](https://github.com/kedacore/keda/pull/5685)) +- **General**: Add Dynatrace Scaler ([#5685](https://github.com/kedacore/keda/pull/5685)) - **General**: Provide capability to filter CloudEvents ([#3533](https://github.com/kedacore/keda/issues/3533)) - **NATS Scaler**: Add TLS authentication ([#2296](https://github.com/kedacore/keda/issues/2296)) From f7661de2fa6d28e98c7553fa30ea9fecd858d173 Mon Sep 17 00:00:00 2001 From: damas <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:09:16 +0100 Subject: [PATCH 07/26] Update values type to float64 Signed-off-by: damas <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index ad981b5ab44..603e1a7e6b7 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -45,7 +45,7 @@ type dynatraceMetadata struct { type dynatraceResponse struct { Result []struct { Data []struct { - Values []int `json:"values"` + Values []float64 `json:"values"` } `json:"data"` } `json:"response"` } From 3b4ca9dd0995a9287123a088a907d0236ef713ab Mon Sep 17 00:00:00 2001 From: damas <19289022+cyrilico@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:16:53 +0100 Subject: [PATCH 08/26] Remove unnecessary conversion Signed-off-by: damas <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index 603e1a7e6b7..a3393ce5f40 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -212,7 +212,7 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { return 0, err } - return float64(dynatraceResponse.Result[0].Data[0].Values[0]), nil + return dynatraceResponse.Result[0].Data[0].Values[0], nil } func (s *dynatraceScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { From 4e3d039bc8c6326c30aef55f77debd90f664cbb1 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Tue, 23 Apr 2024 18:38:54 +0100 Subject: [PATCH 09/26] e2e tests Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 2 +- tests/scalers/dynatrace/dynatrace_test.go | 337 ++++++++++++++++++++++ 2 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 tests/scalers/dynatrace/dynatrace_test.go diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index a3393ce5f40..b6c6897a453 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -65,7 +65,7 @@ func NewDynatraceScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false) - logMsg := fmt.Sprintf("Initializing Dynatrace Scaler (API URL: %s)", meta.host) + logMsg := fmt.Sprintf("Initializing Dynatrace Scaler (Host: %s)", meta.host) logger.Info(logMsg) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go new file mode 100644 index 00000000000..07dd11dbf79 --- /dev/null +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -0,0 +1,337 @@ +//go:build e2e +// +build e2e + +package dynatrace_test + +import ( + "encoding/base64" + "fmt" + "os" + "testing" + + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../.env") + +const ( + testName = "dynatrace-test" +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + monitoredDeploymentName = fmt.Sprintf("%s-monitored-deployment", testName) + serviceName = fmt.Sprintf("%s-service-%d", testName, GetRandomNumber()) + scaledObjectName = fmt.Sprintf("%s-so", testName) + secretName = fmt.Sprintf("%s-secret", testName) + triggerAuthName = fmt.Sprintf("%s-ta", testName) + dynatraceHost = os.Getenv("DYNATRACE_HOST") + dynatraceToken = os.Getenv("DYNATRACE_METRICS_TOKEN") + kubernetesClusterName = "keda-dynatrace-cluster" + deploymentReplicas = 1 + minReplicaCount = 0 + maxReplicaCount = 2 +) + +type templateData struct { + TestNamespace string + DeploymentName string + MonitoredDeploymentName string + ServiceName string + ScaledObjectName string + TriggerAuthName string + SecretName string + DynatraceToken string + DeploymentReplicas string + DynatraceHost string + KubernetesClusterName string + MinReplicaCount string + MaxReplicaCount string +} + +const ( + dynakubeSecretTemplate = `apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + apiToken: {{.DynatraceToken}} + dataIngestToken: {{.DynatraceToken}} +` + + dynakubeTemplate = `apiVersion: dynatrace.com/v1beta1 +kind: DynaKube +metadata: +name: {{.KubernetesClusterName}} +namespace: {{.TestNamespace}} +spec: + apiUrl: {{.DynatraceHost}}/api + + networkZone: {{.KubernetesClusterName}} + + oneAgent: + cloudNativeFullStack: + args: + - --set-host-group={{.KubernetesClusterName}} + + activeGate: + capabilities: + - routing + - dynatrace-api + - metrics-ingest + + group: {{.KubernetesClusterName}} +` + secretTemplate = `apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + token: {{.DynatraceToken}} +` + + triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + secretTargetRef: + - parameter: token + name: {{.SecretName}} + key: token +` + + monitoredDeploymentTemplate = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.MonitoredDeploymentName}} + namespace: {{.TestNamespace}} + labels: + app: {{.MonitoredDeploymentName}} +spec: + replicas: {{.DeploymentReplicas}} + selector: + matchLabels: + app: {{.MonitoredDeploymentName}} + template: + metadata: + labels: + app: {{.MonitoredDeploymentName}} + spec: + containers: + - name: prom-test-app + image: tbickford/simple-web-app-prometheus:a13ade9 + imagePullPolicy: IfNotPresent +` + + deploymentTemplate = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + replicas: 0 + selector: + matchLabels: + app: {{.DeploymentName}} + template: + metadata: + annotations: + data-ingest.dynatrace.com/inject: "true" + dynatrace.com/inject: "true" + oneagent.dynatrace.com/inject: "true" + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: prom-test-app + image: tbickford/simple-web-app-prometheus:a13ade9 + imagePullPolicy: IfNotPresent +` + + serviceTemplate = `apiVersion: v1 +kind: Service +metadata: + labels: + name: {{.ServiceName}} + annotations: + prometheus.io/scrape: "true" + name: {{.ServiceName}} + namespace: {{.TestNamespace}} +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: {{.MonitoredDeploymentName}} + ` + + scaledObjectTemplate = `apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + minReplicaCount: {{.MinReplicaCount}} + maxReplicaCount: {{.MaxReplicaCount}} + pollingInterval: 1 + cooldownPeriod: 1 + advanced: + horizontalPodAutoscalerConfig: + behavior: + scaleDown: + stabilizationWindowSeconds: 10 + triggers: + - type: dynatrace + metadata: + host: "{{.DynatraceHost}}" + threshold: "2" + activationThreshold: "3" + metricSelector: builtin:service.requestCount.total:splitBy():fold + from: now-2m + authenticationRef: + name: {{.TriggerAuthName}} +` + + lightLoadTemplate = `apiVersion: v1 +kind: Pod +metadata: + name: fake-light-traffic + namespace: {{.TestNamespace}} +spec: + containers: + - image: busybox + name: test + command: ["/bin/sh"] + args: ["-c", "while true; do wget -O /dev/null -o /dev/null http://{{.ServiceName}}/; sleep 0.5; done"]` + + heavyLoadTemplate = `apiVersion: v1 +kind: Pod +metadata: + name: fake-heavy-traffic + namespace: {{.TestNamespace}} +spec: + containers: + - image: busybox + name: test + command: ["/bin/sh"] + args: ["-c", "while true; do wget -O /dev/null -o /dev/null http://{{.ServiceName}}/; sleep 0.1; done"]` +) + +func TestDynatraceScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + require.NotEmpty(t, dynatraceToken, "DYNATRACE_METRICS_TOKEN env variable is required for dynatrace tests") + require.NotEmpty(t, dynatraceHost, "DYNATRACE_HOST env variable is required for dynatrace tests") + + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + t.Cleanup(func() { + DeleteKubernetesResources(t, testNamespace, data, templates) + }) + + // Create kubernetes resources + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + installDynatrace(t) + + data, templates = getDynatraceTemplateData() + // Create Dynatrace-specific kubernetes resources + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), + "replica count should be %s after a minute", minReplicaCount) + + // test scaling + testActivation(t, kc, data) + testScaleOut(t, kc, data) + testScaleIn(t, kc, data) +} + +func testActivation(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing activation ---") + KubectlApplyWithTemplate(t, data, "lightLoadTemplate", lightLoadTemplate) + + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, minReplicaCount, 60) +} + +func testScaleOut(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing scale out ---") + KubectlApplyWithTemplate(t, data, "heavyLoadTemplate", heavyLoadTemplate) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 3), + "replica count should be %d after 3 minutes", maxReplicaCount) +} + +func testScaleIn(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing scale in ---") + KubectlDeleteWithTemplate(t, data, "lightLoadTemplate", lightLoadTemplate) + KubectlDeleteWithTemplate(t, data, "heavyLoadTemplate", heavyLoadTemplate) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 3), + "replica count should be %d after 3 minutes", minReplicaCount) +} + +func installDynatrace(t *testing.T) { + cmd := fmt.Sprintf(`helm upgrade dynatrace-operator oci://public.ecr.aws/dynatrace/dynatrace-operator --atomic --install --set platform=kubernetes --timeout 600s --namespace %s`, + testNamespace) + + _, err := ExecuteCommand(cmd) + require.NoErrorf(t, err, "cannot execute command - %s", err) +} + +func getDynatraceTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + SecretName: secretName, + DynatraceHost: dynatraceHost, + DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), + KubernetesClusterName: kubernetesClusterName, + }, []Template{ + {Name: "dynakubeSecretTemplate", Config: dynakubeSecretTemplate}, + {Name: "dynakubeTemplate", Config: dynakubeTemplate}, + } +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + DeploymentName: deploymentName, + MonitoredDeploymentName: monitoredDeploymentName, + ServiceName: serviceName, + TriggerAuthName: triggerAuthName, + ScaledObjectName: scaledObjectName, + SecretName: secretName, + KubernetesClusterName: kubernetesClusterName, + MinReplicaCount: fmt.Sprintf("%v", minReplicaCount), + MaxReplicaCount: fmt.Sprintf("%v", maxReplicaCount), + DeploymentReplicas: fmt.Sprintf("%v", deploymentReplicas), + DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), + DynatraceHost: dynatraceHost, + }, []Template{ + {Name: "secretTemplate", Config: secretTemplate}, + {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, + {Name: "serviceTemplate", Config: serviceTemplate}, + {Name: "monitoredDeploymentTemplate", Config: monitoredDeploymentTemplate}, + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + } +} From 911b0df7bd2585a79a607e433eb06509d5243012 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:48:46 +0100 Subject: [PATCH 10/26] Apply suggestions from code review Co-authored-by: Jorge Turrado Ferrero Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 44 +++++++++++------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index 07dd11dbf79..ce985a6c13e 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -60,11 +60,11 @@ const ( dynakubeSecretTemplate = `apiVersion: v1 kind: Secret metadata: - name: {{.SecretName}} - namespace: {{.TestNamespace}} + name: {{.SecretName}} + namespace: {{.TestNamespace}} data: - apiToken: {{.DynatraceToken}} - dataIngestToken: {{.DynatraceToken}} + apiToken: {{.DynatraceToken}} + dataIngestToken: {{.DynatraceToken}} ` dynakubeTemplate = `apiVersion: dynatrace.com/v1beta1 @@ -73,22 +73,18 @@ metadata: name: {{.KubernetesClusterName}} namespace: {{.TestNamespace}} spec: - apiUrl: {{.DynatraceHost}}/api - - networkZone: {{.KubernetesClusterName}} - - oneAgent: - cloudNativeFullStack: - args: - - --set-host-group={{.KubernetesClusterName}} - - activeGate: - capabilities: - - routing - - dynatrace-api - - metrics-ingest - - group: {{.KubernetesClusterName}} + apiUrl: {{.DynatraceHost}}/api + networkZone: {{.KubernetesClusterName}} + oneAgent: + cloudNativeFullStack: + args: + - --set-host-group={{.KubernetesClusterName}} + activeGate: + capabilities: + - routing + - dynatrace-api + - metrics-ingest + group: {{.KubernetesClusterName}} ` secretTemplate = `apiVersion: v1 kind: Secret @@ -149,10 +145,10 @@ spec: app: {{.DeploymentName}} template: metadata: - annotations: - data-ingest.dynatrace.com/inject: "true" - dynatrace.com/inject: "true" - oneagent.dynatrace.com/inject: "true" + annotations: + data-ingest.dynatrace.com/inject: "true" + dynatrace.com/inject: "true" + oneagent.dynatrace.com/inject: "true" labels: app: {{.DeploymentName}} spec: From 698d6bef6df5e37f26a6deba80aa76340906581a Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:50:32 +0100 Subject: [PATCH 11/26] Update dynatrace_test.go Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index ce985a6c13e..4b74260e3d6 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -122,6 +122,10 @@ spec: app: {{.MonitoredDeploymentName}} template: metadata: + annotations: + data-ingest.dynatrace.com/inject: "true" + dynatrace.com/inject: "true" + oneagent.dynatrace.com/inject: "true" labels: app: {{.MonitoredDeploymentName}} spec: @@ -145,7 +149,7 @@ spec: app: {{.DeploymentName}} template: metadata: - annotations: + annotations: data-ingest.dynatrace.com/inject: "true" dynatrace.com/inject: "true" oneagent.dynatrace.com/inject: "true" @@ -202,8 +206,8 @@ spec: host: "{{.DynatraceHost}}" threshold: "2" activationThreshold: "3" - metricSelector: builtin:service.requestCount.total:splitBy():fold - from: now-2m + metricSelector: builtin:service.requestCount.total:splitBy():fold + from: now-2m authenticationRef: name: {{.TriggerAuthName}} ` From 622266ee1f2e09a6e4b636a4a2facaa379900949 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:13:55 +0100 Subject: [PATCH 12/26] Fix bad templating for e2e tests Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index 4b74260e3d6..07b2a955435 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -70,21 +70,21 @@ data: dynakubeTemplate = `apiVersion: dynatrace.com/v1beta1 kind: DynaKube metadata: -name: {{.KubernetesClusterName}} +name: {{.DeploymentName}} namespace: {{.TestNamespace}} spec: apiUrl: {{.DynatraceHost}}/api - networkZone: {{.KubernetesClusterName}} + networkZone: {{.DeploymentName}} oneAgent: cloudNativeFullStack: args: - - --set-host-group={{.KubernetesClusterName}} + - --set-host-group={{.DeploymentName}} activeGate: capabilities: - routing - dynatrace-api - metrics-ingest - group: {{.KubernetesClusterName}} + group: {{.DeploymentName}} ` secretTemplate = `apiVersion: v1 kind: Secret @@ -149,10 +149,6 @@ spec: app: {{.DeploymentName}} template: metadata: - annotations: - data-ingest.dynatrace.com/inject: "true" - dynatrace.com/inject: "true" - oneagent.dynatrace.com/inject: "true" labels: app: {{.DeploymentName}} spec: @@ -203,10 +199,10 @@ spec: triggers: - type: dynatrace metadata: - host: "{{.DynatraceHost}}" + host: {{.DynatraceHost}} threshold: "2" activationThreshold: "3" - metricSelector: builtin:service.requestCount.total:splitBy():fold + metricSelector: "builtin:service.requestCount.total:splitBy():fold" from: now-2m authenticationRef: name: {{.TriggerAuthName}} @@ -304,7 +300,7 @@ func getDynatraceTemplateData() (templateData, []Template) { SecretName: secretName, DynatraceHost: dynatraceHost, DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), - KubernetesClusterName: kubernetesClusterName, + DeploymentName: deploymentName, }, []Template{ {Name: "dynakubeSecretTemplate", Config: dynakubeSecretTemplate}, {Name: "dynakubeTemplate", Config: dynakubeTemplate}, From e0d42eb78f2ca7bd8dce75351b7342891a1af708 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:15:22 +0100 Subject: [PATCH 13/26] Revert unnecessary (?) template variable change Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index 07b2a955435..a31d5fa2965 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -70,21 +70,21 @@ data: dynakubeTemplate = `apiVersion: dynatrace.com/v1beta1 kind: DynaKube metadata: -name: {{.DeploymentName}} +name: {{.KubernetesClusterName}} namespace: {{.TestNamespace}} spec: - apiUrl: {{.DynatraceHost}}/api - networkZone: {{.DeploymentName}} + apiUrl: "{{.DynatraceHost}}/api" + networkZone: {{.KubernetesClusterName}} oneAgent: cloudNativeFullStack: args: - - --set-host-group={{.DeploymentName}} + - --set-host-group={{.KubernetesClusterName}} activeGate: capabilities: - routing - dynatrace-api - metrics-ingest - group: {{.DeploymentName}} + group: {{.KubernetesClusterName}} ` secretTemplate = `apiVersion: v1 kind: Secret @@ -300,7 +300,7 @@ func getDynatraceTemplateData() (templateData, []Template) { SecretName: secretName, DynatraceHost: dynatraceHost, DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), - DeploymentName: deploymentName, + KubernetesClusterName: kubernetesClusterName, }, []Template{ {Name: "dynakubeSecretTemplate", Config: dynakubeSecretTemplate}, {Name: "dynakubeTemplate", Config: dynakubeTemplate}, From 607805e4d765ee5c49c8217b443a00998d15f6e5 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Sun, 28 Apr 2024 23:20:05 +0200 Subject: [PATCH 14/26] Apply suggestions from code review Signed-off-by: Jorge Turrado Ferrero --- tests/scalers/dynatrace/dynatrace_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index a31d5fa2965..54a18c0cf60 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -202,8 +202,8 @@ spec: host: {{.DynatraceHost}} threshold: "2" activationThreshold: "3" - metricSelector: "builtin:service.requestCount.total:splitBy():fold" - from: now-2m + metricSelector: "builtin:service.requestCount.total:splitBy():fold" + from: now-2m authenticationRef: name: {{.TriggerAuthName}} ` @@ -252,7 +252,7 @@ func TestDynatraceScaler(t *testing.T) { data, templates = getDynatraceTemplateData() // Create Dynatrace-specific kubernetes resources - CreateKubernetesResources(t, kc, testNamespace, data, templates) + KubectlApplyMultipleWithTemplate(t, data, templates) assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), "replica count should be %s after a minute", minReplicaCount) From cde08d3fe02f6120bb3460ebda339e82eeeabbde Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Sun, 28 Apr 2024 23:51:56 +0200 Subject: [PATCH 15/26] Update tests/scalers/dynatrace/dynatrace_test.go Signed-off-by: Jorge Turrado Ferrero --- tests/scalers/dynatrace/dynatrace_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index 54a18c0cf60..bd686624606 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -101,11 +101,10 @@ metadata: name: {{.TriggerAuthName}} namespace: {{.TestNamespace}} spec: - secretTargetRef: secretTargetRef: - parameter: token name: {{.SecretName}} - key: token + key: apiToken ` monitoredDeploymentTemplate = `apiVersion: apps/v1 From 4ffad46963aab2a72103f1571195e38728255a7e Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:06:16 +0100 Subject: [PATCH 16/26] Do not allow token to be passed in scaledobject trigger Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 8 ++++---- pkg/scalers/dynatrace_scaler_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index b6c6897a453..3e2e1c070f3 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -86,11 +86,11 @@ func parseDynatraceMetadata(config *scalersconfig.ScalerConfig, logger logr.Logg } meta.host = host - token, err := GetFromAuthOrMeta(config, "token") - if err != nil { - return nil, err + if val, ok := config.AuthParams["token"]; ok && val != "" { + meta.token = val + } else { + return nil, fmt.Errorf("no token given in trigger auth") } - meta.token = token if val, ok := config.TriggerMetadata["metricSelector"]; ok && val != "" { meta.metricSelector = val diff --git a/pkg/scalers/dynatrace_scaler_test.go b/pkg/scalers/dynatrace_scaler_test.go index c87d998a31e..58e958ccbc6 100644 --- a/pkg/scalers/dynatrace_scaler_test.go +++ b/pkg/scalers/dynatrace_scaler_test.go @@ -25,8 +25,6 @@ type dynatraceMetricIdentifier struct { var testDynatraceMetadata = []dynatraceMetadataTestData{ {map[string]string{}, map[string]string{}, true}, // all properly formed - {map[string]string{"host": "http://dummy:1234", "token": "dummy", "threshold": "100", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{}, false}, - // host & token passed via auth params {map[string]string{"threshold": "100", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, false}, // malformed threshold {map[string]string{"threshold": "abc", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, true}, @@ -36,6 +34,8 @@ var testDynatraceMetadata = []dynatraceMetadataTestData{ {map[string]string{"metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, true}, // missing metricsSelector {map[string]string{"threshold": "100"}, map[string]string{"host": "http://dummy:1234", "token": "dummy"}, true}, + // missing token (must come from auth params) + {map[string]string{"token": "foo", "threshold": "100", "from": "now-3d", "metricSelector": "MyCustomEvent:filter(eq(\"someProperty\",\"someValue\")):count:splitBy(\"dt.entity.process_group\"):fold"}, map[string]string{"host": "http://dummy:1234"}, true}, } var dynatraceMetricIdentifiers = []dynatraceMetricIdentifier{ From f27579cc7201189821b751a19c05fc79f76da772 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:10:31 +0100 Subject: [PATCH 17/26] Remove bad secret, tweak dynakube test config Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index bd686624606..d7886c43380 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -57,22 +57,13 @@ type templateData struct { } const ( - dynakubeSecretTemplate = `apiVersion: v1 -kind: Secret -metadata: - name: {{.SecretName}} - namespace: {{.TestNamespace}} -data: - apiToken: {{.DynatraceToken}} - dataIngestToken: {{.DynatraceToken}} -` - dynakubeTemplate = `apiVersion: dynatrace.com/v1beta1 kind: DynaKube metadata: name: {{.KubernetesClusterName}} namespace: {{.TestNamespace}} spec: + tokens: {{.SecretName}} apiUrl: "{{.DynatraceHost}}/api" networkZone: {{.KubernetesClusterName}} oneAgent: @@ -92,7 +83,8 @@ metadata: name: {{.SecretName}} namespace: {{.TestNamespace}} data: - token: {{.DynatraceToken}} + apiToken: {{.DynatraceToken}} + dataIngestToken: {{.DynatraceToken}} ` triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 @@ -298,10 +290,8 @@ func getDynatraceTemplateData() (templateData, []Template) { TestNamespace: testNamespace, SecretName: secretName, DynatraceHost: dynatraceHost, - DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), KubernetesClusterName: kubernetesClusterName, }, []Template{ - {Name: "dynakubeSecretTemplate", Config: dynakubeSecretTemplate}, {Name: "dynakubeTemplate", Config: dynakubeTemplate}, } } From c2f9467214246d39e42f9facd524ff302f38e748 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:11:57 +0100 Subject: [PATCH 18/26] Rename property in response parsing Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index 3e2e1c070f3..1d253e5f51e 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -47,7 +47,7 @@ type dynatraceResponse struct { Data []struct { Values []float64 `json:"values"` } `json:"data"` - } `json:"response"` + } `json:"result"` } func NewDynatraceScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { From eb0b04abc285343c4fd32cdd27d2f43ce20c55a6 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Mon, 29 Apr 2024 21:44:26 +0200 Subject: [PATCH 19/26] Update tests/scalers/dynatrace/dynatrace_test.go Signed-off-by: Jorge Turrado Ferrero --- tests/scalers/dynatrace/dynatrace_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index d7886c43380..3c71ee705a4 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -60,8 +60,8 @@ const ( dynakubeTemplate = `apiVersion: dynatrace.com/v1beta1 kind: DynaKube metadata: -name: {{.KubernetesClusterName}} -namespace: {{.TestNamespace}} + name: {{.KubernetesClusterName}} + namespace: {{.TestNamespace}} spec: tokens: {{.SecretName}} apiUrl: "{{.DynatraceHost}}/api" From fb5ce7b259d038022d8e7df146c6798fe04182ca Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Mon, 27 May 2024 17:04:15 +0100 Subject: [PATCH 20/26] use new operator secret, update template variable naming Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index 3c71ee705a4..b9a0c082c2e 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -33,6 +33,7 @@ var ( secretName = fmt.Sprintf("%s-secret", testName) triggerAuthName = fmt.Sprintf("%s-ta", testName) dynatraceHost = os.Getenv("DYNATRACE_HOST") + dynatraceOperatorToken = os.Getenv("DYNATRACE_OPERATOR_TOKEN") dynatraceToken = os.Getenv("DYNATRACE_METRICS_TOKEN") kubernetesClusterName = "keda-dynatrace-cluster" deploymentReplicas = 1 @@ -49,6 +50,7 @@ type templateData struct { TriggerAuthName string SecretName string DynatraceToken string + DynatraceOperatorToken string DeploymentReplicas string DynatraceHost string KubernetesClusterName string @@ -84,7 +86,7 @@ metadata: namespace: {{.TestNamespace}} data: apiToken: {{.DynatraceToken}} - dataIngestToken: {{.DynatraceToken}} + dataIngestToken: {{.DynatraceOperatorToken}} ` triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 @@ -241,9 +243,9 @@ func TestDynatraceScaler(t *testing.T) { installDynatrace(t) - data, templates = getDynatraceTemplateData() + dynatraceConfigData, dynatraceConfigTemplates = getDynatraceTemplateData() // Create Dynatrace-specific kubernetes resources - KubectlApplyMultipleWithTemplate(t, data, templates) + KubectlApplyMultipleWithTemplate(t, dynatraceConfigData, dynatraceConfigTemplates) assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), "replica count should be %s after a minute", minReplicaCount) @@ -310,6 +312,7 @@ func getTemplateData() (templateData, []Template) { MaxReplicaCount: fmt.Sprintf("%v", maxReplicaCount), DeploymentReplicas: fmt.Sprintf("%v", deploymentReplicas), DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), + DynatraceOperatorToken: base64.StdEncoding.EncodeToString([]byte(dynatraceOperatorToken)), DynatraceHost: dynatraceHost, }, []Template{ {Name: "secretTemplate", Config: secretTemplate}, From 6e03f9b3b0a84314dc963c54adfc9009332f0d43 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Mon, 27 May 2024 17:30:55 +0100 Subject: [PATCH 21/26] forgotten correct variable definition Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index b9a0c082c2e..e617474312a 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -243,7 +243,7 @@ func TestDynatraceScaler(t *testing.T) { installDynatrace(t) - dynatraceConfigData, dynatraceConfigTemplates = getDynatraceTemplateData() + dynatraceConfigData, dynatraceConfigTemplates := getDynatraceTemplateData() // Create Dynatrace-specific kubernetes resources KubectlApplyMultipleWithTemplate(t, dynatraceConfigData, dynatraceConfigTemplates) From b40f11f7946ff657a477867fd3fc71b52cdcc092 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Sun, 21 Jul 2024 19:48:37 +0100 Subject: [PATCH 22/26] try default value in query for e2e tests Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 48 +++++++---------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index e617474312a..7ee6484f445 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -56,6 +56,7 @@ type templateData struct { KubernetesClusterName string MinReplicaCount string MaxReplicaCount string + QueryDefaultValue int } const ( @@ -195,37 +196,12 @@ spec: host: {{.DynatraceHost}} threshold: "2" activationThreshold: "3" - metricSelector: "builtin:service.requestCount.total:splitBy():fold" + metricSelector: "builtin:service.requestCount.total:splitBy():default({{.QueryDefaultValue}},always):fold(max)" from: now-2m authenticationRef: name: {{.TriggerAuthName}} ` - lightLoadTemplate = `apiVersion: v1 -kind: Pod -metadata: - name: fake-light-traffic - namespace: {{.TestNamespace}} -spec: - containers: - - image: busybox - name: test - command: ["/bin/sh"] - args: ["-c", "while true; do wget -O /dev/null -o /dev/null http://{{.ServiceName}}/; sleep 0.5; done"]` - - heavyLoadTemplate = `apiVersion: v1 -kind: Pod -metadata: - name: fake-heavy-traffic - namespace: {{.TestNamespace}} -spec: - containers: - - image: busybox - name: test - command: ["/bin/sh"] - args: ["-c", "while true; do wget -O /dev/null -o /dev/null http://{{.ServiceName}}/; sleep 0.1; done"]` -) - func TestDynatraceScaler(t *testing.T) { // setup t.Log("--- setting up ---") @@ -258,25 +234,28 @@ func TestDynatraceScaler(t *testing.T) { func testActivation(t *testing.T, kc *kubernetes.Clientset, data templateData) { t.Log("--- testing activation ---") - KubectlApplyWithTemplate(t, data, "lightLoadTemplate", lightLoadTemplate) + data.QueryDefaultValue = 2 + KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, minReplicaCount, 60) } func testScaleOut(t *testing.T, kc *kubernetes.Clientset, data templateData) { t.Log("--- testing scale out ---") - KubectlApplyWithTemplate(t, data, "heavyLoadTemplate", heavyLoadTemplate) + data.QueryDefaultValue = 10 + KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 3), - "replica count should be %d after 3 minutes", maxReplicaCount) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 2), + "replica count should be %d after 2 minutes", maxReplicaCount) } func testScaleIn(t *testing.T, kc *kubernetes.Clientset, data templateData) { t.Log("--- testing scale in ---") - KubectlDeleteWithTemplate(t, data, "lightLoadTemplate", lightLoadTemplate) - KubectlDeleteWithTemplate(t, data, "heavyLoadTemplate", heavyLoadTemplate) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 3), - "replica count should be %d after 3 minutes", minReplicaCount) + data.QueryDefaultValue = 0 + KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 2), + "replica count should be %d after 2 minutes", minReplicaCount) } func installDynatrace(t *testing.T) { @@ -314,6 +293,7 @@ func getTemplateData() (templateData, []Template) { DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), DynatraceOperatorToken: base64.StdEncoding.EncodeToString([]byte(dynatraceOperatorToken)), DynatraceHost: dynatraceHost, + QueryDefaultValue: 0, }, []Template{ {Name: "secretTemplate", Config: secretTemplate}, {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, From fd857af996975c3593cb6e94ed4db012cfc30140 Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Sun, 21 Jul 2024 19:57:38 +0100 Subject: [PATCH 23/26] fix missing closing parenthesis, bad indenting Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- tests/scalers/dynatrace/dynatrace_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index 7ee6484f445..b4e7e99e15a 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -201,6 +201,7 @@ spec: authenticationRef: name: {{.TriggerAuthName}} ` +) func TestDynatraceScaler(t *testing.T) { // setup @@ -293,7 +294,7 @@ func getTemplateData() (templateData, []Template) { DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), DynatraceOperatorToken: base64.StdEncoding.EncodeToString([]byte(dynatraceOperatorToken)), DynatraceHost: dynatraceHost, - QueryDefaultValue: 0, + QueryDefaultValue: 0, }, []Template{ {Name: "secretTemplate", Config: secretTemplate}, {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, From d6971dcc07da3d2c91e26e3c6c35293eec136009 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Thu, 25 Jul 2024 16:31:28 +0200 Subject: [PATCH 24/26] Update e2e test to use custom metrics Signed-off-by: Jorge Turrado --- tests/scalers/dynatrace/dynatrace_test.go | 232 ++++++++-------------- 1 file changed, 78 insertions(+), 154 deletions(-) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index b4e7e99e15a..96f57d036da 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -4,10 +4,13 @@ package dynatrace_test import ( + "bytes" "encoding/base64" "fmt" + "net/http" "os" "testing" + "time" "github.com/joho/godotenv" "github.com/stretchr/testify/assert" @@ -25,61 +28,33 @@ const ( ) var ( - testNamespace = fmt.Sprintf("%s-ns", testName) - deploymentName = fmt.Sprintf("%s-deployment", testName) - monitoredDeploymentName = fmt.Sprintf("%s-monitored-deployment", testName) - serviceName = fmt.Sprintf("%s-service-%d", testName, GetRandomNumber()) - scaledObjectName = fmt.Sprintf("%s-so", testName) - secretName = fmt.Sprintf("%s-secret", testName) - triggerAuthName = fmt.Sprintf("%s-ta", testName) - dynatraceHost = os.Getenv("DYNATRACE_HOST") - dynatraceOperatorToken = os.Getenv("DYNATRACE_OPERATOR_TOKEN") - dynatraceToken = os.Getenv("DYNATRACE_METRICS_TOKEN") - kubernetesClusterName = "keda-dynatrace-cluster" - deploymentReplicas = 1 - minReplicaCount = 0 - maxReplicaCount = 2 + testNamespace = fmt.Sprintf("%s-ns", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + secretName = fmt.Sprintf("%s-secret", testName) + triggerAuthName = fmt.Sprintf("%s-ta", testName) + dynatraceHost = os.Getenv("DYNATRACE_HOST") + dynatraceToken = os.Getenv("DYNATRACE_METRICS_TOKEN") + dynatraceInjestHost = fmt.Sprintf("%s/api/v2/metrics/ingest", dynatraceHost) + dynatraceMetricName = fmt.Sprintf("metric-%d", GetRandomNumber()) + minReplicaCount = 0 + maxReplicaCount = 2 ) type templateData struct { - TestNamespace string - DeploymentName string - MonitoredDeploymentName string - ServiceName string - ScaledObjectName string - TriggerAuthName string - SecretName string - DynatraceToken string - DynatraceOperatorToken string - DeploymentReplicas string - DynatraceHost string - KubernetesClusterName string - MinReplicaCount string - MaxReplicaCount string - QueryDefaultValue int + TestNamespace string + DeploymentName string + ScaledObjectName string + TriggerAuthName string + SecretName string + DynatraceToken string + DynatraceHost string + MinReplicaCount string + MaxReplicaCount string + MetricName string } const ( - dynakubeTemplate = `apiVersion: dynatrace.com/v1beta1 -kind: DynaKube -metadata: - name: {{.KubernetesClusterName}} - namespace: {{.TestNamespace}} -spec: - tokens: {{.SecretName}} - apiUrl: "{{.DynatraceHost}}/api" - networkZone: {{.KubernetesClusterName}} - oneAgent: - cloudNativeFullStack: - args: - - --set-host-group={{.KubernetesClusterName}} - activeGate: - capabilities: - - routing - - dynatrace-api - - metrics-ingest - group: {{.KubernetesClusterName}} -` secretTemplate = `apiVersion: v1 kind: Secret metadata: @@ -87,7 +62,6 @@ metadata: namespace: {{.TestNamespace}} data: apiToken: {{.DynatraceToken}} - dataIngestToken: {{.DynatraceOperatorToken}} ` triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 @@ -101,34 +75,6 @@ spec: name: {{.SecretName}} key: apiToken ` - - monitoredDeploymentTemplate = `apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{.MonitoredDeploymentName}} - namespace: {{.TestNamespace}} - labels: - app: {{.MonitoredDeploymentName}} -spec: - replicas: {{.DeploymentReplicas}} - selector: - matchLabels: - app: {{.MonitoredDeploymentName}} - template: - metadata: - annotations: - data-ingest.dynatrace.com/inject: "true" - dynatrace.com/inject: "true" - oneagent.dynatrace.com/inject: "true" - labels: - app: {{.MonitoredDeploymentName}} - spec: - containers: - - name: prom-test-app - image: tbickford/simple-web-app-prometheus:a13ade9 - imagePullPolicy: IfNotPresent -` - deploymentTemplate = `apiVersion: apps/v1 kind: Deployment metadata: @@ -152,25 +98,6 @@ spec: imagePullPolicy: IfNotPresent ` - serviceTemplate = `apiVersion: v1 -kind: Service -metadata: - labels: - name: {{.ServiceName}} - annotations: - prometheus.io/scrape: "true" - name: {{.ServiceName}} - namespace: {{.TestNamespace}} -spec: - ports: - - name: http - port: 80 - protocol: TCP - targetPort: 8080 - selector: - app: {{.MonitoredDeploymentName}} - ` - scaledObjectTemplate = `apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: @@ -196,7 +123,7 @@ spec: host: {{.DynatraceHost}} threshold: "2" activationThreshold: "3" - metricSelector: "builtin:service.requestCount.total:splitBy():default({{.QueryDefaultValue}},always):fold(max)" + metricSelector: "{{.MetricName}}:max" from: now-2m authenticationRef: name: {{.TriggerAuthName}} @@ -218,88 +145,85 @@ func TestDynatraceScaler(t *testing.T) { // Create kubernetes resources CreateKubernetesResources(t, kc, testNamespace, data, templates) - installDynatrace(t) - - dynatraceConfigData, dynatraceConfigTemplates := getDynatraceTemplateData() - // Create Dynatrace-specific kubernetes resources - KubectlApplyMultipleWithTemplate(t, dynatraceConfigData, dynatraceConfigTemplates) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), "replica count should be %s after a minute", minReplicaCount) // test scaling - testActivation(t, kc, data) - testScaleOut(t, kc, data) - testScaleIn(t, kc, data) + testActivation(t, kc) + testScaleOut(t, kc) + testScaleIn(t, kc) } -func testActivation(t *testing.T, kc *kubernetes.Clientset, data templateData) { +func testActivation(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing activation ---") - data.QueryDefaultValue = 2 - KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) - - AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, minReplicaCount, 60) + stopCh := make(chan struct{}) + go setMetricValue(t, 1, stopCh) + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, minReplicaCount, 120) + close(stopCh) } -func testScaleOut(t *testing.T, kc *kubernetes.Clientset, data templateData) { +func testScaleOut(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale out ---") - data.QueryDefaultValue = 10 - KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) - - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 2), + stopCh := make(chan struct{}) + go setMetricValue(t, 10, stopCh) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 3), "replica count should be %d after 2 minutes", maxReplicaCount) + close(stopCh) } -func testScaleIn(t *testing.T, kc *kubernetes.Clientset, data templateData) { +func testScaleIn(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale in ---") - data.QueryDefaultValue = 0 - KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 2), + stopCh := make(chan struct{}) + go setMetricValue(t, 0, stopCh) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 3), "replica count should be %d after 2 minutes", minReplicaCount) + close(stopCh) } -func installDynatrace(t *testing.T) { - cmd := fmt.Sprintf(`helm upgrade dynatrace-operator oci://public.ecr.aws/dynatrace/dynatrace-operator --atomic --install --set platform=kubernetes --timeout 600s --namespace %s`, - testNamespace) - - _, err := ExecuteCommand(cmd) - require.NoErrorf(t, err, "cannot execute command - %s", err) -} - -func getDynatraceTemplateData() (templateData, []Template) { - return templateData{ - TestNamespace: testNamespace, - SecretName: secretName, - DynatraceHost: dynatraceHost, - KubernetesClusterName: kubernetesClusterName, - }, []Template{ - {Name: "dynakubeTemplate", Config: dynakubeTemplate}, +func setMetricValue(t *testing.T, value float64, stopCh <-chan struct{}) { + metric := fmt.Sprintf("%s %f", dynatraceMetricName, value) + for { + select { + case <-stopCh: + return + default: + time.Sleep(time.Second) + req, err := http.NewRequest("POST", dynatraceInjestHost, bytes.NewBufferString(metric)) + req.Header.Add("'Content-Type", "text/plain") + if err != nil { + t.Log("Invalid injection request") + continue + } + req.Header.Add("Authorization", fmt.Sprintf("Api-Token %s", dynatraceToken)) + r, err := http.DefaultClient.Do(req) + if err != nil { + t.Log("Error executing request") + continue + } + if r.StatusCode != http.StatusAccepted { + msg := fmt.Sprintf("%s: api returned %d", r.Request.URL.Path, r.StatusCode) + t.Log(msg) + } } + } } func getTemplateData() (templateData, []Template) { return templateData{ - TestNamespace: testNamespace, - DeploymentName: deploymentName, - MonitoredDeploymentName: monitoredDeploymentName, - ServiceName: serviceName, - TriggerAuthName: triggerAuthName, - ScaledObjectName: scaledObjectName, - SecretName: secretName, - KubernetesClusterName: kubernetesClusterName, - MinReplicaCount: fmt.Sprintf("%v", minReplicaCount), - MaxReplicaCount: fmt.Sprintf("%v", maxReplicaCount), - DeploymentReplicas: fmt.Sprintf("%v", deploymentReplicas), - DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), - DynatraceOperatorToken: base64.StdEncoding.EncodeToString([]byte(dynatraceOperatorToken)), - DynatraceHost: dynatraceHost, - QueryDefaultValue: 0, + TestNamespace: testNamespace, + DeploymentName: deploymentName, + TriggerAuthName: triggerAuthName, + ScaledObjectName: scaledObjectName, + SecretName: secretName, + MinReplicaCount: fmt.Sprintf("%v", minReplicaCount), + MaxReplicaCount: fmt.Sprintf("%v", maxReplicaCount), + DynatraceToken: base64.StdEncoding.EncodeToString([]byte(dynatraceToken)), + DynatraceHost: dynatraceHost, + MetricName: dynatraceMetricName, }, []Template{ {Name: "secretTemplate", Config: secretTemplate}, {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, - {Name: "serviceTemplate", Config: serviceTemplate}, - {Name: "monitoredDeploymentTemplate", Config: monitoredDeploymentTemplate}, {Name: "deploymentTemplate", Config: deploymentTemplate}, {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, } From 54428f7bbfa08b2d8ef10b85c2d5a6dc419747ea Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Thu, 25 Jul 2024 16:39:16 +0200 Subject: [PATCH 25/26] Close the body to fix static checks Signed-off-by: Jorge Turrado --- tests/scalers/dynatrace/dynatrace_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/scalers/dynatrace/dynatrace_test.go b/tests/scalers/dynatrace/dynatrace_test.go index 96f57d036da..7d20231a91a 100644 --- a/tests/scalers/dynatrace/dynatrace_test.go +++ b/tests/scalers/dynatrace/dynatrace_test.go @@ -201,6 +201,7 @@ func setMetricValue(t *testing.T, value float64, stopCh <-chan struct{}) { t.Log("Error executing request") continue } + defer r.Body.Close() if r.StatusCode != http.StatusAccepted { msg := fmt.Sprintf("%s: api returned %d", r.Request.URL.Path, r.StatusCode) t.Log(msg) From e92d83f4ae58cd8e3047e7632e611398c1e1bd3d Mon Sep 17 00:00:00 2001 From: cyrilico <19289022+cyrilico@users.noreply.github.com> Date: Tue, 30 Jul 2024 11:16:54 +0100 Subject: [PATCH 26/26] use declarative scaler config Signed-off-by: cyrilico <19289022+cyrilico@users.noreply.github.com> --- pkg/scalers/dynatrace_scaler.go | 87 +++++++--------------------- pkg/scalers/dynatrace_scaler_test.go | 6 +- 2 files changed, 22 insertions(+), 71 deletions(-) diff --git a/pkg/scalers/dynatrace_scaler.go b/pkg/scalers/dynatrace_scaler.go index 1d253e5f51e..76e2099eb3e 100644 --- a/pkg/scalers/dynatrace_scaler.go +++ b/pkg/scalers/dynatrace_scaler.go @@ -8,7 +8,6 @@ import ( "io" "net/http" neturl "net/url" - "strconv" "strings" "github.com/go-logr/logr" @@ -31,13 +30,13 @@ type dynatraceScaler struct { } type dynatraceMetadata struct { - host string - token string - metricSelector string - fromTimestamp string - threshold float64 - activationThreshold float64 - triggerIndex int + Host string `keda:"name=host, order=triggerMetadata;authParams"` + Token string `keda:"name=token, order=authParams"` + MetricSelector string `keda:"name=metricSelector, order=triggerMetadata"` + FromTimestamp string `keda:"name=from, order=triggerMetadata, default=now-2h, optional"` + Threshold float64 `keda:"name=threshold, order=triggerMetadata"` + ActivationThreshold float64 `keda:"name=activationThreshold, order=triggerMetadata, optional"` + TriggerIndex int } // Model of relevant part of Dynatrace's Metric Data Points API Response @@ -58,14 +57,14 @@ func NewDynatraceScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { logger := InitializeLogger(config, "dynatrace_scaler") - meta, err := parseDynatraceMetadata(config, logger) + meta, err := parseDynatraceMetadata(config) if err != nil { return nil, fmt.Errorf("error parsing dynatrace metadata: %w", err) } httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false) - logMsg := fmt.Sprintf("Initializing Dynatrace Scaler (Host: %s)", meta.host) + logMsg := fmt.Sprintf("Initializing Dynatrace Scaler (Host: %s)", meta.Host) logger.Info(logMsg) @@ -76,59 +75,13 @@ func NewDynatraceScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { logger: logger}, nil } -func parseDynatraceMetadata(config *scalersconfig.ScalerConfig, logger logr.Logger) (*dynatraceMetadata, error) { +func parseDynatraceMetadata(config *scalersconfig.ScalerConfig) (*dynatraceMetadata, error) { meta := dynatraceMetadata{} - var err error - - host, err := GetFromAuthOrMeta(config, "host") - if err != nil { - return nil, err - } - meta.host = host - - if val, ok := config.AuthParams["token"]; ok && val != "" { - meta.token = val - } else { - return nil, fmt.Errorf("no token given in trigger auth") - } - - if val, ok := config.TriggerMetadata["metricSelector"]; ok && val != "" { - meta.metricSelector = val - } else { - return nil, fmt.Errorf("no metricSelector given") - } - - if val, ok := config.TriggerMetadata["from"]; ok && val != "" { - meta.fromTimestamp = val - } else { - logger.Info("no 'from' timestamp provided, using default value (last 2 hours)") - meta.fromTimestamp = "now-2h" - } - if val, ok := config.TriggerMetadata["threshold"]; ok && val != "" { - t, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("error parsing threshold") - } - meta.threshold = t - } else { - if config.AsMetricSource { - meta.threshold = 0 - } else { - return nil, fmt.Errorf("missing threshold value") - } - } - - meta.activationThreshold = 0 - if val, ok := config.TriggerMetadata["activationThreshold"]; ok { - activationThreshold, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("queryValue parsing error %w", err) - } - meta.activationThreshold = activationThreshold + meta.TriggerIndex = config.TriggerIndex + if err := config.TypedConfig(&meta); err != nil { + return nil, fmt.Errorf("error parsing dynatrace metadata: %w", err) } - - meta.triggerIndex = config.TriggerIndex return &meta, nil } @@ -163,13 +116,13 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { // Append host information to appropriate API endpoint // Trailing slashes are removed from provided host information to avoid double slashes in the URL - dynatraceAPIURL := fmt.Sprintf("%s/%s", strings.TrimRight(s.metadata.host, "/"), dynatraceMetricDataPointsAPI) + dynatraceAPIURL := fmt.Sprintf("%s/%s", strings.TrimRight(s.metadata.Host, "/"), dynatraceMetricDataPointsAPI) // Add query parameters to the URL url, _ := neturl.Parse(dynatraceAPIURL) queryString := url.Query() - queryString.Set("metricSelector", s.metadata.metricSelector) - queryString.Set("from", s.metadata.fromTimestamp) + queryString.Set("metricSelector", s.metadata.MetricSelector) + queryString.Set("from", s.metadata.FromTimestamp) url.RawQuery = queryString.Encode() req, err = http.NewRequestWithContext(ctx, "GET", url.String(), nil) @@ -178,7 +131,7 @@ func (s *dynatraceScaler) GetMetricValue(ctx context.Context) (float64, error) { } // Authentication header as per https://docs.dynatrace.com/docs/dynatrace-api/basics/dynatrace-api-authentication#authenticate - req.Header.Add("Authorization", fmt.Sprintf("Api-Token %s", s.metadata.token)) + req.Header.Add("Authorization", fmt.Sprintf("Api-Token %s", s.metadata.Token)) /* * Execute request @@ -225,15 +178,15 @@ func (s *dynatraceScaler) GetMetricsAndActivity(ctx context.Context, metricName metric := GenerateMetricInMili(metricName, val) - return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.activationThreshold, nil + return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.ActivationThreshold, nil } func (s *dynatraceScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString("dynatrace")), + Name: GenerateMetricNameWithIndex(s.metadata.TriggerIndex, kedautil.NormalizeString("dynatrace")), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.threshold), + Target: GetMetricTargetMili(s.metricType, s.metadata.Threshold), } metricSpec := v2.MetricSpec{ External: externalMetric, Type: externalMetricType, diff --git a/pkg/scalers/dynatrace_scaler_test.go b/pkg/scalers/dynatrace_scaler_test.go index 58e958ccbc6..a29a3de56d6 100644 --- a/pkg/scalers/dynatrace_scaler_test.go +++ b/pkg/scalers/dynatrace_scaler_test.go @@ -5,8 +5,6 @@ import ( "fmt" "testing" - "github.com/go-logr/logr" - "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" ) @@ -45,7 +43,7 @@ var dynatraceMetricIdentifiers = []dynatraceMetricIdentifier{ func TestDynatraceParseMetadata(t *testing.T) { for _, testData := range testDynatraceMetadata { - _, err := parseDynatraceMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams}, logr.Discard()) + _, err := parseDynatraceMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams}) if err != nil && !testData.errorCase { fmt.Printf("X: %s", testData.metadata) t.Error("Expected success but got error", err) @@ -58,7 +56,7 @@ func TestDynatraceParseMetadata(t *testing.T) { } func TestDynatraceGetMetricSpecForScaling(t *testing.T) { for _, testData := range dynatraceMetricIdentifiers { - meta, err := parseDynatraceMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}, logr.Discard()) + meta, err := parseDynatraceMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}) if err != nil { t.Fatal("Could not parse metadata:", err) }