package descheduler import ( "context" "reflect" "testing" "time" "github.com/stretchr/testify/mock" "google.golang.org/grpc" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" estimatorclient "github.com/karmada-io/karmada/pkg/estimator/client" "github.com/karmada-io/karmada/pkg/estimator/pb" estimatorservice "github.com/karmada-io/karmada/pkg/estimator/service" fakekarmadaclient "github.com/karmada-io/karmada/pkg/generated/clientset/versioned/fake" informerfactory "github.com/karmada-io/karmada/pkg/generated/informers/externalversions" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/helper" ) func buildBinding(name, ns string, target, status []workv1alpha2.TargetCluster) (*workv1alpha2.ResourceBinding, error) { bindingStatus := workv1alpha2.ResourceBindingStatus{} for _, cluster := range status { statusMap := map[string]interface{}{ util.ReadyReplicasField: cluster.Replicas, } raw, err := helper.BuildStatusRawExtension(statusMap) if err != nil { return nil, err } bindingStatus.AggregatedStatus = append(bindingStatus.AggregatedStatus, workv1alpha2.AggregatedStatusItem{ ClusterName: cluster.Name, Status: raw, Applied: true, }) } return &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns, }, Spec: workv1alpha2.ResourceBindingSpec{ Clusters: target, }, Status: bindingStatus, }, nil } func TestDescheduler_worker(t *testing.T) { type args struct { target []workv1alpha2.TargetCluster status []workv1alpha2.TargetCluster unschedulable []workv1alpha2.TargetCluster name string namespace string } tests := []struct { name string args args wantResponse []workv1alpha2.TargetCluster wantErr bool }{ { name: "1 cluster without unschedulable replicas", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 0, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, wantErr: false, }, { name: "1 cluster with 1 unschedulable replicas", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 4, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 1, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 4, }, }, wantErr: false, }, { name: "1 cluster with all unschedulable replicas", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 0, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 0, }, }, wantErr: false, }, { name: "1 cluster with 4 ready replicas and 2 unschedulable replicas", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 4, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 2, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 4, }, }, wantErr: false, }, { name: "1 cluster with 0 ready replicas and 2 unschedulable replicas", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 0, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 2, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 3, }, }, wantErr: false, }, { name: "1 cluster with 6 ready replicas and 2 unschedulable replicas", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 6, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 2, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, }, wantErr: false, }, { name: "2 cluster without unschedulable replicas", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 10, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 10, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 0, }, { Name: "member2", Replicas: 0, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 10, }, }, wantErr: false, }, { name: "2 cluster with 1 unschedulable replica", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 10, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 9, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 0, }, { Name: "member2", Replicas: 1, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 9, }, }, wantErr: false, }, { name: "2 cluster with unscheable replicas of every cluster", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 10, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 2, }, { Name: "member2", Replicas: 3, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 3, }, { Name: "member2", Replicas: 7, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 2, }, { Name: "member2", Replicas: 3, }, }, wantErr: false, }, { name: "2 cluster with 1 cluster status loss", args: args{ target: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 5, }, { Name: "member2", Replicas: 10, }, }, status: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 2, }, }, unschedulable: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 3, }, { Name: "member2", Replicas: 7, }, }, name: "foo", namespace: "default", }, wantResponse: []workv1alpha2.TargetCluster{ { Name: "member1", Replicas: 2, }, { Name: "member2", Replicas: 3, }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() binding, err := buildBinding(tt.args.name, tt.args.namespace, tt.args.target, tt.args.status) if err != nil { t.Fatalf("build binding error: %v", err) } karmadaClient := fakekarmadaclient.NewSimpleClientset(binding) factory := informerfactory.NewSharedInformerFactory(karmadaClient, 0) desched := &Descheduler{ KarmadaClient: karmadaClient, informerFactory: factory, bindingInformer: factory.Work().V1alpha2().ResourceBindings().Informer(), bindingLister: factory.Work().V1alpha2().ResourceBindings().Lister(), clusterInformer: factory.Cluster().V1alpha1().Clusters().Informer(), clusterLister: factory.Cluster().V1alpha1().Clusters().Lister(), schedulerEstimatorCache: estimatorclient.NewSchedulerEstimatorCache(), unschedulableThreshold: 5 * time.Minute, eventRecorder: record.NewFakeRecorder(1024), } schedulerEstimator := estimatorclient.NewSchedulerEstimator(desched.schedulerEstimatorCache, 5*time.Second) estimatorclient.RegisterSchedulerEstimator(schedulerEstimator) for _, c := range tt.args.unschedulable { cluster := c mockClient := &estimatorservice.MockEstimatorClient{} mockResultFn := func( ctx context.Context, in *pb.UnschedulableReplicasRequest, opts ...grpc.CallOption, ) *pb.UnschedulableReplicasResponse { return &pb.UnschedulableReplicasResponse{ UnschedulableReplicas: cluster.Replicas, } } mockClient.On( "GetUnschedulableReplicas", mock.MatchedBy(func(context.Context) bool { return true }), mock.MatchedBy(func(in *pb.UnschedulableReplicasRequest) bool { return in.Cluster == cluster.Name }), ).Return(mockResultFn, nil) desched.schedulerEstimatorCache.AddCluster(cluster.Name, nil, mockClient) } desched.informerFactory.Start(ctx.Done()) if !cache.WaitForCacheSync(ctx.Done(), desched.bindingInformer.HasSynced) { t.Fatalf("Failed to wait for cache sync") } key, err := cache.MetaNamespaceKeyFunc(binding) if err != nil { t.Fatalf("Failed to get key of binding: %v", err) } if err := desched.worker(key); (err != nil) != tt.wantErr { t.Errorf("worker() error = %v, wantErr %v", err, tt.wantErr) return } binding, err = desched.KarmadaClient.WorkV1alpha2().ResourceBindings(tt.args.namespace).Get(ctx, tt.args.name, metav1.GetOptions{}) if err != nil { t.Errorf("Failed to get binding: %v", err) return } gotResponse := binding.Spec.Clusters if !reflect.DeepEqual(gotResponse, tt.wantResponse) { t.Errorf("descheduler worker() gotResponse = %v, want %v", gotResponse, tt.wantResponse) } }) } }