Repository: clusternet/clusternet
Pull request #524 (2022-11-07 03:51:34)

add Subscription SchedulingByGroup configuration support clusters grouping
add Subscription SchedulingByGroup configuration support clusters grouping

codecov-commenter: # [Codecov](https://codecov.io/gh/clusternet/clusternet/pull/524?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) Report
> :exclamation: No coverage uploaded for pull request base (`main@3e6c547`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet#section-missing-base-commit).
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #524   +/-   ##
=======================================
  Coverage        ?   13.51%           
=======================================
  Files           ?       65           
  Lines           ?     7120           
  Branches        ?        0           
=======================================
  Hits            ?      962           
  Misses          ?     6081           
  Partials        ?       77           
```



Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet)


Patch:
diff --git a/examples/scheduling-group/nginx-deployment.yaml b/examples/scheduling-group/nginx-deployment.yaml
new file mode 100644
index 0000000..768f064
--- /dev/null
+++ b/examples/scheduling-group/nginx-deployment.yaml
@@ -0,0 +1,35 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: qux
+---
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: my-nginx
+  namespace: qux
+  labels:
+    clusternet-app: multi-cluster-nginx
+spec:
+  selector:
+    matchLabels:
+      app: nginx
+  replicas: 6
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+        - name: nginx
+          image: nginx:1.14.2
+          ports:
+            - containerPort: 80
+          resources:
+            requests:
+              memory: 128Mi
+              cpu: 500m
+            limits:
+              memory: 256Mi
+              cpu: 500m
diff --git a/examples/scheduling-group/nginx-svc.yaml b/examples/scheduling-group/nginx-svc.yaml
new file mode 100644
index 0000000..726a5ce
--- /dev/null
+++ b/examples/scheduling-group/nginx-svc.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-nginx-svc
+  namespace: qux
+  labels:
+    app: nginx
+spec:
+  type: ClusterIP
+  ports:
+    - port: 80
+  selector:
+    app: nginx
diff --git a/examples/scheduling-group/subscription-dynamic-dividing.yaml b/examples/scheduling-group/subscription-dynamic-dividing.yaml
new file mode 100644
index 0000000..f19e446
--- /dev/null
+++ b/examples/scheduling-group/subscription-dynamic-dividing.yaml
@@ -0,0 +1,35 @@
+apiVersion: apps.clusternet.io/v1alpha1
+kind: Subscription
+metadata:
+  name: dynamic-dividing-scheduling-demo
+  namespace: default
+spec:
+  schedulingByGroup: true
+  subscribers: # defines the clusters to be distributed to
+    - clusterAffinity:
+        matchLabels:
+          region: ap-shanghai
+      groupStrategy:
+        pickClustersNum: 1 # the number of pick cluster in every group
+    - clusterAffinity:
+        matchLabels:
+          region: ap-beijing
+      groupStrategy:
+        pickClustersNum: 1 # the number of pick cluster in every group
+  schedulingStrategy: Dividing
+  dividingScheduling:
+    type: Dynamic
+    dynamicDividing:
+      strategy: Spread # currently we only support Spread dividing strategy
+  feeds: # defines all the resources to be deployed with
+    - apiVersion: v1
+      kind: Namespace
+      name: qux
+    - apiVersion: v1
+      kind: Service
+      name: my-nginx-svc
+      namespace: qux
+    - apiVersion: apps/v1 # with a total of 6 replicas
+      kind: Deployment
+      name: my-nginx
+      namespace: qux
diff --git a/examples/scheduling-group/subscription-replication.yaml b/examples/scheduling-group/subscription-replication.yaml
new file mode 100644
index 0000000..04fa0a2
--- /dev/null
+++ b/examples/scheduling-group/subscription-replication.yaml
@@ -0,0 +1,31 @@
+apiVersion: apps.clusternet.io/v1alpha1
+kind: Subscription
+metadata:
+  name: replication-scheduling-demo
+  namespace: default
+spec:
+  schedulingBySubGroup: true
+  subscribers: # defines the clusters to be distributed to
+    - clusterAffinity:
+        matchLabels:
+          region: ap-shanghai
+      subGroupStrategy:
+        minClusters: 2 # the number of pick cluster in every group
+    - clusterAffinity:
+        matchLabels:
+          region: ap-beijing
+      subGroupStrategy:
+        minClusters: 1 # the number of pick cluster in every group
+  schedulingStrategy: Replication
+  feeds: # defines all the resources to be deployed with
+    - apiVersion: v1
+      kind: Namespace
+      name: qux
+    - apiVersion: v1
+      kind: Service
+      name: my-nginx-svc
+      namespace: qux
+    - apiVersion: apps/v1 # with a total of 6 replicas
+      kind: Deployment
+      name: my-nginx
+      namespace: qux
diff --git a/examples/scheduling-group/subscription-static-dividing.yaml b/examples/scheduling-group/subscription-static-dividing.yaml
new file mode 100644
index 0000000..c03fe61
--- /dev/null
+++ b/examples/scheduling-group/subscription-static-dividing.yaml
@@ -0,0 +1,35 @@
+apiVersion: apps.clusternet.io/v1alpha1
+kind: Subscription
+metadata:
+  name: static-dividing-scheduling-demo
+  namespace: default
+spec:
+  schedulingBySubGroup: true
+  subscribers: # defines the clusters to be distributed to
+    - clusterAffinity:
+        matchLabels:
+          region: ap-shanghai
+      subGroupStrategy:
+        minClusters: 1 # the number of pick cluster in every group
+      weight: 2
+    - clusterAffinity:
+        matchLabels:
+          region: ap-beijing
+      subGroupStrategy:
+        minClusters: 1 # the number of pick cluster in every group
+      weight: 1
+  schedulingStrategy: Dividing
+  dividingScheduling:
+    type: Static
+  feeds: # defines all the resources to be deployed with
+    - apiVersion: v1
+      kind: Namespace
+      name: qux
+    - apiVersion: v1
+      kind: Service
+      name: my-nginx-svc
+      namespace: qux
+    - apiVersion: apps/v1 # with a total of 6 replicas
+      kind: Deployment
+      name: my-nginx
+      namespace: qux
diff --git a/manifests/crds/apps.clusternet.io_subscriptions.yaml b/manifests/crds/apps.clusternet.io_subscriptions.yaml
index b91cd51..b0d4c80 100644
--- a/manifests/crds/apps.clusternet.io_subscriptions.yaml
+++ b/manifests/crds/apps.clusternet.io_subscriptions.yaml
@@ -366,6 +366,11 @@ spec:
                   scheduler. If not specified, the Subscription will be handled by
                   default scheduler.
                 type: string
+              schedulingBySubGroup:
+                description: If specified, the Subscription will be handled with SchedulingBySubGroup.
+                  Used together with SubGroupStrategy in every Subscriber. Can work
+                  with all supported SchedulingStrategy, such as Replication, Dividing.
+                type: boolean
               schedulingStrategy:
                 default: Replication
                 description: If specified, the Subscription will be handled with specified
@@ -424,6 +429,23 @@ spec:
                             only "value". The requirements are ANDed.
                           type: object
                       type: object
+                    subGroupStrategy:
+                      description: SubGroupStrategy defines the subgroup strategy
+                        for the clusters matched by this subscriber. During the scheduling,
+                        all the matching clusters will be treated as a subgroup instead
+                        of individual clusters. With subgroup, we can describe clusters
+                        with different regions, zones, etc. Present only when SchedulingBySubGroup
+                        is set.
+                      properties:
+                        minClusters:
+                          description: MinClusters is the minimum number of clusters
+                            to be selected in this subgroup. If this value is more
+                            than the total number of clusters in this subgroup, then
+                            all clusters will be selected.
+                          format: int32
+                          minimum: 0
+                          type: integer
+                      type: object
                     weight:
                       description: Static weight of subscriber when dividing replicas.
                         Present only for static divided scheduling.
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index e793c45..353c9d9 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -93,8 +93,10 @@ type Agent struct {
 
 	deployer *deployer.Deployer
 
-	predictor    *predictor.Server
-	seController *mcs.SeController
+	predictor *predictor.Server
+
+	serviceExportEnabled    bool
+	serviceExportController *mcs.ServiceExportController
 }
 
 // NewAgent returns a new Agent.
@@ -153,6 +155,17 @@ func NewAgent(registrationOpts *ClusterRegistrationOptions, controllerOpts *util
 		return nil, err
 	}
 
+	// parse serviceExportEnabled (EndpointSliceV1Promoted) by server version
+	var serviceExportEnabled bool
+	if serviceExportEnabled, err = utils.EndpointSliceV1Promoted(ver.String()); err != nil {
+		return nil, err
+	}
+
+	var serviceExportController *mcs.ServiceExportController
+	if serviceExportEnabled {
+		serviceExportController = mcs.NewServiceExportController(kubeInformerFactory.Discovery().V1().EndpointSlices(), mcsClientSet, mcsInformerFactory)
+	}
+
 	agent := &Agent{
 		Identity:               identity,
 		childKubeClientSet:     childKubeClientSet,
@@ -172,8 +185,9 @@ func NewAgent(registrationOpts *ClusterRegistrationOptions, controllerOpts *util
 			childKubeConfig.Host,
 			controllerOpts.LeaderElection.ResourceNamespace,
 			saTokenAutoGenerated),
-		predictor:    p,
-		seController: mcs.NewSeController(kubeInformerFactory.Discovery().V1beta1().EndpointSlices(), mcsClientSet, mcsInformerFactory),
+		predictor:               p,
+		serviceExportEnabled:    serviceExportEnabled,
+		serviceExportController: serviceExportController,
 	}
 	return agent, nil
 }
@@ -256,11 +270,13 @@ func (agent *Agent) run(ctx context.Context) {
 		}
 	}, time.Duration(0))
 
-	go wait.UntilWithContext(ctx, func(ctx context.Context) {
-		if err := agent.seController.Run(ctx, agent.parentDedicatedKubeConfig, *agent.DedicatedNamespace); err != nil {
-			klog.Error(err)
-		}
-	}, time.Duration(0))
+	if agent.serviceExportEnabled {
+		go wait.UntilWithContext(ctx, func(ctx context.Context) {
+			if err := agent.serviceExportController.Run(ctx, agent.parentDedicatedKubeConfig, *agent.DedicatedNamespace); err != nil {
+				klog.Error(err)
+			}
+		}, time.Duration(0))
+	}
 
 	if agent.predictor != nil {
 		go wait.UntilWithContext(ctx, func(ctx context.Context) {
diff --git a/pkg/apis/apps/v1alpha1/subscription.go b/pkg/apis/apps/v1alpha1/subscription.go
index c4ef991..d4ec274 100644
--- a/pkg/apis/apps/v1alpha1/subscription.go
+++ b/pkg/apis/apps/v1alpha1/subscription.go
@@ -48,6 +48,13 @@ type SubscriptionSpec struct {
 	// +kubebuilder:default=default
 	SchedulerName string `json:"schedulerName,omitempty"`
 
+	// If specified, the Subscription will be handled with SchedulingBySubGroup.
+	// Used together with SubGroupStrategy in every Subscriber.
+	// Can work with all supported SchedulingStrategy, such as Replication, Dividing.
+	//
+	// +optional
+	SchedulingBySubGroup *bool `json:"schedulingBySubGroup,omitempty"`
+
 	// If specified, the Subscription will be handled with specified SchedulingStrategy.
 	// Otherwise, with generic SchedulingStrategy.
 	//
@@ -226,6 +233,26 @@ type Subscriber struct {
 	// +optional
 	// +kubebuilder:validation:Minimum=0
 	Weight int32 `json:"weight,omitempty"`
+
+	// SubGroupStrategy defines the subgroup strategy for the clusters matched by this subscriber.
+	// During the scheduling, all the matching clusters will be treated as a subgroup instead of individual clusters.
+	// With subgroup, we can describe clusters with different regions, zones, etc.
+	// Present only when SchedulingBySubGroup is set.
+	//
+	// +optional
+	SubGroupStrategy *SubGroupStrategy `json:"subGroupStrategy,omitempty"`
+}
+
+// SubGroupStrategy defines the subgroup strategy
+type SubGroupStrategy struct {
+	// MinClusters is the minimum number of clusters to be selected in this subgroup.
+	// If this value is more than the total number of clusters in this subgroup, then all clusters will be selected.
+	//
+	// +optional
+	// +kubebuilder:validation:Minimum=0
+	MinClusters int32 `json:"minClusters,omitempty"`
+
+	// TODO: other scenarios
 }
 
 // Feed defines the resource to be selected.
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
index 90c0675..0ffcf4a 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
@@ -1076,6 +1076,22 @@ func (in *ReplicaStatus) DeepCopy() *ReplicaStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SubGroupStrategy) DeepCopyInto(out *SubGroupStrategy) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubGroupStrategy.
+func (in *SubGroupStrategy) DeepCopy() *SubGroupStrategy {
+	if in == nil {
+		return nil
+	}
+	out := new(SubGroupStrategy)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Subscriber) DeepCopyInto(out *Subscriber) {
 	*out = *in
@@ -1084,6 +1100,11 @@ func (in *Subscriber) DeepCopyInto(out *Subscriber) {
 		*out = new(metav1.LabelSelector)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.SubGroupStrategy != nil {
+		in, out := &in.SubGroupStrategy, &out.SubGroupStrategy
+		*out = new(SubGroupStrategy)
+		**out = **in
+	}
 	return
 }
 
@@ -1161,6 +1182,11 @@ func (in *SubscriptionList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SubscriptionSpec) DeepCopyInto(out *SubscriptionSpec) {
 	*out = *in
+	if in.SchedulingBySubGroup != nil {
+		in, out := &in.SchedulingBySubGroup, &out.SchedulingBySubGroup
+		*out = new(bool)
+		**out = **in
+	}
 	if in.DividingScheduling != nil {
 		in, out := &in.DividingScheduling, &out.DividingScheduling
 		*out = new(DividingScheduling)
diff --git a/pkg/controllers/mcs/serviceexport_controller.go b/pkg/controllers/mcs/serviceexport_controller.go
index 9b87fd9..92eec28 100644
--- a/pkg/controllers/mcs/serviceexport_controller.go
+++ b/pkg/controllers/mcs/serviceexport_controller.go
@@ -23,16 +23,16 @@ import (
 	"time"
 
 	"github.com/dixudx/yacht"
-	discoveryv1beta1 "k8s.io/api/discovery/v1beta1"
+	discoveryv1 "k8s.io/api/discovery/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
-	discoveryinformerv1beta1 "k8s.io/client-go/informers/discovery/v1beta1"
+	discoveryinformerv1 "k8s.io/client-go/informers/discovery/v1"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/scheme"
-	discoverylisterv1beta1 "k8s.io/client-go/listers/discovery/v1beta1"
+	discoverylisterv1 "k8s.io/client-go/listers/discovery/v1"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/klog/v2"
@@ -50,7 +50,7 @@ func init() {
 	utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
 }
 
-type SeController struct {
+type ServiceExportController struct {
 	//local msc client
 	mcsClientset       *mcsclientset.Clientset
 	parentk8sClient    kubernetes.Interface
@@ -58,15 +58,15 @@ type SeController struct {
 	// child cluster dedicated namespace
 	dedicatedNamespace    string
 	serviceExportLister   alpha1.ServiceExportLister
-	endpointSlicesLister  discoverylisterv1beta1.EndpointSliceLister
+	endpointSlicesLister  discoverylisterv1.EndpointSliceLister
 	serviceExportInformer mcsv1alpha1.ServiceExportInformer
-	endpointSliceInformer discoveryinformerv1beta1.EndpointSliceInformer
+	endpointSliceInformer discoveryinformerv1.EndpointSliceInformer
 }
 
-func NewSeController(epsInformer discoveryinformerv1beta1.EndpointSliceInformer, mcsClientset *mcsclientset.Clientset,
-	mcsInformerFactory mcsInformers.SharedInformerFactory) *SeController {
+func NewServiceExportController(epsInformer discoveryinformerv1.EndpointSliceInformer, mcsClientset *mcsclientset.Clientset,
+	mcsInformerFactory mcsInformers.SharedInformerFactory) *ServiceExportController {
 	seInformer := mcsInformerFactory.Multicluster().V1alpha1().ServiceExports()
-	c := &SeController{
+	c := &ServiceExportController{
 		mcsClientset:          mcsClientset,
 		mcsInformerFactory:    mcsInformerFactory,
 		endpointSlicesLister:  epsInformer.Lister(),
@@ -77,7 +77,7 @@ func NewSeController(epsInformer discoveryinformerv1beta1.EndpointSliceInformer,
 	return c
 }
 
-func (c *SeController) Handle(obj interface{}) (requeueAfter *time.Duration, err error) {
+func (c *ServiceExportController) Handle(obj interface{}) (requeueAfter *time.Duration, err error) {
 	ctx := context.Background()
 	key := obj.(string)
 	namespace, seName, err := cache.SplitMetaNamespaceKey(key)
@@ -109,8 +109,8 @@ func (c *SeController) Handle(obj interface{}) (requeueAfter *time.Duration, err
 
 	// recycle corresponding endpoint slice in parent cluster.
 	if seTerminating {
-		if err = c.parentk8sClient.DiscoveryV1beta1().EndpointSlices(c.dedicatedNamespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
-			LabelSelector: labels.SelectorFromSet(labels.Set{discoveryv1beta1.LabelServiceName: utils.DerivedName(namespace, seName)}).String(),
+		if err = c.parentk8sClient.DiscoveryV1().EndpointSlices(c.dedicatedNamespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
+			LabelSelector: labels.SelectorFromSet(labels.Set{discoveryv1.LabelServiceName: utils.DerivedName(namespace, seName)}).String(),
 		}); err != nil {
 			// try next time, make sure we clear endpoint slice
 			d := time.Second
@@ -127,9 +127,9 @@ func (c *SeController) Handle(obj interface{}) (requeueAfter *time.Duration, err
 		return nil, nil
 	}
 	// src endpoint slice with label of service export name is same to service name.
-	srcLabelMap := labels.Set{discoveryv1beta1.LabelServiceName: se.Name}
+	srcLabelMap := labels.Set{discoveryv1.LabelServiceName: se.Name}
 	// dst endpoint slice with label of derived service name combined with namespace and service export name
-	dstLabelMap := labels.Set{discoveryv1beta1.LabelServiceName: utils.DerivedName(namespace, seName)}
+	dstLabelMap := labels.Set{discoveryv1.LabelServiceName: utils.DerivedName(namespace, seName)}
 	endpointSliceList, err := utils.RemoveUnexistEndpointslice(c.endpointSlicesLister, namespace,
 		srcLabelMap, c.parentk8sClient, c.dedicatedNamespace, dstLabelMap)
 	if err != nil {
@@ -144,7 +144,7 @@ func (c *SeController) Handle(obj interface{}) (requeueAfter *time.Duration, err
 		wg.Add(1)
 		slice := endpointSliceList[index].DeepCopy()
 		newSlice := constructEndpointSlice(slice, se, c.dedicatedNamespace)
-		go func(slice *discoveryv1beta1.EndpointSlice) {
+		go func(slice *discoveryv1.EndpointSlice) {
 			defer wg.Done()
 			if err = utils.ApplyEndPointSliceWithRetry(c.parentk8sClient, slice); err != nil {
 				errCh <- err
@@ -169,7 +169,7 @@ func (c *SeController) Handle(obj interface{}) (requeueAfter *time.Duration, err
 	return nil, nil
 }
 
-func (c *SeController) Run(ctx context.Context, parentDedicatedKubeConfig *rest.Config, delicatedNamespace string) error {
+func (c *ServiceExportController) Run(ctx context.Context, parentDedicatedKubeConfig *rest.Config, delicatedNamespace string) error {
 	c.mcsInformerFactory.Start(ctx.Done())
 	// set parent cluster related filed.
 	c.dedicatedNamespace = delicatedNamespace
@@ -184,8 +184,8 @@ func (c *SeController) Run(ctx context.Context, parentDedicatedKubeConfig *rest.
 
 	c.endpointSliceInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{
 		FilterFunc: func(obj interface{}) bool {
-			endpointSlice := obj.(*discoveryv1beta1.EndpointSlice)
-			if serviceName, ok := endpointSlice.Labels[discoveryv1beta1.LabelServiceName]; ok {
+			endpointSlice := obj.(*discoveryv1.EndpointSlice)
+			if serviceName, ok := endpointSlice.Labels[discoveryv1.LabelServiceName]; ok {
 				if _, err := c.serviceExportLister.ServiceExports(endpointSlice.Namespace).Get(serviceName); err == nil {
 					return true
 				}
@@ -216,9 +216,9 @@ func (c *SeController) Run(ctx context.Context, parentDedicatedKubeConfig *rest.
 	return nil
 }
 
-func (c *SeController) getServiceExportFromEndpointSlice(obj interface{}) (*v1alpha1.ServiceExport, error) {
-	slice := obj.(*discoveryv1beta1.EndpointSlice)
-	if serviceName, ok := slice.Labels[discoveryv1beta1.LabelServiceName]; ok {
+func (c *ServiceExportController) getServiceExportFromEndpointSlice(obj interface{}) (*v1alpha1.ServiceExport, error) {
+	slice := obj.(*discoveryv1.EndpointSlice)
+	if serviceName, ok := slice.Labels[discoveryv1.LabelServiceName]; ok {
 		if se, err := c.serviceExportLister.ServiceExports(slice.Namespace).Get(serviceName); err == nil {
 			return se, nil
 		}
@@ -227,9 +227,9 @@ func (c *SeController) getServiceExportFromEndpointSlice(obj interface{}) (*v1al
 }
 
 // constructEndpointSlice construct a new endpoint slice from local slice.
-func constructEndpointSlice(slice *discoveryv1beta1.EndpointSlice, se *v1alpha1.ServiceExport, namespace string) *discoveryv1beta1.EndpointSlice {
+func constructEndpointSlice(slice *discoveryv1.EndpointSlice, se *v1alpha1.ServiceExport, namespace string) *discoveryv1.EndpointSlice {
 	// mutate slice fields before upload to parent cluster.
-	newSlice := &discoveryv1beta1.EndpointSlice{}
+	newSlice := &discoveryv1.EndpointSlice{}
 	newSlice.AddressType = slice.AddressType
 	newSlice.Endpoints = slice.Endpoints
 	newSlice.Ports = slice.Ports
@@ -238,7 +238,7 @@ func constructEndpointSlice(slice *discoveryv1beta1.EndpointSlice, se *v1alpha1.
 	newSlice.Labels[known.LabelServiceName] = se.Name
 	newSlice.Labels[known.LabelServiceNameSpace] = se.Namespace
 	newSlice.Labels[known.ObjectCreatedByLabel] = known.ClusternetAgentName
-	newSlice.Labels[discoveryv1beta1.LabelServiceName] = utils.DerivedName(se.Namespace, se.Name)
+	newSlice.Labels[discoveryv1.LabelServiceName] = utils.DerivedName(se.Namespace, se.Name)
 
 	if subNamespace, exist := se.Labels[known.ConfigSubscriptionNamespaceLabel]; exist {
 		newSlice.Labels[known.ConfigSubscriptionNamespaceLabel] = subNamespace
diff --git a/pkg/controllers/mcs/serviceimport_controller.go b/pkg/controllers/mcs/serviceimport_controller.go
index c5b8e2d..0677ebb 100644
--- a/pkg/controllers/mcs/serviceimport_controller.go
+++ b/pkg/controllers/mcs/serviceimport_controller.go
@@ -25,16 +25,16 @@ import (
 
 	"github.com/dixudx/yacht"
 	corev1 "k8s.io/api/core/v1"
-	discoveryv1beta1 "k8s.io/api/discovery/v1beta1"
+	discoveryv1 "k8s.io/api/discovery/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
-	discoveryinformerv1beta1 "k8s.io/client-go/informers/discovery/v1beta1"
+	discoveryinformerv1 "k8s.io/client-go/informers/discovery/v1"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/scheme"
-	discoverylisterv1beta1 "k8s.io/client-go/listers/discovery/v1beta1"
+	discoverylisterv1 "k8s.io/client-go/listers/discovery/v1"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/util/retry"
 	"k8s.io/klog/v2"
@@ -56,13 +56,13 @@ type ServiceImportController struct {
 	mcsClientset          *mcsclientset.Clientset
 	localk8sClient        kubernetes.Interface
 	serviceImportLister   alpha1.ServiceImportLister
-	endpointSlicesLister  discoverylisterv1beta1.EndpointSliceLister
+	endpointSlicesLister  discoverylisterv1.EndpointSliceLister
 	serviceImportInformer mcsv1alpha1.ServiceImportInformer
-	endpointSliceInformer discoveryinformerv1beta1.EndpointSliceInformer
+	endpointSliceInformer discoveryinformerv1.EndpointSliceInformer
 	mcsInformerFactory    mcsInformers.SharedInformerFactory
 }
 
-func NewServiceImportController(kubeclient kubernetes.Interface, epsInformer discoveryinformerv1beta1.EndpointSliceInformer, mcsClientset *mcsclientset.Clientset,
+func NewServiceImportController(kubeclient kubernetes.Interface, epsInformer discoveryinformerv1.EndpointSliceInformer, mcsClientset *mcsclientset.Clientset,
 	mcsInformerFactory mcsInformers.SharedInformerFactory) *ServiceImportController {
 	siInformer := mcsInformerFactory.Multicluster().V1alpha1().ServiceImports()
 	si := &ServiceImportController{
@@ -146,7 +146,7 @@ func (c *ServiceImportController) Handle(obj interface{}) (requeueAfter *time.Du
 		wg.Add(1)
 		slice := endpointSliceList[index].DeepCopy()
 		newSlice := forkEndpointSlice(slice, namespace)
-		go func(slice *discoveryv1beta1.EndpointSlice) {
+		go func(slice *discoveryv1.EndpointSlice) {
 			defer wg.Done()
 			if err = utils.ApplyEndPointSliceWithRetry(c.localk8sClient, slice); err != nil {
 				errCh <- err
@@ -268,7 +268,7 @@ func (c *ServiceImportController) recycleServiceImport(ctx context.Context, si *
 	rawServiceName, _ := si.Labels[known.LabelServiceName]
 	rawServiceNamespace, _ := si.Labels[known.LabelServiceNameSpace]
 	// 1. recycle endpoint slices.
-	if err := c.localk8sClient.DiscoveryV1beta1().EndpointSlices(si.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
+	if err := c.localk8sClient.DiscoveryV1().EndpointSlices(si.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
 		LabelSelector: labels.SelectorFromSet(labels.Set{
 			known.LabelServiceName:      rawServiceName,
 			known.LabelServiceNameSpace: rawServiceNamespace}).String(),
@@ -292,7 +292,7 @@ func (c *ServiceImportController) recycleServiceImport(ctx context.Context, si *
 
 // getServiceImportFromEndpointSlice get ServiceImport from endpointSlice labels, get the first if more than one.
 func (c *ServiceImportController) getServiceImportFromEndpointSlice(obj interface{}) (*v1alpha1.ServiceImport, error) {
-	slice := obj.(*discoveryv1beta1.EndpointSlice)
+	slice := obj.(*discoveryv1.EndpointSlice)
 	rawServiceName, serviceExist := slice.Labels[known.LabelServiceName]
 	rawServiceNamespace, serviceNamespaceExsit := slice.Labels[known.LabelServiceNameSpace]
 	subNamespace, subNamespaceExsit := slice.Labels[known.ConfigSubscriptionNamespaceLabel]
@@ -310,9 +310,9 @@ func (c *ServiceImportController) getServiceImportFromEndpointSlice(obj interfac
 }
 
 // forkEndpointSlice construct a new endpoint slice from source slice.
-func forkEndpointSlice(slice *discoveryv1beta1.EndpointSlice, namespace string) *discoveryv1beta1.EndpointSlice {
+func forkEndpointSlice(slice *discoveryv1.EndpointSlice, namespace string) *discoveryv1.EndpointSlice {
 	// mutate slice fields before upload to parent cluster.
-	newSlice := &discoveryv1beta1.EndpointSlice{
+	newSlice := &discoveryv1.EndpointSlice{
 		AddressType: slice.AddressType,
 		Endpoints:   slice.Endpoints,
 		Ports:       slice.Ports,
diff --git a/pkg/hub/hub.go b/pkg/hub/hub.go
index 3bd8fbe..baa0488 100644
--- a/pkg/hub/hub.go
+++ b/pkg/hub/hub.go
@@ -36,6 +36,7 @@ import (
 	utilrand "k8s.io/apimachinery/pkg/util/rand"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/apimachinery/pkg/version"
 	genericapiserver "k8s.io/apiserver/pkg/server"
 	utilfeature "k8s.io/apiserver/pkg/util/feature"
 	"k8s.io/client-go/dynamic"
@@ -102,7 +103,9 @@ type Hub struct {
 
 	socketConnection bool
 	deployerEnabled  bool
-	serviceImport    *mcs.ServiceImportController
+
+	serviceImportEnabled bool
+	serviceImport        *mcs.ServiceImportController
 }
 
 // NewHub returns a new Hub.
@@ -176,7 +179,21 @@ func NewHub(opts *options.HubServerOptions) (*Hub, error) {
 		klog.Warning("will not discovery clusters created by cluster-api providers due to empty kubeconfig path")
 	}
 
-	serviceImport := mcs.NewServiceImportController(kubeClient, kubeInformerFactory.Discovery().V1beta1().EndpointSlices(), mcsClientSet, mcsInformerFactory)
+	// get kubernetes server version
+	var ver *version.Info
+	if ver, err = mcsClientSet.Discovery().ServerVersion(); err != nil {
+		return nil, err
+	}
+
+	var serviceImportEnabled bool
+	if serviceImportEnabled, err = utils.EndpointSliceV1Promoted(ver.String()); err != nil {
+		return nil, err
+	}
+
+	var serviceImport *mcs.ServiceImportController
+	if serviceImportEnabled {
+		serviceImport = mcs.NewServiceImportController(kubeClient, kubeInformerFactory.Discovery().V1().EndpointSlices(), mcsClientSet, mcsInformerFactory)
+	}
 
 	hub := &Hub{
 		peerID:                    utilrand.String(5),
@@ -195,6 +212,7 @@ func NewHub(opts *options.HubServerOptions) (*Hub, error) {
 		deployerEnabled:           deployerEnabled,
 		clusterLifecycle:          clusterLifecycle,
 		clusterDiscovery:          clusterDiscovery,
+		serviceImportEnabled:      serviceImportEnabled,
 		serviceImport:             serviceImport,
 	}
 	return hub, nil
@@ -419,9 +437,11 @@ func (hub *Hub) runControllers(ctx context.Context) {
 		}()
 	}
 
-	go func() {
-		hub.serviceImport.Run(ctx)
-	}()
+	if hub.serviceImportEnabled {
+		go func() {
+			hub.serviceImport.Run(ctx)
+		}()
+	}
 
 	go func() {
 		if hub.clusterDiscovery != nil {
diff --git a/pkg/scheduler/algorithm/generic.go b/pkg/scheduler/algorithm/generic.go
index 3549607..4f3f996 100644
--- a/pkg/scheduler/algorithm/generic.go
+++ b/pkg/scheduler/algorithm/generic.go
@@ -26,6 +26,8 @@ import (
 	"sync/atomic"
 	"time"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/klog/v2"
 	utiltrace "k8s.io/utils/trace"
@@ -101,7 +103,13 @@ func (g *genericScheduler) Schedule(ctx context.Context, fwk framework.Framework
 		return result, err
 	}
 
-	clusters, err := g.selectClusters(ctx, state, priorityList, fwk, sub, finv)
+	// Step 4: Subgroup clusters.
+	subgroupList, err := g.subgroupClusters(sub, priorityList)
+	if err != nil {
+		return result, err
+	}
+
+	clusters, err := g.selectClusters(ctx, state, subgroupList, fwk, sub, finv)
 	trace.Step("Prioritizing done")
 
 	return ScheduleResult{
@@ -393,6 +401,47 @@ func prioritizeClusters(ctx context.Context, fwk framework.Framework, state *fra
 	return result, nil
 }
 
+// subgroupClusters grouping the clusters by subgroups
+func (g *genericScheduler) subgroupClusters(sub *appsapi.Subscription, clusterScoreList framework.ClusterScoreList) (framework.ClusterScoreList, error) {
+	if sub.Spec.SchedulingBySubGroup == nil || !*sub.Spec.SchedulingBySubGroup {
+		return clusterScoreList, nil
+	}
+
+	subgroup := make([]framework.ClusterScoreList, len(sub.Spec.Subscribers))
+	result := make(framework.ClusterScoreList, 0)
+	for i, subscriber := range sub.Spec.Subscribers {
+		selector, err := metav1.LabelSelectorAsSelector(subscriber.ClusterAffinity)
+		if err != nil {
+			continue
+		}
+		for _, clusterScore := range clusterScoreList {
+			cluster, err2 := g.cache.Get(clusterScore.NamespacedName)
+			if err2 != nil {
+				return nil, err2
+			}
+			if !selector.Matches(labels.Set(cluster.Labels)) {
+				continue
+			}
+			subgroup[i] = append(subgroup[i], clusterScore)
+		}
+
+		if subscriber.SubGroupStrategy == nil {
+			return nil, fmt.Errorf("subGroupStrategy filed can not be Empty")
+		}
+		minClusters := subscriber.SubGroupStrategy.MinClusters
+
+		subgroupLen := int32(len(subgroup[i]))
+		if minClusters > subgroupLen {
+			minClusters = subgroupLen
+		}
+		subgroup[i] = subgroup[i][:minClusters]
+
+		result = append(result, subgroup[i]...)
+	}
+
+	return result, nil
+}
+
 // NewGenericScheduler creates a genericScheduler object.
 func NewGenericScheduler(cache schedulercache.Cache) ScheduleAlgorithm {
 	return &genericScheduler{
diff --git a/pkg/utils/endpointslice.go b/pkg/utils/endpointslice.go
index e389aac..87c614a 100644
--- a/pkg/utils/endpointslice.go
+++ b/pkg/utils/endpointslice.go
@@ -19,21 +19,21 @@ import (
 	"context"
 	"fmt"
 
-	discoveryv1beta1 "k8s.io/api/discovery/v1beta1"
+	discoveryv1 "k8s.io/api/discovery/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/client-go/kubernetes"
-	discoverylisterv1beta1 "k8s.io/client-go/listers/discovery/v1beta1"
+	discoverylisterv1 "k8s.io/client-go/listers/discovery/v1"
 	"k8s.io/client-go/util/retry"
 )
 
 // ApplyEndPointSliceWithRetry create or update existed slices.
-func ApplyEndPointSliceWithRetry(client kubernetes.Interface, slice *discoveryv1beta1.EndpointSlice) error {
+func ApplyEndPointSliceWithRetry(client kubernetes.Interface, slice *discoveryv1.EndpointSlice) error {
 	return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
 		var lastError error
-		_, lastError = client.DiscoveryV1beta1().EndpointSlices(slice.GetNamespace()).Create(context.TODO(), slice, metav1.CreateOptions{})
+		_, lastError = client.DiscoveryV1().EndpointSlices(slice.GetNamespace()).Create(context.TODO(), slice, metav1.CreateOptions{})
 		if lastError == nil {
 			return nil
 		}
@@ -41,7 +41,7 @@ func ApplyEndPointSliceWithRetry(client kubernetes.Interface, slice *discoveryv1
 			return lastError
 		}
 
-		curObj, err := client.DiscoveryV1beta1().EndpointSlices(slice.GetNamespace()).Get(context.TODO(), slice.GetName(), metav1.GetOptions{})
+		curObj, err := client.DiscoveryV1().EndpointSlices(slice.GetNamespace()).Get(context.TODO(), slice.GetName(), metav1.GetOptions{})
 		if err != nil {
 			return err
 		}
@@ -52,7 +52,7 @@ func ApplyEndPointSliceWithRetry(client kubernetes.Interface, slice *discoveryv1
 			curObj.Ports = slice.Ports
 			curObj.Endpoints = slice.Endpoints
 			curObj.AddressType = slice.AddressType
-			_, lastError = client.DiscoveryV1beta1().EndpointSlices(slice.GetNamespace()).Update(context.TODO(), curObj, metav1.UpdateOptions{})
+			_, lastError = client.DiscoveryV1().EndpointSlices(slice.GetNamespace()).Update(context.TODO(), curObj, metav1.UpdateOptions{})
 			if lastError == nil {
 				return nil
 			}
@@ -61,8 +61,8 @@ func ApplyEndPointSliceWithRetry(client kubernetes.Interface, slice *discoveryv1
 	})
 }
 
-func RemoveUnexistEndpointslice(srcLister discoverylisterv1beta1.EndpointSliceLister, srcNamespace string, labelMap labels.Set,
-	targetClient kubernetes.Interface, targetNamespace string, dstLabelMap labels.Set) ([]*discoveryv1beta1.EndpointSlice, error) {
+func RemoveUnexistEndpointslice(srcLister discoverylisterv1.EndpointSliceLister, srcNamespace string, labelMap labels.Set,
+	targetClient kubernetes.Interface, targetNamespace string, dstLabelMap labels.Set) ([]*discoveryv1.EndpointSlice, error) {
 	srcEndpointSliceList, err := srcLister.EndpointSlices(srcNamespace).List(
 		labels.SelectorFromSet(labelMap))
 	if err != nil {
@@ -76,11 +76,11 @@ func RemoveUnexistEndpointslice(srcLister discoverylisterv1beta1.EndpointSliceLi
 		srcEndpointSliceMap[fmt.Sprintf("%s-%s", item.Namespace, item.Name)] = true
 	}
 
-	if targetEndpointSliceList, err := targetClient.DiscoveryV1beta1().EndpointSlices(targetNamespace).List(context.TODO(), metav1.ListOptions{
+	if targetEndpointSliceList, err := targetClient.DiscoveryV1().EndpointSlices(targetNamespace).List(context.TODO(), metav1.ListOptions{
 		LabelSelector: labels.SelectorFromSet(dstLabelMap).String()}); err == nil {
 		for _, item := range targetEndpointSliceList.Items {
 			if !srcEndpointSliceMap[item.Name] {
-				if err = targetClient.DiscoveryV1beta1().EndpointSlices(targetNamespace).Delete(context.TODO(), item.Name, metav1.DeleteOptions{}); err != nil {
+				if err = targetClient.DiscoveryV1().EndpointSlices(targetNamespace).Delete(context.TODO(), item.Name, metav1.DeleteOptions{}); err != nil {
 					utilruntime.HandleError(fmt.Errorf("the endpointclise '%s/%s' in target namespace deleted failed", item.Namespace, item.Name))
 					return nil, err
 				}
diff --git a/pkg/utils/version.go b/pkg/utils/version.go
index 4987e60..67030ca 100644
--- a/pkg/utils/version.go
+++ b/pkg/utils/version.go
@@ -23,6 +23,12 @@ import (
 var (
 	// KubeV1240Alpha4 indicates the version of Kubernetes v1.24.0-alpha.4
 	KubeV1240Alpha4 = version.MustParseSemantic("v1.24.0-alpha.4")
+
+	// KubeV1170Beta2 indicates the version of Kubernetes v1.17.0-beta.2
+	KubeV1170Beta2 = version.MustParseSemantic("v1.17.0-beta.2")
+
+	// KubeV1210Beta1 indicates the version of Kubernetes v1.21.0-beta.1
+	KubeV1210Beta1 = version.MustParseSemantic("v1.21.0-beta.1")
 )
 
 // SATokenAutoGenerated is used to indicate whether secret-based service account token can be auto generated
@@ -34,3 +40,24 @@ func SATokenAutoGenerated(kubeServerVersion string) (bool, error) {
 
 	return parsedVersion.LessThan(KubeV1240Alpha4), nil
 }
+
+// EndpointSliceV1beta1Promoted is used to indicate whether EndpointSlice v1beta1 has been promoted
+// Keep it for legacy use.
+func EndpointSliceV1beta1Promoted(kubeServerVersion string) (bool, error) {
+	parsedVersion, err := version.ParseSemantic(kubeServerVersion)
+	if err != nil {
+		return false, err
+	}
+
+	return parsedVersion.AtLeast(KubeV1170Beta2), nil
+}
+
+// EndpointSliceV1Promoted is used to indicate whether EndpointSlice v1 has been promoted
+func EndpointSliceV1Promoted(kubeServerVersion string) (bool, error) {
+	parsedVersion, err := version.ParseSemantic(kubeServerVersion)
+	if err != nil {
+		return false, err
+	}
+
+	return parsedVersion.AtLeast(KubeV1210Beta1), nil
+}


Test patch:
diff --git a/pkg/utils/version_test.go b/pkg/utils/version_test.go
index 674cfcb..25b8133 100644
--- a/pkg/utils/version_test.go
+++ b/pkg/utils/version_test.go
@@ -114,3 +114,185 @@ func TestSATokenAutoGenerated(t *testing.T) {
 		})
 	}
 }
+
+func TestEndpointSliceV1beta1Promoted(t *testing.T) {
+	tests := []struct {
+		name              string
+		kubeServerVersion string
+		want              bool
+		err               error
+	}{
+		// bad versions
+		{
+			kubeServerVersion: "unknown",
+			err:               fmt.Errorf("could not parse \"unknown\" as version"),
+		},
+		// EndpointSlice v1beta1 promoted
+		{
+			kubeServerVersion: KubeV1170Beta2.String(),
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.17.0",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.17.1",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.17.0+k3s1",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.17.0-rc1+k3s1",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.17.0-beta.3",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.17.0-rc.0",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.18.0",
+			want:              true,
+		},
+
+		// EndpointSlice v1beta1 not promoted
+		{
+			kubeServerVersion: "v1.17.0-beta.1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.17.0-alpha.2+k3s1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.1-alpha.0",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.5-rc.1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.6+k3s1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.10",
+			want:              false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			endpointSliceV1beta1Promoted, err := EndpointSliceV1beta1Promoted(tt.kubeServerVersion)
+			if !reflect.DeepEqual(err, tt.err) {
+				t.Errorf("kubeServerVersion %s, error expected %v but got %v", tt.kubeServerVersion, tt.err, err)
+			}
+			if endpointSliceV1beta1Promoted != tt.want {
+				t.Errorf("kubeServerVersion %s, endpointSliceV1beta1Promoted expected %v but got %v", tt.kubeServerVersion, tt.want, endpointSliceV1beta1Promoted)
+			}
+		})
+	}
+}
+
+func TestEndpointSliceV1Promoted(t *testing.T) {
+	tests := []struct {
+		name              string
+		kubeServerVersion string
+		want              bool
+		err               error
+	}{
+		// bad versions
+		{
+			kubeServerVersion: "unknown",
+			err:               fmt.Errorf("could not parse \"unknown\" as version"),
+		},
+		// EndpointSlice v1 promoted
+		{
+			kubeServerVersion: KubeV1210Beta1.String(),
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.25.0",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.22.1",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.22.0-alpha.4",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.21.1",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.21.0",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.21.0+k3s1",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.21.0-rc1+k3s1",
+			want:              true,
+		},
+		{
+			kubeServerVersion: "v1.21.0-rc.0",
+			want:              true,
+		},
+
+		// EndpointSlice v1 not promoted
+		{
+			kubeServerVersion: "v1.21.0-beta.0",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.17.0-alpha.2+k3s1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.20.10",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.21.0-alpha.0",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.5-rc.1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.6+k3s1",
+			want:              false,
+		},
+		{
+			kubeServerVersion: "v1.16.10",
+			want:              false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			endpointSliceV1Promoted, err := EndpointSliceV1Promoted(tt.kubeServerVersion)
+			if !reflect.DeepEqual(err, tt.err) {
+				t.Errorf("kubeServerVersion %s, error expected %v but got %v", tt.kubeServerVersion, tt.err, err)
+			}
+			if endpointSliceV1Promoted != tt.want {
+				t.Errorf("kubeServerVersion %s, EndpointSliceV1Promoted expected %v but got %v", tt.kubeServerVersion, tt.want, endpointSliceV1Promoted)
+			}
+		})
+	}
+}