Files

1018 lines
32 KiB
Go

/*
Copyright 2025 The KServe Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package keda
import (
"strconv"
"testing"
kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
"github.com/kserve/kserve/pkg/constants"
)
func TestNewKedaReconciler(t *testing.T) {
client := fake.NewClientBuilder().Build()
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
r, err := NewKedaReconciler(client, scheme.Scheme, componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.NotNil(t, r)
assert.Equal(t, "test-component", r.ScaledObject.Name)
assert.Equal(t, "test-namespace", r.ScaledObject.Namespace)
}
func TestGetKedaMetrics_ResourceMetricSourceType(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := createComponentExtensionWithResourceMetric()
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "cpu", triggers[0].Type)
assert.Equal(t, "50", triggers[0].Metadata["value"])
}
func TestGetKedaMetrics_ExternalMetricSourceType(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := createComponentExtensionWithExternalMetric()
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "prometheus", triggers[0].Type)
assert.Equal(t, "http://prometheus-server", triggers[0].Metadata["serverAddress"])
assert.Equal(t, "http_requests_total", triggers[0].Metadata["query"])
assert.Equal(t, "100.9", triggers[0].Metadata["threshold"])
}
func TestGetKedaMetrics_PodMetricSourceType(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := createComponentExtensionWithPodMetric()
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "external", triggers[0].Type)
assert.Equal(t, "http://otel-server", triggers[0].Metadata["scalerAddress"])
// The metricQuery should now include namespace and deployment selectors
assert.Equal(t, "sum(otel_query{namespace=\"test-namespace\", deployment=\"test-component\"})", triggers[0].Metadata["metricQuery"])
assert.Equal(t, "200", triggers[0].Metadata["targetValue"])
}
func TestCreateKedaScaledObject(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.NotNil(t, scaledObject)
assert.Equal(t, "test-component", scaledObject.Name)
assert.Equal(t, "test-namespace", scaledObject.Namespace)
assert.Equal(t, int32(1), *scaledObject.Spec.MinReplicaCount)
assert.Equal(t, int32(3), *scaledObject.Spec.MaxReplicaCount)
}
func TestSemanticScaledObjectEquals(t *testing.T) {
desired := createScaledObject(1, 3)
existing := createScaledObject(1, 3)
assert.True(t, semanticScaledObjectEquals(desired, existing))
existing.Spec.MaxReplicaCount = ptr.To(int32(5))
assert.False(t, semanticScaledObjectEquals(desired, existing))
}
func TestReconcile(t *testing.T) {
_ = kedav1alpha1.AddToScheme(scheme.Scheme)
client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
r, err := NewKedaReconciler(client, scheme.Scheme, componentMeta, componentExt, configMap)
require.NoError(t, err)
err = r.Reconcile(t.Context())
require.NoError(t, err)
scaledObject := &kedav1alpha1.ScaledObject{}
err = client.Get(t.Context(), types.NamespacedName{Name: "test-component", Namespace: "test-namespace"}, scaledObject)
require.NoError(t, err)
assert.Equal(t, "test-component", scaledObject.Name)
assert.Equal(t, "test-namespace", scaledObject.Namespace)
}
func TestSetControllerReferences(t *testing.T) {
client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
r, err := NewKedaReconciler(client, scheme.Scheme, componentMeta, componentExt, configMap)
require.NoError(t, err)
owner := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "owner",
Namespace: "test-namespace",
},
}
err = r.SetControllerReferences(owner, scheme.Scheme)
require.NoError(t, err)
assert.Equal(t, owner.Name, r.ScaledObject.OwnerReferences[0].Name)
}
func TestReconcile_CreateScaledObject(t *testing.T) {
_ = kedav1alpha1.AddToScheme(scheme.Scheme)
client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
r, err := NewKedaReconciler(client, scheme.Scheme, componentMeta, componentExt, configMap)
require.NoError(t, err)
err = r.Reconcile(t.Context())
require.NoError(t, err)
scaledObject := &kedav1alpha1.ScaledObject{}
err = client.Get(t.Context(), types.NamespacedName{Name: "test-component", Namespace: "test-namespace"}, scaledObject)
require.NoError(t, err)
assert.Equal(t, "test-component", scaledObject.Name)
assert.Equal(t, "test-namespace", scaledObject.Namespace)
assert.Equal(t, int32(1), *scaledObject.Spec.MinReplicaCount)
assert.Equal(t, int32(3), *scaledObject.Spec.MaxReplicaCount)
}
func TestReconcile_UpdateScaledObject(t *testing.T) {
_ = kedav1alpha1.AddToScheme(scheme.Scheme)
client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
r, err := NewKedaReconciler(client, scheme.Scheme, componentMeta, componentExt, configMap)
require.NoError(t, err)
// Create an existing ScaledObject with different MaxReplicaCount
existingScaledObject := &kedav1alpha1.ScaledObject{
ObjectMeta: metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
},
Spec: kedav1alpha1.ScaledObjectSpec{
MinReplicaCount: ptr.To(int32(1)),
MaxReplicaCount: ptr.To(int32(5)),
},
}
err = client.Create(t.Context(), existingScaledObject)
require.NoError(t, err)
err = r.Reconcile(t.Context())
require.NoError(t, err)
updatedScaledObject := &kedav1alpha1.ScaledObject{}
err = client.Get(t.Context(), types.NamespacedName{Name: "test-component", Namespace: "test-namespace"}, updatedScaledObject)
require.NoError(t, err)
assert.Equal(t, int32(1), *updatedScaledObject.Spec.MinReplicaCount)
assert.Equal(t, int32(3), *updatedScaledObject.Spec.MaxReplicaCount)
}
func TestGetKedaMetrics_AverageValueMetricSourceType(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricCPU,
Target: v1beta1.MetricTarget{
Type: v1beta1.AverageValueMetricType,
AverageValue: v1beta1.NewMetricQuantity("150m"),
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "cpu", triggers[0].Type)
assert.Equal(t, "150m", triggers[0].Metadata["value"])
}
func TestGetKedaMetrics_ValueMetricSourceType(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricMemory,
Target: v1beta1.MetricTarget{
Type: v1beta1.ValueMetricType,
Value: v1beta1.NewMetricQuantity("512Mi"),
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "memory", triggers[0].Type)
assert.Equal(t, "512Mi", triggers[0].Metadata["value"])
}
func TestGetKedaMetrics_DefaultCPUUtilization(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricCPU,
Target: v1beta1.MetricTarget{
Type: v1beta1.UtilizationMetricType,
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "cpu", triggers[0].Type)
assert.Equal(t, strconv.Itoa(int(constants.DefaultCPUUtilization)), triggers[0].Metadata["value"])
}
func TestReconcile_HandleGetError(t *testing.T) {
_ = kedav1alpha1.AddToScheme(scheme.Scheme)
client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
r, err := NewKedaReconciler(client, scheme.Scheme, componentMeta, componentExt, configMap)
require.NoError(t, err)
// Simulate a client error by using an invalid name for the ScaledObject
r.ScaledObject.Name = ""
err = r.Reconcile(t.Context())
assert.Error(t, err)
}
func TestCreateKedaScaledObject_DefaultMinReplicas(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.NotNil(t, scaledObject)
assert.Equal(t, "test-component", scaledObject.Name)
assert.Equal(t, "test-namespace", scaledObject.Namespace)
assert.Equal(t, constants.DefaultMinReplicas, *scaledObject.Spec.MinReplicaCount)
assert.Equal(t, int32(3), *scaledObject.Spec.MaxReplicaCount)
}
func TestCreateKedaScaledObject_MaxReplicasLessThanMinReplicas(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(5)),
MaxReplicas: 3,
}
configMap := &corev1.ConfigMap{}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.NotNil(t, scaledObject)
assert.Equal(t, "test-component", scaledObject.Name)
assert.Equal(t, "test-namespace", scaledObject.Namespace)
assert.Equal(t, int32(5), *scaledObject.Spec.MinReplicaCount)
assert.Equal(t, int32(5), *scaledObject.Spec.MaxReplicaCount)
}
func TestGetKedaMetrics_NilAutoScaling(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: nil,
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Empty(t, triggers)
}
func TestGetKedaMetrics_ResourceMetricSourceType_Utilization(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricCPU,
Target: v1beta1.MetricTarget{
Type: v1beta1.UtilizationMetricType,
AverageUtilization: ptr.To(int32(75)),
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "cpu", triggers[0].Type)
assert.Equal(t, "75", triggers[0].Metadata["value"])
}
func TestGetKedaMetrics_ResourceMetricSourceType_Utilization_DefaultCPU(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricCPU,
Target: v1beta1.MetricTarget{
Type: v1beta1.UtilizationMetricType,
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "cpu", triggers[0].Type)
assert.Equal(t, strconv.Itoa(int(constants.DefaultCPUUtilization)), triggers[0].Metadata["value"])
}
func TestGetKedaMetrics_ResourceMetricSourceType_AverageValue(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricMemory,
Target: v1beta1.MetricTarget{
Type: v1beta1.AverageValueMetricType,
AverageValue: v1beta1.NewMetricQuantity("256Mi"),
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "memory", triggers[0].Type)
assert.Equal(t, "256Mi", triggers[0].Metadata["value"])
}
func TestGetKedaMetrics_ResourceMetricSourceType_Value(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricMemory,
Target: v1beta1.MetricTarget{
Type: v1beta1.ValueMetricType,
Value: v1beta1.NewMetricQuantity("512Mi"),
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
assert.Equal(t, "memory", triggers[0].Type)
assert.Equal(t, "512Mi", triggers[0].Metadata["value"])
}
func TestGetKedaMetrics_ExternalMetricSourceType_WithNamespaceAndAuth(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ExternalMetricSourceType,
External: &v1beta1.ExternalMetricSource{
Metric: v1beta1.ExternalMetrics{
Backend: v1beta1.PrometheusBackend,
ServerAddress: "http://prometheus-server",
Query: "http_requests_total",
Namespace: "test-ns",
},
Target: v1beta1.MetricTarget{
Value: v1beta1.NewMetricQuantity("123"),
},
Authentication: &v1beta1.ExtMetricAuthentication{
AuthModes: "bearer",
AuthenticationRef: v1beta1.AuthenticationRef{
Name: "auth-secret",
},
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
trigger := triggers[0]
assert.Equal(t, "prometheus", trigger.Type)
assert.Equal(t, "http://prometheus-server", trigger.Metadata["serverAddress"])
assert.Equal(t, "http_requests_total", trigger.Metadata["query"])
assert.Equal(t, "123", trigger.Metadata["threshold"])
assert.Equal(t, "test-ns", trigger.Metadata["namespace"])
assert.Equal(t, "bearer", trigger.Metadata["authModes"])
assert.NotNil(t, trigger.AuthenticationRef)
assert.Equal(t, "auth-secret", trigger.AuthenticationRef.Name)
}
func TestGetKedaMetrics_ExternalMetricSourceType_WithoutNamespaceOrAuth(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ExternalMetricSourceType,
External: &v1beta1.ExternalMetricSource{
Metric: v1beta1.ExternalMetrics{
Backend: v1beta1.PrometheusBackend,
ServerAddress: "http://prometheus-server",
Query: "http_requests_total",
},
Target: v1beta1.MetricTarget{
Value: v1beta1.NewMetricQuantity("99"),
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
trigger := triggers[0]
assert.Equal(t, "prometheus", trigger.Type)
assert.Equal(t, "http://prometheus-server", trigger.Metadata["serverAddress"])
assert.Equal(t, "http_requests_total", trigger.Metadata["query"])
assert.Equal(t, "99", trigger.Metadata["threshold"])
assert.Nil(t, trigger.AuthenticationRef)
}
func TestGetKedaMetrics_PodMetricSourceType_Success(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.PodMetricSourceType,
PodMetric: &v1beta1.PodMetricSource{
Metric: v1beta1.PodMetrics{
Backend: v1beta1.OpenTelemetryBackend,
Query: "otel_query",
ServerAddress: "http://otel-server",
OperationOverTime: "sum",
},
Target: v1beta1.MetricTarget{
Value: v1beta1.NewMetricQuantity("200"),
},
},
},
},
},
}
configMap := &corev1.ConfigMap{}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, triggers, 1)
trigger := triggers[0]
assert.Equal(t, "external", trigger.Type)
assert.Equal(t, "sum(otel_query{namespace=\"test-namespace\", deployment=\"test-component\"})", trigger.Metadata["metricQuery"])
assert.Equal(t, "200", trigger.Metadata["targetValue"])
assert.Equal(t, "http://otel-server", trigger.Metadata["scalerAddress"])
assert.Equal(t, "sum", trigger.Metadata["operationOverTime"])
}
func TestCreateKedaScaledObject_SetsBasicFields(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "basic-component",
Namespace: "basic-namespace",
Labels: map[string]string{"foo": "bar"},
Annotations: map[string]string{"anno": "val"},
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(2)),
MaxReplicas: 5,
}
configMap := &corev1.ConfigMap{}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Equal(t, "basic-component", scaledObject.Name)
assert.Equal(t, "basic-namespace", scaledObject.Namespace)
assert.Equal(t, map[string]string{"foo": "bar"}, scaledObject.Labels)
assert.Equal(t, map[string]string{"anno": "val"}, scaledObject.Annotations)
assert.Equal(t, int32(2), *scaledObject.Spec.MinReplicaCount)
assert.Equal(t, int32(5), *scaledObject.Spec.MaxReplicaCount)
assert.Equal(t, "basic-component", scaledObject.Spec.ScaleTargetRef.Name)
}
func TestCreateKedaScaledObject_SetsTriggers(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "trigger-component",
Namespace: "trigger-namespace",
}
componentExt := createComponentExtensionWithResourceMetric()
configMap := &corev1.ConfigMap{}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Len(t, scaledObject.Spec.Triggers, 1)
assert.Equal(t, "cpu", scaledObject.Spec.Triggers[0].Type)
}
func TestCreateKedaScaledObject_UsesDefaultMinReplicas(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "default-min",
Namespace: "default-ns",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MaxReplicas: 4,
}
configMap := &corev1.ConfigMap{}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
assert.Equal(t, constants.DefaultMinReplicas, *scaledObject.Spec.MinReplicaCount)
assert.Equal(t, int32(4), *scaledObject.Spec.MaxReplicaCount)
}
func TestCreateKedaScaledObject_AdvancedConfigFromComponentExt(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "advanced",
Namespace: "ns",
}
sdWin := int32(60)
suWin := int32(30)
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
AutoScaling: &v1beta1.AutoScalingSpec{
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: &sdWin,
},
ScaleUp: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: &suWin,
},
},
},
}
configMap := &corev1.ConfigMap{}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
require.NotNil(t, scaledObject.Spec.Advanced)
require.NotNil(t, scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig)
require.NotNil(t, scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig.Behavior)
assert.Equal(t, &sdWin, scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig.Behavior.ScaleDown.StabilizationWindowSeconds)
assert.Equal(t, &suWin, scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig.Behavior.ScaleUp.StabilizationWindowSeconds)
}
func TestCreateKedaScaledObject_AdvancedConfigFromConfigMap(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "from-cm",
Namespace: "ns",
}
componentExt := &v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 2,
AutoScaling: &v1beta1.AutoScalingSpec{},
}
configMap := &corev1.ConfigMap{
Data: map[string]string{
"autoscaler": `{
"scaleUpStabilizationWindowSeconds": "15",
"scaleDownStabilizationWindowSeconds": "45"
}`,
},
}
scaledObject, err := createKedaScaledObject(componentMeta, componentExt, configMap)
require.NoError(t, err)
require.NotNil(t, scaledObject.Spec.Advanced)
require.NotNil(t, scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig)
require.NotNil(t, scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig.Behavior)
assert.Equal(t, ptr.To(int32(45)), scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig.Behavior.ScaleDown.StabilizationWindowSeconds)
assert.Equal(t, ptr.To(int32(15)), scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig.Behavior.ScaleUp.StabilizationWindowSeconds)
}
// Helper functions for creating test data
func createComponentExtensionWithResourceMetric() *v1beta1.ComponentExtensionSpec {
return &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricCPU,
Target: v1beta1.MetricTarget{
Type: v1beta1.UtilizationMetricType,
AverageUtilization: ptr.To(int32(50)),
},
},
},
},
},
}
}
func createComponentExtensionWithExternalMetric() *v1beta1.ComponentExtensionSpec {
return &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.ExternalMetricSourceType,
External: &v1beta1.ExternalMetricSource{
Metric: v1beta1.ExternalMetrics{
Backend: v1beta1.PrometheusBackend,
ServerAddress: "http://prometheus-server",
Query: "http_requests_total",
Namespace: "test-namespace",
},
Target: v1beta1.MetricTarget{
Value: v1beta1.NewMetricQuantity("100.9"),
},
},
},
},
},
}
}
func createComponentExtensionWithPodMetric() *v1beta1.ComponentExtensionSpec {
return &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{
{
Type: v1beta1.PodMetricSourceType,
PodMetric: &v1beta1.PodMetricSource{
Metric: v1beta1.PodMetrics{
Backend: v1beta1.OpenTelemetryBackend,
Query: "otel_query",
ServerAddress: "http://otel-server",
OperationOverTime: "sum",
},
Target: v1beta1.MetricTarget{
Value: v1beta1.NewMetricQuantity("200"),
},
},
},
},
},
}
}
func createScaledObject(minReplicas, maxReplicas int32) *kedav1alpha1.ScaledObject {
return &kedav1alpha1.ScaledObject{
Spec: kedav1alpha1.ScaledObjectSpec{
MinReplicaCount: ptr.To(minReplicas),
MaxReplicaCount: ptr.To(maxReplicas),
},
}
}
func TestGetKedaMetrics_StringPreservation(t *testing.T) {
tests := []struct {
name string
metricType v1beta1.MetricSourceType
targetType v1beta1.MetricTargetType
inputValue string
expectedOutput string
setupMetric func(string) *v1beta1.MetricsSpec
}{
{
name: "Resource AverageValue preserves decimal",
metricType: v1beta1.ResourceMetricSourceType,
targetType: v1beta1.AverageValueMetricType,
inputValue: "0.6",
expectedOutput: "0.6",
setupMetric: func(value string) *v1beta1.MetricsSpec {
mq := &v1beta1.MetricQuantity{
Original: value,
Quantity: resource.MustParse("600m"),
}
return &v1beta1.MetricsSpec{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricCPU,
Target: v1beta1.MetricTarget{
Type: v1beta1.AverageValueMetricType,
AverageValue: mq,
},
},
}
},
},
{
name: "Resource Value preserves memory unit",
metricType: v1beta1.ResourceMetricSourceType,
targetType: v1beta1.ValueMetricType,
inputValue: "50Gi",
expectedOutput: "50Gi",
setupMetric: func(value string) *v1beta1.MetricsSpec {
mq := &v1beta1.MetricQuantity{
Original: value,
Quantity: resource.MustParse("50Gi"),
}
return &v1beta1.MetricsSpec{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricMemory,
Target: v1beta1.MetricTarget{
Type: v1beta1.ValueMetricType,
Value: mq,
},
},
}
},
},
{
name: "Milli values preserved as-is",
metricType: v1beta1.ResourceMetricSourceType,
targetType: v1beta1.AverageValueMetricType,
inputValue: "900m",
expectedOutput: "900m",
setupMetric: func(value string) *v1beta1.MetricsSpec {
mq := &v1beta1.MetricQuantity{
Original: value,
Quantity: resource.MustParse("900m"),
}
return &v1beta1.MetricsSpec{
Type: v1beta1.ResourceMetricSourceType,
Resource: &v1beta1.ResourceMetricSource{
Name: v1beta1.ResourceMetricCPU,
Target: v1beta1.MetricTarget{
Type: v1beta1.AverageValueMetricType,
AverageValue: mq,
},
},
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
componentMeta := metav1.ObjectMeta{
Name: "test-component",
Namespace: "test-namespace",
}
metric := tt.setupMetric(tt.inputValue)
componentExt := &v1beta1.ComponentExtensionSpec{
AutoScaling: &v1beta1.AutoScalingSpec{
Metrics: []v1beta1.MetricsSpec{*metric},
},
}
configMap := &corev1.ConfigMap{
Data: map[string]string{
"otelCollector": `{"endpoint": "http://otlp:4317"}`,
},
}
triggers, err := getKedaMetrics(componentMeta, componentExt, configMap)
require.NoError(t, err)
require.Len(t, triggers, 1)
trigger := triggers[0]
// Verify the original string value is preserved in metadata
switch tt.metricType {
case v1beta1.ResourceMetricSourceType:
assert.Equal(t, tt.expectedOutput, trigger.Metadata["value"],
"Resource metric should preserve original string value")
case v1beta1.PodMetricSourceType:
assert.Equal(t, tt.expectedOutput, trigger.Metadata["threshold"],
"PodMetric should preserve original string value")
case v1beta1.ExternalMetricSourceType:
assert.Equal(t, tt.expectedOutput, trigger.Metadata["threshold"],
"External metric should preserve original string value")
}
})
}
}
func TestGetOriginalStringMQ(t *testing.T) {
tests := []struct {
name string
mq *v1beta1.MetricQuantity
fallback string
expectedOut string
}{
{
name: "Valid MetricQuantity with raw value",
mq: &v1beta1.MetricQuantity{
Original: "0.6",
Quantity: resource.MustParse("600m"),
},
fallback: "default",
expectedOut: "0.6",
},
{
name: "Nil MetricQuantity returns fallback",
mq: nil,
fallback: "fallback",
expectedOut: "fallback",
},
{
name: "Empty raw value returns fallback",
mq: &v1beta1.MetricQuantity{
Original: "",
Quantity: resource.MustParse("1"),
},
fallback: "fallback",
expectedOut: "fallback",
},
{
name: "Memory unit preserved",
mq: &v1beta1.MetricQuantity{
Original: "50Gi",
Quantity: resource.MustParse("50Gi"),
},
fallback: "default",
expectedOut: "50Gi",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getOriginalStringMQ(tt.mq, tt.fallback)
assert.Equal(t, tt.expectedOut, result)
})
}
}