karmada/pkg/util/lifted/validatingfhpa_test.go

1321 lines
34 KiB
Go

/*
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)
}
})
}
}