Repository: antrea-io/antrea
Pull request #3509 (2022-03-23 08:58:48)

Graduate Egress feature from Alpha to Beta
Egress will be graduated to Beta and enabled by default starting with
v1.6.

Signed-off-by: Quan Tian <qtian@vmware.com>

tnqn: /test-all
/test-windows-all
tnqn: /test-all
/test-windows-all
/test-ipv6-all
/test-ipv6-only-all
tnqn: /test-all
/test-windows-all
/test-ipv6-all
/test-ipv6-only-all
codecov-commenter: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/3509?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#3509](https://codecov.io/gh/antrea-io/antrea/pull/3509?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (4f7f45b) into [main](https://codecov.io/gh/antrea-io/antrea/commit/c2a664b7f0cff855edecd91e42fe8bbcf9773a13?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (c2a664b) will **decrease** coverage by `22.15%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/3509/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=None)](https://codecov.io/gh/antrea-io/antrea/pull/3509?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@             Coverage Diff             @@
##             main    #3509       +/-   ##
===========================================
- Coverage   65.51%   43.36%   -22.16%     
===========================================
  Files         278      210       -68     
  Lines       27771    25171     -2600     
===========================================
- Hits        18195    10915     -7280     
- Misses       7656    13166     +5510     
+ Partials     1920     1090      -830     
```

| Flag | Coverage Δ | |
|---|---|---|
| kind-e2e-tests | `?` | |
| unit-tests | `43.36% <0.00%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/3509?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [pkg/agent/openflow/pipeline.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L29wZW5mbG93L3BpcGVsaW5lLmdv) | `24.56% <0.00%> (-49.67%)` | :arrow_down: |
| [pkg/ovs/openflow/default.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL292cy9vcGVuZmxvdy9kZWZhdWx0Lmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/util/runtime/runtime.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL3V0aWwvcnVudGltZS9ydW50aW1lLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/agent/openflow/groups.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L29wZW5mbG93L2dyb3Vwcy5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/apis/controlplane/types.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FwaXMvY29udHJvbHBsYW5lL3R5cGVzLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/apis/controlplane/helper.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FwaXMvY29udHJvbHBsYW5lL2hlbHBlci5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/agent/nodeportlocal/k8s/annotations.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L25vZGVwb3J0bG9jYWwvazhzL2Fubm90YXRpb25zLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/ovs/openflow/ofctrl\_pipeline.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL292cy9vcGVuZmxvdy9vZmN0cmxfcGlwZWxpbmUuZ28=) | `0.00% <0.00%> (-93.34%)` | :arrow_down: |
| [pkg/ovs/openflow/logs.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL292cy9vcGVuZmxvdy9sb2dzLmdv) | `9.52% <0.00%> (-90.48%)` | :arrow_down: |
| [pkg/apis/controlplane/register.go](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FwaXMvY29udHJvbHBsYW5lL3JlZ2lzdGVyLmdv) | `0.00% <0.00%> (-90.48%)` | :arrow_down: |
| ... and [197 more](https://codecov.io/gh/antrea-io/antrea/pull/3509/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |


Patch:
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 71a1b70b4..eee762e7e 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -2768,7 +2768,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
     # bridging mode and allocates IPs to Pods in bridging mode.
@@ -3011,7 +3011,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Run Kubernetes NodeIPAMController with Antrea.
     #  NodeIPAM: false
@@ -3074,7 +3074,7 @@ kind: ConfigMap
 metadata:
   labels:
     app: antrea
-  name: antrea-config-665mgk228m
+  name: antrea-config-82h2mk24gg
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3145,7 +3145,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-665mgk228m
+          value: antrea-config-82h2mk24gg
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3196,7 +3196,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-665mgk228m
+          name: antrea-config-82h2mk24gg
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3435,7 +3435,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-665mgk228m
+          name: antrea-config-82h2mk24gg
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index c69134a13..53a6ef8c0 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -2768,7 +2768,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
     # bridging mode and allocates IPs to Pods in bridging mode.
@@ -3011,7 +3011,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Run Kubernetes NodeIPAMController with Antrea.
     #  NodeIPAM: false
@@ -3074,7 +3074,7 @@ kind: ConfigMap
 metadata:
   labels:
     app: antrea
-  name: antrea-config-665mgk228m
+  name: antrea-config-82h2mk24gg
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3145,7 +3145,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-665mgk228m
+          value: antrea-config-82h2mk24gg
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3196,7 +3196,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-665mgk228m
+          name: antrea-config-82h2mk24gg
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3437,7 +3437,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-665mgk228m
+          name: antrea-config-82h2mk24gg
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 9987ca496..78552174c 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -2768,7 +2768,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
     # bridging mode and allocates IPs to Pods in bridging mode.
@@ -3011,7 +3011,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Run Kubernetes NodeIPAMController with Antrea.
     #  NodeIPAM: false
@@ -3074,7 +3074,7 @@ kind: ConfigMap
 metadata:
   labels:
     app: antrea
-  name: antrea-config-2cfft84t59
+  name: antrea-config-c9ck44454h
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3145,7 +3145,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-2cfft84t59
+          value: antrea-config-c9ck44454h
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3196,7 +3196,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-2cfft84t59
+          name: antrea-config-c9ck44454h
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3438,7 +3438,7 @@ spec:
           path: /home/kubernetes/bin
         name: host-cni-bin
       - configMap:
-          name: antrea-config-2cfft84t59
+          name: antrea-config-c9ck44454h
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 95a0c9a25..9241703a4 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -2768,7 +2768,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
     # bridging mode and allocates IPs to Pods in bridging mode.
@@ -3016,7 +3016,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Run Kubernetes NodeIPAMController with Antrea.
     #  NodeIPAM: false
@@ -3079,7 +3079,7 @@ kind: ConfigMap
 metadata:
   labels:
     app: antrea
-  name: antrea-config-2m674972kf
+  name: antrea-config-tmhkc66d6c
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3159,7 +3159,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-2m674972kf
+          value: antrea-config-tmhkc66d6c
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3210,7 +3210,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-2m674972kf
+          name: antrea-config-tmhkc66d6c
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3484,7 +3484,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-2m674972kf
+          name: antrea-config-tmhkc66d6c
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index 36fd83723..d5a7d4db2 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -2768,7 +2768,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
     # bridging mode and allocates IPs to Pods in bridging mode.
@@ -3016,7 +3016,7 @@ data:
     #  NetworkPolicyStats: true
 
     # Enable controlling SNAT IPs of Pod egress traffic.
-    #  Egress: false
+    #  Egress: true
 
     # Run Kubernetes NodeIPAMController with Antrea.
     #  NodeIPAM: false
@@ -3079,7 +3079,7 @@ kind: ConfigMap
 metadata:
   labels:
     app: antrea
-  name: antrea-config-h6d7mmd9hg
+  name: antrea-config-hkhbh5gf99
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3150,7 +3150,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-h6d7mmd9hg
+          value: antrea-config-hkhbh5gf99
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3201,7 +3201,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-h6d7mmd9hg
+          name: antrea-config-hkhbh5gf99
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3440,7 +3440,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-h6d7mmd9hg
+          name: antrea-config-hkhbh5gf99
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/base/conf/antrea-agent.conf b/build/yamls/base/conf/antrea-agent.conf
index 476885fbc..912cd971b 100644
--- a/build/yamls/base/conf/antrea-agent.conf
+++ b/build/yamls/base/conf/antrea-agent.conf
@@ -29,7 +29,7 @@ featureGates:
 #  NetworkPolicyStats: true
 
 # Enable controlling SNAT IPs of Pod egress traffic.
-#  Egress: false
+#  Egress: true
 
 # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
 # bridging mode and allocates IPs to Pods in bridging mode.
diff --git a/build/yamls/base/conf/antrea-controller.conf b/build/yamls/base/conf/antrea-controller.conf
index 6669237da..7db2983b3 100644
--- a/build/yamls/base/conf/antrea-controller.conf
+++ b/build/yamls/base/conf/antrea-controller.conf
@@ -12,7 +12,7 @@ featureGates:
 #  NetworkPolicyStats: true
 
 # Enable controlling SNAT IPs of Pod egress traffic.
-#  Egress: false
+#  Egress: true
 
 # Run Kubernetes NodeIPAMController with Antrea.
 #  NodeIPAM: false
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index cf3198f23..0f3bd816c 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -64,6 +64,7 @@ import (
 	"antrea.io/antrea/pkg/util/channel"
 	"antrea.io/antrea/pkg/util/cipher"
 	"antrea.io/antrea/pkg/util/k8s"
+	"antrea.io/antrea/pkg/util/runtime"
 	"antrea.io/antrea/pkg/version"
 )
 
@@ -82,6 +83,10 @@ var excludeNodePortDevices = []string{"antrea-egress0", "antrea-ingress0", "kube
 // run starts Antrea agent with the given options and waits for termination signal.
 func run(o *Options) error {
 	klog.Infof("Starting Antrea agent (version %s)", version.GetFullVersion())
+
+	// Windows platform doesn't support Egress feature yet.
+	egressEnabled := features.DefaultFeatureGate.Enabled(features.Egress) && !runtime.IsWindowsPlatform()
+
 	// Create K8s Clientset, CRD Clientset and SharedInformerFactory for the given config.
 	k8sClient, _, crdClient, _, err := k8s.CreateClients(o.config.ClientConnection, o.config.KubeAPIServerOverride)
 	if err != nil {
@@ -124,7 +129,7 @@ func run(o *Options) error {
 	ofClient := openflow.NewClient(o.config.OVSBridge, ovsBridgeMgmtAddr, ovsDatapathType,
 		features.DefaultFeatureGate.Enabled(features.AntreaProxy),
 		features.DefaultFeatureGate.Enabled(features.AntreaPolicy),
-		features.DefaultFeatureGate.Enabled(features.Egress),
+		egressEnabled,
 		features.DefaultFeatureGate.Enabled(features.FlowExporter),
 		o.config.AntreaProxy.ProxyAll,
 		connectUplinkToBridge,
@@ -306,7 +311,7 @@ func run(o *Options) error {
 	var externalIPController *serviceexternalip.ServiceExternalIPController
 	var memberlistCluster *memberlist.Cluster
 
-	if features.DefaultFeatureGate.Enabled(features.Egress) || features.DefaultFeatureGate.Enabled(features.ServiceExternalIP) {
+	if egressEnabled || features.DefaultFeatureGate.Enabled(features.ServiceExternalIP) {
 		externalIPPoolController = externalippool.NewExternalIPPoolController(
 			crdClient, externalIPPoolInformer,
 		)
@@ -325,7 +330,7 @@ func run(o *Options) error {
 			return fmt.Errorf("error creating new memberlist cluster: %v", err)
 		}
 	}
-	if features.DefaultFeatureGate.Enabled(features.Egress) {
+	if egressEnabled {
 		egressController, err = egress.NewEgressController(
 			ofClient, antreaClientProvider, crdClient, ifaceStore, routeClient, nodeConfig.Name, nodeConfig.NodeTransportInterfaceName,
 			memberlistCluster, egressInformer, podUpdateChannel,
@@ -528,12 +533,12 @@ func run(o *Options) error {
 	informerFactory.Start(stopCh)
 	crdInformerFactory.Start(stopCh)
 
-	if features.DefaultFeatureGate.Enabled(features.Egress) || features.DefaultFeatureGate.Enabled(features.ServiceExternalIP) {
+	if egressEnabled || features.DefaultFeatureGate.Enabled(features.ServiceExternalIP) {
 		go externalIPPoolController.Run(stopCh)
 		go memberlistCluster.Run(stopCh)
 	}
 
-	if features.DefaultFeatureGate.Enabled(features.Egress) {
+	if egressEnabled {
 		go egressController.Run(stopCh)
 	}
 
diff --git a/docs/egress.md b/docs/egress.md
index 75d2508ad..5cacd2087 100644
--- a/docs/egress.md
+++ b/docs/egress.md
@@ -40,10 +40,10 @@ This guide demonstrates how to configure `Egress` to achieve the above result.
 
 ## Prerequisites
 
-Egress is introduced in v1.0 as an alpha feature. As with other alpha features,
-a feature gate `Egress` must be enabled on the antrea-controller and
-antrea-agent for the feature to work. The following options in the
-`antrea-config` ConfigMap need to be set:
+Egress was introduced in v1.0 as an alpha feature, and was graduated to beta in
+v1.6, at which time it was enabled by default. Prior to v1.6, a feature gate,
+`Egress` must be enabled on the antrea-controller and antrea-agent in the
+`antrea-config` ConfigMap like the following options for the feature to work:
 
 ```yaml
 kind: ConfigMap
diff --git a/docs/feature-gates.md b/docs/feature-gates.md
index afaded600..3037b19b3 100644
--- a/docs/feature-gates.md
+++ b/docs/feature-gates.md
@@ -42,7 +42,7 @@ example, to enable `AntreaProxy` on Linux, edit the Agent configuration in the
 | `FlowExporter`          | Agent              | `false` | Alpha | v0.9          | N/A          | N/A        | Yes                |       |
 | `NetworkPolicyStats`    | Agent + Controller | `true`  | Beta  | v0.10         | v1.2         | N/A        | No                 |       |
 | `NodePortLocal`         | Agent              | `true`  | Beta  | v0.13         | v1.4         | N/A        | Yes                | Important user-facing change in v1.2.0 |
-| `Egress`                | Agent + Controller | `false` | Alpha | v1.0          | N/A          | N/A        | Yes                |       |
+| `Egress`                | Agent + Controller | `true`  | Beta  | v1.0          | v1.6         | N/A        | Yes                |       |
 | `NodeIPAM`              | Controller         | `false` | Alpha | v1.4          | N/A          | N/A        | Yes                |       |
 | `AntreaIPAM`            | Agent + Controller | `false` | Alpha | v1.4          | N/A          | N/A        | Yes                |       |
 | `Multicast`             | Agent              | `false` | Alpha | v1.5          | N/A          | N/A        | Yes                |       |
diff --git a/pkg/agent/cniserver/ipam/antrea_ipam_controller.go b/pkg/agent/cniserver/ipam/antrea_ipam_controller.go
index 74d951b8e..3de98391d 100644
--- a/pkg/agent/cniserver/ipam/antrea_ipam_controller.go
+++ b/pkg/agent/cniserver/ipam/antrea_ipam_controller.go
@@ -32,6 +32,7 @@ import (
 	"antrea.io/antrea/pkg/client/informers/externalversions"
 	crdinformers "antrea.io/antrea/pkg/client/informers/externalversions/crd/v1alpha2"
 	crdlisters "antrea.io/antrea/pkg/client/listers/crd/v1alpha2"
+	annotation "antrea.io/antrea/pkg/ipam"
 	"antrea.io/antrea/pkg/ipam/poolallocator"
 	"antrea.io/antrea/pkg/util/k8s"
 )
@@ -148,25 +149,25 @@ func (c *AntreaIPAMController) getIPPoolsByPod(namespace, name string) ([]string
 		return nil, nil, nil, err
 	}
 
-	annotations, exists := pod.Annotations[AntreaIPAMAnnotationKey]
+	annotations, exists := pod.Annotations[annotation.AntreaIPAMAnnotationKey]
 	if !exists {
 		// Find IPPool by Namespace
 		ns, err := c.namespaceLister.Get(namespace)
 		if err != nil {
 			return nil, nil, nil, nil
 		}
-		annotations, exists = ns.Annotations[AntreaIPAMAnnotationKey]
+		annotations, exists = ns.Annotations[annotation.AntreaIPAMAnnotationKey]
 		if !exists {
 			return nil, nil, nil, nil
 		}
 	}
 
 	// Collect specified IPs if exist
-	ipStrings, _ := pod.Annotations[AntreaIPAMPodIPAnnotationKey]
+	ipStrings, _ := pod.Annotations[annotation.AntreaIPAMPodIPAnnotationKey]
 	ipStrings = strings.ReplaceAll(ipStrings, " ", "")
 	var ipErr error
 	if ipStrings != "" {
-		splittedIPStrings := strings.Split(ipStrings, AntreaIPAMAnnotationDelimiter)
+		splittedIPStrings := strings.Split(ipStrings, annotation.AntreaIPAMAnnotationDelimiter)
 		for _, ipString := range splittedIPStrings {
 			ip := net.ParseIP(ipString)
 			if ipString != "" && ip == nil {
@@ -200,7 +201,7 @@ ownerReferenceLoop:
 		}
 	}
 
-	return strings.Split(annotations, AntreaIPAMAnnotationDelimiter), ips, reservedOwner, ipErr
+	return strings.Split(annotations, annotation.AntreaIPAMAnnotationDelimiter), ips, reservedOwner, ipErr
 }
 
 func (c *AntreaIPAMController) getPoolAllocatorByPod(namespace, podName string) (mineType, *poolallocator.IPPoolAllocator, []net.IP, *crdv1a2.IPAddressOwner, error) {
diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go
index 5cff57d17..367535865 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -1371,7 +1371,7 @@ func (f *featurePodConnectivity) l3FwdFlowToRemoteViaGW(localGatewayMAC net.Hard
 	}
 	return fb.Action().SetDstMAC(localGatewayMAC).
 		Action().LoadRegMark(ToGatewayRegMark).
-		Action().NextTable().
+		Action().GotoTable(L3DecTTLTable.GetID()). // Traffic to in-cluster destination should skip EgressMark table.
 		Done()
 }
 
diff --git a/pkg/controller/ipam/antrea_ipam_controller.go b/pkg/controller/ipam/antrea_ipam_controller.go
index 59f38d0f8..da8522c0d 100644
--- a/pkg/controller/ipam/antrea_ipam_controller.go
+++ b/pkg/controller/ipam/antrea_ipam_controller.go
@@ -19,14 +19,17 @@ package ipam
 
 import (
 	"fmt"
+	"strings"
 	"time"
 
 	appsv1 "k8s.io/api/apps/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/informers"
 	appsinformers "k8s.io/client-go/informers/apps/v1"
+	corelisters "k8s.io/client-go/listers/core/v1"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/util/workqueue"
 	"k8s.io/klog/v2"
@@ -36,6 +39,7 @@ import (
 	"antrea.io/antrea/pkg/client/informers/externalversions"
 	crdinformers "antrea.io/antrea/pkg/client/informers/externalversions/crd/v1alpha2"
 	crdlisters "antrea.io/antrea/pkg/client/listers/crd/v1alpha2"
+	annotation "antrea.io/antrea/pkg/ipam"
 	"antrea.io/antrea/pkg/ipam/poolallocator"
 	"antrea.io/antrea/pkg/util/k8s"
 )
@@ -43,10 +47,6 @@ import (
 const (
 	controllerName = "AntreaIPAMController"
 
-	// TODO: currently these constants are duplicated, move to a shared place
-	AntreaIPAMAnnotationKey       = "ipam.antrea.io/ippools"
-	AntreaIPAMAnnotationDelimiter = ","
-
 	// StatefulSet index name for IPPool cache.
 	statefulSetIndex = "statefulSet"
 
@@ -63,8 +63,12 @@ type AntreaIPAMController struct {
 	// crdClient is the clientset for CRD API group.
 	crdClient versioned.Interface
 
-	// Pool cleanup events triggered by StatefulSet delete
-	statefulSetCleanupQueue workqueue.RateLimitingInterface
+	// Pool cleanup events triggered by StatefulSet add/delete
+	statefulSetQueue workqueue.RateLimitingInterface
+
+	// follow changes for Namespace objects
+	namespaceLister       corelisters.NamespaceLister
+	namespaceListerSynced cache.InformerSynced
 
 	// follow changes for StatefulSet objects
 	statefulSetInformer     appsinformers.StatefulSetInformer
@@ -97,11 +101,15 @@ func NewAntreaIPAMController(crdClient versioned.Interface,
 	ipPoolInformer := crdInformerFactory.Crd().V1alpha2().IPPools()
 	ipPoolInformer.Informer().AddIndexers(cache.Indexers{statefulSetIndex: statefulSetIndexFunc})
 
+	namespaceInformer := informerFactory.Core().V1().Namespaces()
+
 	statefulSetInformer := informerFactory.Apps().V1().StatefulSets()
 
 	c := &AntreaIPAMController{
 		crdClient:               crdClient,
-		statefulSetCleanupQueue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "statefulSetCleanup"),
+		statefulSetQueue:        workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "statefulSetPreallocationAndCleanup"),
+		namespaceLister:         namespaceInformer.Lister(),
+		namespaceListerSynced:   namespaceInformer.Informer().HasSynced,
 		statefulSetInformer:     statefulSetInformer,
 		statefulSetListerSynced: statefulSetInformer.Informer().HasSynced,
 		ipPoolInformer:          ipPoolInformer,
@@ -110,9 +118,12 @@ func NewAntreaIPAMController(crdClient versioned.Interface,
 	}
 
 	// Add handlers for Stateful Set events.
+	// Note that update is not handled here: IP Pool annotation should not be
+	// updated without recreating the resource
 	klog.V(2).InfoS("Subscribing for StatefulSet notifications", "controller", controllerName)
 	statefulSetInformer.Informer().AddEventHandler(
 		cache.ResourceEventHandlerFuncs{
+			AddFunc:    c.enqueueStatefulSetCreateEvent,
 			DeleteFunc: c.enqueueStatefulSetDeleteEvent,
 		},
 	)
@@ -120,13 +131,22 @@ func NewAntreaIPAMController(crdClient versioned.Interface,
 	return c
 }
 
+// Enqueue the StatefulSet create notification to be processed by the worker
+func (c *AntreaIPAMController) enqueueStatefulSetCreateEvent(obj interface{}) {
+	ss := obj.(*appsv1.StatefulSet)
+	klog.V(2).InfoS("Create notification", "Namespace", ss.Namespace, "StatefulSet", ss.Name)
+
+	key := k8s.NamespacedName(ss.Namespace, ss.Name)
+	c.statefulSetQueue.Add(key)
+}
+
 // Enqueue the StatefulSet delete notification to be processed by the worker
 func (c *AntreaIPAMController) enqueueStatefulSetDeleteEvent(obj interface{}) {
 	ss := obj.(*appsv1.StatefulSet)
-	klog.V(2).InfoS("Delete notification", "StatefulSet", ss.Name)
+	klog.V(2).InfoS("Delete notification", "Namespace", ss.Namespace, "StatefulSet", ss.Name)
 
 	key := k8s.NamespacedName(ss.Namespace, ss.Name)
-	c.statefulSetCleanupQueue.Add(key)
+	c.statefulSetQueue.Add(key)
 }
 
 // Inspect all IPPools for stale IP Address entries.
@@ -150,7 +170,7 @@ func (c *AntreaIPAMController) cleanupStaleStatefulSets() {
 				if _, ok := statefulSetMap[key]; !ok {
 					// This entry refers to StatefulSet that no longer exists
 					klog.InfoS("IPPool contains stale IPAddress for StatefulSet that no longer exists", "IPPool", ipPool.Name, "StatefulSet", key)
-					c.statefulSetCleanupQueue.Add(key)
+					c.statefulSetQueue.Add(key)
 					// Mark this entry in map to ensure cleanup is enqueued only once
 					statefulSetMap[key] = true
 				}
@@ -159,9 +179,8 @@ func (c *AntreaIPAMController) cleanupStaleStatefulSets() {
 	}
 }
 
-// Look for an IP Pool associated with this StatefulSet, either a dedicated one or
-// annotated to the namespace. If IPPool is found, this routine will clear all IP
-// addresses that might be reserved for the pool.
+// Look for an IP Pool associated with this StatefulSet.
+// If IPPool is found, this routine will clear all addresses that might be reserved for the pool.
 func (c *AntreaIPAMController) cleanIPPoolForStatefulSet(namespacedName string) error {
 	klog.InfoS("Processing delete notification", "StatefulSet", namespacedName)
 	ipPools, _ := c.ipPoolInformer.Informer().GetIndexer().ByIndex(statefulSetIndex, namespacedName)
@@ -187,40 +206,124 @@ func (c *AntreaIPAMController) cleanIPPoolForStatefulSet(namespacedName string)
 	return nil
 }
 
+// Find IP Pools annotated to StatefulSet via direct annotation or Namespace annotation
+func (c *AntreaIPAMController) getIPPoolsForStatefulSet(ss *appsv1.StatefulSet) []string {
+
+	// Inspect pool annotation for the Pods
+	// In order to avoid extra API call in IPAM driver, IPAM annotations are defined
+	// on Pods rather than on StatefulSet
+	annotations, exists := ss.Spec.Template.Annotations[annotation.AntreaIPAMAnnotationKey]
+	if exists {
+		// Stateful Set Pod is annotated with dedicated IP pool
+		return strings.Split(annotations, annotation.AntreaIPAMAnnotationDelimiter)
+	}
+
+	// Inspect Namespace
+	namespace, err := c.namespaceLister.Get(ss.Namespace)
+	if err != nil {
+		// Should never happen
+		klog.Errorf("Namespace %s not found for StatefulSet %s", ss.Namespace, ss.Name)
+		return nil
+	}
+
+	annotations, exists = namespace.Annotations[annotation.AntreaIPAMAnnotationKey]
+	if exists {
+		return strings.Split(annotations, annotation.AntreaIPAMAnnotationDelimiter)
+	}
+
+	return nil
+
+}
+
+// Look for an IP Pool associated with this StatefulSet, either a dedicated one or
+// annotated to the Namespace. If such IP Pool is found, preallocate IPs for the StatefulSet.
+// This function returns error if pool is not found, or allocation fails.
+func (c *AntreaIPAMController) preallocateIPPoolForStatefulSet(ss *appsv1.StatefulSet) error {
+	klog.InfoS("Processing create notification", "Namespace", ss.Namespace, "StatefulSet", ss.Name)
+
+	ipPools := c.getIPPoolsForStatefulSet(ss)
+
+	if ipPools == nil {
+		// nothing to preallocate
+		return nil
+	}
+
+	if len(ipPools) > 1 {
+		return fmt.Errorf("annotation of multiple IP Pools is not supported")
+	}
+
+	// Only one pool is supported for now. Dual stack support coming in future.
+	ipPoolName := ipPools[0]
+	allocator, err := poolallocator.NewIPPoolAllocator(ipPoolName, c.crdClient, c.ipPoolLister)
+	if err != nil {
+		return fmt.Errorf("failed to find IP Pool %s: %s", ipPoolName, err)
+	}
+
+	size := int(*ss.Spec.Replicas)
+	// Note that AllocateStatefulSet would not preallocate IPs if this StatefulSet is already present
+	// in the pool. This safeguards us from double allocation in case agent allocated IP by the time
+	// controller task is executed. Note also that StatefulSet resize will not be handled.
+	err = allocator.AllocateStatefulSet(ss.Namespace, ss.Name, size)
+	if err != nil {
+		return fmt.Errorf("failed to preallocate continuous IP space of size %d from Pool %s: %s", size, ipPoolName, err)
+	}
+
+	return nil
+}
 func (c *AntreaIPAMController) statefulSetWorker() {
 	for c.processNextStatefulSetWorkItem() {
 	}
 }
 
 func (c *AntreaIPAMController) processNextStatefulSetWorkItem() bool {
-	key, quit := c.statefulSetCleanupQueue.Get()
+	key, quit := c.statefulSetQueue.Get()
 	if quit {
 		return false
 	}
 
-	defer c.statefulSetCleanupQueue.Done(key)
-	err := c.cleanIPPoolForStatefulSet(key.(string))
+	defer c.statefulSetQueue.Done(key)
+
+	namespacedName := key.(string)
+	namespace, name := k8s.SplitNamespacedName(namespacedName)
+	ss, err := c.statefulSetInformer.Lister().StatefulSets(namespace).Get(name)
 
 	if err != nil {
-		// Put the item back on the workqueue to handle any transient errors.
-		c.statefulSetCleanupQueue.AddRateLimited(key)
-		klog.ErrorS(err, "Failed to clean IP Pool for StatefulSet", "StatefulSet", key)
-		return true
+		if errors.IsNotFound(err) {
+			// StatefulSet no longer present - clean up reserved pool IPs
+			err = c.cleanIPPoolForStatefulSet(namespacedName)
+			if err != nil {
+				// Put the item back on the workqueue to handle any transient errors.
+				c.statefulSetQueue.AddRateLimited(key)
+				klog.ErrorS(err, "Failed to clean IP addresses reserved for StatefulSet from IP Pool", "StatefulSet", key)
+				return true
+			}
+		} else {
+			klog.ErrorS(err, "Failed to get StatefulSet", "StatefulSet", key)
+		}
+	} else {
+		// StatefulSet was created - preallocate IPs based on replicas with best effort
+		err := c.preallocateIPPoolForStatefulSet(ss)
+		if err != nil {
+			// Preallocation is best effort - we do not retry even with transient errors,
+			// since we don't want to implement logic that would delay Pods while waiting for
+			// preallocation.
+			klog.ErrorS(err, "No IPs reserved for StatefulSet", "StatefulSet", key)
+		}
 	}
 
-	c.statefulSetCleanupQueue.Forget(key)
+	c.statefulSetQueue.Forget(key)
 	return true
 }
 
 // Run begins watching and syncing of a AntreaIPAMController.
 func (c *AntreaIPAMController) Run(stopCh <-chan struct{}) {
 
-	defer c.statefulSetCleanupQueue.ShutDown()
+	defer c.statefulSetQueue.ShutDown()
 
 	klog.InfoS("Starting", "controller", controllerName)
 	defer klog.InfoS("Shutting down", "controller", controllerName)
 
-	cacheSyncs := []cache.InformerSynced{c.statefulSetListerSynced, c.ipPoolListerSynced}
+	cacheSyncs := []cache.InformerSynced{c.namespaceListerSynced, c.statefulSetListerSynced, c.ipPoolListerSynced}
 	if !cache.WaitForNamedCacheSync(controllerName, stopCh, cacheSyncs...) {
 		return
 	}
diff --git a/pkg/features/antrea_features.go b/pkg/features/antrea_features.go
index ab111114e..01cd6ecf5 100644
--- a/pkg/features/antrea_features.go
+++ b/pkg/features/antrea_features.go
@@ -66,6 +66,7 @@ const (
 	NodePortLocal featuregate.Feature = "NodePortLocal"
 
 	// alpha: v1.0
+	// beta: v1.6
 	// Enable controlling SNAT IPs of Pod egress traffic.
 	Egress featuregate.Feature = "Egress"
 
@@ -104,7 +105,7 @@ var (
 	DefaultAntreaFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
 		AntreaPolicy:       {Default: true, PreRelease: featuregate.Beta},
 		AntreaProxy:        {Default: true, PreRelease: featuregate.Beta},
-		Egress:             {Default: false, PreRelease: featuregate.Alpha},
+		Egress:             {Default: true, PreRelease: featuregate.Beta},
 		EndpointSlice:      {Default: false, PreRelease: featuregate.Alpha},
 		Traceflow:          {Default: true, PreRelease: featuregate.Beta},
 		AntreaIPAM:         {Default: false, PreRelease: featuregate.Alpha},
diff --git a/pkg/ipam/poolallocator/allocator.go b/pkg/ipam/poolallocator/allocator.go
index 2ea90c396..92545a6f4 100644
--- a/pkg/ipam/poolallocator/allocator.go
+++ b/pkg/ipam/poolallocator/allocator.go
@@ -194,7 +194,7 @@ func (a *IPPoolAllocator) appendPoolUsageForStatefulSet(ipPool *v1alpha2.IPPool,
 		}
 		usageEntry := v1alpha2.IPAddressState{
 			IPAddress: ip.String(),
-			Phase:     v1alpha2.IPAddressPhasePreallocated,
+			Phase:     v1alpha2.IPAddressPhaseReserved,
 			Owner:     owner,
 		}
 
@@ -409,6 +409,13 @@ func (a *IPPoolAllocator) AllocateStatefulSet(namespace, name string, size int)
 			return err
 		}
 
+		// Make sure there is no double allocation for this StatefulSet
+		for _, ip := range ipPool.Status.IPAddresses {
+			if ip.Owner.StatefulSet != nil && ip.Owner.StatefulSet.Namespace == namespace && ip.Owner.StatefulSet.Name == name {
+				return fmt.Errorf("StatefulSet %s/%s is already present in IPPool %s", namespace, name, ipPool.Name)
+			}
+		}
+
 		ips, err := allocators.AllocateRange(size)
 		if err != nil {
 			return err
@@ -608,3 +615,11 @@ func (a *IPPoolAllocator) getReservedIP(reservedOwner v1alpha2.IPAddressOwner) (
 	}
 	return nil, nil
 }
+
+func (a IPPoolAllocator) Total() int {
+	_, allocators, err := a.getPoolAndInitIPAllocators()
+	if err != nil {
+		return 0
+	}
+	return allocators.Total()
+}


Test patch:
diff --git a/pkg/agent/cniserver/ipam/antrea_ipam_test.go b/pkg/agent/cniserver/ipam/antrea_ipam_test.go
index da651947f..53724d361 100644
--- a/pkg/agent/cniserver/ipam/antrea_ipam_test.go
+++ b/pkg/agent/cniserver/ipam/antrea_ipam_test.go
@@ -39,6 +39,7 @@ import (
 	argtypes "antrea.io/antrea/pkg/agent/cniserver/types"
 	crdv1a2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
 	crdinformers "antrea.io/antrea/pkg/client/informers/externalversions"
+	annotations "antrea.io/antrea/pkg/ipam"
 	fakepoolclient "antrea.io/antrea/pkg/ipam/poolallocator/testing"
 )
 
@@ -154,13 +155,13 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 		&corev1.Namespace{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        testApple,
-				Annotations: map[string]string{AntreaIPAMAnnotationKey: testApple, "junk": "garbage"},
+				Annotations: map[string]string{annotations.AntreaIPAMAnnotationKey: testApple, "junk": "garbage"},
 			},
 		},
 		&corev1.Namespace{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        testOrange,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testOrange},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testOrange},
 			},
 		},
 		&corev1.Namespace{
@@ -172,7 +173,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 		&corev1.Namespace{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        testJunkAnnotation,
-				Annotations: map[string]string{AntreaIPAMAnnotationKey: testJunkAnnotation},
+				Annotations: map[string]string{annotations.AntreaIPAMAnnotationKey: testJunkAnnotation},
 			},
 		},
 		&corev1.Namespace{
@@ -220,7 +221,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        "pear1",
 				Namespace:   testPear,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testPear},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testPear},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
 		},
@@ -228,7 +229,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        "pear2",
 				Namespace:   testPear,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testPear, AntreaIPAMPodIPAnnotationKey: " "},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testPear, annotations.AntreaIPAMPodIPAnnotationKey: " "},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
 		},
@@ -236,7 +237,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        "pear3",
 				Namespace:   testPear,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testPear, AntreaIPAMPodIPAnnotationKey: "10.2.3.199"},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testPear, annotations.AntreaIPAMPodIPAnnotationKey: "10.2.3.199"},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
 		},
@@ -245,7 +246,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 				// conflict
 				Name:        "pear4",
 				Namespace:   testPear,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testPear, AntreaIPAMPodIPAnnotationKey: "10.2.3.199"},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testPear, annotations.AntreaIPAMPodIPAnnotationKey: "10.2.3.199"},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
 		},
@@ -254,7 +255,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 				// out of range
 				Name:        "pear5",
 				Namespace:   testPear,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testPear, AntreaIPAMPodIPAnnotationKey: "10.2.4.199"},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testPear, annotations.AntreaIPAMPodIPAnnotationKey: "10.2.4.199"},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
 		},
@@ -263,7 +264,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 				// invalid IP
 				Name:        "pear6",
 				Namespace:   testPear,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testPear, AntreaIPAMPodIPAnnotationKey: "junk"},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testPear, annotations.AntreaIPAMPodIPAnnotationKey: "junk"},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
 		},
@@ -272,7 +273,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 				// invalid IPPool
 				Name:        "pear7",
 				Namespace:   testPear,
-				Annotations: map[string]string{"junk": "garbage", AntreaIPAMAnnotationKey: testJunkAnnotation},
+				Annotations: map[string]string{"junk": "garbage", annotations.AntreaIPAMAnnotationKey: testJunkAnnotation},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
 		},
@@ -280,7 +281,7 @@ func initTestClients() (*fake.Clientset, *fakepoolclient.IPPoolClientset) {
 			ObjectMeta: metav1.ObjectMeta{
 				Name:            "pear-sts-8",
 				Namespace:       testPear,
-				Annotations:     map[string]string{AntreaIPAMAnnotationKey: testPear},
+				Annotations:     map[string]string{annotations.AntreaIPAMAnnotationKey: testPear},
 				OwnerReferences: []metav1.OwnerReference{{Controller: &bTrue, Kind: "StatefulSet"}},
 			},
 			Spec: corev1.PodSpec{NodeName: "fakeNode"},
diff --git a/pkg/apiserver/handlers/featuregates/handler_test.go b/pkg/apiserver/handlers/featuregates/handler_test.go
index 957e3bab5..ec2d29210 100644
--- a/pkg/apiserver/handlers/featuregates/handler_test.go
+++ b/pkg/apiserver/handlers/featuregates/handler_test.go
@@ -47,7 +47,7 @@ func Test_getGatesResponse(t *testing.T) {
 			want: []Response{
 				{Component: "agent", Name: "AntreaPolicy", Status: "Disabled", Version: "BETA"},
 				{Component: "agent", Name: "AntreaProxy", Status: "Enabled", Version: "BETA"},
-				{Component: "agent", Name: "Egress", Status: "Disabled", Version: "ALPHA"},
+				{Component: "agent", Name: "Egress", Status: "Enabled", Version: "BETA"},
 				{Component: "agent", Name: "EndpointSlice", Status: "Disabled", Version: "ALPHA"},
 				{Component: "agent", Name: "AntreaIPAM", Status: "Disabled", Version: "ALPHA"},
 				{Component: "agent", Name: "Traceflow", Status: "Enabled", Version: "BETA"},
@@ -191,7 +191,7 @@ func Test_getControllerGatesResponse(t *testing.T) {
 			name: "good path",
 			want: []Response{
 				{Component: "controller", Name: "AntreaPolicy", Status: "Enabled", Version: "BETA"},
-				{Component: "controller", Name: "Egress", Status: "Disabled", Version: "ALPHA"},
+				{Component: "controller", Name: "Egress", Status: "Enabled", Version: "BETA"},
 				{Component: "controller", Name: "Traceflow", Status: "Enabled", Version: "BETA"},
 				{Component: "controller", Name: "NetworkPolicyStats", Status: "Enabled", Version: "BETA"},
 				{Component: "controller", Name: "NodeIPAM", Status: "Disabled", Version: "ALPHA"},
diff --git a/pkg/controller/ipam/antrea_ipam_controller_test.go b/pkg/controller/ipam/antrea_ipam_controller_test.go
index 1d6e993e7..b59e9fdef 100644
--- a/pkg/controller/ipam/antrea_ipam_controller_test.go
+++ b/pkg/controller/ipam/antrea_ipam_controller_test.go
@@ -18,9 +18,11 @@
 package ipam
 
 import (
+	"context"
 	"testing"
 	"time"
 
+	"github.com/google/uuid"
 	"github.com/stretchr/testify/require"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -28,67 +30,72 @@ import (
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes/fake"
+	"k8s.io/klog/v2"
 
 	crdv1a2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
 	fakecrd "antrea.io/antrea/pkg/client/clientset/versioned/fake"
 	crdinformers "antrea.io/antrea/pkg/client/informers/externalversions"
 	listers "antrea.io/antrea/pkg/client/listers/crd/v1alpha2"
+	annotation "antrea.io/antrea/pkg/ipam"
 	"antrea.io/antrea/pkg/ipam/poolallocator"
 )
 
-var (
-	testPool     = "test-pool"
-	testWithPool = "test-pool"
-	testNoPool   = "test-no-pool"
-	testStale    = "test-stale"
-)
-
-// StatefulSet objects are not defined here, since IPAM annotations
-// are configured on Pods which belong to the StatefulSet via "app" label.
-func initTestClients(pool *crdv1a2.IPPool) (*fake.Clientset, *fakecrd.Clientset) {
-	crdClient := fakecrd.NewSimpleClientset(pool)
+func initTestObjects(annotateNamespace bool, annotateStatefulSet bool, replicas int32) (*corev1.Namespace, *crdv1a2.IPPool, *appsv1.StatefulSet) {
+	namespace := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: uuid.New().String(),
+		},
+	}
 
-	k8sClient := fake.NewSimpleClientset(
-		&corev1.Namespace{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:        testWithPool,
-				Annotations: map[string]string{AntreaIPAMAnnotationKey: testPool},
-			},
+	subnetRange := crdv1a2.SubnetIPRange{
+		IPRange: crdv1a2.IPRange{
+			Start: "10.2.2.100",
+			End:   "10.2.2.110",
 		},
-		&corev1.Namespace{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: testNoPool,
-			},
+		SubnetInfo: crdv1a2.SubnetInfo{
+			Gateway:      "10.2.2.1",
+			PrefixLength: 24,
 		},
-		&corev1.Pod{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      testNoPool,
-				Namespace: testWithPool,
-				Labels:    map[string]string{"app": testNoPool},
-			},
+	}
+
+	pool := &crdv1a2.IPPool{
+		ObjectMeta: metav1.ObjectMeta{Name: uuid.New().String()},
+		Spec: crdv1a2.IPPoolSpec{
+			IPRanges: []crdv1a2.SubnetIPRange{subnetRange},
 		},
-		&corev1.Pod{
-			ObjectMeta: metav1.ObjectMeta{
-				Annotations: map[string]string{AntreaIPAMAnnotationKey: testPool},
-				Labels:      map[string]string{"app": testWithPool},
-				Namespace:   testNoPool,
-			},
+	}
+
+	if annotateNamespace {
+		namespace.Annotations = map[string]string{annotation.AntreaIPAMAnnotationKey: pool.Name}
+	}
+
+	statefulSet := &appsv1.StatefulSet{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      uuid.New().String(),
+			Namespace: namespace.Name,
 		},
-		&appsv1.StatefulSet{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      testWithPool,
-				Namespace: testNoPool,
+		Spec: appsv1.StatefulSetSpec{
+			Template: corev1.PodTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        namespace.Name,
+					Annotations: map[string]string{annotation.AntreaIPAMAnnotationKey: pool.Name},
+				},
 			},
+			Replicas: &replicas,
 		},
-	)
+	}
 
-	return k8sClient, crdClient
+	if annotateStatefulSet {
+		statefulSet.Spec.Template.Annotations = map[string]string{annotation.AntreaIPAMAnnotationKey: pool.Name}
+	}
+
+	return namespace, pool, statefulSet
 }
 
-func verifyPoolAllocatedSize(t *testing.T, poolLister listers.IPPoolLister, size int) {
+func verifyPoolAllocatedSize(t *testing.T, poolName string, poolLister listers.IPPoolLister, size int) {
 
 	err := wait.PollImmediate(100*time.Millisecond, 1*time.Second, func() (bool, error) {
-		pool, err := poolLister.Get(testPool)
+		pool, err := poolLister.Get(poolName)
 		if err != nil {
 			return false, nil
 		}
@@ -102,90 +109,84 @@ func verifyPoolAllocatedSize(t *testing.T, poolLister listers.IPPoolLister, size
 	require.NoError(t, err)
 }
 
-// This test verifies release of reserved IPs for dedicated IP pool annotation,
-// as well as namespace-based IP Pool annotation.
-func TestReleaseStatefulSet(t *testing.T) {
-	stopCh := make(chan struct{})
-	defer close(stopCh)
-
-	ipRange := crdv1a2.IPRange{
-		Start: "10.2.2.100",
-		End:   "10.2.2.200",
-	}
-
-	subnetInfo := crdv1a2.SubnetInfo{
-		Gateway:      "10.2.2.1",
-		PrefixLength: 24,
-	}
-
-	subnetRange := crdv1a2.SubnetIPRange{IPRange: ipRange,
-		SubnetInfo: subnetInfo}
-
-	pool := crdv1a2.IPPool{
-		ObjectMeta: metav1.ObjectMeta{Name: testPool},
-		Spec: crdv1a2.IPPoolSpec{
-			IPRanges: []crdv1a2.SubnetIPRange{subnetRange},
+func TestStatefulSetLifecycle(t *testing.T) {
+
+	tests := []struct {
+		name                string
+		dedicatedPool       bool
+		replicas            int32
+		expectAllocatedSize int
+	}{
+		{
+			name:                "Dedicated pool",
+			dedicatedPool:       true,
+			replicas:            5,
+			expectAllocatedSize: 5,
+		},
+		{
+			name:                "Full reservation of dedicated pool",
+			dedicatedPool:       true,
+			replicas:            11,
+			expectAllocatedSize: 11,
+		},
+		{
+			name:                "Namespace pool",
+			dedicatedPool:       false,
+			replicas:            7,
+			expectAllocatedSize: 7,
+		},
+		{
+			name:                "No enough IPs",
+			dedicatedPool:       false,
+			replicas:            20,
+			expectAllocatedSize: 0,
 		},
 	}
 
-	k8sClient, crdClient := initTestClients(&pool)
+	for _, tt := range tests {
+		stopCh := make(chan struct{})
+		defer close(stopCh)
 
-	informerFactory := informers.NewSharedInformerFactory(k8sClient, 0)
-
-	crdInformerFactory := crdinformers.NewSharedInformerFactory(crdClient, 0)
-	poolInformer := crdInformerFactory.Crd().V1alpha2().IPPools()
-	poolLister := poolInformer.Lister()
-
-	controller := NewAntreaIPAMController(crdClient, informerFactory, crdInformerFactory)
-	require.NotNil(t, controller)
-	informerFactory.Start(stopCh)
-	crdInformerFactory.Start(stopCh)
+		klog.InfoS("Running", "test", tt.name)
+		namespace, pool, statefulSet := initTestObjects(!tt.dedicatedPool, tt.dedicatedPool, tt.replicas)
+		crdClient := fakecrd.NewSimpleClientset(pool)
+		k8sClient := fake.NewSimpleClientset(namespace, statefulSet)
 
-	go controller.Run(stopCh)
+		informerFactory := informers.NewSharedInformerFactory(k8sClient, 0)
 
-	var allocator *poolallocator.IPPoolAllocator
-	var err error
-	// Wait until pool propagates to the informer
-	pollErr := wait.PollImmediate(100*time.Millisecond, 1*time.Second, func() (bool, error) {
-		allocator, err = poolallocator.NewIPPoolAllocator(testPool, crdClient, poolLister)
-		if err != nil {
-			return false, nil
-		}
-		return true, nil
-	})
-	require.NoError(t, pollErr)
+		crdInformerFactory := crdinformers.NewSharedInformerFactory(crdClient, 0)
+		poolInformer := crdInformerFactory.Crd().V1alpha2().IPPools()
+		poolLister := poolInformer.Lister()
 
-	verifyPoolAllocatedSize(t, poolLister, 0)
+		controller := NewAntreaIPAMController(crdClient, informerFactory, crdInformerFactory)
+		require.NotNil(t, controller)
+		informerFactory.Start(stopCh)
+		crdInformerFactory.Start(stopCh)
 
-	// Allocate StatefulSet with dedicated IP Pool
-	err = allocator.AllocateStatefulSet(testNoPool, testWithPool, 5)
-	require.NoError(t, err, "Failed to reserve IPs for StatefulSet")
-	verifyPoolAllocatedSize(t, poolLister, 5)
+		go controller.Run(stopCh)
 
-	// Allocate StatefulSet with namespace-based IP Pool annotation
-	err = allocator.AllocateStatefulSet(testWithPool, testNoPool, 3)
-	require.NoError(t, err, "Failed to reserve IPs for StatefulSet")
-	verifyPoolAllocatedSize(t, poolLister, 8)
+		var allocator *poolallocator.IPPoolAllocator
+		var err error
+		// Wait until pool propagates to the informer
+		pollErr := wait.PollImmediate(100*time.Millisecond, 1*time.Second, func() (bool, error) {
+			allocator, err = poolallocator.NewIPPoolAllocator(pool.Name, crdClient, poolLister)
+			if err != nil {
+				return false, nil
+			}
+			return true, nil
+		})
+		require.NoError(t, pollErr)
+		defer allocator.ReleaseStatefulSet(statefulSet.Namespace, statefulSet.Name)
 
-	// Delete StatefulSet with namespace-based IP Pool annotation
-	controller.enqueueStatefulSetDeleteEvent(&appsv1.StatefulSet{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      testNoPool,
-			Namespace: testWithPool,
-		}})
+		// Verify create event was handled by the controller
+		verifyPoolAllocatedSize(t, pool.Name, poolLister, tt.expectAllocatedSize)
 
-	// Verify delete event was handled by the controller
-	verifyPoolAllocatedSize(t, poolLister, 5)
+		// Delete StatefulSet
+		k8sClient.AppsV1().StatefulSets(namespace.Name).Delete(context.TODO(), statefulSet.Name, metav1.DeleteOptions{})
 
-	// Delete StatefulSet with dedicated IP Pool
-	controller.enqueueStatefulSetDeleteEvent(&appsv1.StatefulSet{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      testWithPool,
-			Namespace: testNoPool,
-		}})
-
-	// Verify delete event was handled by the controller
-	verifyPoolAllocatedSize(t, poolLister, 0)
+		// Verify Delete event was processed
+		verifyPoolAllocatedSize(t, pool.Name, poolLister, 0)
+	}
 }
 
 // Test for cleanup on controller startup: stale addresses that belong no StatefulSet objects
@@ -194,54 +195,36 @@ func TestReleaseStaleAddresses(t *testing.T) {
 	stopCh := make(chan struct{})
 	defer close(stopCh)
 
-	ipRange := crdv1a2.IPRange{
-		CIDR: "10.2.2.0/24",
-	}
-
-	subnetInfo := crdv1a2.SubnetInfo{
-		Gateway:      "10.2.2.1",
-		PrefixLength: 24,
-	}
+	namespace, pool, statefulSet := initTestObjects(true, false, 7)
 
-	subnetRange := crdv1a2.SubnetIPRange{IPRange: ipRange,
-		SubnetInfo: subnetInfo}
-
-	// pool includes two entries for non-existent StatefulSet at startup
-	// as well as one legit entry
 	activeSetOwner := crdv1a2.StatefulSetOwner{
-		Name:      testWithPool,
-		Namespace: testNoPool,
-		Index:     0,
+		Name:      statefulSet.Name,
+		Namespace: namespace.Name,
 	}
 
 	staleSetOwner := crdv1a2.StatefulSetOwner{
-		Name:      testStale,
-		Namespace: testNoPool,
-		Index:     0,
+		Name:      uuid.New().String(),
+		Namespace: namespace.Name,
 	}
 
 	addresses := []crdv1a2.IPAddressState{
 		{IPAddress: "10.2.2.12",
-			Phase: crdv1a2.IPAddressPhasePreallocated,
+			Phase: crdv1a2.IPAddressPhaseReserved,
 			Owner: crdv1a2.IPAddressOwner{StatefulSet: &activeSetOwner}},
 		{IPAddress: "20.1.1.100",
-			Phase: crdv1a2.IPAddressPhasePreallocated,
+			Phase: crdv1a2.IPAddressPhaseReserved,
 			Owner: crdv1a2.IPAddressOwner{StatefulSet: &staleSetOwner}},
 		{IPAddress: "20.1.1.200",
-			Phase: crdv1a2.IPAddressPhasePreallocated,
+			Phase: crdv1a2.IPAddressPhaseReserved,
 			Owner: crdv1a2.IPAddressOwner{StatefulSet: &staleSetOwner}},
 	}
-	pool := crdv1a2.IPPool{
-		ObjectMeta: metav1.ObjectMeta{Name: testPool},
-		Spec: crdv1a2.IPPoolSpec{
-			IPRanges: []crdv1a2.SubnetIPRange{subnetRange},
-		},
-		Status: crdv1a2.IPPoolStatus{
-			IPAddresses: addresses,
-		},
+
+	pool.Status = crdv1a2.IPPoolStatus{
+		IPAddresses: addresses,
 	}
 
-	k8sClient, crdClient := initTestClients(&pool)
+	crdClient := fakecrd.NewSimpleClientset(pool)
+	k8sClient := fake.NewSimpleClientset(namespace, statefulSet)
 
 	informerFactory := informers.NewSharedInformerFactory(k8sClient, 0)
 
@@ -256,16 +239,6 @@ func TestReleaseStaleAddresses(t *testing.T) {
 
 	go controller.Run(stopCh)
 
-	// Wait until pool propagates to the informer
-	pollErr := wait.PollImmediate(100*time.Millisecond, 1*time.Second, func() (bool, error) {
-		_, err := poolallocator.NewIPPoolAllocator(testPool, crdClient, poolLister)
-		if err != nil {
-			return false, nil
-		}
-		return true, nil
-	})
-	require.NoError(t, pollErr)
-
 	// after cleanup pool should have single entry
-	verifyPoolAllocatedSize(t, poolLister, 1)
+	verifyPoolAllocatedSize(t, pool.Name, poolLister, 1)
 }
diff --git a/pkg/agent/cniserver/ipam/annotations.go b/pkg/ipam/annotations.go
similarity index 100%
rename from pkg/agent/cniserver/ipam/annotations.go
rename to pkg/ipam/annotations.go
diff --git a/pkg/ipam/ipallocator/allocator_test.go b/pkg/ipam/ipallocator/allocator_test.go
index 7d10d6738..7624e9ddf 100644
--- a/pkg/ipam/ipallocator/allocator_test.go
+++ b/pkg/ipam/ipallocator/allocator_test.go
@@ -308,7 +308,8 @@ func TestAllocateRange(t *testing.T) {
 	}
 }
 
-func TestName(t *testing.T) {
+func TestNameAndTotal(t *testing.T) {
 	ma := MultiIPAllocator{newIPRangeAllocator("1.1.1.10", "1.1.1.20"), newCIDRAllocator("10.10.10.128/30", nil)}
 	assert.Equal(t, []string{"1.1.1.10-1.1.1.20", "10.10.10.128/30"}, ma.Names())
+	assert.Equal(t, 14, ma.Total())
 }
diff --git a/test/e2e/antreaipam_service_test.go b/test/e2e/antreaipam_service_test.go
index 239f32a9d..cee0c1fc0 100644
--- a/test/e2e/antreaipam_service_test.go
+++ b/test/e2e/antreaipam_service_test.go
@@ -17,7 +17,7 @@ package e2e
 import (
 	"testing"
 
-	"antrea.io/antrea/pkg/agent/cniserver/ipam"
+	annotation "antrea.io/antrea/pkg/ipam"
 )
 
 func TestAntreaIPAMService(t *testing.T) {
@@ -39,7 +39,7 @@ func TestAntreaIPAMService(t *testing.T) {
 		defer deleteIPPoolWrapper(t, data, ipPool.Name)
 		ipPools = append(ipPools, ipPool.Name)
 		annotations := map[string]string{}
-		annotations[ipam.AntreaIPAMAnnotationKey] = ipPool.Name
+		annotations[annotation.AntreaIPAMAnnotationKey] = ipPool.Name
 		err = data.createNamespaceWithAnnotations(namespace, annotations)
 		if err != nil {
 			t.Fatalf("Creating AntreaIPAM Namespace failed, err=%+v", err)
diff --git a/test/e2e/antreaipam_test.go b/test/e2e/antreaipam_test.go
index e1f3c7432..8d8eaf551 100644
--- a/test/e2e/antreaipam_test.go
+++ b/test/e2e/antreaipam_test.go
@@ -33,8 +33,8 @@ import (
 	"k8s.io/apimachinery/pkg/util/wait"
 	utilnet "k8s.io/utils/net"
 
-	"antrea.io/antrea/pkg/agent/cniserver/ipam"
 	crdv1alpha2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
+	annotation "antrea.io/antrea/pkg/ipam"
 )
 
 var (
@@ -131,7 +131,7 @@ func TestAntreaIPAM(t *testing.T) {
 		defer deleteIPPoolWrapper(t, data, ipPool.Name)
 		ipPools = append(ipPools, ipPool.Name)
 		annotations := map[string]string{}
-		annotations[ipam.AntreaIPAMAnnotationKey] = ipPool.Name
+		annotations[annotation.AntreaIPAMAnnotationKey] = ipPool.Name
 		err = data.createNamespaceWithAnnotations(namespace, annotations)
 		if err != nil {
 			t.Fatalf("Creating AntreaIPAM Namespace failed, err=%+v", err)
@@ -304,7 +304,7 @@ func testAntreaIPAMStatefulSet(t *testing.T, data *TestData, dedicatedIPPoolKey
 			sts.Spec.Template.Annotations = map[string]string{}
 		}
 		if dedicatedIPPoolKey != nil {
-			sts.Spec.Template.Annotations[ipam.AntreaIPAMAnnotationKey] = ipPoolName
+			sts.Spec.Template.Annotations[annotation.AntreaIPAMAnnotationKey] = ipPoolName
 		}
 	}
 	_, cleanup, err := data.createStatefulSet(stsName, testAntreaIPAMNamespace, int32(size), agnhostContainerName, agnhostImage, []string{"sleep", "3600"}, nil, mutateFunc)
@@ -333,7 +333,7 @@ func testAntreaIPAMStatefulSet(t *testing.T, data *TestData, dedicatedIPPoolKey
 			pod.Annotations = map[string]string{}
 		}
 		if dedicatedIPPoolKey != nil {
-			pod.Annotations[ipam.AntreaIPAMAnnotationKey] = ipPoolName
+			pod.Annotations[annotation.AntreaIPAMAnnotationKey] = ipPoolName
 		}
 	}
 	podName := randName("test-standalone-pod-")
diff --git a/test/e2e/egress_test.go b/test/e2e/egress_test.go
index a5d599a06..72eb7c7a0 100644
--- a/test/e2e/egress_test.go
+++ b/test/e2e/egress_test.go
@@ -34,8 +34,6 @@ import (
 
 	"antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/apis/crd/v1alpha2"
-	agentconfig "antrea.io/antrea/pkg/config/agent"
-	controllerconfig "antrea.io/antrea/pkg/config/controller"
 )
 
 const waitEgressRealizedTimeout = 3 * time.Second
@@ -53,17 +51,6 @@ func TestEgress(t *testing.T) {
 	// Egress works for encap mode only.
 	skipIfEncapModeIsNot(t, data, config.TrafficEncapModeEncap)
 
-	cc := func(config *controllerconfig.ControllerConfig) {
-		config.FeatureGates["Egress"] = true
-	}
-	ac := func(config *agentconfig.AgentConfig) {
-		config.FeatureGates["Egress"] = true
-	}
-
-	if err := data.mutateAntreaConfigMap(cc, ac, true, true); err != nil {
-		t.Fatalf("Failed to enable Egress feature: %v", err)
-	}
-
 	t.Run("testEgressClientIP", func(t *testing.T) { testEgressClientIP(t, data) })
 	t.Run("testEgressCRUD", func(t *testing.T) { testEgressCRUD(t, data) })
 	t.Run("testEgressUpdateEgressIP", func(t *testing.T) { testEgressUpdateEgressIP(t, data) })