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