Repository: antrea-io/antrea
Pull request #4194 (2022-09-01 23:18:06)

Update Dependabot configuration
* Ignore non-patch version updates for github.com/k8snetworkplumbingwg/*:
  minor updates may require updating github.com/containernetworking/cni
  as well.
* Ignore version updates for antrea.io/ofnet and antrea.io/libOpenflow:
  history shows that we prefer to update these manually.
* Ignore version updates for github.com/aws/*: releases for the AWS SDK
  are too frequent.

Signed-off-by: Antonin Bas <abas@vmware.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/4194?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) Report
> Merging [#4194](https://codecov.io/gh/antrea-io/antrea/pull/4194?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (bb669a5) into [main](https://codecov.io/gh/antrea-io/antrea/commit/7e6fce265384b22cfbd20f19444edb5279f7640c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (7e6fce2) will **decrease** coverage by `0.00%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/4194/graphs/tree.svg?width=650&height=150&src=pr&token=TjWaLL2GoC&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io)](https://codecov.io/gh/antrea-io/antrea/pull/4194?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io)

```diff
@@            Coverage Diff             @@
##             main    #4194      +/-   ##
==========================================
- Coverage   57.03%   57.03%   -0.01%     
==========================================
  Files         374      374              
  Lines       53818    53818              
==========================================
- Hits        30697    30696       -1     
- Misses      20798    20803       +5     
+ Partials     2323     2319       -4     
```

| Flag | Coverage Δ | | *Carryforward flag |
|---|---|---|---|
| integration-tests | `34.99% <ø> (+0.05%)` | :arrow_up: | |
| kind-e2e-tests | `47.56% <ø> (ø)` | | Carriedforward from [5cecf08](https://codecov.io/gh/antrea-io/antrea/commit/5cecf0808e19d8639fc866001f83c81f924ec3d1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |
| unit-tests | `40.65% <ø> (-0.05%)` | :arrow_down: | Carriedforward from [5cecf08](https://codecov.io/gh/antrea-io/antrea/commit/5cecf0808e19d8639fc866001f83c81f924ec3d1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |

*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) to find out more.

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/4194?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | Coverage Δ | |
|---|---|---|
| [...gent/controller/noderoute/node\_route\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbm9kZXJvdXRlL25vZGVfcm91dGVfY29udHJvbGxlci5nbw==) | `57.54% <0.00%> (-1.87%)` | :arrow_down: |
| [pkg/controller/egress/controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2NvbnRyb2xsZXIvZWdyZXNzL2NvbnRyb2xsZXIuZ28=) | `87.98% <0.00%> (-0.71%)` | :arrow_down: |
| [pkg/agent/openflow/pipeline.go](https://codecov.io/gh/antrea-io/antrea/pull/4194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L29wZW5mbG93L3BpcGVsaW5lLmdv) | `83.76% <0.00%> (-0.05%)` | :arrow_down: |
| [...trollers/multicluster/resourceexport\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-bXVsdGljbHVzdGVyL2NvbnRyb2xsZXJzL211bHRpY2x1c3Rlci9yZXNvdXJjZWV4cG9ydF9jb250cm9sbGVyLmdv) | `71.85% <0.00%> (+0.81%)` | :arrow_up: |
| [...lticluster/commonarea/resourceimport\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-bXVsdGljbHVzdGVyL2NvbnRyb2xsZXJzL211bHRpY2x1c3Rlci9jb21tb25hcmVhL3Jlc291cmNlaW1wb3J0X2NvbnRyb2xsZXIuZ28=) | `71.37% <0.00%> (+3.34%)` | :arrow_up: |


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c90c7c6b1..c95255c6a 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,11 +9,15 @@ updates:
     - dependency-name: "k8s.io/*"
     - dependency-name: "sigs.k8s.io/*"
     - dependency-name: "github.com/containernetworking/*"
+    - dependency-name: "github.com/k8snetworkplumbingwg/*"
+      update-types: ["version-update:semver-major", "version-update:semver-minor"] # ignore all except for patch updates
     - dependency-name: "github.com/vmware/go-ipfix"
     - dependency-name: "github.com/TomCodeLV/OVSDB-golang-lib"
     - dependency-name: "github.com/vmware-tanzu/octant"
       update-types: ["version-update:semver-major", "version-update:semver-minor"] # ignore all except for patch updates
-    - dependency-name: "github.com/Microsoft/hcsshim" # we use a replace directive for this dependency
+    - dependency-name: "github.com/aws/*" # updates are too frequent
+    - dependency-name: "antrea.io/ofnet"
+    - dependency-name: "antrea.io/libOpenflow"
   - package-ecosystem: "github-actions"
     # Workflow files stored in the default location of `.github/workflows`
     directory: "/"
diff --git a/ci/jenkins/README.md b/ci/jenkins/README.md
index b591e8418..b93e724e0 100644
--- a/ci/jenkins/README.md
+++ b/ci/jenkins/README.md
@@ -199,6 +199,8 @@ subset of community tests for Antrea:
   [--kubeconfig <Kubeconfig>]`.
 * To run network policy tests: `./run-k8s-e2e-tests.sh --e2e-network-policy
   [--kubeconfig <Kubeconfig>]`.
+* To run sig-network tests: `./run-k8s-e2e-tests.sh --e2e-sig-network
+  [--kubeconfig <Kubeconfig>]`.
 * To run a single test by name: `./run-k8s-e2e-tests.sh --e2e-focus <TestRegex>
   [--kubeconfig <Kubeconfig>]`.
 
diff --git a/docs/windows.md b/docs/windows.md
index 7667be5de..a406db3b0 100644
--- a/docs/windows.md
+++ b/docs/windows.md
@@ -99,7 +99,7 @@ New-KubeProxyServiceInterface
 
 New-DirectoryIfNotExist "${AntreaHome}/logs"
 New-DirectoryIfNotExist "${KubeProxyLogPath}"
-nssm install kube-proxy "${KubernetesHome}/kube-proxy.exe" "--proxy-mode=userspace --kubeconfig=${KubeProxyKubeconfigPath} --log-dir=${KubeProxyLogPath} --logtostderr=false --alsologtostderr"
+nssm install kube-proxy "${KubernetesHome}/kube-proxy.exe" "--proxy-mode=vmkernel --kubeconfig=${KubeProxyKubeconfigPath} --log-dir=${KubeProxyLogPath} --logtostderr=false --alsologtostderr"
 nssm install antrea-agent "${AntreaHome}/bin/antrea-agent.exe" "--config=${AntreaHome}/etc/antrea-agent.conf --logtostderr=false --log_dir=${AntreaHome}/logs --alsologtostderr --log_file_max_size=100 --log_file_max_num=4"
 
 nssm set antrea-agent DependOnService kube-proxy ovs-vswitchd
@@ -152,7 +152,7 @@ data:
     cp -force /var/lib/kube-proxy/* /host/var/lib/kube-proxy
     cp -force /var/run/secrets/kubernetes.io/serviceaccount/* /host/var/lib/kube-proxy/var/run/secrets/kubernetes.io/serviceaccount
 
-    wins cli process run --path /k/kube-proxy/kube-proxy.exe --args "--v=3 --config=/var/lib/kube-proxy/config.conf --proxy-mode=userspace --hostname-override=$env:NODE_NAME"
+    wins cli process run --path /k/kube-proxy/kube-proxy.exe --args "--v=3 --config=/var/lib/kube-proxy/config.conf --proxy-mode=vmkernel --hostname-override=$env:NODE_NAME"
 
 kind: ConfigMap
 metadata:
diff --git a/hack/update-codegen-dockerized.sh b/hack/update-codegen-dockerized.sh
index 6053ef2da..9f51f365d 100755
--- a/hack/update-codegen-dockerized.sh
+++ b/hack/update-codegen-dockerized.sh
@@ -58,7 +58,7 @@ function generate_mocks {
     "pkg/controller/querier ControllerQuerier testing"
     "pkg/flowaggregator/exporter Interface testing"
     "pkg/ipfix IPFIXExportingProcess,IPFIXRegistry,IPFIXCollectingProcess,IPFIXAggregationProcess testing"
-    "pkg/ovs/openflow Bridge,Table,Flow,Action,CTAction,FlowBuilder testing"
+    "pkg/ovs/openflow Bridge,Table,Flow,Action,CTAction,FlowBuilder,Group,BucketBuilder testing"
     "pkg/ovs/ovsconfig OVSBridgeClient testing"
     "pkg/ovs/ovsctl OVSCtlClient testing"
     "pkg/querier AgentNetworkPolicyInfoQuerier,AgentMulticastInfoQuerier testing"
diff --git a/hack/windows/Helper.psm1 b/hack/windows/Helper.psm1
index 7dffad7b3..483b2a85f 100644
--- a/hack/windows/Helper.psm1
+++ b/hack/windows/Helper.psm1
@@ -190,7 +190,7 @@ function Start-KubeProxy {
 
     New-KubeProxyServiceInterface
 
-    Start-Process -FilePath $KubeProxy -ArgumentList "--proxy-mode=userspace --kubeconfig=$KubeConfig --log-dir=$LogDir --logtostderr=false --alsologtostderr"
+    Start-Process -FilePath $KubeProxy -ArgumentList "--proxy-mode=vmkernel --kubeconfig=$KubeConfig --log-dir=$LogDir --logtostderr=false --alsologtostderr"
     return $true
 }
 
diff --git a/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml b/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml
index 5a7841334..db5624c29 100644
--- a/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml
+++ b/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml
@@ -450,7 +450,6 @@ webhooks:
 apiVersion: admissionregistration.k8s.io/v1
 kind: ValidatingWebhookConfiguration
 metadata:
-  creationTimestamp: null
   labels:
     app: antrea
   name: antrea-multicluster-antrea-mc-validating-webhook-configuration
diff --git a/multicluster/build/yamls/antrea-multicluster-member.yml b/multicluster/build/yamls/antrea-multicluster-member.yml
index d6ff5d17f..08a065090 100644
--- a/multicluster/build/yamls/antrea-multicluster-member.yml
+++ b/multicluster/build/yamls/antrea-multicluster-member.yml
@@ -1102,3 +1102,24 @@ webhooks:
     resources:
     - clustersets
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: antrea-mc-webhook-service
+      namespace: kube-system
+      path: /validate-multicluster-crd-antrea-io-v1alpha1-gateway
+  failurePolicy: Fail
+  name: vgateway.kb.io
+  rules:
+  - apiGroups:
+    - multicluster.crd.antrea.io
+    apiVersions:
+    - v1alpha1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - gateways
+  sideEffects: None
diff --git a/multicluster/cmd/multicluster-controller/gateway_webhook.go b/multicluster/cmd/multicluster-controller/gateway_webhook.go
new file mode 100644
index 000000000..b22377c31
--- /dev/null
+++ b/multicluster/cmd/multicluster-controller/gateway_webhook.go
@@ -0,0 +1,68 @@
+/*
+Copyright 2022 Antrea 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 main
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	admissionv1 "k8s.io/api/admission/v1"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	mcv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+)
+
+//+kubebuilder:webhook:path=/validate-multicluster-crd-antrea-io-v1alpha1-gateway,mutating=false,failurePolicy=fail,sideEffects=None,groups=multicluster.crd.antrea.io,resources=gateways,verbs=create;update,versions=v1alpha1,name=vgateway.kb.io,admissionReviewVersions={v1,v1beta1}
+
+// Gateway validator
+type gatewayValidator struct {
+	Client    client.Client
+	decoder   *admission.Decoder
+	namespace string
+}
+
+// Handle handles admission requests.
+func (v *gatewayValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
+	gateway := &mcv1alpha1.Gateway{}
+	err := v.decoder.Decode(req, gateway)
+	if err != nil {
+		klog.ErrorS(err, "Error while decoding Gateway", "Gateway", req.Namespace+"/"+req.Name)
+		return admission.Errored(http.StatusBadRequest, err)
+	}
+
+	// Check if there is any existing Gateway.
+	gatewayList := &mcv1alpha1.GatewayList{}
+	if err := v.Client.List(context.TODO(), gatewayList, client.InNamespace(v.namespace)); err != nil {
+		klog.ErrorS(err, "Error reading Gateway", "Namespace", v.namespace)
+		return admission.Errored(http.StatusPreconditionFailed, err)
+	}
+
+	if req.Operation == admissionv1.Create && len(gatewayList.Items) > 0 {
+		err := fmt.Errorf("multiple Gateways in a Namespace are not allowed")
+		klog.ErrorS(err, "failed to create Gateway", "Gateway", klog.KObj(gateway), "Namespace", v.namespace)
+		return admission.Errored(http.StatusPreconditionFailed, err)
+	}
+	return admission.Allowed("")
+}
+
+func (v *gatewayValidator) InjectDecoder(d *admission.Decoder) error {
+	v.decoder = d
+	return nil
+}
diff --git a/multicluster/cmd/multicluster-controller/member.go b/multicluster/cmd/multicluster-controller/member.go
index 04e16c10c..89341055c 100644
--- a/multicluster/cmd/multicluster-controller/member.go
+++ b/multicluster/cmd/multicluster-controller/member.go
@@ -21,6 +21,7 @@ import (
 	"github.com/spf13/cobra"
 	"k8s.io/klog/v2"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
 
 	multiclustercontrollers "antrea.io/antrea/multicluster/controllers/multicluster"
 	"antrea.io/antrea/pkg/log"
@@ -54,6 +55,12 @@ func runMember(o *Options) error {
 		return err
 	}
 
+	hookServer := mgr.GetWebhookServer()
+	hookServer.Register("/validate-multicluster-crd-antrea-io-v1alpha1-gateway",
+		&webhook.Admission{Handler: &gatewayValidator{
+			Client:    mgr.GetClient(),
+			namespace: env.GetPodNamespace()}})
+
 	clusterSetReconciler := multiclustercontrollers.NewMemberClusterSetReconciler(mgr.GetClient(),
 		mgr.GetScheme(),
 		env.GetPodNamespace(),
diff --git a/multicluster/config/overlays/leader-ns/kustomization.yaml b/multicluster/config/overlays/leader-ns/kustomization.yaml
index a85f54bd9..7ef94f1cd 100644
--- a/multicluster/config/overlays/leader-ns/kustomization.yaml
+++ b/multicluster/config/overlays/leader-ns/kustomization.yaml
@@ -54,3 +54,4 @@ resources:
 
 patchesStrategicMerge:
   - manager_command_patch.yaml
+  - webhook_patch.yaml
diff --git a/multicluster/config/overlays/leader-ns/webhook_patch.yaml b/multicluster/config/overlays/leader-ns/webhook_patch.yaml
new file mode 100644
index 000000000..92bd74c16
--- /dev/null
+++ b/multicluster/config/overlays/leader-ns/webhook_patch.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  name: validating-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+  name: vgateway.kb.io
+  $patch: delete
diff --git a/multicluster/config/webhook/manifests.yaml b/multicluster/config/webhook/manifests.yaml
index 7b3f6137d..d4952da4d 100644
--- a/multicluster/config/webhook/manifests.yaml
+++ b/multicluster/config/webhook/manifests.yaml
@@ -76,6 +76,27 @@ webhooks:
     resources:
     - clustersets
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-multicluster-crd-antrea-io-v1alpha1-gateway
+  failurePolicy: Fail
+  name: vgateway.kb.io
+  rules:
+  - apiGroups:
+    - multicluster.crd.antrea.io
+    apiVersions:
+    - v1alpha1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - gateways
+  sideEffects: None
 - admissionReviewVersions:
   - v1
   - v1beta1
diff --git a/multicluster/controllers/multicluster/gateway_controller.go b/multicluster/controllers/multicluster/gateway_controller.go
index 247e03de9..5efe37cad 100644
--- a/multicluster/controllers/multicluster/gateway_controller.go
+++ b/multicluster/controllers/multicluster/gateway_controller.go
@@ -99,7 +99,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 		},
 	}
 
-	createOrUpdate := func(gwIP string, gwInfo *mcsv1alpha1.GatewayInfo) error {
+	createOrUpdate := func(gwIP string) error {
 		existingResExport := &mcsv1alpha1.ResourceExport{}
 		if err := commonArea.Get(ctx, resExportNamespacedName, existingResExport); err != nil {
 			if !apierrors.IsNotFound(err) {
@@ -110,7 +110,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 			}
 			return nil
 		}
-		if err = r.updateResourceExport(ctx, req, commonArea, existingResExport, gwInfo); err != nil {
+		if err = r.updateResourceExport(ctx, req, commonArea, existingResExport, &mcsv1alpha1.GatewayInfo{GatewayIP: gwIP}); err != nil {
 			return err
 		}
 		return nil
@@ -121,52 +121,18 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 		if !apierrors.IsNotFound(err) {
 			return ctrl.Result{}, err
 		}
-		gwInfo, err := r.getLastCreatedGateway()
-		if err != nil {
-			klog.ErrorS(err, "Failed to get Gateways")
-			return ctrl.Result{}, err
-		}
-		if gwInfo == nil {
-			// When the last Gateway is deleted, we will remove the ClusterInfo kind of ResourceExport
-			if err := commonArea.Delete(ctx, resExport, &client.DeleteOptions{}); err != nil {
-				return ctrl.Result{}, client.IgnoreNotFound(err)
-			}
-			return ctrl.Result{}, nil
-		}
-		// When there are still Gateways exist, we should create or update existing ResourceExport
-		// with the latest Gateway in remaining Gateways.
-		if err := createOrUpdate(gwInfo.GatewayIP, gwInfo); err != nil {
-			return ctrl.Result{}, err
+		if err := commonArea.Delete(ctx, resExport, &client.DeleteOptions{}); err != nil {
+			return ctrl.Result{}, client.IgnoreNotFound(err)
 		}
 		return ctrl.Result{}, nil
 	}
-	if err := createOrUpdate(gw.GatewayIP, nil); err != nil {
+
+	if err := createOrUpdate(gw.GatewayIP); err != nil {
 		return ctrl.Result{}, err
 	}
 	return ctrl.Result{}, nil
 }
 
-func (r *GatewayReconciler) getLastCreatedGateway() (*mcsv1alpha1.GatewayInfo, error) {
-	gws := &mcsv1alpha1.GatewayList{}
-	if err := r.Client.List(ctx, gws, &client.ListOptions{}); err != nil {
-		return nil, err
-	}
-	if len(gws.Items) == 0 {
-		return nil, nil
-	}
-
-	// Comparing Gateway's CreationTimestamp to get the last created Gateway.
-	lastCreatedGW := gws.Items[0]
-	for _, gw := range gws.Items {
-		if lastCreatedGW.CreationTimestamp.Before(&gw.CreationTimestamp) {
-			lastCreatedGW = gw
-		}
-	}
-
-	// Make sure we only return the last created Gateway for now.
-	return &mcsv1alpha1.GatewayInfo{GatewayIP: lastCreatedGW.GatewayIP}, nil
-}
-
 func (r *GatewayReconciler) updateResourceExport(ctx context.Context, req ctrl.Request,
 	commonArea commonarea.RemoteCommonArea, existingResExport *mcsv1alpha1.ResourceExport, gwInfo *mcsv1alpha1.GatewayInfo) error {
 	resExportSpec := mcsv1alpha1.ResourceExportSpec{
@@ -175,13 +141,6 @@ func (r *GatewayReconciler) updateResourceExport(ctx context.Context, req ctrl.R
 		Name:      r.localClusterID,
 		Namespace: r.namespace,
 	}
-	var err error
-	if gwInfo == nil {
-		gwInfo, err = r.getLastCreatedGateway()
-		if err != nil {
-			return err
-		}
-	}
 	resExportSpec.ClusterInfo = &mcsv1alpha1.ClusterInfo{
 		ClusterID:    r.localClusterID,
 		ServiceCIDR:  r.serviceCIDR,
diff --git a/multicluster/controllers/multicluster/node_controller.go b/multicluster/controllers/multicluster/node_controller.go
index dc03f8ecc..8e836aafe 100644
--- a/multicluster/controllers/multicluster/node_controller.go
+++ b/multicluster/controllers/multicluster/node_controller.go
@@ -37,18 +37,22 @@ import (
 
 type (
 	// NodeReconciler is for member cluster only.
-	// It will create a Gateway object if a Node has an annotation `multicluster.antrea.io/gateway:true`
-	// and update corresponding Gateway if any subnets changes.
 	NodeReconciler struct {
 		client.Client
-		Scheme     *runtime.Scheme
-		namespace  string
-		precedence mcsv1alpha1.Precedence
+		Scheme            *runtime.Scheme
+		namespace         string
+		precedence        mcsv1alpha1.Precedence
+		gatewayCandidates map[string]bool
+		activeGateway     string
+		initialized       bool
 	}
 )
 
-// NewNodeReconciler creates a NodeReconciler to watch Node object changes and create a
-// corresponding Gateway if the Node has the annotation `multicluster.antrea.io/gateway:true`.
+// NewNodeReconciler creates a NodeReconciler to watch Node resource changes.
+// It's responsible for creating a Gateway for the first ready Node with
+// annotation `multicluster.antrea.io/gateway:true` if there is no existing Gateway.
+// It guarantees there is always only one Gateway CR when there are multiple Nodes
+// with annotation `multicluster.antrea.io/gateway:true`.
 func NewNodeReconciler(
 	client client.Client,
 	scheme *runtime.Scheme,
@@ -58,10 +62,11 @@ func NewNodeReconciler(
 		precedence = mcsv1alpha1.PrecedenceInternal
 	}
 	reconciler := &NodeReconciler{
-		Client:     client,
-		Scheme:     scheme,
-		namespace:  namespace,
-		precedence: precedence,
+		Client:            client,
+		Scheme:            scheme,
+		namespace:         namespace,
+		precedence:        precedence,
+		gatewayCandidates: make(map[string]bool),
 	}
 	return reconciler
 }
@@ -73,65 +78,194 @@ func NewNodeReconciler(
 
 func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	klog.V(2).InfoS("Reconciling Node", "node", req.Name)
+	if !r.initialized {
+		if err := r.initialize(); err != nil {
+			return ctrl.Result{}, err
+		}
+		r.initialized = true
+	}
 	gw := &mcsv1alpha1.Gateway{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      req.Name,
 			Namespace: r.namespace,
 		},
 	}
-	// When the Node is annotated with 'multicluster.antrea.io/gateway=true' as a Gateway:
-	//   - Delete the Gateway if the Node is deleted
-	//   - Update the Gateway if Node's InternalIP or GatewayIP is updated
-	//   - Create a new Gateway if there is no existing Gateway
+
+	noActiveGateway := r.activeGateway == ""
+	isActiveGateway := r.activeGateway == req.Name
+	stillGatewayNode := false
+
 	node := &corev1.Node{}
 	if err := r.Client.Get(ctx, req.NamespacedName, node); err != nil {
-		if apierrors.IsNotFound(err) {
-			err := r.Client.Delete(ctx, gw, &client.DeleteOptions{})
-			return ctrl.Result{}, client.IgnoreNotFound(err)
+		if !apierrors.IsNotFound(err) {
+			klog.ErrorS(err, "Failed to get Node", "node", req.Name)
+			return ctrl.Result{}, err
 		}
-		klog.ErrorS(err, "Failed to get Node", "node", req.Name)
-		return ctrl.Result{}, err
+	} else {
+		_, hasGWAnnotation := node.Annotations[common.GatewayAnnotation]
+		stillGatewayNode = hasGWAnnotation
 	}
 
-	_, isGW := node.Annotations[common.GatewayAnnotation]
-	var err error
-	gwNamespacedName := types.NamespacedName{
-		Name:      node.Name,
-		Namespace: r.namespace,
+	if stillGatewayNode {
+		r.gatewayCandidates[req.Name] = true
+	} else {
+		delete(r.gatewayCandidates, req.Name)
 	}
 
-	// TODO: cache might be stale. Need to revisit here and other reconcilers to
-	// check if we can improve this with 'Owns' or other methods.
-	var gwIP, internalIP string
-	if isGW {
-		if internalIP, gwIP, err = r.getGatawayNodeIP(node); err != nil {
-			klog.ErrorS(err, "There is no valid Gateway IP for Node, will retry later when there is any new Node update", "node", node.Name)
-			return ctrl.Result{}, nil
+	var err error
+	var isValidGateway bool
+
+	if stillGatewayNode {
+		gw.InternalIP, gw.GatewayIP, err = r.getGatawayNodeIP(node)
+		if err != nil {
+			klog.ErrorS(err, "There is no valid Gateway IP for Node", "node", node.Name)
+		} else {
+			isValidGateway = true
 		}
 	}
-	if err := r.Client.Get(ctx, gwNamespacedName, gw); err != nil {
-		if apierrors.IsNotFound(err) && isGW {
-			gw.GatewayIP = gwIP
-			gw.InternalIP = internalIP
-			if err := r.Client.Create(ctx, gw, &client.CreateOptions{}); err != nil {
+
+	if isActiveGateway {
+		if !isValidGateway || !isReadyNode(node) {
+			if err := r.recreateActiveGateway(ctx, gw); err != nil {
+				return ctrl.Result{}, err
+			}
+		} else {
+			if err := r.updateActiveGateway(ctx, gw); err != nil {
 				return ctrl.Result{}, err
 			}
-			return ctrl.Result{}, nil
 		}
-		return ctrl.Result{}, client.IgnoreNotFound(err)
+		return ctrl.Result{}, nil
 	}
 
-	if !isGW {
-		err := r.Client.Delete(ctx, gw, &client.DeleteOptions{})
-		return ctrl.Result{}, client.IgnoreNotFound(err)
+	if noActiveGateway && isValidGateway && isReadyNode(node) {
+		if err := r.createGateway(gw); err != nil {
+			return ctrl.Result{}, err
+		}
 	}
+	return ctrl.Result{}, nil
+}
 
-	gw.GatewayIP = gwIP
-	gw.InternalIP = internalIP
-	if err := r.Client.Update(ctx, gw, &client.UpdateOptions{}); err != nil {
-		return ctrl.Result{}, err
+// initialize initializes 'activeGateway' and 'gatewayCandidates' and removes
+// stale Gateway during controller startup.
+func (r *NodeReconciler) initialize() error {
+	ctx := context.Background()
+	nodeList := &corev1.NodeList{}
+	if err := r.Client.List(ctx, nodeList, &client.ListOptions{}); err != nil {
+		return err
 	}
-	return ctrl.Result{}, nil
+
+	gwList := &mcsv1alpha1.GatewayList{}
+	if err := r.Client.List(ctx, gwList, &client.ListOptions{}); err != nil {
+		return err
+	}
+	// Gateway webhook guarantees that there is at most one Gateway in the member cluster.
+	if len(gwList.Items) > 0 {
+		existingGWName := gwList.Items[0].Name
+		node := &corev1.Node{}
+		if err := r.Client.Get(ctx, types.NamespacedName{Name: existingGWName}, node); err != nil {
+			if !apierrors.IsNotFound(err) {
+				return err
+			}
+			staleGateway := &mcsv1alpha1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: r.namespace,
+					Name:      existingGWName},
+			}
+			err := r.Client.Delete(ctx, staleGateway, &client.DeleteOptions{})
+			if err != nil && !apierrors.IsNotFound(err) {
+				return err
+			}
+		} else {
+			r.activeGateway = existingGWName
+		}
+	}
+	for _, n := range nodeList.Items {
+		if _, isGW := n.Annotations[common.GatewayAnnotation]; isGW {
+			r.gatewayCandidates[n.Name] = true
+		}
+	}
+	return nil
+}
+
+func (r *NodeReconciler) updateActiveGateway(ctx context.Context, newGateway *mcsv1alpha1.Gateway) error {
+	existingGW := &mcsv1alpha1.Gateway{}
+	// TODO: cache might be stale. Need to revisit here and other reconcilers to
+	// check if we can improve this with 'Owns' or other methods.
+	if err := r.Client.Get(ctx, types.NamespacedName{Name: newGateway.Name, Namespace: r.namespace}, existingGW); err != nil {
+		return err
+	}
+	if existingGW.GatewayIP == newGateway.GatewayIP && existingGW.InternalIP == newGateway.InternalIP {
+		return nil
+	}
+	existingGW.GatewayIP = newGateway.GatewayIP
+	existingGW.InternalIP = newGateway.InternalIP
+	// If the Gateway version in the client cache is stale, the update operation will fail,
+	// then the reconciler will retry with latest state again.
+	if err := r.Client.Update(ctx, existingGW, &client.UpdateOptions{}); err != nil {
+		return err
+	}
+	return nil
+}
+
+// recreateActiveGateway will delete the existing Gateway CR and create a new Gateway
+// from the pool of Gateway candidates.
+func (r *NodeReconciler) recreateActiveGateway(ctx context.Context, gateway *mcsv1alpha1.Gateway) error {
+	err := r.Client.Delete(ctx, gateway, &client.DeleteOptions{})
+	if err != nil && !apierrors.IsNotFound(err) {
+		return err
+	}
+	r.activeGateway = ""
+	// Check remaining Gateway candidates and create a new Gateway.
+	newGateway, err := r.getValidGatewayFromCandidates()
+	if err != nil {
+		return err
+	}
+	if newGateway != nil {
+		return r.createGateway(newGateway)
+	}
+	return nil
+}
+
+// getValidGatewayFromCandidates picks a valid Node from Gateway candidates and
+// creates a Gateway. It returns no error if no good Gateway candidate.
+func (r *NodeReconciler) getValidGatewayFromCandidates() (*mcsv1alpha1.Gateway, error) {
+	var activeGateway *mcsv1alpha1.Gateway
+	var internalIP, gwIP string
+	var err error
+	gatewayNode := &corev1.Node{}
+	for name := range r.gatewayCandidates {
+		if err = r.Client.Get(ctx, types.NamespacedName{Name: name}, gatewayNode); err == nil {
+			if !isReadyNode(gatewayNode) {
+				continue
+			}
+			if internalIP, gwIP, err = r.getGatawayNodeIP(gatewayNode); err != nil {
+				klog.V(2).ErrorS(err, "Node has no valid IP", "node", gatewayNode.Name)
+				continue
+			}
+			activeGateway = &mcsv1alpha1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      gatewayNode.Name,
+					Namespace: r.namespace,
+				},
+				GatewayIP:  gwIP,
+				InternalIP: internalIP,
+			}
+			klog.InfoS("Found good Gateway candidate", "node", gatewayNode.Name)
+			return activeGateway, nil
+		}
+		if !apierrors.IsNotFound(err) {
+			return nil, err
+		}
+	}
+	return nil, nil
+}
+
+func (r *NodeReconciler) createGateway(gateway *mcsv1alpha1.Gateway) error {
+	if err := r.Client.Create(ctx, gateway, &client.CreateOptions{}); err != nil {
+		return err
+	}
+	r.activeGateway = gateway.Name
+	return nil
 }
 
 func (r *NodeReconciler) getGatawayNodeIP(node *corev1.Node) (string, string, error) {
@@ -172,3 +306,14 @@ func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		}).
 		Complete(r)
 }
+
+func isReadyNode(node *corev1.Node) bool {
+	var nodeIsReady bool
+	for _, s := range node.Status.Conditions {
+		if s.Type == corev1.NodeReady && s.Status == corev1.ConditionTrue {
+			nodeIsReady = true
+			break
+		}
+	}
+	return nodeIsReady
+}
diff --git a/pkg/agent/multicast/mcast_controller.go b/pkg/agent/multicast/mcast_controller.go
index f557041a2..e04b5dd64 100644
--- a/pkg/agent/multicast/mcast_controller.go
+++ b/pkg/agent/multicast/mcast_controller.go
@@ -520,7 +520,7 @@ func (c *Controller) syncGroup(groupKey string) error {
 					return err
 				}
 				// Remove the multicast flow entry if no local Pod is in the group.
-				if err := c.ofClient.UninstallGroup(status.ofGroupID); err != nil {
+				if err := c.ofClient.UninstallMulticastGroup(status.ofGroupID); err != nil {
 					klog.ErrorS(err, "Failed to uninstall multicast group", "group", groupKey)
 					return err
 				}
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index af55e43e1..2102d8211 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -84,9 +84,9 @@ type Client interface {
 	// InstallServiceGroup installs a group for Service LB. Each endpoint
 	// is a bucket of the group. For now, each bucket has the same weight.
 	InstallServiceGroup(groupID binding.GroupIDType, withSessionAffinity bool, endpoints []proxy.Endpoint) error
-	// UninstallGroup removes the group and its buckets that are
-	// installed by InstallServiceGroup or InstallMulticastGroup.
-	UninstallGroup(groupID binding.GroupIDType) error
+	// UninstallServiceGroup removes the group and its buckets that are
+	// installed by InstallServiceGroup.
+	UninstallServiceGroup(groupID binding.GroupIDType) error
 
 	// InstallEndpointFlows installs flows for accessing Endpoints.
 	// If an Endpoint is on the current Node, then flows for hairpin and endpoint
@@ -314,6 +314,9 @@ type Client interface {
 	UninstallTrafficControlReturnPortFlow(returnOFPort uint32) error
 
 	InstallMulticastGroup(ofGroupID binding.GroupIDType, localReceivers []uint32, remoteNodeReceivers []net.IP) error
+	// UninstallMulticastGroup removes the group and its buckets that are
+	// installed by InstallMulticastGroup.
+	UninstallMulticastGroup(groupID binding.GroupIDType) error
 
 	// SendIGMPRemoteReportPacketOut sends the IGMP report packet as a packet-out to remote Nodes via the tunnel port.
 	SendIGMPRemoteReportPacketOut(
@@ -627,7 +630,7 @@ func (c *client) InstallServiceGroup(groupID binding.GroupIDType, withSessionAff
 	return nil
 }
 
-func (c *client) UninstallGroup(groupID binding.GroupIDType) error {
+func (c *client) UninstallServiceGroup(groupID binding.GroupIDType) error {
 	c.replayMutex.RLock()
 	defer c.replayMutex.RUnlock()
 	if !c.bridge.DeleteGroup(groupID) {
@@ -1322,6 +1325,16 @@ func (c *client) InstallMulticastGroup(groupID binding.GroupIDType, localReceive
 	return nil
 }
 
+func (c *client) UninstallMulticastGroup(groupID binding.GroupIDType) error {
+	c.replayMutex.RLock()
+	defer c.replayMutex.RUnlock()
+	if !c.bridge.DeleteGroup(groupID) {
+		return fmt.Errorf("group %d delete failed", groupID)
+	}
+	c.featureMulticast.groupCache.Delete(groupID)
+	return nil
+}
+
 // InstallMulticlusterNodeFlows installs flows to handle cross-cluster packets between a regular
 // Node and a local Gateway.
 func (c *client) InstallMulticlusterNodeFlows(clusterID string,
diff --git a/pkg/agent/proxy/proxier.go b/pkg/agent/proxy/proxier.go
index dba951d1c..5cd74b150 100644
--- a/pkg/agent/proxy/proxier.go
+++ b/pkg/agent/proxy/proxier.go
@@ -183,7 +183,7 @@ func (p *proxier) removeStaleServices() {
 		// Remove Service group whose Endpoints are local.
 		if svcInfo.NodeLocalExternal() {
 			if groupIDLocal, exist := p.groupCounter.Get(svcPortName, true); exist {
-				if err := p.ofClient.UninstallGroup(groupIDLocal); err != nil {
+				if err := p.ofClient.UninstallServiceGroup(groupIDLocal); err != nil {
 					klog.ErrorS(err, "Failed to remove Group of local Endpoints for Service", "Service", svcPortName)
 					continue
 				}
@@ -192,7 +192,7 @@ func (p *proxier) removeStaleServices() {
 		}
 		// Remove Service group which has all Endpoints.
 		if groupID, exist := p.groupCounter.Get(svcPortName, false); exist {
-			if err := p.ofClient.UninstallGroup(groupID); err != nil {
+			if err := p.ofClient.UninstallServiceGroup(groupID); err != nil {
 				klog.ErrorS(err, "Failed to remove Group of all Endpoints for Service", "Service", svcPortName)
 				continue
 			}
@@ -568,7 +568,7 @@ func (p *proxier) installServices() {
 					continue
 				}
 				if groupID, exist := p.groupCounter.Get(svcPortName, !nodeLocalVal); exist {
-					if err := p.ofClient.UninstallGroup(groupID); err != nil {
+					if err := p.ofClient.UninstallServiceGroup(groupID); err != nil {
 						klog.ErrorS(err, "Failed to uninstall Group of all Endpoints for Service", "Service", svcPortName)
 						continue
 					}
diff --git a/pkg/antctl/raw/multicluster/get/clusterset.go b/pkg/antctl/raw/multicluster/get/clusterset.go
index 01c2c982a..9c2834c4f 100644
--- a/pkg/antctl/raw/multicluster/get/clusterset.go
+++ b/pkg/antctl/raw/multicluster/get/clusterset.go
@@ -36,6 +36,7 @@ type clusterSetOptions struct {
 	namespace     string
 	outputFormat  string
 	allNamespaces bool
+	k8sClient     client.Client
 }
 
 var optionsClusterSet *clusterSetOptions
@@ -53,15 +54,23 @@ Get the specified ClusterSet
 $ antctl mc get clusterset <CLUSTERSETID>
 `, "\n")
 
-func (o *clusterSetOptions) validateAndComplete() {
+func (o *clusterSetOptions) validateAndComplete(cmd *cobra.Command) error {
 	if o.allNamespaces {
 		o.namespace = metav1.NamespaceAll
-		return
-	}
-	if o.namespace == "" {
+	} else if o.namespace == "" {
 		o.namespace = metav1.NamespaceDefault
-		return
 	}
+	if o.k8sClient == nil {
+		kubeconfig, err := raw.ResolveKubeconfig(cmd)
+		if err != nil {
+			return err
+		}
+		o.k8sClient, err = client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme})
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
 func NewClusterSetCommand() *cobra.Command {
@@ -79,42 +88,28 @@ func NewClusterSetCommand() *cobra.Command {
 	optionsClusterSet = o
 	cmdClusterSet.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of ClusterSets")
 	cmdClusterSet.Flags().StringVarP(&o.outputFormat, "output", "o", "", "Output format. Supported formats: json|yaml")
-	cmdClusterSet.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", false, "If present, list ClusterSets across all namespaces")
+	cmdClusterSet.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", false, "If present, list ClusterSets across all Namespaces")
 
 	return cmdClusterSet
 }
 
 func runEClusterSet(cmd *cobra.Command, args []string) error {
-	optionsClusterSet.validateAndComplete()
-
-	kubeconfig, err := raw.ResolveKubeconfig(cmd)
+	err := optionsClusterSet.validateAndComplete(cmd)
 	if err != nil {
 		return err
 	}
-
-	argsNum := len(args)
-	singleResource := false
-	if argsNum > 0 {
-		singleResource = true
-	}
-
-	k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme})
-	if err != nil {
-		return err
-	}
-
 	var clusterSets []multiclusterv1alpha1.ClusterSet
-	if singleResource {
+	if len(args) > 0 {
 		clusterSetName := args[0]
 		clusterSet := multiclusterv1alpha1.ClusterSet{}
-		err = k8sClient.Get(context.TODO(), types.NamespacedName{
+		err = optionsClusterSet.k8sClient.Get(context.TODO(), types.NamespacedName{
 			Namespace: optionsClusterSet.namespace,
 			Name:      clusterSetName,
 		}, &clusterSet)
 		if err != nil {
 			return err
 		}
-		gvks, unversioned, err := k8sClient.Scheme().ObjectKinds(&clusterSet)
+		gvks, unversioned, err := optionsClusterSet.k8sClient.Scheme().ObjectKinds(&clusterSet)
 		if err != nil {
 			return err
 		}
@@ -124,7 +119,7 @@ func runEClusterSet(cmd *cobra.Command, args []string) error {
 		clusterSets = append(clusterSets, clusterSet)
 	} else {
 		clusterSetList := &multiclusterv1alpha1.ClusterSetList{}
-		err = k8sClient.List(context.TODO(), clusterSetList, &client.ListOptions{Namespace: optionsClusterSet.namespace})
+		err = optionsClusterSet.k8sClient.List(context.TODO(), clusterSetList, &client.ListOptions{Namespace: optionsClusterSet.namespace})
 		if err != nil {
 			return err
 		}
@@ -140,23 +135,9 @@ func runEClusterSet(cmd *cobra.Command, args []string) error {
 		return nil
 	}
 
-	switch optionsClusterSet.outputFormat {
-	case "json", "yaml":
-		err := output(clusterSets, true, optionsClusterSet.outputFormat, clusterset.Transform)
-		if err != nil {
-			return err
-		}
-	default:
-		clusterSetsNum := len(clusterSets)
-		for i, singleclusterset := range clusterSets {
-			err := output(singleclusterset, true, optionsClusterSet.outputFormat, clusterset.Transform)
-			if err != nil {
-				return err
-			}
-			if i != clusterSetsNum-1 {
-				fmt.Print("\n")
-			}
-		}
+	err = output(clusterSets, false, optionsClusterSet.outputFormat, cmd.OutOrStdout(), clusterset.Transform)
+	if err != nil {
+		return err
 	}
 	return nil
 }
diff --git a/pkg/antctl/raw/multicluster/get/output.go b/pkg/antctl/raw/multicluster/get/output.go
index 3395ffa1d..87c342482 100644
--- a/pkg/antctl/raw/multicluster/get/output.go
+++ b/pkg/antctl/raw/multicluster/get/output.go
@@ -15,19 +15,20 @@
 package get
 
 import (
-	"os"
+	"io"
 
-	antcltOutput "antrea.io/antrea/pkg/antctl/output"
+	antctlOutput "antrea.io/antrea/pkg/antctl/output"
 )
 
-func output(resources interface{}, single bool, outputFormat string, transform func(r interface{}, single bool) (interface{}, error)) error {
+func output(resources interface{}, single bool, outputFormat string, output io.Writer,
+	transform func(r interface{}, single bool) (interface{}, error)) error {
 	switch outputFormat {
 	case "json":
-		if err := antcltOutput.JsonOutput(resources, os.Stdout); err != nil {
+		if err := antctlOutput.JsonOutput(resources, output); err != nil {
 			return err
 		}
 	case "yaml":
-		if err := antcltOutput.YamlOutput(resources, os.Stdout); err != nil {
+		if err := antctlOutput.YamlOutput(resources, output); err != nil {
 			return err
 		}
 	default:
@@ -35,7 +36,7 @@ func output(resources interface{}, single bool, outputFormat string, transform f
 		if err != nil {
 			return err
 		}
-		err = antcltOutput.TableOutputForGetCommands(obj, os.Stdout)
+		err = antctlOutput.TableOutputForGetCommands(obj, output)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/antctl/raw/multicluster/get/resourceexport.go b/pkg/antctl/raw/multicluster/get/resourceexport.go
index 745be0e26..a9079fa44 100644
--- a/pkg/antctl/raw/multicluster/get/resourceexport.go
+++ b/pkg/antctl/raw/multicluster/get/resourceexport.go
@@ -151,6 +151,6 @@ func runEResourceExport(cmd *cobra.Command, args []string) error {
 		return nil
 	}
 
-	return output(resExports, false, optionsResourceExport.outputFormat, resourceexport.Transform)
+	return output(resExports, false, optionsResourceExport.outputFormat, cmd.OutOrStdout(), resourceexport.Transform)
 
 }
diff --git a/pkg/antctl/raw/multicluster/get/resourceimport.go b/pkg/antctl/raw/multicluster/get/resourceimport.go
index 79cd0ca77..7b69127d4 100644
--- a/pkg/antctl/raw/multicluster/get/resourceimport.go
+++ b/pkg/antctl/raw/multicluster/get/resourceimport.go
@@ -153,5 +153,5 @@ func runE(cmd *cobra.Command, args []string) error {
 		res = resourceImportList.Items
 	}
 
-	return output(res, singleResource, options.outputFormat, resourceimport.Transform)
+	return output(res, singleResource, options.outputFormat, cmd.OutOrStdout(), resourceimport.Transform)
 }
diff --git a/pkg/antctl/transform/clusterset/transform.go b/pkg/antctl/transform/clusterset/transform.go
index d3efce06f..9d0c26a0f 100644
--- a/pkg/antctl/transform/clusterset/transform.go
+++ b/pkg/antctl/transform/clusterset/transform.go
@@ -29,18 +29,30 @@ type Response struct {
 }
 
 func Transform(r interface{}, single bool) (interface{}, error) {
+	if single {
+		return listTransform([]multiclusterv1alpha1.ClusterSet{r.(multiclusterv1alpha1.ClusterSet)})
+	}
 	return listTransform(r)
 }
 
 func listTransform(l interface{}) (interface{}, error) {
-	clusterSet := l.(multiclusterv1alpha1.ClusterSet)
+	clusterSets := l.([]multiclusterv1alpha1.ClusterSet)
 	var result []interface{}
 
-	for i := range clusterSet.Status.ClusterStatuses {
-		r := clusterSet.Status.ClusterStatuses[i]
-		for j := range r.Conditions {
-			condition := r.Conditions[j]
-			o, _ := objectTransform(clusterSet, r, condition)
+	for _, clusterSet := range clusterSets {
+		if len(clusterSet.Status.ClusterStatuses) > 0 {
+			for i := range clusterSet.Status.ClusterStatuses {
+				r := clusterSet.Status.ClusterStatuses[i]
+				for j := range r.Conditions {
+					condition := r.Conditions[j]
+					o, _ := objectTransform(clusterSet, r, condition)
+					result = append(result, o.(Response))
+				}
+			}
+		} else {
+			// When the ClusterSet has no status, we should print it with empty status.
+			o, _ := objectTransform(clusterSet, multiclusterv1alpha1.ClusterStatus{},
+				multiclusterv1alpha1.ClusterCondition{})
 			result = append(result, o.(Response))
 		}
 	}


Test patch:
diff --git a/ci/jenkins/test-vmc.sh b/ci/jenkins/test-vmc.sh
index 4515db2bb..aeecab701 100755
--- a/ci/jenkins/test-vmc.sh
+++ b/ci/jenkins/test-vmc.sh
@@ -564,7 +564,7 @@ function run_conformance {
     ${SCP_WITH_ANTREA_CI_KEY} $GIT_CHECKOUT_DIR/jenkins/out/kubeconfig capv@${control_plane_ip}:~/.kube/config
 
     if [[ "$TESTCASE" == "conformance" ]]; then
-        ${GIT_CHECKOUT_DIR}/ci/run-k8s-e2e-tests.sh --e2e-conformance --log-mode ${MODE} --kubeconfig ${GIT_CHECKOUT_DIR}/jenkins/out/kubeconfig > ${GIT_CHECKOUT_DIR}/vmc-test.log
+        ${GIT_CHECKOUT_DIR}/ci/run-k8s-e2e-tests.sh --e2e-conformance --e2e-sig-network --log-mode ${MODE} --kubeconfig ${GIT_CHECKOUT_DIR}/jenkins/out/kubeconfig > ${GIT_CHECKOUT_DIR}/vmc-test.log
     elif [[ "$TESTCASE" == "all-features-conformance" ]]; then
         ${GIT_CHECKOUT_DIR}/ci/run-k8s-e2e-tests.sh --e2e-conformance --log-mode ${MODE} --kubeconfig ${GIT_CHECKOUT_DIR}/jenkins/out/kubeconfig > ${GIT_CHECKOUT_DIR}/vmc-test.log
     elif [[ "$TESTCASE" == "whole-conformance" ]]; then
diff --git a/ci/jenkins/test.sh b/ci/jenkins/test.sh
index 99b0dad59..b94487d67 100755
--- a/ci/jenkins/test.sh
+++ b/ci/jenkins/test.sh
@@ -373,8 +373,8 @@ function deliver_antrea_windows {
         sleep 5
         # Some tests need us.gcr.io/k8s-artifacts-prod/e2e-test-images/agnhost:2.13 image but it is not for windows/amd64 10.0.17763
         # Use e2eteam/agnhost:2.13 instead
-        harbor_images=("sigwindowstools-kube-proxy:v1.18.0" "agnhost:2.13" "agnhost:2.13" "agnhost:2.29" "e2eteam-jessie-dnsutils:1.0" "e2eteam-pause:3.2")
-        antrea_images=("sigwindowstools/kube-proxy:v1.18.0" "e2eteam/agnhost:2.13" "us.gcr.io/k8s-artifacts-prod/e2e-test-images/agnhost:2.13" "k8s.gcr.io/e2e-test-images/agnhost:2.29" "e2eteam/jessie-dnsutils:1.0" "e2eteam/pause:3.2")
+        harbor_images=("sigwindowstools-kube-proxy:v1.23.5-nanoserver" "agnhost:2.13" "agnhost:2.13" "agnhost:2.29" "e2eteam-jessie-dnsutils:1.0" "e2eteam-pause:3.2")
+        antrea_images=("sigwindowstools/kube-proxy:v1.23.5-nanoserver" "e2eteam/agnhost:2.13" "us.gcr.io/k8s-artifacts-prod/e2e-test-images/agnhost:2.13" "k8s.gcr.io/e2e-test-images/agnhost:2.29" "e2eteam/jessie-dnsutils:1.0" "e2eteam/pause:3.2")
         # Pull necessary images in advance to avoid transient error
         for i in "${!harbor_images[@]}"; do
             ssh -o StrictHostKeyChecking=no -n Administrator@${IP} "docker pull -q ${DOCKER_REGISTRY}/antrea/${harbor_images[i]} && docker tag ${DOCKER_REGISTRY}/antrea/${harbor_images[i]} ${antrea_images[i]}" || true
diff --git a/ci/run-k8s-e2e-tests.sh b/ci/run-k8s-e2e-tests.sh
index fe692537d..a798eca32 100755
--- a/ci/run-k8s-e2e-tests.sh
+++ b/ci/run-k8s-e2e-tests.sh
@@ -28,6 +28,7 @@ function echoerr {
 RUN_CONFORMANCE=false
 RUN_WHOLE_CONFORMANCE=false
 RUN_NETWORK_POLICY=false
+RUN_SIG_NETWORK=false
 RUN_E2E_FOCUS=""
 RUN_E2E_SKIP=""
 KUBECONFIG_OPTION=""
@@ -35,6 +36,8 @@ DEFAULT_E2E_CONFORMANCE_FOCUS="\[Conformance\]"
 DEFAULT_E2E_CONFORMANCE_SKIP="\[Slow\]|\[Serial\]|\[Disruptive\]|\[Flaky\]|\[Feature:.+\]|\[sig-cli\]|\[sig-storage\]|\[sig-auth\]|\[sig-api-machinery\]|\[sig-apps\]|\[sig-node\]|\[sig-instrumentation\]"
 DEFAULT_E2E_NETWORKPOLICY_FOCUS="\[Feature:NetworkPolicy\]"
 DEFAULT_E2E_NETWORKPOLICY_SKIP=""
+DEFAULT_E2E_SIG_NETWORK_FOCUS="\[sig-network\]"
+DEFAULT_E2E_SIG_NETWORK_SKIP="\[Slow\]|\[Serial\]|\[Disruptive\]|\[GCE\]|\[Feature:.+\]|\[Feature:IPv6DualStack\]|\[Feature:IPv6DualStackAlphaFeature\]|should create pod that uses dns|should provide Internet connection for containers"
 MODE="report"
 THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 KUBE_CONFORMANCE_IMAGE_OPTION=""
@@ -53,6 +56,7 @@ least one failed) and 2 (internal error when running tests, not a test failure).
         --e2e-conformance                                         Run Conformance tests.
         --e2e-whole-conformance                                   Run whole Conformance tests.
         --e2e-network-policy                                      Run Network Policy tests.
+        --e2e-sig-network                                         Run Sig-network tests.
         --e2e-all                                                 Run both Conformance and Network Policy tests.
         --e2e-focus TestRegex                                     Run only tests matching a specific regex, this is useful to run a single tests for example.
         --e2e-skip TestRegex                                      Skip some tests matching a specific regex.
@@ -106,9 +110,14 @@ case $key in
     RUN_NETWORK_POLICY=true
     shift
     ;;
+    --e2e-sig-network)
+    RUN_SIG_NETWORK=true
+    shift
+    ;;
     --e2e-all)
     RUN_CONFORMANCE=true
     RUN_NETWORK_POLICY=true
+    RUN_SIG_NETWORK=true
     shift
     ;;
     --e2e-focus)
@@ -222,6 +231,19 @@ function run_network_policy() {
     run_sonobuoy "${DEFAULT_E2E_NETWORKPOLICY_FOCUS}" "${e2e_skip}"
 }
 
+function run_sig_network() {
+    local e2e_skip="${DEFAULT_E2E_SIG_NETWORK_SKIP}"
+
+    if [[ "$RUN_E2E_FOCUS" != "" ]]; then
+        echo "It is not allowed to specify focus when running sig-network tests"
+        exit 1
+    fi
+    if [[ "$RUN_E2E_SKIP" != "" ]]; then
+        e2e_skip="$RUN_E2E_SKIP"
+    fi
+    run_sonobuoy "${DEFAULT_E2E_SIG_NETWORK_FOCUS}" "${e2e_skip}"
+}
+
 if [[ "$RUN_E2E_FOCUS" != "" ]]; then
     run_sonobuoy "$RUN_E2E_FOCUS" "$RUN_E2E_SKIP"
 fi
@@ -238,6 +260,10 @@ if $RUN_NETWORK_POLICY; then
     run_network_policy
 fi
 
+if $RUN_SIG_NETWORK; then
+    run_sig_network
+fi
+
 echoerr "Deleting sonobuoy resources"
 $SONOBUOY delete --wait $KUBECONFIG_OPTION
 
diff --git a/multicluster/cmd/multicluster-controller/gateway_webhook_test.go b/multicluster/cmd/multicluster-controller/gateway_webhook_test.go
new file mode 100644
index 000000000..268cb78ed
--- /dev/null
+++ b/multicluster/cmd/multicluster-controller/gateway_webhook_test.go
@@ -0,0 +1,134 @@
+/*
+Copyright 2022 Antrea 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 main
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/admission/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+	k8smcsv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+)
+
+var gatewayWebhookUnderTest *gatewayValidator
+
+func TestWebhookGatewayEvents(t *testing.T) {
+	newGateway := &mcsv1alpha1.Gateway{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+			Name:      "node-1",
+		},
+		GatewayIP:  "1.2.3.4",
+		InternalIP: "172.168.3.4",
+	}
+	existingGateway := &mcsv1alpha1.Gateway{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+			Name:      "node-2",
+		},
+	}
+
+	newGW, _ := json.Marshal(newGateway)
+
+	newReq := admission.Request{
+		AdmissionRequest: v1.AdmissionRequest{
+			UID: "07e52e8d-4513-11e9-a716-42010a800270",
+			Kind: metav1.GroupVersionKind{
+				Group:   "multicluster.crd.antrea.io",
+				Version: "v1alpha1",
+				Kind:    "Gateway",
+			},
+			Resource: metav1.GroupVersionResource{
+				Group:    "multicluster.crd.antrea.io",
+				Version:  "v1alpha1",
+				Resource: "Gateways",
+			},
+			Name:      "node-1",
+			Namespace: "default",
+			Operation: v1.Create,
+			Object: runtime.RawExtension{
+				Raw: newGW,
+			},
+		},
+	}
+
+	newReqCopy := newReq.DeepCopy()
+	invalidReq := admission.Request{
+		AdmissionRequest: *newReqCopy,
+	}
+	invalidReq.Object = runtime.RawExtension{Raw: []byte("a")}
+
+	tests := []struct {
+		name            string
+		req             admission.Request
+		existingGateway *mcsv1alpha1.Gateway
+		newGateway      *mcsv1alpha1.Gateway
+		isAllowed       bool
+	}{
+		{
+			name:      "create a new Gateway successfully",
+			req:       newReq,
+			isAllowed: true,
+		},
+		{
+			name:            "failed to create a Gateway when there is an existing one",
+			existingGateway: existingGateway,
+			req:             newReq,
+			isAllowed:       false,
+		},
+		{
+			name:      "failed to decode request",
+			req:       invalidReq,
+			isAllowed: false,
+		},
+	}
+
+	newScheme := runtime.NewScheme()
+	utilruntime.Must(clientgoscheme.AddToScheme(newScheme))
+	utilruntime.Must(k8smcsv1alpha1.AddToScheme(newScheme))
+	utilruntime.Must(mcsv1alpha1.AddToScheme(newScheme))
+	decoder, err := admission.NewDecoder(newScheme)
+	if err != nil {
+		klog.ErrorS(err, "Error constructing a decoder")
+	}
+	for _, tt := range tests {
+		fakeClient := fake.NewClientBuilder().WithScheme(newScheme).WithObjects().Build()
+		if tt.existingGateway != nil {
+			fakeClient = fake.NewClientBuilder().WithScheme(newScheme).WithObjects(tt.existingGateway).Build()
+		}
+		gatewayWebhookUnderTest = &gatewayValidator{
+			Client:    fakeClient,
+			namespace: "default"}
+		gatewayWebhookUnderTest.InjectDecoder(decoder)
+
+		t.Run(tt.name, func(t *testing.T) {
+			response := gatewayWebhookUnderTest.Handle(context.Background(), tt.req)
+			assert.Equal(t, tt.isAllowed, response.Allowed)
+		})
+	}
+}
diff --git a/multicluster/controllers/multicluster/gateway_controller_test.go b/multicluster/controllers/multicluster/gateway_controller_test.go
index edad64867..d2f78d7ae 100644
--- a/multicluster/controllers/multicluster/gateway_controller_test.go
+++ b/multicluster/controllers/multicluster/gateway_controller_test.go
@@ -17,7 +17,6 @@ limitations under the License.
 package multicluster
 
 import (
-	"fmt"
 	"reflect"
 	"testing"
 	"time"
@@ -39,7 +38,6 @@ var (
 	clusterID   = "cluster-a"
 
 	gw1CreationTime = metav1.NewTime(time.Now())
-	gw2CreationTime = metav1.NewTime(time.Now().Add(10 * time.Minute))
 
 	gwNode1 = mcsv1alpha1.Gateway{
 		ObjectMeta: metav1.ObjectMeta{
@@ -50,15 +48,7 @@ var (
 		GatewayIP:  "10.10.10.10",
 		InternalIP: "172.11.10.1",
 	}
-	gwNode2 = mcsv1alpha1.Gateway{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:              "node-2",
-			Namespace:         "default",
-			CreationTimestamp: gw2CreationTime,
-		},
-		GatewayIP:  "10.8.8.8",
-		InternalIP: "172.11.10.1",
-	}
+
 	existingResExport = &mcsv1alpha1.ResourceExport{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "cluster-a-clusterinfo",
@@ -79,26 +69,6 @@ var (
 			},
 		},
 	}
-	existingResExport2 = &mcsv1alpha1.ResourceExport{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "cluster-a-clusterinfo",
-			Namespace: leaderNamespace,
-		},
-		Spec: mcsv1alpha1.ResourceExportSpec{
-			Name:      clusterID,
-			Namespace: "default",
-			Kind:      common.ClusterInfoKind,
-			ClusterInfo: &mcsv1alpha1.ClusterInfo{
-				ServiceCIDR: serviceCIDR,
-				ClusterID:   clusterID,
-				GatewayInfos: []mcsv1alpha1.GatewayInfo{
-					{
-						GatewayIP: "101.101.101.101",
-					},
-				},
-			},
-		},
-	}
 )
 
 func TestGatewayReconciler(t *testing.T) {
@@ -123,39 +93,6 @@ func TestGatewayReconciler(t *testing.T) {
 			gateway: []mcsv1alpha1.Gateway{
 				gwNode1,
 			},
-			resExport: existingResExport,
-			expectedInfo: []mcsv1alpha1.GatewayInfo{
-				{
-					GatewayIP: "10.10.10.10",
-				},
-			},
-		},
-		{
-			name: "update a ResourceExport successfully by creating a new Gateway",
-			namespacedName: types.NamespacedName{
-				Namespace: "default",
-				Name:      "node-2",
-			},
-			gateway: []mcsv1alpha1.Gateway{
-				gwNode1, gwNode2,
-			},
-			resExport: existingResExport,
-			expectedInfo: []mcsv1alpha1.GatewayInfo{
-				{
-					GatewayIP: "10.8.8.8",
-				},
-			},
-		},
-		{
-			name: "update a ResourceExport successfully by deleting a Gateway",
-			namespacedName: types.NamespacedName{
-				Namespace: "default",
-				Name:      "node-2",
-			},
-			gateway: []mcsv1alpha1.Gateway{
-				gwNode1,
-			},
-			resExport: existingResExport2,
 			expectedInfo: []mcsv1alpha1.GatewayInfo{
 				{
 					GatewayIP: "10.10.10.10",
@@ -210,27 +147,20 @@ func TestGatewayReconciler(t *testing.T) {
 			if _, err := r.Reconcile(ctx, req); err != nil {
 				t.Errorf("Gateway Reconciler should handle ResourceExports events successfully but got error = %v", err)
 			} else {
-				gws := &mcsv1alpha1.GatewayList{}
-				_ = fakeClient.List(ctx, gws, &client.ListOptions{})
-				fmt.Printf("output list: %v", gws)
 				ciExport := mcsv1alpha1.ResourceExport{}
 				ciExportName := types.NamespacedName{
 					Namespace: leaderNamespace,
 					Name:      newClusterInfoResourceExportName(localClusterID),
 				}
 				err := fakeRemoteClient.Get(ctx, ciExportName, &ciExport)
-				if err == nil {
-					if !reflect.DeepEqual(ciExport.Spec.ClusterInfo.GatewayInfos, tt.expectedInfo) {
-						t.Errorf("Expected GatewayInfos are %v but got %v", tt.expectedInfo, ciExport.Spec.ClusterInfo.GatewayInfos)
-					}
-				} else {
-					if tt.isDelete {
-						if !apierrors.IsNotFound(err) {
-							t.Errorf("Gateway Reconciler expects not found error but got error = %v", err)
-						}
-					} else {
-						t.Errorf("Expected a ClusterInfo kind of ResourceExport but got error = %v", err)
-					}
+				if tt.isDelete && !apierrors.IsNotFound(err) {
+					t.Errorf("Gateway Reconciler expects not found error but got error = %v", err)
+				}
+				if err == nil && !reflect.DeepEqual(ciExport.Spec.ClusterInfo.GatewayInfos, tt.expectedInfo) {
+					t.Errorf("Expected GatewayInfos are %v but got %v", tt.expectedInfo, ciExport.Spec.ClusterInfo.GatewayInfos)
+				}
+				if !tt.isDelete && apierrors.IsNotFound(err) {
+					t.Errorf("Expected a ClusterInfo kind of ResourceExport but got error = %v", err)
 				}
 			}
 		})
diff --git a/multicluster/controllers/multicluster/node_controller_test.go b/multicluster/controllers/multicluster/node_controller_test.go
index b3ed403f0..8dfed6e7e 100644
--- a/multicluster/controllers/multicluster/node_controller_test.go
+++ b/multicluster/controllers/multicluster/node_controller_test.go
@@ -32,8 +32,17 @@ import (
 	"antrea.io/antrea/multicluster/controllers/multicluster/common"
 )
 
-func TestNodeReconciler(t *testing.T) {
-	node1 := &corev1.Node{
+var (
+	node1           *corev1.Node
+	node2           *corev1.Node
+	node3           *corev1.Node
+	node4           *corev1.Node
+	updatedGateway2 *mcsv1alpha1.Gateway
+	gateway3        *mcsv1alpha1.Gateway
+)
+
+func initializeCommonData() {
+	node1 = &corev1.Node{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "node-1",
 			Annotations: map[string]string{
@@ -51,10 +60,59 @@ func TestNodeReconciler(t *testing.T) {
 					Address: "172.11.10.1",
 				},
 			},
+			Conditions: []corev1.NodeCondition{
+				{
+					Type:   corev1.NodeReady,
+					Status: corev1.ConditionTrue,
+				},
+			},
+		},
+	}
+
+	node2 = node1.DeepCopy()
+	node2.Name = "node-2"
+	node2.Status.Addresses = []corev1.NodeAddress{
+		{
+			Type:    corev1.NodeExternalIP,
+			Address: "10.10.10.12",
+		},
+		{
+			Type:    corev1.NodeInternalIP,
+			Address: "172.11.10.2",
+		},
+	}
+
+	node3 = node1.DeepCopy()
+	node3.Name = "node-3"
+	node3.Status.Conditions = []corev1.NodeCondition{
+		{
+			Type:   corev1.NodeReady,
+			Status: corev1.ConditionFalse,
+		},
+	}
+
+	node4 = node1.DeepCopy()
+	node4.Name = "node-4"
+	node4.Annotations = map[string]string{
+		common.GatewayAnnotation:   "true",
+		common.GatewayIPAnnotation: "invalid-gatewayip",
+	}
+
+	updatedGateway2 = &mcsv1alpha1.Gateway{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "node-2",
+			Namespace: "default",
 		},
+		GatewayIP:  "10.10.10.12",
+		InternalIP: "172.11.10.2",
 	}
-	node1NoValidUpdate := *node1
-	node1NoValidUpdate.Labels = map[string]string{"hostname.k8s.io": "node-1"}
+
+	gateway3 = gwNode1.DeepCopy()
+	gateway3.Name = "node-3"
+}
+
+func TestNodeReconciler(t *testing.T) {
+	initializeCommonData()
 	node1NoAnnotation := *node1
 	node1NoAnnotation.Annotations = map[string]string{}
 	node1WithIPAnnotation := *node1
@@ -62,26 +120,41 @@ func TestNodeReconciler(t *testing.T) {
 		common.GatewayAnnotation:   "true",
 		common.GatewayIPAnnotation: "11.11.10.10",
 	}
+	gateway4 := gwNode1.DeepCopy()
+	gateway4.Name = "node-4"
+	newGateway1 := gwNode1.DeepCopy()
+	newGateway1.GatewayIP = "172.11.10.1"
+	newNode1 := node1.DeepCopy()
+	newNode1.Name = "node-1"
+	newNode1.Status.Addresses = []corev1.NodeAddress{
+		{
+			Type:    corev1.NodeHostName,
+			Address: "node-1",
+		},
+	}
 
 	tests := []struct {
-		name        string
-		nodes       []*corev1.Node
-		req         reconcile.Request
-		existingGW  *mcsv1alpha1.Gateway
-		expectedGW  *mcsv1alpha1.Gateway
-		isDelete    bool
-		expectedErr string
+		name          string
+		nodes         []*corev1.Node
+		req           reconcile.Request
+		precedence    mcsv1alpha1.Precedence
+		existingGW    *mcsv1alpha1.Gateway
+		expectedGW    *mcsv1alpha1.Gateway
+		activeGateway string
+		candidates    map[string]bool
+		isDelete      bool
 	}{
 		{
 			name:       "create a Gateway successfully",
 			nodes:      []*corev1.Node{node1},
-			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "", Name: node1.Name}},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Name: node1.Name}},
 			expectedGW: &gwNode1,
+			precedence: mcsv1alpha1.PrecedencePublic,
 		},
 		{
 			name:       "update a Gateway successfully by changing GatewayIP",
 			nodes:      []*corev1.Node{&node1WithIPAnnotation},
-			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "", Name: node1.Name}},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Name: node1.Name}},
 			existingGW: &gwNode1,
 			expectedGW: &mcsv1alpha1.Gateway{
 				ObjectMeta: metav1.ObjectMeta{
@@ -91,20 +164,61 @@ func TestNodeReconciler(t *testing.T) {
 				GatewayIP:  "11.11.10.10",
 				InternalIP: "172.11.10.1",
 			},
+			activeGateway: "node-1",
+			precedence:    mcsv1alpha1.PrecedencePublic,
 		},
 		{
-			name:       "remove a Gateway Node to delete a Gateway successfully",
-			nodes:      []*corev1.Node{},
-			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "", Name: node1.Name}},
-			existingGW: &gwNode1,
-			isDelete:   true,
+			name:          "remove a Gateway Node to delete a Gateway successfully",
+			nodes:         []*corev1.Node{},
+			req:           reconcile.Request{NamespacedName: types.NamespacedName{Name: node1.Name}},
+			existingGW:    &gwNode1,
+			activeGateway: "node-1",
+			isDelete:      true,
+			precedence:    mcsv1alpha1.PrecedencePublic,
 		},
 		{
-			name:       "remove a Gateway Node's annotation to delete a Gateway successfully",
-			nodes:      []*corev1.Node{&node1NoAnnotation},
-			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "", Name: node1.Name}},
+			name:          "remove a Gateway Node's annotation to delete a Gateway successfully",
+			nodes:         []*corev1.Node{&node1NoAnnotation},
+			req:           reconcile.Request{NamespacedName: types.NamespacedName{Name: node1.Name}},
+			existingGW:    &gwNode1,
+			activeGateway: "node-1",
+			isDelete:      true,
+			precedence:    mcsv1alpha1.PrecedencePublic,
+		},
+		{
+			name:       "remote a Gateway due to no IPs",
+			nodes:      []*corev1.Node{newNode1},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Name: newNode1.Name}},
 			existingGW: &gwNode1,
 			isDelete:   true,
+			precedence: mcsv1alpha1.PrecedencePrivate,
+		},
+		{
+			name:          "remove a Gateway Node to create a new Gateway from candidates successfully",
+			nodes:         []*corev1.Node{node2, node4},
+			req:           reconcile.Request{NamespacedName: types.NamespacedName{Name: node1.Name}},
+			existingGW:    &gwNode1,
+			expectedGW:    updatedGateway2,
+			activeGateway: "node-1",
+			precedence:    mcsv1alpha1.PrecedencePublic,
+		},
+		{
+			name:          "create a new Gateway successfully when active Gateway Node is not ready",
+			nodes:         []*corev1.Node{node2, node3},
+			req:           reconcile.Request{NamespacedName: types.NamespacedName{Name: node3.Name}},
+			existingGW:    gateway3,
+			expectedGW:    updatedGateway2,
+			activeGateway: "node-3",
+			precedence:    mcsv1alpha1.PrecedencePublic,
+		},
+		{
+			name:          "create a new Gateway successfully when active Gateway Node has no valid IP",
+			nodes:         []*corev1.Node{node2, node4},
+			req:           reconcile.Request{NamespacedName: types.NamespacedName{Name: node4.Name}},
+			existingGW:    gateway4,
+			expectedGW:    updatedGateway2,
+			activeGateway: "node-4",
+			precedence:    mcsv1alpha1.PrecedencePublic,
 		},
 	}
 	for _, tt := range tests {
@@ -117,27 +231,95 @@ func TestNodeReconciler(t *testing.T) {
 				obj = append(obj, tt.existingGW)
 			}
 			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(obj...).Build()
-			r := NewNodeReconciler(fakeClient, scheme, "default", mcsv1alpha1.PrecedencePublic)
+			r := NewNodeReconciler(fakeClient, scheme, "default", tt.precedence)
+			r.activeGateway = tt.activeGateway
 			if _, err := r.Reconcile(ctx, tt.req); err != nil {
-				if tt.expectedErr != "" {
-					assert.Contains(t, err.Error(), tt.expectedErr)
-				} else {
-					t.Errorf("Node Reconciler should handle Node events successfully but got error = %v", err)
-				}
+				t.Errorf("Node Reconciler should handle Node events successfully but got error = %v", err)
 			} else {
 				newGW := &mcsv1alpha1.Gateway{}
-				gwNamespcedName := types.NamespacedName{Name: "node-1", Namespace: "default"}
+				gwNamespcedName := types.NamespacedName{Name: tt.req.Name, Namespace: "default"}
+				if tt.expectedGW != nil {
+					gwNamespcedName = types.NamespacedName{Name: tt.expectedGW.Name, Namespace: "default"}
+				}
 				err := fakeClient.Get(ctx, gwNamespcedName, newGW)
-				if err != nil {
-					if tt.isDelete {
-						if !apierrors.IsNotFound(err) {
-							t.Errorf("Expected to get not found error but got err: %v", err)
-						}
-					} else {
+				if tt.isDelete {
+					if err == nil || (err != nil && !apierrors.IsNotFound(err)) {
+						t.Errorf("Expected to get not found error but got err: %v", err)
+					}
+				} else {
+					if err != nil {
 						t.Errorf("Expected to get Gateway but got err: %v", err)
+					} else {
+						if tt.expectedGW.GatewayIP != newGW.GatewayIP || tt.expectedGW.InternalIP != newGW.InternalIP {
+							t.Errorf("Expected Gateway %v but got: %v", tt.expectedGW, newGW)
+						}
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestInitialize(t *testing.T) {
+	initializeCommonData()
+	node5 := node1.DeepCopy()
+	node5.Name = "node-5"
+	node5.Annotations = map[string]string{}
+	tests := []struct {
+		name                  string
+		nodes                 []*corev1.Node
+		req                   reconcile.Request
+		existingGW            *mcsv1alpha1.Gateway
+		expectedActiveGateway string
+		isDelete              bool
+		candidatesSize        int
+	}{
+		{
+			name:                  "initialize and set active Gateway successfully",
+			nodes:                 []*corev1.Node{node1, node2, node5},
+			existingGW:            &gwNode1,
+			expectedActiveGateway: "node-1",
+			candidatesSize:        2,
+		},
+		{
+			name:                  "initialize successfully without Gateway",
+			nodes:                 []*corev1.Node{node3, node4, node5},
+			expectedActiveGateway: "",
+			candidatesSize:        2,
+		},
+		{
+			name:                  "initialize and delete Gateway successfully",
+			nodes:                 []*corev1.Node{node1, node5},
+			existingGW:            gateway3,
+			isDelete:              true,
+			expectedActiveGateway: "",
+			candidatesSize:        1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var obj []client.Object
+			for _, n := range tt.nodes {
+				obj = append(obj, n)
+			}
+			if tt.existingGW != nil {
+				obj = append(obj, tt.existingGW)
+			}
+			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(obj...).Build()
+			r := NewNodeReconciler(fakeClient, scheme, "default", mcsv1alpha1.PrecedencePublic)
+			if err := r.initialize(); err != nil {
+				t.Errorf("Expected initialize() successfully but got err: %v", err)
+			} else {
+				assert.Equal(t, tt.expectedActiveGateway, r.activeGateway)
+				assert.Equal(t, tt.candidatesSize, len(r.gatewayCandidates))
+				if tt.isDelete {
+					deletedGW := &mcsv1alpha1.Gateway{}
+					gwNamespcedName := types.NamespacedName{Name: tt.existingGW.Name, Namespace: "default"}
+					err := fakeClient.Get(ctx, gwNamespcedName, deletedGW)
+					if !apierrors.IsNotFound(err) {
+						t.Errorf("Expected to get not found error but got err: %v", err)
 					}
-				} else if tt.expectedGW.GatewayIP != newGW.GatewayIP || tt.expectedGW.InternalIP != newGW.InternalIP {
-					t.Errorf("Expected Gateway %v but got: %v", tt.expectedGW, newGW)
 				}
 			}
 		})
diff --git a/pkg/agent/multicast/mcast_controller_test.go b/pkg/agent/multicast/mcast_controller_test.go
index af1ec5931..06fedf573 100644
--- a/pkg/agent/multicast/mcast_controller_test.go
+++ b/pkg/agent/multicast/mcast_controller_test.go
@@ -157,12 +157,12 @@ func TestCheckLastMember(t *testing.T) {
 		if ev != nil {
 			status.group = ev.group
 			if ev.eType == groupLeave {
-				mockOFClient.EXPECT().UninstallGroup(gomock.Any())
+				mockOFClient.EXPECT().UninstallMulticastGroup(gomock.Any())
 				mockOFClient.EXPECT().UninstallMulticastFlows(status.group)
 			}
 		} else {
 			status.group = mgroup
-			mockOFClient.EXPECT().UninstallGroup(gomock.Any())
+			mockOFClient.EXPECT().UninstallMulticastGroup(gomock.Any())
 			mockOFClient.EXPECT().UninstallMulticastFlows(status.group)
 		}
 		_ = mctrl.groupCache.Add(status)
@@ -268,7 +268,7 @@ func TestClearStaleGroups(t *testing.T) {
 			fakePort++
 		}
 	}
-	mockOFClient.EXPECT().UninstallGroup(gomock.Any()).Times(len(staleGroups))
+	mockOFClient.EXPECT().UninstallMulticastGroup(gomock.Any()).Times(len(staleGroups))
 	mockOFClient.EXPECT().UninstallMulticastFlows(gomock.Any()).Times(len(staleGroups))
 	mockMulticastSocket.EXPECT().MulticastInterfaceLeaveMgroup(gomock.Any(), gomock.Any(), gomock.Any()).Times(len(staleGroups))
 	mctrl.clearStaleGroups()
@@ -511,7 +511,7 @@ func TestEncapLocalReportAndNotifyRemote(t *testing.T) {
 				}
 			}
 			if tc.groupChanged {
-				mockOFClient.EXPECT().UninstallGroup(gomock.Any())
+				mockOFClient.EXPECT().UninstallMulticastGroup(gomock.Any())
 				mockOFClient.EXPECT().UninstallMulticastFlows(tc.e.group)
 				mockMulticastSocket.EXPECT().MulticastInterfaceLeaveMgroup(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
 				mockOFClient.EXPECT().SendIGMPRemoteReportPacketOut(igmpReportDstMac, types.IGMPv3Router, gomock.Any())
diff --git a/pkg/agent/openflow/client_test.go b/pkg/agent/openflow/client_test.go
index 4193a9ebd..c19d698c4 100644
--- a/pkg/agent/openflow/client_test.go
+++ b/pkg/agent/openflow/client_test.go
@@ -34,6 +34,7 @@ import (
 	ovsoftest "antrea.io/antrea/pkg/ovs/openflow/testing"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
 	utilip "antrea.io/antrea/pkg/util/ip"
+	k8sproxy "antrea.io/antrea/third_party/proxy"
 )
 
 const bridgeName = "dummy-br"
@@ -583,3 +584,128 @@ func TestMulticlusterFlowsInstallation(t *testing.T) {
 	_, ok = client.featureMulticluster.cachedFlows.Load(cacheKey)
 	require.False(t, ok)
 }
+
+func TestServiceGroupInstallAndUninstall(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	mockOFBridge := ovsoftest.NewMockBridge(ctrl)
+	m := oftest.NewMockOFEntryOperations(ctrl)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, true, false, false, false, true, false, false, false, false)
+	client := ofClient.(*client)
+	client.bridge = mockOFBridge
+	client.cookieAllocator = cookie.NewAllocator(0)
+	client.ofEntryOperations = m
+	client.nodeConfig = nodeConfig
+	client.networkConfig = networkConfig
+	client.serviceConfig = serviceConfig
+	client.ipProtocols = []binding.Protocol{binding.ProtocolIP}
+	client.generatePipelines()
+	endpoints := []k8sproxy.Endpoint{
+		&k8sproxy.BaseEndpointInfo{
+			Endpoint: net.JoinHostPort("192.168.1.2", "8081"),
+			IsLocal:  true,
+		},
+		&k8sproxy.BaseEndpointInfo{
+			Endpoint: net.JoinHostPort("10.20.1.11", "8081"),
+			IsLocal:  false,
+		},
+	}
+	groupID1 := binding.GroupIDType(1)
+	groupID2 := binding.GroupIDType(2)
+	for _, tc := range []struct {
+		groupID         binding.GroupIDType
+		sessionAffinity bool
+		deleteSucceeded bool
+	}{
+		{groupID: groupID1, deleteSucceeded: false, sessionAffinity: true},
+		{groupID: groupID2, deleteSucceeded: true, sessionAffinity: false},
+	} {
+		mockGroup := ovsoftest.NewMockGroup(ctrl)
+		mockBucketBuilder := ovsoftest.NewMockBucketBuilder(ctrl)
+		mockOFBridge.EXPECT().CreateGroup(tc.groupID).Return(mockGroup).Times(1)
+		mockGroup.EXPECT().ResetBuckets().Return(mockGroup).Times(1)
+		mockGroup.EXPECT().Bucket().Return(mockBucketBuilder).Times(len(endpoints))
+		mockBucketBuilder.EXPECT().Weight(gomock.Any()).Return(mockBucketBuilder).Times(len(endpoints))
+		mockBucketBuilder.EXPECT().LoadToRegField(gomock.Any(), gomock.Any()).Return(mockBucketBuilder).AnyTimes()
+		if tc.sessionAffinity {
+			mockBucketBuilder.EXPECT().ResubmitToTable(ServiceLBTable.GetID()).Return(mockBucketBuilder).Times(len(endpoints))
+		} else {
+			mockBucketBuilder.EXPECT().ResubmitToTable(EndpointDNATTable.GetID()).Return(mockBucketBuilder).Times(len(endpoints))
+		}
+		mockBucketBuilder.EXPECT().Done().Return(mockGroup).Times(len(endpoints))
+		mockGroup.EXPECT().Add().Return(nil)
+		err := client.InstallServiceGroup(tc.groupID, tc.sessionAffinity, endpoints)
+		require.NoError(t, err)
+		_, exists := client.featureService.groupCache.Load(tc.groupID)
+		assert.True(t, exists)
+		mockOFBridge.EXPECT().DeleteGroup(tc.groupID).Return(tc.deleteSucceeded)
+		err = client.UninstallServiceGroup(tc.groupID)
+		_, exists = client.featureService.groupCache.Load(tc.groupID)
+		if tc.deleteSucceeded {
+			assert.NoError(t, err)
+			assert.False(t, exists)
+		} else {
+			assert.Error(t, err)
+			assert.True(t, exists)
+		}
+	}
+}
+
+func TestMulticastGroupInstallAndUninstall(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	mockOFBridge := ovsoftest.NewMockBridge(ctrl)
+	m := oftest.NewMockOFEntryOperations(ctrl)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, false, true, false, false, false, false, true, false, false)
+	client := ofClient.(*client)
+	client.bridge = mockOFBridge
+	client.cookieAllocator = cookie.NewAllocator(0)
+	client.ofEntryOperations = m
+	client.nodeConfig = nodeConfig
+	client.networkConfig = networkConfig
+	client.serviceConfig = serviceConfig
+	client.ipProtocols = []binding.Protocol{binding.ProtocolIP}
+	client.generatePipelines()
+	localReceivers := []uint32{101, 102}
+	remoteIPs := []net.IP{
+		net.ParseIP("1.1.1.2"),
+		net.ParseIP("1.1.1.3"),
+		net.ParseIP("1.1.1.4"),
+	}
+	groupID1 := binding.GroupIDType(1)
+	groupID2 := binding.GroupIDType(2)
+	for _, tc := range []struct {
+		groupID         binding.GroupIDType
+		deleteSucceeded bool
+	}{
+		{groupID: groupID1, deleteSucceeded: false},
+		{groupID: groupID2, deleteSucceeded: true},
+	} {
+		mockGroup := ovsoftest.NewMockGroup(ctrl)
+		mockBucketBuilder := ovsoftest.NewMockBucketBuilder(ctrl)
+		mockOFBridge.EXPECT().CreateGroupTypeAll(tc.groupID).Return(mockGroup).Times(1)
+		mockGroup.EXPECT().ResetBuckets().Return(mockGroup).Times(1)
+		mockGroup.EXPECT().Bucket().Return(mockBucketBuilder).Times(len(localReceivers) + len(remoteIPs))
+		mockBucketBuilder.EXPECT().LoadToRegField(gomock.Any(), gomock.Any()).Return(mockBucketBuilder).AnyTimes()
+		mockBucketBuilder.EXPECT().ResubmitToTable(MulticastIngressRuleTable.GetID()).Return(mockBucketBuilder).Times(len(localReceivers))
+		mockBucketBuilder.EXPECT().ResubmitToTable(MulticastOutputTable.GetID()).Return(mockBucketBuilder).Times(len(remoteIPs))
+		mockBucketBuilder.EXPECT().SetTunnelDst(gomock.Any()).Return(mockBucketBuilder).Times(len(remoteIPs))
+		mockBucketBuilder.EXPECT().Done().Return(mockGroup).Times(len(localReceivers) + len(remoteIPs))
+		mockGroup.EXPECT().Add().Return(nil)
+		err := client.InstallMulticastGroup(tc.groupID, localReceivers, remoteIPs)
+		require.NoError(t, err)
+		_, exists := client.featureMulticast.groupCache.Load(tc.groupID)
+		assert.True(t, exists)
+		mockOFBridge.EXPECT().DeleteGroup(tc.groupID).Return(tc.deleteSucceeded)
+		err = client.UninstallMulticastGroup(tc.groupID)
+		if tc.deleteSucceeded {
+			assert.NoError(t, err)
+			_, exists = client.featureMulticast.groupCache.Load(tc.groupID)
+			assert.False(t, exists)
+		} else {
+			assert.Error(t, err)
+			_, exists = client.featureMulticast.groupCache.Load(tc.groupID)
+			assert.True(t, exists)
+		}
+	}
+}
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index b118ee1b3..be27e495a 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -808,32 +808,32 @@ func (mr *MockClientMockRecorder) UninstallEndpointFlows(arg0, arg1 interface{})
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallEndpointFlows", reflect.TypeOf((*MockClient)(nil).UninstallEndpointFlows), arg0, arg1)
 }
 
-// UninstallGroup mocks base method
-func (m *MockClient) UninstallGroup(arg0 openflow.GroupIDType) error {
+// UninstallMulticastFlows mocks base method
+func (m *MockClient) UninstallMulticastFlows(arg0 net.IP) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UninstallGroup", arg0)
+	ret := m.ctrl.Call(m, "UninstallMulticastFlows", arg0)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// UninstallGroup indicates an expected call of UninstallGroup
-func (mr *MockClientMockRecorder) UninstallGroup(arg0 interface{}) *gomock.Call {
+// UninstallMulticastFlows indicates an expected call of UninstallMulticastFlows
+func (mr *MockClientMockRecorder) UninstallMulticastFlows(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallGroup", reflect.TypeOf((*MockClient)(nil).UninstallGroup), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallMulticastFlows", reflect.TypeOf((*MockClient)(nil).UninstallMulticastFlows), arg0)
 }
 
-// UninstallMulticastFlows mocks base method
-func (m *MockClient) UninstallMulticastFlows(arg0 net.IP) error {
+// UninstallMulticastGroup mocks base method
+func (m *MockClient) UninstallMulticastGroup(arg0 openflow.GroupIDType) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UninstallMulticastFlows", arg0)
+	ret := m.ctrl.Call(m, "UninstallMulticastGroup", arg0)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// UninstallMulticastFlows indicates an expected call of UninstallMulticastFlows
-func (mr *MockClientMockRecorder) UninstallMulticastFlows(arg0 interface{}) *gomock.Call {
+// UninstallMulticastGroup indicates an expected call of UninstallMulticastGroup
+func (mr *MockClientMockRecorder) UninstallMulticastGroup(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallMulticastFlows", reflect.TypeOf((*MockClient)(nil).UninstallMulticastFlows), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallMulticastGroup", reflect.TypeOf((*MockClient)(nil).UninstallMulticastGroup), arg0)
 }
 
 // UninstallMulticlusterFlows mocks base method
@@ -935,6 +935,20 @@ func (mr *MockClientMockRecorder) UninstallServiceFlows(arg0, arg1, arg2 interfa
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallServiceFlows", reflect.TypeOf((*MockClient)(nil).UninstallServiceFlows), arg0, arg1, arg2)
 }
 
+// UninstallServiceGroup mocks base method
+func (m *MockClient) UninstallServiceGroup(arg0 openflow.GroupIDType) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UninstallServiceGroup", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UninstallServiceGroup indicates an expected call of UninstallServiceGroup
+func (mr *MockClientMockRecorder) UninstallServiceGroup(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallServiceGroup", reflect.TypeOf((*MockClient)(nil).UninstallServiceGroup), arg0)
+}
+
 // UninstallTraceflowFlows mocks base method
 func (m *MockClient) UninstallTraceflowFlows(arg0 byte) error {
 	m.ctrl.T.Helper()
diff --git a/pkg/agent/proxy/proxier_test.go b/pkg/agent/proxy/proxier_test.go
index acc769d93..a4fa05814 100644
--- a/pkg/agent/proxy/proxier_test.go
+++ b/pkg/agent/proxy/proxier_test.go
@@ -348,7 +348,7 @@ func testLoadBalancer(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep
 			mockOFClient.EXPECT().InstallServiceFlows(groupID, loadBalancerIP, uint16(svcPort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeLoadBalancer).Times(1)
 		}
 		groupID = fp.groupCounter.AllocateIfNotExist(svcPortName, !nodeLocalVal)
-		mockOFClient.EXPECT().UninstallGroup(groupID).Times(1)
+		mockOFClient.EXPECT().UninstallServiceGroup(groupID).Times(1)
 	}
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	if proxyLoadBalancerIPs {
@@ -467,7 +467,7 @@ func testNodePort(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep2IP
 		mockOFClient.EXPECT().InstallServiceFlows(groupID, gomock.Any(), uint16(svcNodePort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeNodePort).Times(1)
 
 		groupID = fp.groupCounter.AllocateIfNotExist(svcPortName, !nodeLocalVal)
-		mockOFClient.EXPECT().UninstallGroup(groupID).Times(1)
+		mockOFClient.EXPECT().UninstallServiceGroup(groupID).Times(1)
 	}
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	mockRouteClient.EXPECT().AddNodePort(gomock.Any(), uint16(svcNodePort), bindingProtocol).Times(1)
@@ -749,7 +749,7 @@ func testClusterIPRemoval(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool)
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP, uint16(svcPort), bindingProtocol).Times(1)
 	mockOFClient.EXPECT().UninstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().UninstallGroup(gomock.Any()).Times(1)
+	mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(1)
 	fp.syncProxyRules()
 
 	fp.serviceChanges.OnServiceUpdate(service, nil)
@@ -1232,8 +1232,8 @@ func TestServicesWithSameEndpoints(t *testing.T) {
 	mockOFClient.EXPECT().InstallServiceFlows(groupID2, svcIP2, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP1, uint16(svcPort), bindingProtocol).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP2, uint16(svcPort), bindingProtocol).Times(1)
-	mockOFClient.EXPECT().UninstallGroup(groupID1).Times(1)
-	mockOFClient.EXPECT().UninstallGroup(groupID2).Times(1)
+	mockOFClient.EXPECT().UninstallServiceGroup(groupID1).Times(1)
+	mockOFClient.EXPECT().UninstallServiceGroup(groupID2).Times(1)
 	// Since these two Services reference to the same Endpoint, there should only be one operation.
 	mockOFClient.EXPECT().UninstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 
diff --git a/pkg/antctl/raw/multicluster/get/clusterset_test.go b/pkg/antctl/raw/multicluster/get/clusterset_test.go
new file mode 100644
index 000000000..27597740e
--- /dev/null
+++ b/pkg/antctl/raw/multicluster/get/clusterset_test.go
@@ -0,0 +1,160 @@
+// Copyright 2022 Antrea 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 get
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme"
+)
+
+func TestGetClusterSet(t *testing.T) {
+	clusterSetList := &mcsv1alpha1.ClusterSetList{
+		Items: []mcsv1alpha1.ClusterSet{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "clusterset-name",
+				},
+				Status: mcsv1alpha1.ClusterSetStatus{},
+			},
+		},
+	}
+	tests := []struct {
+		name                string
+		existingClusterSets *mcsv1alpha1.ClusterSetList
+		args                []string
+		output              string
+		allNamespaces       bool
+		donotFake           bool
+		expectedErr         string
+		expectedOutput      string
+	}{
+		{
+			name:                "get single ClusterSet",
+			existingClusterSets: clusterSetList,
+			args:                []string{"clusterset-name"},
+			expectedOutput:      "CLUSTER-ID NAMESPACE CLUSTER-SET-ID  TYPE   STATUS REASON\n<NONE>     default   clusterset-name <NONE> <NONE> <NONE>\n",
+		},
+		{
+			name:                "get single ClusterSet with json output",
+			existingClusterSets: clusterSetList,
+			args:                []string{"clusterset-name"},
+			output:              "json",
+			expectedOutput:      "[\n  {\n    \"kind\": \"ClusterSet\",\n    \"apiVersion\": \"multicluster.crd.antrea.io/v1alpha1\",\n    \"metadata\": {\n      \"name\": \"clusterset-name\",\n      \"namespace\": \"default\",\n      \"resourceVersion\": \"999\",\n      \"creationTimestamp\": null\n    },\n    \"spec\": {\n      \"leaders\": null\n    },\n    \"status\": {}\n  }\n]\n",
+		},
+		{
+			name:                "get single ClusterSet with yaml output",
+			existingClusterSets: clusterSetList,
+			args:                []string{"clusterset-name"},
+			output:              "yaml",
+			expectedOutput:      "- apiVersion: multicluster.crd.antrea.io/v1alpha1\n  kind: ClusterSet\n  metadata:\n    creationTimestamp: null\n    name: clusterset-name\n    namespace: default\n    resourceVersion: \"999\"\n  spec:\n    leaders: null\n  status: {}\n",
+		},
+		{
+			name:        "get non-existing ClusterSet",
+			args:        []string{"clusterset1"},
+			expectedErr: "clustersets.multicluster.crd.antrea.io \"clusterset1\" not found",
+		},
+		{
+			name:          "get all ClusterSets",
+			allNamespaces: true,
+			existingClusterSets: &mcsv1alpha1.ClusterSetList{
+				Items: []mcsv1alpha1.ClusterSet{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "clusterset-name",
+						},
+						Status: mcsv1alpha1.ClusterSetStatus{},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "kube-system",
+							Name:      "clusterset-1",
+						},
+						Status: mcsv1alpha1.ClusterSetStatus{
+							ClusterStatuses: []mcsv1alpha1.ClusterStatus{{
+								ClusterID: "cluster-a",
+								Conditions: []mcsv1alpha1.ClusterCondition{
+									{
+										Message: "Member Connected",
+										Reason:  "Connected",
+										Status:  v1.ConditionTrue,
+										Type:    mcsv1alpha1.ClusterReady,
+									},
+								},
+							},
+							},
+						},
+					},
+				},
+			},
+			expectedOutput: "CLUSTER-ID NAMESPACE   CLUSTER-SET-ID  TYPE   STATUS REASON   \n<NONE>     default     clusterset-name <NONE> <NONE> <NONE>   \ncluster-a  kube-system clusterset-1    Ready  True   Connected\n",
+		},
+		{
+			name:           "get all ClusterSets but empty result",
+			allNamespaces:  true,
+			expectedOutput: "No resources found\n",
+		},
+		{
+			name:           "get all ClusterSets but empty result in default Namespace",
+			expectedOutput: "No resource found in Namespace default\n",
+		},
+		{
+			name:        "error due to no kubeconfig",
+			expectedErr: "flag accessed but not defined: kubeconfig",
+			donotFake:   true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			cmd := NewClusterSetCommand()
+			buf := new(bytes.Buffer)
+			cmd.SetOutput(buf)
+			cmd.SetOut(buf)
+			cmd.SetErr(buf)
+			cmd.SetArgs(tt.args)
+
+			fakeClient := fake.NewClientBuilder().WithScheme(mcscheme.Scheme).Build()
+			if tt.existingClusterSets != nil {
+				fakeClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithLists(tt.existingClusterSets).Build()
+			}
+			if !tt.donotFake {
+				optionsClusterSet.k8sClient = fakeClient
+			}
+
+			if tt.allNamespaces {
+				optionsClusterSet.allNamespaces = true
+			}
+			if tt.output != "" {
+				optionsClusterSet.outputFormat = tt.output
+			}
+			err := cmd.Execute()
+			if err != nil {
+				assert.Equal(t, tt.expectedErr, err.Error())
+			} else {
+				assert.Equal(t, tt.expectedOutput, buf.String())
+			}
+		})
+	}
+}
diff --git a/pkg/antctl/raw/multicluster/get/output_test.go b/pkg/antctl/raw/multicluster/get/output_test.go
new file mode 100644
index 000000000..f2b099986
--- /dev/null
+++ b/pkg/antctl/raw/multicluster/get/output_test.go
@@ -0,0 +1,85 @@
+// Copyright 2022 Antrea 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 get
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	"antrea.io/antrea/pkg/antctl/transform/clusterset"
+)
+
+type fakeWriter struct{}
+
+func (f fakeWriter) Write(p []byte) (n int, err error) {
+	return 0, errors.New("not supported")
+}
+
+func TestOutput(t *testing.T) {
+	// Here we test error cases only since common cases will be
+	// covered by 'antctl mc get *' commands.
+	tests := []struct {
+		name         string
+		outputFormat string
+		expectedErr  string
+		resources    interface{}
+	}{
+		{
+			name:         "error to parse json output",
+			outputFormat: "json",
+			expectedErr:  "error when encoding data in json: error when encoding data in json: json: unsupported type: chan int",
+			resources:    make(chan int),
+		},
+		{
+			name:         "error to parse yaml output",
+			outputFormat: "yaml",
+			expectedErr:  "error when outputing in yaml format: json: unsupported type: chan int",
+			resources:    make(chan int),
+		},
+		{
+			name:        "error to print default output",
+			expectedErr: "error when copy output into writer: not supported",
+			resources: mcsv1alpha1.ClusterSet{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "clusterset-name",
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var writer io.Writer
+			if tt.outputFormat != "" {
+				writer = new(bytes.Buffer)
+			} else {
+				writer = fakeWriter{}
+			}
+			err := output(tt.resources, true, tt.outputFormat, writer, clusterset.Transform)
+			if err == nil {
+				t.Error("Expected to get error but it's nil")
+			} else {
+				assert.Equal(t, tt.expectedErr, err.Error())
+			}
+		})
+	}
+}
diff --git a/pkg/ovs/openflow/testing/mock_openflow.go b/pkg/ovs/openflow/testing/mock_openflow.go
index 6f1b6ef6e..ee337edb3 100644
--- a/pkg/ovs/openflow/testing/mock_openflow.go
+++ b/pkg/ovs/openflow/testing/mock_openflow.go
@@ -14,7 +14,7 @@
 //
 
 // Code generated by MockGen. DO NOT EDIT.
-// Source: antrea.io/antrea/pkg/ovs/openflow (interfaces: Bridge,Table,Flow,Action,CTAction,FlowBuilder)
+// Source: antrea.io/antrea/pkg/ovs/openflow (interfaces: Bridge,Table,Flow,Action,CTAction,FlowBuilder,Group,BucketBuilder)
 
 // Package testing is a generated GoMock package.
 package testing
@@ -2168,3 +2168,286 @@ func (mr *MockFlowBuilderMockRecorder) SetIdleTimeout(arg0 interface{}) *gomock.
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIdleTimeout", reflect.TypeOf((*MockFlowBuilder)(nil).SetIdleTimeout), arg0)
 }
+
+// MockGroup is a mock of Group interface
+type MockGroup struct {
+	ctrl     *gomock.Controller
+	recorder *MockGroupMockRecorder
+}
+
+// MockGroupMockRecorder is the mock recorder for MockGroup
+type MockGroupMockRecorder struct {
+	mock *MockGroup
+}
+
+// NewMockGroup creates a new mock instance
+func NewMockGroup(ctrl *gomock.Controller) *MockGroup {
+	mock := &MockGroup{ctrl: ctrl}
+	mock.recorder = &MockGroupMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockGroup) EXPECT() *MockGroupMockRecorder {
+	return m.recorder
+}
+
+// Add mocks base method
+func (m *MockGroup) Add() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Add")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Add indicates an expected call of Add
+func (mr *MockGroupMockRecorder) Add() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockGroup)(nil).Add))
+}
+
+// Bucket mocks base method
+func (m *MockGroup) Bucket() openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Bucket")
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// Bucket indicates an expected call of Bucket
+func (mr *MockGroupMockRecorder) Bucket() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bucket", reflect.TypeOf((*MockGroup)(nil).Bucket))
+}
+
+// Delete mocks base method
+func (m *MockGroup) Delete() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete
+func (mr *MockGroupMockRecorder) Delete() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockGroup)(nil).Delete))
+}
+
+// GetBundleMessage mocks base method
+func (m *MockGroup) GetBundleMessage(arg0 openflow.OFOperation) (ofctrl.OpenFlowModMessage, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetBundleMessage", arg0)
+	ret0, _ := ret[0].(ofctrl.OpenFlowModMessage)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetBundleMessage indicates an expected call of GetBundleMessage
+func (mr *MockGroupMockRecorder) GetBundleMessage(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBundleMessage", reflect.TypeOf((*MockGroup)(nil).GetBundleMessage), arg0)
+}
+
+// KeyString mocks base method
+func (m *MockGroup) KeyString() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "KeyString")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// KeyString indicates an expected call of KeyString
+func (mr *MockGroupMockRecorder) KeyString() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyString", reflect.TypeOf((*MockGroup)(nil).KeyString))
+}
+
+// Modify mocks base method
+func (m *MockGroup) Modify() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Modify")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Modify indicates an expected call of Modify
+func (mr *MockGroupMockRecorder) Modify() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockGroup)(nil).Modify))
+}
+
+// Reset mocks base method
+func (m *MockGroup) Reset() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Reset")
+}
+
+// Reset indicates an expected call of Reset
+func (mr *MockGroupMockRecorder) Reset() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockGroup)(nil).Reset))
+}
+
+// ResetBuckets mocks base method
+func (m *MockGroup) ResetBuckets() openflow.Group {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ResetBuckets")
+	ret0, _ := ret[0].(openflow.Group)
+	return ret0
+}
+
+// ResetBuckets indicates an expected call of ResetBuckets
+func (mr *MockGroupMockRecorder) ResetBuckets() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetBuckets", reflect.TypeOf((*MockGroup)(nil).ResetBuckets))
+}
+
+// Type mocks base method
+func (m *MockGroup) Type() openflow.EntryType {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Type")
+	ret0, _ := ret[0].(openflow.EntryType)
+	return ret0
+}
+
+// Type indicates an expected call of Type
+func (mr *MockGroupMockRecorder) Type() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockGroup)(nil).Type))
+}
+
+// MockBucketBuilder is a mock of BucketBuilder interface
+type MockBucketBuilder struct {
+	ctrl     *gomock.Controller
+	recorder *MockBucketBuilderMockRecorder
+}
+
+// MockBucketBuilderMockRecorder is the mock recorder for MockBucketBuilder
+type MockBucketBuilderMockRecorder struct {
+	mock *MockBucketBuilder
+}
+
+// NewMockBucketBuilder creates a new mock instance
+func NewMockBucketBuilder(ctrl *gomock.Controller) *MockBucketBuilder {
+	mock := &MockBucketBuilder{ctrl: ctrl}
+	mock.recorder = &MockBucketBuilderMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockBucketBuilder) EXPECT() *MockBucketBuilderMockRecorder {
+	return m.recorder
+}
+
+// Done mocks base method
+func (m *MockBucketBuilder) Done() openflow.Group {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Done")
+	ret0, _ := ret[0].(openflow.Group)
+	return ret0
+}
+
+// Done indicates an expected call of Done
+func (mr *MockBucketBuilderMockRecorder) Done() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Done", reflect.TypeOf((*MockBucketBuilder)(nil).Done))
+}
+
+// LoadReg mocks base method
+func (m *MockBucketBuilder) LoadReg(arg0 int, arg1 uint32) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadReg", arg0, arg1)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadReg indicates an expected call of LoadReg
+func (mr *MockBucketBuilderMockRecorder) LoadReg(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadReg", reflect.TypeOf((*MockBucketBuilder)(nil).LoadReg), arg0, arg1)
+}
+
+// LoadRegRange mocks base method
+func (m *MockBucketBuilder) LoadRegRange(arg0 int, arg1 uint32, arg2 *openflow.Range) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadRegRange", arg0, arg1, arg2)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadRegRange indicates an expected call of LoadRegRange
+func (mr *MockBucketBuilderMockRecorder) LoadRegRange(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadRegRange", reflect.TypeOf((*MockBucketBuilder)(nil).LoadRegRange), arg0, arg1, arg2)
+}
+
+// LoadToRegField mocks base method
+func (m *MockBucketBuilder) LoadToRegField(arg0 *openflow.RegField, arg1 uint32) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadToRegField", arg0, arg1)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadToRegField indicates an expected call of LoadToRegField
+func (mr *MockBucketBuilderMockRecorder) LoadToRegField(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadToRegField", reflect.TypeOf((*MockBucketBuilder)(nil).LoadToRegField), arg0, arg1)
+}
+
+// LoadXXReg mocks base method
+func (m *MockBucketBuilder) LoadXXReg(arg0 int, arg1 []byte) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadXXReg", arg0, arg1)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadXXReg indicates an expected call of LoadXXReg
+func (mr *MockBucketBuilderMockRecorder) LoadXXReg(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadXXReg", reflect.TypeOf((*MockBucketBuilder)(nil).LoadXXReg), arg0, arg1)
+}
+
+// ResubmitToTable mocks base method
+func (m *MockBucketBuilder) ResubmitToTable(arg0 byte) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ResubmitToTable", arg0)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// ResubmitToTable indicates an expected call of ResubmitToTable
+func (mr *MockBucketBuilderMockRecorder) ResubmitToTable(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResubmitToTable", reflect.TypeOf((*MockBucketBuilder)(nil).ResubmitToTable), arg0)
+}
+
+// SetTunnelDst mocks base method
+func (m *MockBucketBuilder) SetTunnelDst(arg0 net.IP) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SetTunnelDst", arg0)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// SetTunnelDst indicates an expected call of SetTunnelDst
+func (mr *MockBucketBuilderMockRecorder) SetTunnelDst(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTunnelDst", reflect.TypeOf((*MockBucketBuilder)(nil).SetTunnelDst), arg0)
+}
+
+// Weight mocks base method
+func (m *MockBucketBuilder) Weight(arg0 uint16) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Weight", arg0)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// Weight indicates an expected call of Weight
+func (mr *MockBucketBuilderMockRecorder) Weight(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Weight", reflect.TypeOf((*MockBucketBuilder)(nil).Weight), arg0)
+}
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index 851853f4d..7a59344e3 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -708,7 +708,7 @@ func uninstallServiceFlowsFunc(t *testing.T, gid uint32, svc svcConfig, endpoint
 	groupID := ofconfig.GroupIDType(gid)
 	err := c.UninstallServiceFlows(svc.ip, svc.port, svc.protocol)
 	assert.Nil(t, err)
-	err = c.UninstallGroup(groupID)
+	err = c.UninstallServiceGroup(groupID)
 	assert.Nil(t, err)
 	for _, ep := range endpointList {
 		err := c.UninstallEndpointFlows(svc.protocol, ep)