mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
Signed-off-by: Bartosz Majsak <[email protected]> Co-authored-by: Sivanantham <[email protected]>
1248 lines
37 KiB
Go
1248 lines
37 KiB
Go
/*
|
|
Copyright 2023 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 hpa
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
apierr "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/utils/ptr"
|
|
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
|
|
"github.com/kserve/kserve/pkg/constants"
|
|
)
|
|
|
|
func TestCreateHPA(t *testing.T) {
|
|
type args struct {
|
|
objectMeta metav1.ObjectMeta
|
|
componentExt *v1beta1.ComponentExtensionSpec
|
|
}
|
|
|
|
cpuResource := v1beta1.MetricCPU
|
|
memoryResource := v1beta1.MetricMemory
|
|
|
|
defaultMinReplicas := int32(1)
|
|
defaultUtilization := int32(80)
|
|
igMinReplicas := int32(2)
|
|
igUtilization := int32(30)
|
|
predictorMinReplicas := int32(5)
|
|
predictorUtilization := int32(50)
|
|
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
expected *autoscalingv2.HorizontalPodAutoscaler
|
|
err error
|
|
}{
|
|
{
|
|
name: "inference graph default hpa",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{
|
|
Name: "basic-ig",
|
|
Namespace: "basic-ig-namespace",
|
|
Annotations: map[string]string{
|
|
"annotation": "annotation-value",
|
|
},
|
|
Labels: map[string]string{
|
|
"label": "label-value",
|
|
"serving.kserve.io/inferencegraph": "basic-ig",
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{},
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "basic-ig",
|
|
Namespace: "basic-ig-namespace",
|
|
Annotations: map[string]string{
|
|
"annotation": "annotation-value",
|
|
},
|
|
Labels: map[string]string{
|
|
"label": "label-value",
|
|
"serving.kserve.io/inferencegraph": "basic-ig",
|
|
},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: "basic-ig",
|
|
},
|
|
MinReplicas: &defaultMinReplicas,
|
|
MaxReplicas: 1,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceName("cpu"),
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: &defaultUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "inference graph specified hpa",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{
|
|
Name: "basic-ig",
|
|
Namespace: "basic-ig-namespace",
|
|
Annotations: map[string]string{
|
|
"annotation": "annotation-value",
|
|
},
|
|
Labels: map[string]string{
|
|
"label": "label-value",
|
|
"serving.kserve.io/inferencegraph": "basic-ig",
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(2)),
|
|
MaxReplicas: 5,
|
|
ScaleTarget: ptr.To(int32(30)),
|
|
ScaleMetric: &cpuResource,
|
|
},
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "basic-ig",
|
|
Namespace: "basic-ig-namespace",
|
|
Annotations: map[string]string{
|
|
"annotation": "annotation-value",
|
|
},
|
|
Labels: map[string]string{
|
|
"label": "label-value",
|
|
"serving.kserve.io/inferencegraph": "basic-ig",
|
|
},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: "basic-ig",
|
|
},
|
|
MinReplicas: &igMinReplicas,
|
|
MaxReplicas: 5,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceName("cpu"),
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &igUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "predictor hpa cpu metric",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: nil,
|
|
MaxReplicas: 0,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
MinReplicas: &defaultMinReplicas,
|
|
MaxReplicas: 1,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceName("cpu"),
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(80)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "predictor hpa memory utilization metric",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: nil,
|
|
MaxReplicas: 0,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(80)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
MinReplicas: &defaultMinReplicas,
|
|
MaxReplicas: 1,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceName("memory"),
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(80)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "predictor hpa memory metric",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: nil,
|
|
MaxReplicas: 0,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.AverageValueMetricType,
|
|
AverageValue: v1beta1.NewMetricQuantity("1Gi"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
MinReplicas: &defaultMinReplicas,
|
|
MaxReplicas: 1,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceName("memory"),
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.AverageValueMetricType,
|
|
AverageValue: ptr.To(resource.MustParse("1Gi")),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "predictor hpa with ScaleMetric",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(5)),
|
|
MaxReplicas: 10,
|
|
ScaleTarget: ptr.To(int32(50)),
|
|
ScaleMetric: &cpuResource,
|
|
},
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
MinReplicas: &predictorMinReplicas,
|
|
MaxReplicas: 10,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceName("cpu"),
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: &predictorUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "invalid memory scale target for hpa",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(0)),
|
|
MaxReplicas: -10,
|
|
ScaleTarget: nil,
|
|
ScaleMetric: &memoryResource,
|
|
},
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{Kind: "Deployment", APIVersion: "apps/v1"},
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: int32(1),
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "nil componentExt for createHPA",
|
|
args: args{
|
|
objectMeta: metav1.ObjectMeta{
|
|
Name: "nil-component-ext",
|
|
Namespace: "default",
|
|
},
|
|
componentExt: nil,
|
|
},
|
|
expected: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nil-component-ext",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: "nil-component-ext",
|
|
},
|
|
MinReplicas: &defaultMinReplicas,
|
|
MaxReplicas: 1,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: &defaultUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := createHPA(tt.args.objectMeta, tt.args.componentExt)
|
|
if diff := cmp.Diff(tt.expected, got); diff != "" {
|
|
t.Errorf("Test %q unexpected hpa (-want +got): %v", tt.name, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSemanticHPAEquals(t *testing.T) {
|
|
assert.True(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{},
|
|
}))
|
|
|
|
assert.False(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(4))},
|
|
}))
|
|
|
|
assert.False(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{constants.AutoscalerClass: "hpa"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{constants.AutoscalerClass: "external"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
}))
|
|
|
|
assert.False(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{constants.AutoscalerClass: "external"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
}))
|
|
|
|
assert.False(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{constants.AutoscalerClass: "hpa"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{constants.AutoscalerClass: "none"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
}))
|
|
|
|
assert.True(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{constants.AutoscalerClass: "hpa"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{constants.AutoscalerClass: "hpa"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
}))
|
|
|
|
assert.True(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
}))
|
|
|
|
assert.True(t, semanticHPAEquals(
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"unrelated": "true"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
},
|
|
&autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"unrelated": "false"}},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{MinReplicas: ptr.To(int32(3))},
|
|
}))
|
|
}
|
|
|
|
func TestCheckHPAExist(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
_ = autoscalingv2.AddToScheme(scheme)
|
|
|
|
type testCase struct {
|
|
name string
|
|
existingHPA *autoscalingv2.HorizontalPodAutoscaler
|
|
desiredHPA *autoscalingv2.HorizontalPodAutoscaler
|
|
expectedResult constants.CheckResultType
|
|
}
|
|
|
|
tests := []testCase{
|
|
{
|
|
name: "hpa does not exist and should be created",
|
|
existingHPA: nil,
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
},
|
|
expectedResult: constants.CheckResultCreate,
|
|
},
|
|
{
|
|
name: "hpa does not exist and should be skipped due to external autoscaler",
|
|
existingHPA: nil,
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{constants.AutoscalerClass: string(constants.AutoscalerClassExternal)},
|
|
},
|
|
},
|
|
expectedResult: constants.CheckResultSkipped,
|
|
},
|
|
{
|
|
name: "hpa exists and is equivalent",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
expectedResult: constants.CheckResultExisted,
|
|
},
|
|
{
|
|
name: "hpa exists but should be updated",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(2)),
|
|
MaxReplicas: 5,
|
|
},
|
|
},
|
|
expectedResult: constants.CheckResultUpdate,
|
|
},
|
|
{
|
|
name: "hpa exists but should be deleted due to external autoscaler, owner is kserve",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: "serving.kserve.io/v1beta1",
|
|
Kind: "InferenceService",
|
|
Name: "my-inferenceservice",
|
|
UID: "12345",
|
|
Controller: ptr.To(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{constants.AutoscalerClass: string(constants.AutoscalerClassExternal)},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
expectedResult: constants.CheckResultDelete,
|
|
},
|
|
{
|
|
name: "hpa with different autoscaler class",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{constants.AutoscalerClass: string(constants.AutoscalerClassHPA)},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{constants.AutoscalerClass: string(constants.AutoscalerClassKeda)},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
expectedResult: constants.CheckResultUpdate,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
client := fake.NewClientBuilder().WithScheme(scheme).Build()
|
|
// Create the existing HPA if it's provided
|
|
if tc.existingHPA != nil {
|
|
err := client.Create(t.Context(), tc.existingHPA)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
reconciler := &HPAReconciler{
|
|
client: client,
|
|
scheme: scheme,
|
|
HPA: tc.desiredHPA,
|
|
}
|
|
|
|
result, hpa, err := reconciler.checkHPAExist(t.Context(), client)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.expectedResult, result)
|
|
|
|
if tc.existingHPA != nil {
|
|
assert.NotNil(t, hpa)
|
|
} else if result == constants.CheckResultCreate || result == constants.CheckResultSkipped {
|
|
assert.Nil(t, hpa)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetHPAMetrics(t *testing.T) {
|
|
cpuResource := v1beta1.MetricCPU
|
|
memoryResource := v1beta1.MetricMemory
|
|
|
|
tests := []struct {
|
|
name string
|
|
componentExt *v1beta1.ComponentExtensionSpec
|
|
expectedSpecs []autoscalingv2.MetricSpec
|
|
}{
|
|
{
|
|
name: "default cpu metric with scaleMetric",
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
ScaleMetric: &cpuResource,
|
|
ScaleTarget: ptr.To(int32(70)),
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(70)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "default cpu metric without scaleTarget",
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
ScaleMetric: &cpuResource,
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(80)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "memory metric with scaleTarget",
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
ScaleMetric: &memoryResource,
|
|
ScaleTarget: ptr.To(int32(60)),
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceMemory,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(60)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "memory metric without scaleTarget",
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
ScaleMetric: &memoryResource,
|
|
},
|
|
expectedSpecs: nil,
|
|
},
|
|
{
|
|
name: "autoscaling with CPU utilization metric",
|
|
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)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "autoscaling with CPU metric without utilization",
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(80)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "autoscaling with memory utilization metric",
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(65)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceMemory,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(65)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "autoscaling with memory average value metric",
|
|
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("500Mi"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceMemory,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.AverageValueMetricType,
|
|
AverageValue: ptr.To(resource.MustParse("500Mi")),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple metrics in autoscaling",
|
|
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)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.AverageValueMetricType,
|
|
AverageValue: v1beta1.NewMetricQuantity("1Gi"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceMemory,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.AverageValueMetricType,
|
|
AverageValue: ptr.To(resource.MustParse("1Gi")),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nil componentExt should return default CPU metric",
|
|
componentExt: nil,
|
|
expectedSpecs: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(80)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
metrics := getHPAMetrics(tt.componentExt)
|
|
if diff := cmp.Diff(tt.expectedSpecs, metrics); diff != "" {
|
|
t.Errorf("Test %q unexpected metrics (-want +got): %v", tt.name, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// trackingClient is a wrapper around client.Client that tracks actions performed
|
|
type trackingClient struct {
|
|
client.Client
|
|
actualAction *string
|
|
}
|
|
|
|
// Create tracks create actions
|
|
func (c *trackingClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
|
|
*c.actualAction = "create"
|
|
return c.Client.Create(ctx, obj, opts...)
|
|
}
|
|
|
|
// Update tracks update actions
|
|
func (c *trackingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
|
*c.actualAction = "update"
|
|
return c.Client.Update(ctx, obj, opts...)
|
|
}
|
|
|
|
// Delete tracks delete actions
|
|
func (c *trackingClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
|
|
*c.actualAction = "delete"
|
|
return c.Client.Delete(ctx, obj, opts...)
|
|
}
|
|
|
|
func TestReconcile(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
_ = autoscalingv2.AddToScheme(scheme)
|
|
|
|
type testCase struct {
|
|
name string
|
|
existingHPA *autoscalingv2.HorizontalPodAutoscaler
|
|
desiredHPA *autoscalingv2.HorizontalPodAutoscaler
|
|
componentExt *v1beta1.ComponentExtensionSpec
|
|
expectedResult error
|
|
expectedAction string
|
|
}
|
|
|
|
tests := []testCase{
|
|
{
|
|
name: "create new hpa",
|
|
existingHPA: nil,
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
expectedResult: nil,
|
|
expectedAction: "create",
|
|
},
|
|
{
|
|
name: "update existing hpa",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(2)),
|
|
MaxReplicas: 5,
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(2)),
|
|
MaxReplicas: 5,
|
|
},
|
|
expectedResult: nil,
|
|
expectedAction: "update",
|
|
},
|
|
{
|
|
name: "delete hpa with external autoscaler and kserve owner",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: "serving.kserve.io/v1beta1",
|
|
Kind: "InferenceService",
|
|
Name: "my-inferenceservice",
|
|
UID: "12345",
|
|
Controller: ptr.To(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{constants.AutoscalerClass: string(constants.AutoscalerClassExternal)},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
expectedResult: nil,
|
|
expectedAction: "delete",
|
|
},
|
|
{
|
|
name: "do not delete hpa with external autoscaler and not owned by kserve",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: "some-deployment",
|
|
UID: "54321",
|
|
Controller: ptr.To(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{constants.AutoscalerClass: string(constants.AutoscalerClassExternal)},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
expectedResult: nil,
|
|
expectedAction: "update",
|
|
},
|
|
{
|
|
name: "skip when hpa exists and is equivalent",
|
|
existingHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
expectedResult: nil,
|
|
expectedAction: "skip",
|
|
},
|
|
{
|
|
name: "skip creating hpa with external autoscaler",
|
|
existingHPA: nil,
|
|
desiredHPA: &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{constants.AutoscalerClass: string(constants.AutoscalerClassExternal)},
|
|
},
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
},
|
|
componentExt: &v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
expectedResult: nil,
|
|
expectedAction: "skip",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
clientBuilder := fake.NewClientBuilder().WithScheme(scheme)
|
|
|
|
// Create the existing HPA if it's provided
|
|
if tc.existingHPA != nil {
|
|
clientBuilder = clientBuilder.WithObjects(tc.existingHPA)
|
|
}
|
|
|
|
client := clientBuilder.Build()
|
|
|
|
// Create a tracking client wrapper
|
|
var actualAction string
|
|
trackingClient := &trackingClient{
|
|
Client: client,
|
|
actualAction: &actualAction,
|
|
}
|
|
|
|
reconciler := &HPAReconciler{
|
|
client: trackingClient,
|
|
scheme: scheme,
|
|
HPA: tc.desiredHPA,
|
|
componentExt: tc.componentExt,
|
|
}
|
|
|
|
result := reconciler.Reconcile(t.Context())
|
|
|
|
assert.Equal(t, tc.expectedResult, result)
|
|
if tc.expectedAction == "skip" {
|
|
assert.Equal(t, "", actualAction, "Expected no action but got %s", actualAction)
|
|
} else if tc.expectedAction != "" {
|
|
assert.Equal(t, tc.expectedAction, actualAction, "Expected action %s but got %s", tc.expectedAction, actualAction)
|
|
}
|
|
// Verify the HPA state after reconciliation
|
|
resultHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
err := client.Get(t.Context(), types.NamespacedName{
|
|
Namespace: tc.desiredHPA.Namespace,
|
|
Name: tc.desiredHPA.Name,
|
|
}, resultHPA)
|
|
|
|
switch tc.expectedAction {
|
|
case "create", "update":
|
|
require.NoError(t, err, "Expected HPA to exist after %s action", tc.expectedAction)
|
|
assert.Equal(t, tc.desiredHPA.Spec.MinReplicas, resultHPA.Spec.MinReplicas)
|
|
assert.Equal(t, tc.desiredHPA.Spec.MaxReplicas, resultHPA.Spec.MaxReplicas)
|
|
case "delete":
|
|
assert.True(t, apierr.IsNotFound(err), "Expected HPA to be deleted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetControllerReferences(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
_ = autoscalingv2.AddToScheme(scheme)
|
|
_ = v1beta1.AddToScheme(scheme)
|
|
|
|
owner := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "owner-service",
|
|
Namespace: "default",
|
|
UID: "owner-uid",
|
|
},
|
|
}
|
|
|
|
hpa := &autoscalingv2.HorizontalPodAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-hpa",
|
|
Namespace: "default",
|
|
},
|
|
}
|
|
|
|
reconciler := &HPAReconciler{
|
|
HPA: hpa,
|
|
}
|
|
|
|
err := reconciler.SetControllerReferences(owner, scheme)
|
|
require.NoError(t, err)
|
|
|
|
// Verify owner reference is set
|
|
assert.Len(t, hpa.OwnerReferences, 1)
|
|
assert.Equal(t, "owner-service", hpa.OwnerReferences[0].Name)
|
|
assert.Equal(t, "owner-uid", string(hpa.OwnerReferences[0].UID))
|
|
assert.True(t, *hpa.OwnerReferences[0].Controller)
|
|
}
|