From e9f4e7a6caf0e6d5a7325901714d284d03692d5f Mon Sep 17 00:00:00 2001 From: Anuj Agrawal Date: Thu, 10 Oct 2024 13:55:15 +0530 Subject: [PATCH] Added unit tests for FederatedHPA validation Signed-off-by: Anuj Agrawal --- pkg/util/lifted/validatingfhpa_test.go | 1320 ++++++++++++++++++++++++ 1 file changed, 1320 insertions(+) create mode 100644 pkg/util/lifted/validatingfhpa_test.go diff --git a/pkg/util/lifted/validatingfhpa_test.go b/pkg/util/lifted/validatingfhpa_test.go new file mode 100644 index 000000000000..4233adec0090 --- /dev/null +++ b/pkg/util/lifted/validatingfhpa_test.go @@ -0,0 +1,1320 @@ +/* +Copyright 2024 The Karmada 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 lifted + +import ( + "testing" + + "github.com/stretchr/testify/assert" + autoscalingv2 "k8s.io/api/autoscaling/v2" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + + autoscalingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/autoscaling/v1alpha1" +) + +func TestValidateFederatedHPA(t *testing.T) { + tests := []struct { + name string + fhpa *autoscalingv1alpha1.FederatedHPA + wantErr bool + }{ + { + name: "valid FederatedHPA", + fhpa: &autoscalingv1alpha1.FederatedHPA{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-fhpa", + Namespace: "default", + }, + Spec: autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 10, + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "invalid name", + fhpa: &autoscalingv1alpha1.FederatedHPA{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid/name", + Namespace: "default", + }, + Spec: autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 10, + }, + }, + wantErr: true, + }, + { + name: "invalid spec", + fhpa: &autoscalingv1alpha1.FederatedHPA{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-fhpa", + Namespace: "default", + }, + Spec: autoscalingv1alpha1.FederatedHPASpec{ + MinReplicas: ptr.To[int32](0), + MaxReplicas: 0, + }, + }, + wantErr: true, + }, + { + name: "missing namespace", + fhpa: &autoscalingv1alpha1.FederatedHPA{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-fhpa", + }, + Spec: autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 10, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := ValidateFederatedHPA(tt.fhpa) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateFederatedHPASpec(t *testing.T) { + tests := []struct { + name string + spec *autoscalingv1alpha1.FederatedHPASpec + wantErr bool + }{ + { + name: "valid spec", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 10, + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "invalid minReplicas", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](0), + MaxReplicas: 10, + }, + wantErr: true, + }, + { + name: "maxReplicas less than minReplicas", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](5), + MaxReplicas: 3, + }, + wantErr: true, + }, + { + name: "invalid scaleTargetRef", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 10, + }, + wantErr: true, + }, + { + name: "invalid metrics", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 10, + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + // Missing Resource field to trigger a validation error + }, + }, + }, + wantErr: true, + }, + { + name: "invalid behavior", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 10, + Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleDown: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](-1), // Invalid: negative value + }, + }, + }, + wantErr: true, + }, + { + name: "maxReplicas less than 1", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](1), + MaxReplicas: 0, + }, + wantErr: true, + }, + { + name: "minReplicas equals maxReplicas", + spec: &autoscalingv1alpha1.FederatedHPASpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "test-deployment", + }, + MinReplicas: ptr.To[int32](5), + MaxReplicas: 5, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateFederatedHPASpec(tt.spec, field.NewPath("spec"), 1) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + // Check for specific errors + if tt.name == "invalid minReplicas" { + assert.Contains(t, errors.ToAggregate().Error(), "minReplicas", "Expected error related to minReplicas") + } + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateCrossVersionObjectReference(t *testing.T) { + tests := []struct { + name string + ref autoscalingv2.CrossVersionObjectReference + wantErr bool + }{ + { + name: "valid reference", + ref: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "my-deployment", + }, + wantErr: false, + }, + { + name: "missing kind", + ref: autoscalingv2.CrossVersionObjectReference{ + Name: "my-deployment", + }, + wantErr: true, + }, + { + name: "missing name", + ref: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + }, + wantErr: true, + }, + { + name: "invalid kind", + ref: autoscalingv2.CrossVersionObjectReference{ + Kind: "Invalid/Kind", + Name: "my-deployment", + }, + wantErr: true, + }, + { + name: "invalid name", + ref: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "my/deployment", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := ValidateCrossVersionObjectReference(tt.ref, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateFederatedHPAStatus(t *testing.T) { + tests := []struct { + name string + status *autoscalingv2.HorizontalPodAutoscalerStatus + wantErr bool + }{ + { + name: "valid status", + status: &autoscalingv2.HorizontalPodAutoscalerStatus{ + CurrentReplicas: 3, + DesiredReplicas: 5, + }, + wantErr: false, + }, + { + name: "negative current replicas", + status: &autoscalingv2.HorizontalPodAutoscalerStatus{ + CurrentReplicas: -1, + DesiredReplicas: 5, + }, + wantErr: true, + }, + { + name: "negative desired replicas", + status: &autoscalingv2.HorizontalPodAutoscalerStatus{ + CurrentReplicas: 3, + DesiredReplicas: -1, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateFederatedHPAStatus(tt.status) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateBehavior(t *testing.T) { + tests := []struct { + name string + behavior *autoscalingv2.HorizontalPodAutoscalerBehavior + wantErr bool + }{ + { + name: "valid behavior", + behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleUp: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](60), + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PercentScalingPolicy, + Value: 100, + PeriodSeconds: 15, + }, + }, + }, + ScaleDown: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](300), + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PercentScalingPolicy, + Value: 100, + PeriodSeconds: 15, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "invalid scale up stabilization window", + behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleUp: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](-1), + }, + }, + wantErr: true, + }, + { + name: "invalid scale down policy", + behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleDown: &autoscalingv2.HPAScalingRules{ + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PercentScalingPolicy, + Value: -1, + PeriodSeconds: 15, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "nil behavior", + behavior: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateBehavior(tt.behavior, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} +func TestValidateScalingRules(t *testing.T) { + validPolicy := autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PercentScalingPolicy, + Value: 100, + PeriodSeconds: 15, + } + + tests := []struct { + name string + rules *autoscalingv2.HPAScalingRules + wantErr bool + }{ + { + name: "nil rules", + rules: nil, + wantErr: false, + }, + { + name: "valid rules with Max select policy", + rules: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](300), + SelectPolicy: ptr.To[autoscalingv2.ScalingPolicySelect](autoscalingv2.MaxChangePolicySelect), + Policies: []autoscalingv2.HPAScalingPolicy{validPolicy}, + }, + wantErr: false, + }, + { + name: "valid rules with Min select policy", + rules: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](300), + SelectPolicy: ptr.To[autoscalingv2.ScalingPolicySelect](autoscalingv2.MinChangePolicySelect), + Policies: []autoscalingv2.HPAScalingPolicy{validPolicy}, + }, + wantErr: false, + }, + { + name: "valid rules with Disabled select policy", + rules: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](300), + SelectPolicy: ptr.To[autoscalingv2.ScalingPolicySelect](autoscalingv2.DisabledPolicySelect), + Policies: []autoscalingv2.HPAScalingPolicy{validPolicy}, + }, + wantErr: false, + }, + { + name: "negative stabilization window", + rules: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](-1), + Policies: []autoscalingv2.HPAScalingPolicy{validPolicy}, + }, + wantErr: true, + }, + { + name: "stabilization window exceeding max", + rules: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: ptr.To[int32](MaxStabilizationWindowSeconds + 1), + Policies: []autoscalingv2.HPAScalingPolicy{validPolicy}, + }, + wantErr: true, + }, + { + name: "invalid select policy", + rules: &autoscalingv2.HPAScalingRules{ + SelectPolicy: ptr.To[autoscalingv2.ScalingPolicySelect]("InvalidPolicy"), + Policies: []autoscalingv2.HPAScalingPolicy{validPolicy}, + }, + wantErr: true, + }, + { + name: "no policies", + rules: &autoscalingv2.HPAScalingRules{ + Policies: []autoscalingv2.HPAScalingPolicy{}, + }, + wantErr: true, + }, + { + name: "invalid policy", + rules: &autoscalingv2.HPAScalingRules{ + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: "InvalidType", + Value: 0, + PeriodSeconds: 0, + }, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateScalingRules(tt.rules, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateScalingPolicy(t *testing.T) { + tests := []struct { + name string + policy autoscalingv2.HPAScalingPolicy + wantErr bool + }{ + { + name: "valid pods scaling policy", + policy: autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: 15, + }, + wantErr: false, + }, + { + name: "valid percent scaling policy", + policy: autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PercentScalingPolicy, + Value: 10, + PeriodSeconds: 15, + }, + wantErr: false, + }, + { + name: "invalid policy type", + policy: autoscalingv2.HPAScalingPolicy{ + Type: "InvalidType", + Value: 1, + PeriodSeconds: 15, + }, + wantErr: true, + }, + { + name: "zero value", + policy: autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PodsScalingPolicy, + Value: 0, + PeriodSeconds: 15, + }, + wantErr: true, + }, + { + name: "negative value", + policy: autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PodsScalingPolicy, + Value: -1, + PeriodSeconds: 15, + }, + wantErr: true, + }, + { + name: "zero period seconds", + policy: autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: 0, + }, + wantErr: true, + }, + { + name: "negative period seconds", + policy: autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: -1, + }, + wantErr: true, + }, + { + name: "period seconds exceeding max", + policy: autoscalingv2.HPAScalingPolicy{ + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: MaxPeriodSeconds + 1, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateScalingPolicy(tt.policy, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateMetricSpec(t *testing.T) { + tests := []struct { + name string + spec autoscalingv2.MetricSpec + wantErr bool + }{ + { + name: "valid resource metric", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + }, + wantErr: false, + }, + { + name: "empty metric type", + spec: autoscalingv2.MetricSpec{}, + wantErr: true, + }, + { + name: "invalid metric type", + spec: autoscalingv2.MetricSpec{ + Type: "InvalidType", + }, + wantErr: true, + }, + { + name: "object metric without object", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ObjectMetricSourceType, + }, + wantErr: true, + }, + { + name: "pods metric without pods", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.PodsMetricSourceType, + }, + wantErr: true, + }, + { + name: "resource metric without resource", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ResourceMetricSourceType, + }, + wantErr: true, + }, + { + name: "container resource metric without container resource", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ContainerResourceMetricSourceType, + }, + wantErr: true, + }, + { + name: "multiple metric sources", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + Pods: &autoscalingv2.PodsMetricSource{}, + }, + wantErr: true, + }, { + name: "valid object metric", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ObjectMetricSourceType, + Object: &autoscalingv2.ObjectMetricSource{ + DescribedObject: autoscalingv2.CrossVersionObjectReference{ + Kind: "Service", + Name: "my-service", + }, + Metric: autoscalingv2.MetricIdentifier{ + Name: "requests-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + }, + }, + wantErr: false, + }, + { + name: "valid container resource metric", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ContainerResourceMetricSourceType, + ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ + Name: "cpu", + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + }, + wantErr: false, + }, + { + name: "multiple metric sources - object and container resource", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ObjectMetricSourceType, + Object: &autoscalingv2.ObjectMetricSource{ + DescribedObject: autoscalingv2.CrossVersionObjectReference{ + Kind: "Service", + Name: "my-service", + }, + Metric: autoscalingv2.MetricIdentifier{ + Name: "requests-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + }, + ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ + Name: "cpu", + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + }, + wantErr: true, + }, + { + name: "multiple metric sources - all types", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ObjectMetricSourceType, + Object: &autoscalingv2.ObjectMetricSource{ + DescribedObject: autoscalingv2.CrossVersionObjectReference{ + Kind: "Service", + Name: "my-service", + }, + Metric: autoscalingv2.MetricIdentifier{ + Name: "requests-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + }, + Pods: &autoscalingv2.PodsMetricSource{ + Metric: autoscalingv2.MetricIdentifier{ + Name: "packets-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: ptr.To(resource.MustParse("1k")), + }, + }, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ + Name: "memory", + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](60), + }, + }, + }, + wantErr: true, + }, + { + name: "mismatched type and source - object", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.PodsMetricSourceType, + Object: &autoscalingv2.ObjectMetricSource{ + DescribedObject: autoscalingv2.CrossVersionObjectReference{ + Kind: "Service", + Name: "my-service", + }, + Metric: autoscalingv2.MetricIdentifier{ + Name: "requests-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + }, + }, + wantErr: true, + }, + { + name: "mismatched type and source - container resource", + spec: autoscalingv2.MetricSpec{ + Type: autoscalingv2.ResourceMetricSourceType, + ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ + Name: "cpu", + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateMetricSpec(tt.spec, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateObjectSource(t *testing.T) { + tests := []struct { + name string + src autoscalingv2.ObjectMetricSource + wantErr bool + }{ + { + name: "valid object metric", + src: autoscalingv2.ObjectMetricSource{ + DescribedObject: autoscalingv2.CrossVersionObjectReference{ + Kind: "Service", + Name: "my-service", + }, + Metric: autoscalingv2.MetricIdentifier{ + Name: "requests-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + }, + wantErr: false, + }, + { + name: "missing described object", + src: autoscalingv2.ObjectMetricSource{ + Metric: autoscalingv2.MetricIdentifier{ + Name: "requests-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + }, + wantErr: true, + }, + { + name: "missing metric name", + src: autoscalingv2.ObjectMetricSource{ + DescribedObject: autoscalingv2.CrossVersionObjectReference{ + Kind: "Service", + Name: "my-service", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + }, + wantErr: true, + }, + { + name: "missing target value and average value", + src: autoscalingv2.ObjectMetricSource{ + DescribedObject: autoscalingv2.CrossVersionObjectReference{ + Kind: "Service", + Name: "my-service", + }, + Metric: autoscalingv2.MetricIdentifier{ + Name: "requests-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateObjectSource(&tt.src, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidatePodsSource(t *testing.T) { + tests := []struct { + name string + src autoscalingv2.PodsMetricSource + wantErr bool + }{ + { + name: "valid pods metric", + src: autoscalingv2.PodsMetricSource{ + Metric: autoscalingv2.MetricIdentifier{ + Name: "packets-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: ptr.To(resource.MustParse("1k")), + }, + }, + wantErr: false, + }, + { + name: "valid pods metric with selector", + src: autoscalingv2.PodsMetricSource{ + Metric: autoscalingv2.MetricIdentifier{ + Name: "packets-per-second", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: ptr.To(resource.MustParse("1k")), + }, + }, + wantErr: false, + }, + { + name: "missing metric name", + src: autoscalingv2.PodsMetricSource{ + Metric: autoscalingv2.MetricIdentifier{}, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: ptr.To(resource.MustParse("1k")), + }, + }, + wantErr: true, + }, + { + name: "missing average value", + src: autoscalingv2.PodsMetricSource{ + Metric: autoscalingv2.MetricIdentifier{ + Name: "packets-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + }, + }, + wantErr: true, + }, + { + name: "invalid target type", + src: autoscalingv2.PodsMetricSource{ + Metric: autoscalingv2.MetricIdentifier{ + Name: "packets-per-second", + }, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + Value: ptr.To(resource.MustParse("1k")), + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validatePodsSource(&tt.src, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateContainerResourceSource(t *testing.T) { + tests := []struct { + name string + src autoscalingv2.ContainerResourceMetricSource + wantErr bool + }{ + { + name: "valid container resource metric", + src: autoscalingv2.ContainerResourceMetricSource{ + Name: "cpu", + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + wantErr: false, + }, + { + name: "missing resource name", + src: autoscalingv2.ContainerResourceMetricSource{ + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + wantErr: true, + }, + { + name: "missing container name", + src: autoscalingv2.ContainerResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + wantErr: true, + }, + { + name: "both average utilization and average value set", + src: autoscalingv2.ContainerResourceMetricSource{ + Name: "cpu", + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + AverageValue: ptr.To(resource.MustParse("100m")), + }, + }, + wantErr: true, + }, + { + name: "neither average utilization nor average value set", + src: autoscalingv2.ContainerResourceMetricSource{ + Name: "cpu", + Container: "app", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateContainerResourceSource(&tt.src, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateResourceSource(t *testing.T) { + tests := []struct { + name string + src autoscalingv2.ResourceMetricSource + wantErr bool + }{ + { + name: "valid utilization", + src: autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + wantErr: false, + }, + { + name: "valid average value", + src: autoscalingv2.ResourceMetricSource{ + Name: "memory", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: ptr.To(resource.MustParse("100Mi")), + }, + }, + wantErr: false, + }, + { + name: "empty resource name", + src: autoscalingv2.ResourceMetricSource{ + Name: "", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + }, + }, + wantErr: true, + }, + { + name: "missing target", + src: autoscalingv2.ResourceMetricSource{ + Name: "cpu", + }, + wantErr: true, + }, + { + name: "both utilization and value set", + src: autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](50), + AverageValue: ptr.To(resource.MustParse("100m")), + }, + }, + wantErr: true, + }, + { + name: "neither utilization nor value set", + src: autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateResourceSource(&tt.src, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateMetricTarget(t *testing.T) { + tests := []struct { + name string + target autoscalingv2.MetricTarget + wantErr bool + }{ + { + name: "valid utilization target", + target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](80), + }, + wantErr: false, + }, + { + name: "valid value target", + target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("100")), + }, + wantErr: false, + }, + { + name: "valid average value target", + target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: ptr.To(resource.MustParse("50")), + }, + wantErr: false, + }, + { + name: "missing type", + target: autoscalingv2.MetricTarget{ + AverageUtilization: ptr.To[int32](80), + }, + wantErr: true, + }, + { + name: "invalid type", + target: autoscalingv2.MetricTarget{ + Type: "InvalidType", + AverageUtilization: ptr.To[int32](80), + }, + wantErr: true, + }, + { + name: "negative utilization", + target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To[int32](-1), + }, + wantErr: true, + }, + { + name: "negative value", + target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.ValueMetricType, + Value: ptr.To(resource.MustParse("-100")), + }, + wantErr: true, + }, + { + name: "negative average value", + target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: ptr.To(resource.MustParse("-50")), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateMetricTarget(tt.target, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +} + +func TestValidateMetricIdentifier(t *testing.T) { + tests := []struct { + name string + id autoscalingv2.MetricIdentifier + wantErr bool + }{ + { + name: "valid identifier", + id: autoscalingv2.MetricIdentifier{ + Name: "my-metric", + }, + wantErr: false, + }, + { + name: "empty name", + id: autoscalingv2.MetricIdentifier{ + Name: "", + }, + wantErr: true, + }, + { + name: "invalid name", + id: autoscalingv2.MetricIdentifier{ + Name: "my/metric", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := validateMetricIdentifier(tt.id, field.NewPath("test")) + if tt.wantErr { + assert.NotEmpty(t, errors, "Expected validation errors, but got none") + } else { + assert.Empty(t, errors, "Expected no validation errors, but got: %v", errors) + } + }) + } +}