From 0b61f9740343b4567cb5c6ba4f897b7d6522c271 Mon Sep 17 00:00:00 2001 From: Rob Pickerill Date: Wed, 3 Apr 2024 20:02:01 +0100 Subject: [PATCH] add e2e for error state for cw, and improve e2e for min values for cw Signed-off-by: Rob Pickerill --- pkg/scalers/aws_cloudwatch_scaler.go | 12 +- pkg/scalers/aws_cloudwatch_scaler_test.go | 674 ++++++++++-------- tests/helper/helper.go | 62 +- .../aws_cloudwatch_error_null_values_test.go | 245 +++++++ ...s_cloudwatch_min_value_null_values_test.go | 243 +++++++ 5 files changed, 925 insertions(+), 311 deletions(-) create mode 100644 tests/scalers/aws/aws_cloudwatch_error_null_values/aws_cloudwatch_error_null_values_test.go create mode 100644 tests/scalers/aws/aws_cloudwatch_min_value_null_values/aws_cloudwatch_min_value_null_values_test.go diff --git a/pkg/scalers/aws_cloudwatch_scaler.go b/pkg/scalers/aws_cloudwatch_scaler.go index b9ce7e3ddda..9a78a9d8924 100644 --- a/pkg/scalers/aws_cloudwatch_scaler.go +++ b/pkg/scalers/aws_cloudwatch_scaler.go @@ -117,7 +117,6 @@ func getFloatMetadataValue(metadata map[string]string, key string, required bool func createCloudwatchClient(ctx context.Context, metadata *awsCloudwatchMetadata) (*cloudwatch.Client, error) { cfg, err := awsutils.GetAwsConfig(ctx, metadata.awsRegion, metadata.awsAuthorization) - if err != nil { return nil, err } @@ -308,7 +307,6 @@ func computeQueryWindow(current time.Time, metricPeriodSec, metricEndTimeOffsetS func (s *awsCloudwatchScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { metricValue, err := s.GetCloudwatchMetrics(ctx) - if err != nil { s.logger.Error(err, "Error getting metric value") return []external_metrics.ExternalMetricValue{}, false, err @@ -391,7 +389,6 @@ func (s *awsCloudwatchScaler) GetCloudwatchMetrics(ctx context.Context) (float64 } output, err := s.cwClient.GetMetricData(ctx, &input) - if err != nil { s.logger.Error(err, "Failed to get output") return -1, err @@ -400,10 +397,11 @@ func (s *awsCloudwatchScaler) GetCloudwatchMetrics(ctx context.Context) (float64 s.logger.V(1).Info("Received Metric Data", "data", output) var metricValue float64 - // If no metric data is received and errorWhenNullValues is set to true, return error, - // otherwise continue with either the metric value received, or the minMetricValue - if len(output.MetricDataResults) == 0 && s.metadata.errorWhenNullValues { - emptyMetricsErrMessage := "empty result of metric values received, and errorWhenNullValues is set to true" + fmt.Printf("output.MetricDataResults: %+v", output.MetricDataResults) + + // If no metric data is received and errorWhenNullValues is set to true, return an error + if len(output.MetricDataResults) > 0 && len(output.MetricDataResults[0].Values) == 0 && s.metadata.errorWhenNullValues { + emptyMetricsErrMessage := "empty metric data received, and errorWhenNullValues is set to true, returning error" s.logger.Error(err, emptyMetricsErrMessage) return -1, fmt.Errorf(emptyMetricsErrMessage) } diff --git a/pkg/scalers/aws_cloudwatch_scaler_test.go b/pkg/scalers/aws_cloudwatch_scaler_test.go index 473318a8250..99149541d50 100644 --- a/pkg/scalers/aws_cloudwatch_scaler_test.go +++ b/pkg/scalers/aws_cloudwatch_scaler_test.go @@ -22,6 +22,7 @@ const ( testAWSCloudwatchSessionToken = "none" testAWSCloudwatchErrorMetric = "Error" testAWSCloudwatchNoValueMetric = "NoValue" + testAWSCloudwatchEmptyValues = "EmptyValues" ) var testAWSCloudwatchResolvedEnv = map[string]string{ @@ -50,354 +51,433 @@ type awsCloudwatchMetricIdentifier struct { var testAWSCloudwatchMetadata = []parseAWSCloudwatchMetadataTestData{ {map[string]string{}, testAWSAuthentication, true, "Empty structures"}, // properly formed cloudwatch query and awsRegion - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "activationTargetMetricValue": "0", - "minMetricValue": "0", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "activationTargetMetricValue": "0", + "minMetricValue": "0", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "properly formed cloudwatch query and awsRegion"}, + "properly formed cloudwatch query and awsRegion", + }, // properly formed cloudwatch expression query and awsRegion - {map[string]string{ - "namespace": "AWS/SQS", - "expression": "SELECT MIN(MessageCount) FROM \"AWS/AmazonMQ\" WHERE Broker = 'production' and Queue = 'worker'", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "activationTargetMetricValue": "0", - "minMetricValue": "0", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "expression": "SELECT MIN(MessageCount) FROM \"AWS/AmazonMQ\" WHERE Broker = 'production' and Queue = 'worker'", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "activationTargetMetricValue": "0", + "minMetricValue": "0", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "properly formed cloudwatch expression query and awsRegion"}, + "properly formed cloudwatch expression query and awsRegion", + }, // Properly formed cloudwatch query with optional parameters - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "activationTargetMetricValue": "0", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "Average", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1", - "awsEndpoint": "http://localhost:4566"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "activationTargetMetricValue": "0", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "Average", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + "awsEndpoint": "http://localhost:4566", + }, testAWSAuthentication, false, - "Properly formed cloudwatch query with optional parameters"}, + "Properly formed cloudwatch query with optional parameters", + }, // properly formed cloudwatch query but Region is empty - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "activationTargetMetricValue": "0", - "minMetricValue": "0", - "awsRegion": ""}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "activationTargetMetricValue": "0", + "minMetricValue": "0", + "awsRegion": "", + }, testAWSAuthentication, true, - "properly formed cloudwatch query but Region is empty"}, + "properly formed cloudwatch query but Region is empty", + }, // Missing namespace - {map[string]string{"dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "Missing namespace"}, + "Missing namespace", + }, // Missing dimensionName - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "Missing dimensionName"}, + "Missing dimensionName", + }, // Missing dimensionValue - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "Missing dimensionValue"}, + "Missing dimensionValue", + }, // Missing metricName - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "targetMetricValue": "2", - "minMetricValue": "0", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "targetMetricValue": "2", + "minMetricValue": "0", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "Missing metricName"}, + "Missing metricName", + }, // with static "aws_credentials" from TriggerAuthentication - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "Average", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "Average", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + }, map[string]string{ "awsAccessKeyId": testAWSCloudwatchAccessKeyID, "awsSecretAccessKey": testAWSCloudwatchSecretAccessKey, }, false, - "with AWS Credentials from TriggerAuthentication"}, + "with AWS Credentials from TriggerAuthentication", + }, // with temporary "aws_credentials" from TriggerAuthentication - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "Average", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "Average", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + }, map[string]string{ "awsAccessKeyId": testAWSCloudwatchAccessKeyID, "awsSecretAccessKey": testAWSCloudwatchSecretAccessKey, "awsSessionToken": testAWSCloudwatchSessionToken, }, false, - "with AWS Credentials from TriggerAuthentication"}, + "with AWS Credentials from TriggerAuthentication", + }, // with "aws_role" from TriggerAuthentication - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "Average", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "Average", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + }, map[string]string{ "awsRoleArn": testAWSCloudwatchRoleArn, }, false, - "with AWS Role from TriggerAuthentication"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "Average", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1", - "identityOwner": "operator"}, + "with AWS Role from TriggerAuthentication", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "Average", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + "identityOwner": "operator", + }, map[string]string{}, false, - "with AWS Role assigned on KEDA operator itself"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "a", - "metricStat": "Average", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1", - "identityOwner": "operator"}, + "with AWS Role assigned on KEDA operator itself", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "a", + "metricStat": "Average", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + "identityOwner": "operator", + }, map[string]string{}, true, - "if metricCollectionTime assigned with a string, need to be a number"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "Average", - "metricStatPeriod": "a", - "awsRegion": "eu-west-1", - "identityOwner": "operator"}, + "if metricCollectionTime assigned with a string, need to be a number", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "Average", + "metricStatPeriod": "a", + "awsRegion": "eu-west-1", + "identityOwner": "operator", + }, map[string]string{}, true, - "if metricStatPeriod assigned with a string, need to be a number"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStat": "Average", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1"}, + "if metricStatPeriod assigned with a string, need to be a number", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStat": "Average", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "Missing metricCollectionTime not generate error because will get the default value"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStatPeriod": "300", - "awsRegion": "eu-west-1"}, + "Missing metricCollectionTime not generate error because will get the default value", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStatPeriod": "300", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "Missing metricStat not generate error because will get the default value"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "Average", - "awsRegion": "eu-west-1"}, + "Missing metricStat not generate error because will get the default value", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "Average", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "Missing metricStatPeriod not generate error because will get the default value"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStat": "Average", - "metricUnit": "Count", - "metricEndTimeOffset": "60", - "awsRegion": "eu-west-1"}, + "Missing metricStatPeriod not generate error because will get the default value", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStat": "Average", + "metricUnit": "Count", + "metricEndTimeOffset": "60", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "set a supported metricUnit"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricCollectionTime": "300", - "metricStat": "SomeStat", - "awsRegion": "eu-west-1"}, + "set a supported metricUnit", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricCollectionTime": "300", + "metricStat": "SomeStat", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "metricStat is not supported"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStatPeriod": "300", - "metricCollectionTime": "100", - "metricStat": "Average", - "awsRegion": "eu-west-1"}, + "metricStat is not supported", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStatPeriod": "300", + "metricCollectionTime": "100", + "metricStat": "Average", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "metricCollectionTime smaller than metricStatPeriod"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStatPeriod": "250", - "metricStat": "Average", - "awsRegion": "eu-west-1"}, + "metricCollectionTime smaller than metricStatPeriod", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStatPeriod": "250", + "metricStat": "Average", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "unsupported metricStatPeriod"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStatPeriod": "25", - "metricStat": "Average", - "awsRegion": "eu-west-1"}, + "unsupported metricStatPeriod", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStatPeriod": "25", + "metricStat": "Average", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "unsupported metricStatPeriod"}, - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStatPeriod": "25", - "metricStat": "Average", - "metricUnit": "Hour", - "awsRegion": "eu-west-1"}, + "unsupported metricStatPeriod", + }, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStatPeriod": "25", + "metricStat": "Average", + "metricUnit": "Hour", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "unsupported metricUnit"}, + "unsupported metricUnit", + }, // test errorWhenNullValues is false - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStatPeriod": "60", - "metricStat": "Average", - "errorWhenNullValues": "false", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStatPeriod": "60", + "metricStat": "Average", + "errorWhenNullValues": "false", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "with errorWhenNullValues set to false"}, + "with errorWhenNullValues set to false", + }, // test errorWhenNullValues is true - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStatPeriod": "60", - "metricStat": "Average", - "errorWhenNullValues": "true", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStatPeriod": "60", + "metricStat": "Average", + "errorWhenNullValues": "true", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, false, - "with errorWhenNullValues set to true"}, + "with errorWhenNullValues set to true", + }, // test errorWhenNullValues is incorrect - {map[string]string{ - "namespace": "AWS/SQS", - "dimensionName": "QueueName", - "dimensionValue": "keda", - "metricName": "ApproximateNumberOfMessagesVisible", - "targetMetricValue": "2", - "minMetricValue": "0", - "metricStatPeriod": "60", - "metricStat": "Average", - "errorWhenNullValues": "maybe", - "awsRegion": "eu-west-1"}, + { + map[string]string{ + "namespace": "AWS/SQS", + "dimensionName": "QueueName", + "dimensionValue": "keda", + "metricName": "ApproximateNumberOfMessagesVisible", + "targetMetricValue": "2", + "minMetricValue": "0", + "metricStatPeriod": "60", + "metricStat": "Average", + "errorWhenNullValues": "maybe", + "awsRegion": "eu-west-1", + }, testAWSAuthentication, true, - "unsupported value for errorWhenNullValues"}, + "unsupported value for errorWhenNullValues", + }, } var awsCloudwatchMetricIdentifiers = []awsCloudwatchMetricIdentifier{ @@ -489,7 +569,7 @@ var awsCloudwatchGetMetricTestData = []awsCloudwatchMetadata{ // Test for metric with no data { namespace: "Custom", - metricsName: testAWSCloudwatchNoValueMetric, + metricsName: testAWSCloudwatchEmptyValues, dimensionName: []string{"DIM"}, dimensionValue: []string{"DIM_VALUE"}, targetMetricValue: 100, @@ -507,7 +587,7 @@ var awsCloudwatchGetMetricTestData = []awsCloudwatchMetadata{ // Test for metric with no data, and the scaler errors when metric data values are empty { namespace: "Custom", - metricsName: testAWSCloudwatchNoValueMetric, + metricsName: testAWSCloudwatchEmptyValues, dimensionName: []string{"DIM"}, dimensionValue: []string{"DIM_VALUE"}, targetMetricValue: 100, @@ -524,8 +604,7 @@ var awsCloudwatchGetMetricTestData = []awsCloudwatchMetadata{ }, } -type mockCloudwatch struct { -} +type mockCloudwatch struct{} func (m *mockCloudwatch) GetMetricData(_ context.Context, input *cloudwatch.GetMetricDataInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.GetMetricDataOutput, error) { if input.MetricDataQueries[0].MetricStat != nil { @@ -536,6 +615,14 @@ func (m *mockCloudwatch) GetMetricData(_ context.Context, input *cloudwatch.GetM return &cloudwatch.GetMetricDataOutput{ MetricDataResults: []types.MetricDataResult{}, }, nil + case testAWSCloudwatchEmptyValues: + return &cloudwatch.GetMetricDataOutput{ + MetricDataResults: []types.MetricDataResult{ + { + Values: []float64{}, + }, + }, + }, nil } } @@ -585,9 +672,10 @@ func TestAWSCloudwatchScalerGetMetrics(t *testing.T) { case testAWSCloudwatchErrorMetric: assert.Error(t, err, "expect error because of cloudwatch api error") case testAWSCloudwatchNoValueMetric: - // if errorWhenNullValues is defined, then an error is expected + assert.NoError(t, err, "dont expect error when returning empty metric list from cloudwatch") + case testAWSCloudwatchEmptyValues: if meta.errorWhenNullValues { - assert.Error(t, err, "expected an error when returning empty metric list from cloudwatch") + assert.Error(t, err, "expect error when returning empty metric list from cloudwatch, because errorWhenNullValues is true") } else { assert.NoError(t, err, "dont expect error when returning empty metric list from cloudwatch") } diff --git a/tests/helper/helper.go b/tests/helper/helper.go index bbb3b8124b1..4cee58ca0af 100644 --- a/tests/helper/helper.go +++ b/tests/helper/helper.go @@ -38,6 +38,7 @@ import ( "k8s.io/client-go/tools/remotecommand" "sigs.k8s.io/controller-runtime/pkg/client/config" + v1alpha1Api "github.com/kedacore/keda/v2/apis/keda/v1alpha1" "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/typed/keda/v1alpha1" ) @@ -309,17 +310,20 @@ func WaitForNamespaceDeletion(t *testing.T, nsName string) bool { } func WaitForScaledJobCount(t *testing.T, kc *kubernetes.Clientset, scaledJobName, namespace string, - target, iterations, intervalSeconds int) bool { + target, iterations, intervalSeconds int, +) bool { return waitForJobCount(t, kc, fmt.Sprintf("scaledjob.keda.sh/name=%s", scaledJobName), namespace, target, iterations, intervalSeconds) } func WaitForJobCount(t *testing.T, kc *kubernetes.Clientset, namespace string, - target, iterations, intervalSeconds int) bool { + target, iterations, intervalSeconds int, +) bool { return waitForJobCount(t, kc, "", namespace, target, iterations, intervalSeconds) } func waitForJobCount(t *testing.T, kc *kubernetes.Clientset, selector, namespace string, - target, iterations, intervalSeconds int) bool { + target, iterations, intervalSeconds int, +) bool { for i := 0; i < iterations; i++ { jobList, _ := kc.BatchV1().Jobs(namespace).List(context.Background(), metav1.ListOptions{ LabelSelector: selector, @@ -340,8 +344,9 @@ func waitForJobCount(t *testing.T, kc *kubernetes.Clientset, selector, namespace } func WaitForJobCountUntilIteration(t *testing.T, kc *kubernetes.Clientset, namespace string, - target, iterations, intervalSeconds int) bool { - var isTargetAchieved = false + target, iterations, intervalSeconds int, +) bool { + isTargetAchieved := false for i := 0; i < iterations; i++ { jobList, _ := kc.BatchV1().Jobs(namespace).List(context.Background(), metav1.ListOptions{}) @@ -364,7 +369,8 @@ func WaitForJobCountUntilIteration(t *testing.T, kc *kubernetes.Clientset, names // Waits until deployment count hits target or number of iterations are done. func WaitForPodCountInNamespace(t *testing.T, kc *kubernetes.Clientset, namespace string, - target, iterations, intervalSeconds int) bool { + target, iterations, intervalSeconds int, +) bool { for i := 0; i < iterations; i++ { pods, _ := kc.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{}) @@ -410,7 +416,8 @@ func WaitForAllPodRunningInNamespace(t *testing.T, kc *kubernetes.Clientset, nam // Waits until the Horizontal Pod Autoscaler for the scaledObject reports that it has metrics available // to calculate, or until the number of iterations are done, whichever happens first. func WaitForHPAMetricsToPopulate(t *testing.T, kc *kubernetes.Clientset, name, namespace string, - iterations, intervalSeconds int) bool { + iterations, intervalSeconds int, +) bool { totalWaitDuration := time.Duration(iterations) * time.Duration(intervalSeconds) * time.Second startedWaiting := time.Now() for i := 0; i < iterations; i++ { @@ -436,7 +443,8 @@ func WaitForHPAMetricsToPopulate(t *testing.T, kc *kubernetes.Clientset, name, n // Waits until deployment ready replica count hits target or number of iterations are done. func WaitForDeploymentReplicaReadyCount(t *testing.T, kc *kubernetes.Clientset, name, namespace string, - target, iterations, intervalSeconds int) bool { + target, iterations, intervalSeconds int, +) bool { for i := 0; i < iterations; i++ { deployment, _ := kc.AppsV1().Deployments(namespace).Get(context.Background(), name, metav1.GetOptions{}) replicas := deployment.Status.ReadyReplicas @@ -456,7 +464,8 @@ func WaitForDeploymentReplicaReadyCount(t *testing.T, kc *kubernetes.Clientset, // Waits until statefulset count hits target or number of iterations are done. func WaitForStatefulsetReplicaReadyCount(t *testing.T, kc *kubernetes.Clientset, name, namespace string, - target, iterations, intervalSeconds int) bool { + target, iterations, intervalSeconds int, +) bool { for i := 0; i < iterations; i++ { statefulset, _ := kc.AppsV1().StatefulSets(namespace).Get(context.Background(), name, metav1.GetOptions{}) replicas := statefulset.Status.ReadyReplicas @@ -518,7 +527,8 @@ func AssertReplicaCountNotChangeDuringTimePeriod(t *testing.T, kc *kubernetes.Cl } func WaitForHpaCreation(t *testing.T, kc *kubernetes.Clientset, name, namespace string, - iterations, intervalSeconds int) (*autoscalingv2.HorizontalPodAutoscaler, error) { + iterations, intervalSeconds int, +) (*autoscalingv2.HorizontalPodAutoscaler, error) { hpa := &autoscalingv2.HorizontalPodAutoscaler{} var err error for i := 0; i < iterations; i++ { @@ -754,7 +764,8 @@ func DeletePodsInNamespaceBySelector(t *testing.T, kc *kubernetes.Clientset, sel // Wait for Pods identified by selector to complete termination func WaitForPodsTerminated(t *testing.T, kc *kubernetes.Clientset, selector, namespace string, - iterations, intervalSeconds int) bool { + iterations, intervalSeconds int, +) bool { for i := 0; i < iterations; i++ { pods, err := kc.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: selector}) if (err != nil && errors.IsNotFound(err)) || len(pods.Items) == 0 { @@ -910,3 +921,32 @@ func CheckKubectlGetResult(t *testing.T, kind string, name string, namespace str unqoutedOutput := strings.ReplaceAll(string(output), "\"", "") assert.Equal(t, expected, unqoutedOutput) } + +// FailIfScaledObjectStatusNotReachedWithTimeout waits for the scaledobject to reach the desired state, within the timeout +// or fails the test if the timeout is reached. +func FailIfScaledObjectStatusNotReachedWithTimeout(t *testing.T, kedaClient *v1alpha1.KedaV1alpha1Client, namespace, scaledObjectName string, timeout time.Duration, desiredState v1alpha1Api.ConditionType) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for { + select { + case <-ctx.Done(): + t.Fatalf("timeout waiting for scaledobject to be in %s state", desiredState) + default: + scaledObject, err := kedaClient.ScaledObjects(namespace).Get(context.Background(), scaledObjectName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("error getting scaledobject: %s", err) + } + + conditions := scaledObject.Status.Conditions + + t.Logf("scaledobject status: %+v", conditions) + + if len(conditions) > 0 && conditions[len(conditions)-1].Type == desiredState { + return + } + + time.Sleep(10 * time.Second) + } + } +} diff --git a/tests/scalers/aws/aws_cloudwatch_error_null_values/aws_cloudwatch_error_null_values_test.go b/tests/scalers/aws/aws_cloudwatch_error_null_values/aws_cloudwatch_error_null_values_test.go new file mode 100644 index 00000000000..55ae762867a --- /dev/null +++ b/tests/scalers/aws/aws_cloudwatch_error_null_values/aws_cloudwatch_error_null_values_test.go @@ -0,0 +1,245 @@ +//go:build e2e +// +build e2e + +package aws_cloudwatch_error_null_metric_values_test + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" + "github.com/joho/godotenv" + v1alpha1Api "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + "github.com/stretchr/testify/assert" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "aws-cloudwatch-error-null-metrics-test" +) + +type templateData struct { + TestNamespace string + DeploymentName string + ScaledObjectName string + SecretName string + AwsAccessKeyID string + AwsSecretAccessKey string + AwsRegion string + CloudWatchMetricName string + CloudWatchMetricNamespace string + CloudWatchMetricDimensionName string + CloudWatchMetricDimensionValue string +} + +const ( + secretTemplate = `apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + AWS_ACCESS_KEY_ID: {{.AwsAccessKeyID}} + AWS_SECRET_ACCESS_KEY: {{.AwsSecretAccessKey}} +` + + triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: keda-trigger-auth-aws-credentials + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: awsAccessKeyID # Required. + name: {{.SecretName}} # Required. + key: AWS_ACCESS_KEY_ID # Required. + - parameter: awsSecretAccessKey # Required. + name: {{.SecretName}} # Required. + key: AWS_SECRET_ACCESS_KEY # Required. +` + + deploymentTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + replicas: 0 + selector: + matchLabels: + app: {{.DeploymentName}} + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: nginx + image: nginxinc/nginx-unprivileged + ports: + - containerPort: 80 +` + + scaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + maxReplicaCount: 2 + minReplicaCount: 0 + cooldownPeriod: 1 + triggers: + - type: aws-cloudwatch + authenticationRef: + name: keda-trigger-auth-aws-credentials + metadata: + awsRegion: {{.AwsRegion}} + namespace: {{.CloudWatchMetricNamespace}} + dimensionName: {{.CloudWatchMetricDimensionName}} + dimensionValue: {{.CloudWatchMetricDimensionValue}} + metricName: {{.CloudWatchMetricName}} + targetMetricValue: "1" + minMetricValue: "1" + errorWhenNullValues: "true" + metricCollectionTime: "120" + metricStatPeriod: "60" +` +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + secretName = fmt.Sprintf("%s-secret", testName) + cloudwatchMetricName = fmt.Sprintf("cw-%d", GetRandomNumber()) + awsAccessKeyID = os.Getenv("TF_AWS_ACCESS_KEY") + awsSecretAccessKey = os.Getenv("TF_AWS_SECRET_KEY") + awsRegion = os.Getenv("TF_AWS_REGION") + cloudwatchMetricNamespace = "DoesNotExist" + cloudwatchMetricDimensionName = "dimensionName" + cloudwatchMetricDimensionValue = "dimensionValue" + maxReplicaCount = 2 + minReplicaCount = 0 +) + +func TestCloudWatchScalerWithErrorWhenNullValues(t *testing.T) { + // setup cloudwatch + cloudwatchClient := createCloudWatchClient() + + // check that the metric in question is not already present, and is returning + // an empty set of values. + checkCloudWatchCustomMetric(t, cloudwatchClient) + + // Create kubernetes resources + kc := GetKubernetesClient(t) + kedaClient := GetKedaKubernetesClient(t) + data, templates := getTemplateData() + CreateKubernetesResources(t, kc, testNamespace, data, templates) + defer DeleteKubernetesResources(t, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), + "replica count should be %d after 1 minute", minReplicaCount) + + // check that the scaledobject is in paused state + FailIfScaledObjectStatusNotReachedWithTimeout(t, kedaClient, testNamespace, scaledObjectName, 2*time.Minute, v1alpha1Api.ConditionPaused) + + // check that the deployment did not scale, as the metric query is returning + // null values and the scaledobject is receiving errors, the deployment + // should not scale. + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), + "replica count should be %d after 1 minute", minReplicaCount) +} + +// checkCloudWatchCustomMetric will evaluate the custom metric for any metric values, if any +// values are found the test will be failed. +func checkCloudWatchCustomMetric(t *testing.T, cloudwatchClient *cloudwatch.Client) { + metricData, err := cloudwatchClient.GetMetricData(context.Background(), &cloudwatch.GetMetricDataInput{ + MetricDataQueries: []types.MetricDataQuery{ + { + Id: aws.String("m1"), + ReturnData: aws.Bool(true), + MetricStat: &types.MetricStat{ + Metric: &types.Metric{ + Namespace: aws.String(cloudwatchMetricNamespace), + MetricName: aws.String(cloudwatchMetricName), + Dimensions: []types.Dimension{ + { + Name: aws.String(cloudwatchMetricDimensionName), + Value: aws.String(cloudwatchMetricDimensionValue), + }, + }, + }, + Period: aws.Int32(60), + Stat: aws.String("Average"), + }, + }, + }, + // evaluate +/- 5 minutes from now to be sure we cover the query window + // leading into the e2e test. + EndTime: aws.Time(time.Now().Add(time.Minute * 5)), + StartTime: aws.Time(time.Now().Add(-time.Minute * 5)), + }) + if err != nil { + t.Fatalf("error checking cloudwatch metric: %s", err) + return + } + + // This is a e2e preflight check for returning an error when there are no + // metric values. If there are any metric values, then the test should fail + // here, as the scaler will never enter an error state if there are metric + // values in the query window. + if len(metricData.MetricDataResults) != 1 || len(metricData.MetricDataResults[0].Values) > 0 { + t.Fatalf("found unexpected metric data results for namespace: %s: %+v", cloudwatchMetricNamespace, metricData.MetricDataResults) + return + } +} + +func createCloudWatchClient() *cloudwatch.Client { + configOptions := make([]func(*config.LoadOptions) error, 0) + configOptions = append(configOptions, config.WithRegion(awsRegion)) + cfg, _ := config.LoadDefaultConfig(context.TODO(), configOptions...) + cfg.Credentials = credentials.NewStaticCredentialsProvider(awsAccessKeyID, awsSecretAccessKey, "") + return cloudwatch.NewFromConfig(cfg) +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + DeploymentName: deploymentName, + ScaledObjectName: scaledObjectName, + SecretName: secretName, + AwsAccessKeyID: base64.StdEncoding.EncodeToString([]byte(awsAccessKeyID)), + AwsSecretAccessKey: base64.StdEncoding.EncodeToString([]byte(awsSecretAccessKey)), + AwsRegion: awsRegion, + CloudWatchMetricName: cloudwatchMetricName, + CloudWatchMetricNamespace: cloudwatchMetricNamespace, + CloudWatchMetricDimensionName: cloudwatchMetricDimensionName, + CloudWatchMetricDimensionValue: cloudwatchMetricDimensionValue, + }, []Template{ + {Name: "secretTemplate", Config: secretTemplate}, + {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + } +} diff --git a/tests/scalers/aws/aws_cloudwatch_min_value_null_values/aws_cloudwatch_min_value_null_values_test.go b/tests/scalers/aws/aws_cloudwatch_min_value_null_values/aws_cloudwatch_min_value_null_values_test.go new file mode 100644 index 00000000000..7729075c4e9 --- /dev/null +++ b/tests/scalers/aws/aws_cloudwatch_min_value_null_values/aws_cloudwatch_min_value_null_values_test.go @@ -0,0 +1,243 @@ +//go:build e2e +// +build e2e + +package aws_cloudwatch_min_value_null_values_test + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" + "github.com/joho/godotenv" + v1alpha1Api "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + "github.com/stretchr/testify/assert" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "aws-cloudwatch-min-value-null-metrics-test" +) + +type templateData struct { + TestNamespace string + DeploymentName string + ScaledObjectName string + SecretName string + AwsAccessKeyID string + AwsSecretAccessKey string + AwsRegion string + CloudWatchMetricName string + CloudWatchMetricNamespace string + CloudWatchMetricDimensionName string + CloudWatchMetricDimensionValue string +} + +const ( + secretTemplate = `apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + AWS_ACCESS_KEY_ID: {{.AwsAccessKeyID}} + AWS_SECRET_ACCESS_KEY: {{.AwsSecretAccessKey}} +` + + triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: keda-trigger-auth-aws-credentials + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: awsAccessKeyID # Required. + name: {{.SecretName}} # Required. + key: AWS_ACCESS_KEY_ID # Required. + - parameter: awsSecretAccessKey # Required. + name: {{.SecretName}} # Required. + key: AWS_SECRET_ACCESS_KEY # Required. +` + + deploymentTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + replicas: 0 + selector: + matchLabels: + app: {{.DeploymentName}} + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: nginx + image: nginxinc/nginx-unprivileged + ports: + - containerPort: 80 +` + + scaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + maxReplicaCount: 2 + minReplicaCount: 0 + cooldownPeriod: 1 + triggers: + - type: aws-cloudwatch + authenticationRef: + name: keda-trigger-auth-aws-credentials + metadata: + awsRegion: {{.AwsRegion}} + namespace: {{.CloudWatchMetricNamespace}} + dimensionName: {{.CloudWatchMetricDimensionName}} + dimensionValue: {{.CloudWatchMetricDimensionValue}} + metricName: {{.CloudWatchMetricName}} + targetMetricValue: "1" + minMetricValue: "1" + metricCollectionTime: "120" + metricStatPeriod: "60" +` +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + secretName = fmt.Sprintf("%s-secret", testName) + cloudwatchMetricName = fmt.Sprintf("cw-%d", GetRandomNumber()) + awsAccessKeyID = os.Getenv("TF_AWS_ACCESS_KEY") + awsSecretAccessKey = os.Getenv("TF_AWS_SECRET_KEY") + awsRegion = os.Getenv("TF_AWS_REGION") + cloudwatchMetricNamespace = "DoesNotExist" + cloudwatchMetricDimensionName = "dimensionName" + cloudwatchMetricDimensionValue = "dimensionValue" + maxReplicaCount = 2 + minReplicaCount = 0 + minMetricValueReplicaCount = 1 +) + +func TestCloudWatchScalerWithMinValueWhenNullValues(t *testing.T) { + // setup cloudwatch + cloudwatchClient := createCloudWatchClient() + + // check that the metric in question is not already present, and is returning + // an empty set of values. + checkCloudWatchCustomMetric(t, cloudwatchClient) + + // Create kubernetes resources + kc := GetKubernetesClient(t) + kedaClient := GetKedaKubernetesClient(t) + data, templates := getTemplateData() + CreateKubernetesResources(t, kc, testNamespace, data, templates) + defer DeleteKubernetesResources(t, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), + "replica count should be %d after 1 minute", minReplicaCount) + + // check that the scaledobject is in paused state + FailIfScaledObjectStatusNotReachedWithTimeout(t, kedaClient, testNamespace, scaledObjectName, 2*time.Minute, v1alpha1Api.ConditionPaused) + + // check that the deployment scaled up to the minMetricValueReplicaCount + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minMetricValueReplicaCount, 60, 1), + "replica count should be %d after 1 minute", minMetricValueReplicaCount) +} + +func createCloudWatchClient() *cloudwatch.Client { + configOptions := make([]func(*config.LoadOptions) error, 0) + configOptions = append(configOptions, config.WithRegion(awsRegion)) + cfg, _ := config.LoadDefaultConfig(context.TODO(), configOptions...) + cfg.Credentials = credentials.NewStaticCredentialsProvider(awsAccessKeyID, awsSecretAccessKey, "") + return cloudwatch.NewFromConfig(cfg) +} + +// checkCloudWatchCustomMetric will evaluate the custom metric for any metric values, if any +// values are found the test will be failed. +func checkCloudWatchCustomMetric(t *testing.T, cloudwatchClient *cloudwatch.Client) { + metricData, err := cloudwatchClient.GetMetricData(context.Background(), &cloudwatch.GetMetricDataInput{ + MetricDataQueries: []types.MetricDataQuery{ + { + Id: aws.String("m1"), + ReturnData: aws.Bool(true), + MetricStat: &types.MetricStat{ + Metric: &types.Metric{ + Namespace: aws.String(cloudwatchMetricNamespace), + MetricName: aws.String(cloudwatchMetricName), + Dimensions: []types.Dimension{ + { + Name: aws.String(cloudwatchMetricDimensionName), + Value: aws.String(cloudwatchMetricDimensionValue), + }, + }, + }, + Period: aws.Int32(60), + Stat: aws.String("Average"), + }, + }, + }, + // evaluate +/- 5 minutes from now to be sure we cover the query window + // leading into the e2e test. + EndTime: aws.Time(time.Now().Add(time.Minute * 5)), + StartTime: aws.Time(time.Now().Add(-time.Minute * 5)), + }) + if err != nil { + t.Fatalf("error checking cloudwatch metric: %s", err) + return + } + + // This is a e2e preflight check for returning an error when there are no + // metric values. If there are any metric values, then the test should fail + // here, as the scaler will never enter an error state if there are metric + // values in the query window. + if len(metricData.MetricDataResults) != 1 || len(metricData.MetricDataResults[0].Values) > 0 { + t.Fatalf("found unexpected metric data results for namespace: %s: %+v", cloudwatchMetricNamespace, metricData.MetricDataResults) + return + } +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + DeploymentName: deploymentName, + ScaledObjectName: scaledObjectName, + SecretName: secretName, + AwsAccessKeyID: base64.StdEncoding.EncodeToString([]byte(awsAccessKeyID)), + AwsSecretAccessKey: base64.StdEncoding.EncodeToString([]byte(awsSecretAccessKey)), + AwsRegion: awsRegion, + CloudWatchMetricName: cloudwatchMetricName, + CloudWatchMetricNamespace: cloudwatchMetricNamespace, + CloudWatchMetricDimensionName: cloudwatchMetricDimensionName, + CloudWatchMetricDimensionValue: cloudwatchMetricDimensionValue, + }, []Template{ + {Name: "secretTemplate", Config: secretTemplate}, + {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + } +}