karmada/pkg/controllers/hpareplicassyncer/hpa_replicas_syncer_control...

342 lines
9.8 KiB
Go

/*
Copyright 2023 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 hpareplicassyncer
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
scalefake "k8s.io/client-go/scale/fake"
coretesting "k8s.io/client-go/testing"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
workloadv1alpha1 "github.com/karmada-io/karmada/examples/customresourceinterpreter/apis/workload/v1alpha1"
"github.com/karmada-io/karmada/pkg/util/gclient"
)
func TestGetGroupResourceAndScaleForWorkloadFromHPA(t *testing.T) {
deployment := newDeployment("deployment-1", 1)
workload := newWorkload("workload-1", 1)
syncer := newHPAReplicasSyncer(deployment, workload)
cases := []struct {
name string
hpa *autoscalingv2.HorizontalPodAutoscaler
expectedError bool
expectedScale bool
expectedGR schema.GroupResource
}{
{
name: "normal case",
hpa: newHPA(appsv1.SchemeGroupVersion.String(), "Deployment", "deployment-1", 0),
expectedError: false,
expectedScale: true,
expectedGR: schema.GroupResource{Group: appsv1.SchemeGroupVersion.Group, Resource: "deployments"},
},
{
name: "customized resource case",
hpa: newHPA(workloadv1alpha1.SchemeGroupVersion.String(), "Workload", "workload-1", 0),
expectedError: false,
expectedScale: true,
expectedGR: schema.GroupResource{Group: workloadv1alpha1.SchemeGroupVersion.Group, Resource: "workloads"},
},
{
name: "scale not found",
hpa: newHPA(appsv1.SchemeGroupVersion.String(), "Deployment", "deployment-2", 0),
expectedError: false,
expectedScale: false,
expectedGR: schema.GroupResource{Group: appsv1.SchemeGroupVersion.Group, Resource: "deployments"},
},
{
name: "resource not registered",
hpa: newHPA("fake/v1", "FakeWorkload", "fake-workload-1", 0),
expectedError: true,
expectedScale: false,
expectedGR: schema.GroupResource{},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
gr, scale, err := syncer.getGroupResourceAndScaleForWorkloadFromHPA(context.TODO(), tt.hpa)
if tt.expectedError {
assert.NotEmpty(t, err)
return
}
assert.Empty(t, err)
if tt.expectedScale {
assert.NotEmpty(t, scale)
} else {
assert.Empty(t, scale)
}
assert.Equal(t, tt.expectedGR, gr)
})
}
}
func TestUpdateScaleIfNeed(t *testing.T) {
cases := []struct {
name string
object client.Object
gr schema.GroupResource
scale *autoscalingv1.Scale
hpa *autoscalingv2.HorizontalPodAutoscaler
expectedError bool
}{
{
name: "normal case",
object: newDeployment("deployment-1", 0),
gr: schema.GroupResource{Group: appsv1.SchemeGroupVersion.Group, Resource: "deployments"},
scale: newScale("deployment-1", 0),
hpa: newHPA(appsv1.SchemeGroupVersion.String(), "Deployment", "deployment-1", 3),
expectedError: false,
},
{
name: "custom resource case",
object: newWorkload("workload-1", 0),
gr: schema.GroupResource{Group: workloadv1alpha1.SchemeGroupVersion.Group, Resource: "workloads"},
scale: newScale("workload-1", 0),
hpa: newHPA(workloadv1alpha1.SchemeGroupVersion.String(), "Workload", "workload-1", 3),
expectedError: false,
},
{
name: "scale not found",
object: newDeployment("deployment-1", 0),
gr: schema.GroupResource{Group: "fake", Resource: "fakeworkloads"},
scale: newScale("fake-workload-1", 0),
hpa: newHPA("fake/v1", "FakeWorkload", "fake-workload-1", 3),
expectedError: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
syncer := newHPAReplicasSyncer(tt.object)
err := syncer.updateScaleIfNeed(context.TODO(), tt.gr, tt.scale, tt.hpa)
if tt.expectedError {
assert.NotEmpty(t, err)
return
}
assert.Empty(t, err)
obj := &unstructured.Unstructured{}
obj.SetAPIVersion(tt.hpa.Spec.ScaleTargetRef.APIVersion)
obj.SetKind(tt.hpa.Spec.ScaleTargetRef.Kind)
err = syncer.Client.Get(context.TODO(), types.NamespacedName{Namespace: tt.scale.Namespace, Name: tt.scale.Name}, obj)
assert.Empty(t, err)
if err != nil {
return
}
scale, err := getScaleFromUnstructured(obj)
assert.Empty(t, err)
if err != nil {
return
}
assert.Equal(t, tt.hpa.Status.DesiredReplicas, scale.Spec.Replicas)
})
}
}
func newHPAReplicasSyncer(objs ...client.Object) *HPAReplicasSyncer {
scheme := gclient.NewSchema()
_ = workloadv1alpha1.AddToScheme(scheme)
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build()
fakeMapper := newMapper()
fakeScaleClient := &scalefake.FakeScaleClient{}
fakeScaleClient.AddReactor("get", "*", reactionFuncForGetting(fakeClient, fakeMapper))
fakeScaleClient.AddReactor("update", "*", reactionFuncForUpdating(fakeClient, fakeMapper))
return &HPAReplicasSyncer{
Client: fakeClient,
RESTMapper: fakeMapper,
ScaleClient: fakeScaleClient,
}
}
func reactionFuncForGetting(c client.Client, mapper meta.RESTMapper) coretesting.ReactionFunc {
return func(action coretesting.Action) (bool, runtime.Object, error) {
getAction, ok := action.(coretesting.GetAction)
if !ok {
return false, nil, fmt.Errorf("not GET Action")
}
obj, err := newUnstructured(getAction.GetResource(), mapper)
if err != nil {
return true, nil, err
}
nn := types.NamespacedName{Namespace: getAction.GetNamespace(), Name: getAction.GetName()}
err = c.Get(context.TODO(), nn, obj)
if err != nil {
return true, nil, err
}
scale, err := getScaleFromUnstructured(obj)
return true, scale, err
}
}
func newUnstructured(gvr schema.GroupVersionResource, mapper meta.RESTMapper) (*unstructured.Unstructured, error) {
gvk, err := mapper.KindFor(gvr)
if err != nil {
return nil, err
}
un := &unstructured.Unstructured{}
un.SetGroupVersionKind(gvk)
return un, nil
}
func getScaleFromUnstructured(obj *unstructured.Unstructured) (*autoscalingv1.Scale, error) {
replicas := int32(0)
spec, ok := obj.Object["spec"].(map[string]interface{})
if ok {
replicas = int32(spec["replicas"].(int64))
}
return &autoscalingv1.Scale{
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: replicas,
},
}, nil
}
func reactionFuncForUpdating(c client.Client, mapper meta.RESTMapper) coretesting.ReactionFunc {
return func(action coretesting.Action) (bool, runtime.Object, error) {
updateAction, ok := action.(coretesting.UpdateAction)
if !ok {
return false, nil, fmt.Errorf("not UPDATE Action")
}
scale, ok := updateAction.GetObject().(*autoscalingv1.Scale)
if !ok {
return false, nil, fmt.Errorf("not autoscalingv1.Scale Object")
}
obj, err := newUnstructured(updateAction.GetResource(), mapper)
if err != nil {
return true, nil, err
}
nn := types.NamespacedName{Namespace: scale.Namespace, Name: scale.Name}
err = c.Get(context.TODO(), nn, obj)
if err != nil {
return true, nil, err
}
updateScaleForUnstructured(obj, scale)
return true, scale, c.Update(context.TODO(), obj)
}
}
func updateScaleForUnstructured(obj *unstructured.Unstructured, scale *autoscalingv1.Scale) {
spec, ok := obj.Object["spec"].(map[string]interface{})
if !ok {
spec = map[string]interface{}{}
obj.Object["spec"] = spec
}
spec["replicas"] = scale.Spec.Replicas
}
func newMapper() meta.RESTMapper {
m := meta.NewDefaultRESTMapper([]schema.GroupVersion{})
m.Add(appsv1.SchemeGroupVersion.WithKind("Deployment"), meta.RESTScopeNamespace)
m.Add(workloadv1alpha1.SchemeGroupVersion.WithKind("Workload"), meta.RESTScopeNamespace)
return m
}
func newDeployment(name string, replicas int32) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
},
}
}
func newWorkload(name string, replicas int32) *workloadv1alpha1.Workload {
return &workloadv1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: workloadv1alpha1.WorkloadSpec{
Replicas: &replicas,
},
}
}
func newHPA(apiVersion, kind, name string, replicas int32) *autoscalingv2.HorizontalPodAutoscaler {
return &autoscalingv2.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
APIVersion: apiVersion,
Kind: kind,
Name: name,
},
},
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
DesiredReplicas: replicas,
},
}
}
func newScale(name string, replicas int32) *autoscalingv1.Scale {
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
}
}