Repository: antrea-io/antrea
Pull request #5943 (2024-01-30 07:40:04)

NPL code tidy-up for Antrea v2.0
For #5070

XinShuYang: /test-windows-containerd-e2e
tnqn: https://github.com/antrea-io/antrea/pull/5917 has been merged, the PR needs to rebase on main and fix the new unit test.
luolanzone: @tnqn  the unit test failure is fixed, could you take a look again? Thanks.

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index eb5134596..d474073ce 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -36,6 +36,8 @@ jobs:
     - uses: actions/checkout@v4
       with:
         show-progress: false
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v3
     - name: Build Antrea amd64 Docker image without pushing to registry
       if: ${{ github.repository != 'antrea-io/antrea' || github.event_name != 'push' || github.ref != 'refs/heads/main' }}
       run: |
@@ -72,6 +74,8 @@ jobs:
     - uses: actions/checkout@v4
       with:
         show-progress: false
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v3
     - name: Build Antrea UBI8 Docker image without pushing to registry
       if: ${{ github.repository != 'antrea-io/antrea' || github.event_name != 'push' || github.ref != 'refs/heads/main' }}
       run: |
diff --git a/Makefile b/Makefile
index 62b9b3075..91d47c718 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,15 @@ GOLANGCI_LINT_VERSION := v1.54.0
 GOLANGCI_LINT_BINDIR  := $(CURDIR)/.golangci-bin
 GOLANGCI_LINT_BIN     := $(GOLANGCI_LINT_BINDIR)/$(GOLANGCI_LINT_VERSION)/golangci-lint
 
-DOCKER_BUILD_ARGS := --build-arg OVS_VERSION=$(OVS_VERSION)
+DOCKER_BUILD_ARGS :=
+ifeq ($(NO_PULL),)
+	DOCKER_BUILD_ARGS += --pull
+endif
+ifneq ($(NO_CACHE),)
+	DOCKER_BUILD_ARGS += --no-cache
+endif
+WIN_BUILD_ARGS := DOCKER_BUILD_ARGS
+DOCKER_BUILD_ARGS += --build-arg OVS_VERSION=$(OVS_VERSION)
 DOCKER_BUILD_ARGS += --build-arg GO_VERSION=$(GO_VERSION)
 DOCKER_BUILD_ARGS += --build-arg BUILD_TAG=$(BUILD_TAG)
 WIN_BUILD_ARGS := --build-arg GO_VERSION=$(GO_VERSION)
@@ -198,11 +206,7 @@ docker-test-unit: $(DOCKER_CACHE)
 .PHONY: docker-test-integration
 docker-test-integration: .coverage
 	@echo "===> Building Antrea Integration Test Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/test -f build/images/test/Dockerfile $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/test -f build/images/test/Dockerfile $(DOCKER_BUILD_ARGS) .
-endif
 	@docker run --privileged --rm \
 		-e "GOCACHE=/tmp/gocache" \
 		-e "GOPATH=/tmp/gopath" \
@@ -330,113 +334,69 @@ mockgen:
 .PHONY: ubuntu
 ubuntu:
 	@echo "===> Building antrea/antrea-ubuntu Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.ubuntu $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.ubuntu $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-ubuntu:$(DOCKER_IMG_VERSION) antrea/antrea-ubuntu
 
 .PHONY: build-controller-ubuntu
 build-controller-ubuntu:
 	@echo "===> Building antrea/antrea-controller-ubuntu Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-controller-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.controller.ubuntu $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-controller-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.controller.ubuntu $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-controller-ubuntu:$(DOCKER_IMG_VERSION) antrea/antrea-controller-ubuntu
 
 .PHONY: build-agent-ubuntu
 build-agent-ubuntu:
 	@echo "===> Building antrea/antrea-agent-ubuntu Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-agent-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.agent.ubuntu $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-agent-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.agent.ubuntu $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-agent-ubuntu:$(DOCKER_IMG_VERSION) antrea/antrea-agent-ubuntu
 
 # Build bins in a golang container, and build the antrea-ubuntu Docker image.
 .PHONY: build-ubuntu
 build-ubuntu:
 	@echo "===> Building Antrea bins and antrea/antrea-ubuntu Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.ubuntu $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-ubuntu:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.ubuntu $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-ubuntu:$(DOCKER_IMG_VERSION) antrea/antrea-ubuntu
 
 # Build bins in a golang container, and build the antrea-ubi Docker image.
 .PHONY: build-ubi
 build-ubi:
 	@echo "===> Building Antrea bins and antrea/antrea-ubi Docker image <==="
-ifneq ($(NO_PULL),"")
 	docker build -t antrea/antrea-ubi:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.ubi $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-ubi:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.ubi $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-ubi:$(DOCKER_IMG_VERSION) antrea/antrea-ubi
 
 .PHONY: build-agent-ubi
 build-agent-ubi:
 	@echo "===> Building Antrea bins and antrea/antrea-agent-ubi Docker image <==="
-ifneq ($(NO_PULL),"")
 	docker build -t antrea/antrea-agent-ubi:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.agent.ubi $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-agent-ubi:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.agent.ubi $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-agent-ubi:$(DOCKER_IMG_VERSION) antrea/antrea-agent-ubi
 
 .PHONY: build-controller-ubi
 build-controller-ubi:
 	@echo "===> Building Antrea bins and antrea/antrea-controller-ubi Docker image <==="
-ifneq ($(NO_PULL),"")
 	docker build -t antrea/antrea-controller-ubi:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.controller.ubi $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-controller-ubi:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.controller.ubi $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-controller-ubi:$(DOCKER_IMG_VERSION) antrea/antrea-controller-ubi
 
 .PHONY: build-windows
 build-windows:
 	@echo "===> Building Antrea bins and antrea/antrea-windows Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-windows:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.windows --network $(DOCKER_NETWORK) $(WIN_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-windows:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.windows --network $(DOCKER_NETWORK) $(WIN_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-windows:$(DOCKER_IMG_VERSION) antrea/antrea-windows
 
 .PHONY: build-ubuntu-coverage
 build-ubuntu-coverage:
 	@echo "===> Building Antrea bins and antrea/antrea-ubuntu-coverage Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-ubuntu-coverage:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.coverage $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-ubuntu-coverage:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.coverage $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-ubuntu-coverage:$(DOCKER_IMG_VERSION) antrea/antrea-ubuntu-coverage
 
 .PHONY: build-controller-ubuntu-coverage
 build-controller-ubuntu-coverage:
 	@echo "===> Building Antrea bins and antrea/antrea-controller-ubuntu-coverage Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-controller-ubuntu-coverage:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.controller.coverage $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-controller-ubuntu-coverage:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.controller.coverage $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-controller-ubuntu-coverage:$(DOCKER_IMG_VERSION) antrea/antrea-controller-ubuntu-coverage
 
 .PHONY: build-agent-ubuntu-coverage
 build-agent-ubuntu-coverage:
 	@echo "===> Building Antrea bins and antrea/antrea-agent-ubuntu-coverage Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-agent-ubuntu-coverage:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.agent.coverage $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-agent-ubuntu-coverage:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.agent.coverage $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-agent-ubuntu-coverage:$(DOCKER_IMG_VERSION) antrea/antrea-agent-ubuntu-coverage
 
 .PHONY: build-scale-simulator
@@ -448,11 +408,7 @@ build-scale-simulator:
 .PHONY: build-migrator
 build-migrator:
 	@echo "===> Building antrea/antrea-migrator Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-migrator:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.migrator $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-migrator:$(DOCKER_IMG_VERSION) -f build/images/Dockerfile.build.migrator $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-migrator:$(DOCKER_IMG_VERSION) antrea/antrea-migrator
 
 .PHONY: manifest
@@ -479,52 +435,32 @@ manifest-coverage:
 .PHONY: antrea-mc-controller
 antrea-mc-controller:
 	@echo "===> Building antrea/antrea-mc-controller Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-mc-controller:$(DOCKER_IMG_VERSION) -f multicluster/build/images/Dockerfile $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-mc-controller:$(DOCKER_IMG_VERSION) -f multicluster/build/images/Dockerfile $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-mc-controller:$(DOCKER_IMG_VERSION) antrea/antrea-mc-controller
 
 # Build bins in a golang container, and build the antrea-mc-controller Docker image.
 .PHONY: build-antrea-mc-controller
 build-antrea-mc-controller:
 	@echo "===> Building antrea/antrea-mc-controller Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-mc-controller:$(DOCKER_IMG_VERSION) -f multicluster/build/images/Dockerfile.build $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-mc-controller:$(DOCKER_IMG_VERSION) -f multicluster/build/images/Dockerfile.build $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-mc-controller:$(DOCKER_IMG_VERSION) antrea/antrea-mc-controller
 
 .PHONY: build-antrea-mc-controller-coverage
 build-antrea-mc-controller-coverage:
 	@echo "===> Building antrea/antrea-mc-controller-coverage Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/antrea-mc-controller-coverage:$(DOCKER_IMG_VERSION) -f multicluster/build/images/Dockerfile.build.coverage $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/antrea-mc-controller-coverage:$(DOCKER_IMG_VERSION) -f multicluster/build/images/Dockerfile.build.coverage $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/antrea-mc-controller-coverage:$(DOCKER_IMG_VERSION) antrea/antrea-mc-controller-coverage
 
 .PHONY: flow-aggregator-image
 flow-aggregator-image:
 	@echo "===> Building antrea/flow-aggregator Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/flow-aggregator:$(DOCKER_IMG_VERSION) -f build/images/flow-aggregator/Dockerfile $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/flow-aggregator:$(DOCKER_IMG_VERSION) -f build/images/flow-aggregator/Dockerfile $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/flow-aggregator:$(DOCKER_IMG_VERSION) antrea/flow-aggregator
 
 .PHONY: flow-aggregator-ubuntu-coverage
 flow-aggregator-ubuntu-coverage:
 	@echo "===> Building antrea/flow-aggregator-coverage Docker image <==="
-ifneq ($(NO_PULL),)
 	docker build -t antrea/flow-aggregator-coverage:$(DOCKER_IMG_VERSION) -f build/images/flow-aggregator/Dockerfile.coverage $(DOCKER_BUILD_ARGS) .
-else
-	docker build --pull -t antrea/flow-aggregator-coverage:$(DOCKER_IMG_VERSION) -f build/images/flow-aggregator/Dockerfile.coverage $(DOCKER_BUILD_ARGS) .
-endif
 	docker tag antrea/flow-aggregator-coverage:$(DOCKER_IMG_VERSION) antrea/flow-aggregator-coverage
 
 .PHONY: verify
diff --git a/build/images/base/build.sh b/build/images/base/build.sh
index 36d282345..80ad459cf 100755
--- a/build/images/base/build.sh
+++ b/build/images/base/build.sh
@@ -19,23 +19,21 @@
 
 set -eo pipefail
 
-function echoerr {
-    >&2 echo "$@"
-}
+THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+source $THIS_DIR/../build-utils.sh
 
 _usage="Usage: $0 [--pull] [--push] [--platform <PLATFORM>] [--distro [ubuntu|ubi]]
 Build the antrea base image.
         --pull                  Always attempt to pull a newer version of the base images
         --push                  Push the built image to the registry
         --platform <PLATFORM>   Target platform for the image if server is multi-platform capable
-        --distro <distro>       Target Linux distribution"
-
-function print_usage {
-    echoerr "$_usage"
-}
+        --distro <distro>       Target Linux distribution
+        --no-cache              Do not use the local build cache nor the cached image from the registry"
 
 PULL=false
 PUSH=false
+NO_CACHE=false
 PLATFORM=""
 DISTRO="ubuntu"
 
@@ -60,6 +58,10 @@ case $key in
     DISTRO="$2"
     shift 2
     ;;
+    --no-cache)
+    NO_CACHE=true
+    shift
+    ;;
     -h|--help)
     print_usage
     exit 0
@@ -71,6 +73,15 @@ case $key in
 esac
 done
 
+# When --push is provided, we assume that we want to use --cache-to, which will
+# push the "cache image" to the registry. This functionality is not supported
+# with the default docker driver.
+# See https://docs.docker.com/build/cache/backends/registry/
+if $PUSH && ! check_docker_build_driver "docker-container"; then
+    echoerr "--push requires the docker-container build driver"
+    exit 1
+fi
+
 if [ "$PLATFORM" != "" ] && $PUSH; then
     echoerr "Cannot use --platform with --push"
     exit 1
@@ -86,8 +97,6 @@ if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "ubi" ]; then
     exit 1
 fi
 
-THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-
 pushd $THIS_DIR > /dev/null
 
 CNI_BINARIES_VERSION=$(head -n 1 ../deps/cni-binaries-version)
@@ -96,6 +105,7 @@ SURICATA_VERSION=$(head -n 1 ../deps/suricata-version)
 BUILD_TAG=$(../build-tag.sh)
 
 if $PULL; then
+    # The ubuntu image is also used for the UBI build (for the cni-binaries intermediate image).
     if [[ ${DOCKER_REGISTRY} == "" ]]; then
         docker pull $PLATFORM_ARG ubuntu:22.04
     else
@@ -106,14 +116,10 @@ if $PULL; then
     if [ "$DISTRO" == "ubuntu" ]; then
         IMAGES_LIST=(
             "antrea/openvswitch:$BUILD_TAG"
-            "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubuntu:$BUILD_TAG"
         )
     elif [ "$DISTRO" == "ubi" ]; then
         IMAGES_LIST=(
             "antrea/openvswitch-ubi:$BUILD_TAG"
-            "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubi:$BUILD_TAG"
         )
     fi
     for image in "${IMAGES_LIST[@]}"; do
@@ -129,34 +135,31 @@ if $PULL; then
     done
 fi
 
-docker build $PLATFORM_ARG --target cni-binaries \
-       --cache-from antrea/cni-binaries:$CNI_BINARIES_VERSION \
-       -t antrea/cni-binaries:$CNI_BINARIES_VERSION \
-       --build-arg CNI_BINARIES_VERSION=$CNI_BINARIES_VERSION \
-       --build-arg BUILD_TAG=$BUILD_TAG .
+function docker_build_and_push() {
+    local image="$1"
+    local dockerfile="$2"
+    local build_args="--build-arg CNI_BINARIES_VERSION=$CNI_BINARIES_VERSION --build-arg SURICATA_VERSION=$SURICATA_VERSION --build-arg BUILD_TAG=$BUILD_TAG"
+    local cache_args=""
+    if $PUSH; then
+        cache_args="$cache_args --cache-to type=registry,ref=$image-cache:$BUILD_TAG,mode=max"
+    fi
+    if $NO_CACHE; then
+        cache_args="$cache_args --no-cache"
+    else
+        cache_args="$cache_args --cache-from type=registry,ref=$image-cache:$BUILD_TAG,mode=max"
+    fi
+    docker buildx build $PLATFORM_ARG -o type=docker -t $image:$BUILD_TAG $cache_args $build_args -f $dockerfile .
+
+    if $PUSH; then
+        docker push $image:$BUILD_TAG
+    fi
+}
+
 
 if [ "$DISTRO" == "ubuntu" ]; then
-    docker build $PLATFORM_ARG \
-           --cache-from antrea/cni-binaries:$CNI_BINARIES_VERSION \
-           --cache-from antrea/base-ubuntu:$BUILD_TAG \
-           -t antrea/base-ubuntu:$BUILD_TAG \
-           --build-arg CNI_BINARIES_VERSION=$CNI_BINARIES_VERSION \
-           --build-arg SURICATA_VERSION=$SURICATA_VERSION \
-           --build-arg BUILD_TAG=$BUILD_TAG .
+    docker_build_and_push "antrea/base-ubuntu" Dockerfile
 elif [ "$DISTRO" == "ubi" ]; then
-    docker build $PLATFORM_ARG \
-           --cache-from antrea/cni-binaries:$CNI_BINARIES_VERSION \
-           --cache-from antrea/base-ubi:$BUILD_TAG \
-           -t antrea/base-ubi:$BUILD_TAG \
-           -f Dockerfile.ubi \
-           --build-arg CNI_BINARIES_VERSION=$CNI_BINARIES_VERSION \
-           --build-arg SURICATA_VERSION=$SURICATA_VERSION \
-           --build-arg BUILD_TAG=$BUILD_TAG .
-fi
-
-if $PUSH; then
-    docker push antrea/cni-binaries:$CNI_BINARIES_VERSION
-    docker push antrea/base-$DISTRO:$BUILD_TAG
+    docker_build_and_push "antrea/base-ubi" Dockerfile.ubi
 fi
 
 popd > /dev/null
diff --git a/build/images/build-utils.sh b/build/images/build-utils.sh
new file mode 100644
index 000000000..986801d19
--- /dev/null
+++ b/build/images/build-utils.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+# Copyright 2024 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.
+
+function echoerr {
+    >&2 echo "$@"
+}
+
+function print_usage {
+    echoerr "$_usage"
+}
+
+function check_for_buildx() {
+    local rc=0
+    docker buildx inspect > /dev/null 2>&1 || rc=1
+    return $rc
+}
+
+function get_docker_build_driver() {
+    local driver=$(docker buildx inspect | grep "^Driver:" | awk '{print $2}')
+    echo "$driver"
+}
+
+function check_docker_build_driver() {
+    local expected="$1"
+    local actual="$(get_docker_build_driver)"
+    if [ "$actual" != "$expected" ]; then
+       return 1
+    fi
+    return 0
+}
diff --git a/build/images/ovs/build.sh b/build/images/ovs/build.sh
index 4dc13c81c..ac15f1269 100755
--- a/build/images/ovs/build.sh
+++ b/build/images/ovs/build.sh
@@ -19,23 +19,21 @@
 
 set -eo pipefail
 
-function echoerr {
-    >&2 echo "$@"
-}
+THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+source $THIS_DIR/../build-utils.sh
 
 _usage="Usage: $0 [--pull] [--push] [--platform <PLATFORM>] [--distro [ubuntu|ubi]]
 Build the antrea openvswitch image.
         --pull                  Always attempt to pull a newer version of the base images
         --push                  Push the built image to the registry
         --platform <PLATFORM>   Target platform for the image if server is multi-platform capable
-        --distro <distro>       Target Linux distribution"
-
-function print_usage {
-    echoerr "$_usage"
-}
+        --distro <distro>       Target Linux distribution
+        --no-cache              Do not use the local build cache nor the cached image from the registry"
 
 PULL=false
 PUSH=false
+NO_CACHE=false
 PLATFORM=""
 DISTRO="ubuntu"
 
@@ -60,6 +58,10 @@ case $key in
     DISTRO="$2"
     shift 2
     ;;
+    --no-cache)
+    NO_CACHE=true
+    shift
+    ;;
     -h|--help)
     print_usage
     exit 0
@@ -71,6 +73,15 @@ case $key in
 esac
 done
 
+# When --push is provided, we assume that we want to use --cache-to, which will
+# push the "cache image" to the registry. This functionality is not supported
+# with the default docker driver.
+# See https://docs.docker.com/build/cache/backends/registry/
+if $PUSH && ! check_docker_build_driver "docker-container"; then
+    echoerr "--push requires the docker-container build driver"
+    exit 1
+fi
+
 if [ "$PLATFORM" != "" ] && $PUSH; then
     echoerr "Cannot use --platform with --push"
     exit 1
@@ -86,86 +97,50 @@ if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "ubi" ]; then
     exit 1
 fi
 
-THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-
 pushd $THIS_DIR > /dev/null
 
 OVS_VERSION=$(head -n 1 ../deps/ovs-version)
 
 BUILD_TAG=$(../build-tag.sh)
 
-# This is a bit complicated but we make sure that we only build OVS if
-# necessary, and at the moment --cache-from does not play nicely with multistage
-# builds: we need to push the intermediate image to the registry. Note that the
-# --cache-from option will have no effect if the image doesn't exist
-# locally.
-# See https://github.com/moby/moby/issues/34715.
-
 if $PULL; then
-    if [[ ${DOCKER_REGISTRY} == "" ]]; then
-        docker pull $PLATFORM_ARG ubuntu:22.04
-    else
-        docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:22.04
-        docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:22.04 ubuntu:22.04
-    fi
     if [ "$DISTRO" == "ubuntu" ]; then
-        IMAGES_LIST=(
-            "antrea/openvswitch-debs:$BUILD_TAG"
-            "antrea/openvswitch:$BUILD_TAG"
-        )
-    elif [ "$DISTRO" == "ubi" ]; then
-        IMAGES_LIST=(
-            "antrea/openvswitch-rpms:$BUILD_TAG"
-            "antrea/openvswitch-ubi:$BUILD_TAG"
-        )
-    fi
-    for image in "${IMAGES_LIST[@]}"; do
         if [[ ${DOCKER_REGISTRY} == "" ]]; then
-            docker pull $PLATFORM_ARG "${image}" || true
+            docker pull $PLATFORM_ARG ubuntu:22.04
         else
-            rc=0
-            docker pull "${DOCKER_REGISTRY}/${image}" || rc=$?
-            if [[ $rc -eq 0 ]]; then
-                docker tag "${DOCKER_REGISTRY}/${image}" "${image}"
-            fi
+            docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:22.04
+            docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:22.04 ubuntu:22.04
         fi
-    done
+    elif [ "$DISTRO" == "ubi" ]; then
+        docker pull $PLATFORM_ARG centos:centos7
+        docker pull $PLATFORM_ARG registry.access.redhat.com/ubi8
+    fi
 fi
 
-if [ "$DISTRO" == "ubuntu" ]; then
-    docker build $PLATFORM_ARG --target ovs-debs \
-           --cache-from antrea/openvswitch-debs:$BUILD_TAG \
-           -t antrea/openvswitch-debs:$BUILD_TAG \
-           --build-arg OVS_VERSION=$OVS_VERSION .
-
-    docker build $PLATFORM_ARG \
-           --cache-from antrea/openvswitch-debs:$BUILD_TAG \
-           --cache-from antrea/openvswitch:$BUILD_TAG \
-           -t antrea/openvswitch:$BUILD_TAG \
-           --build-arg OVS_VERSION=$OVS_VERSION .
-elif [ "$DISTRO" == "ubi" ]; then
-    docker build $PLATFORM_ARG --target ovs-rpms \
-           --cache-from antrea/openvswitch-rpms:$BUILD_TAG \
-           -t antrea/openvswitch-rpms:$BUILD_TAG \
-           --build-arg OVS_VERSION=$OVS_VERSION \
-           -f Dockerfile.ubi .
-
-    docker build \
-           --cache-from antrea/openvswitch-rpms:$BUILD_TAG \
-           --cache-from antrea/openvswitch-ubi:$BUILD_TAG \
-           -t antrea/openvswitch-ubi:$BUILD_TAG \
-           --build-arg OVS_VERSION=$OVS_VERSION \
-           -f Dockerfile.ubi .
-fi
+function docker_build_and_push() {
+    local image="$1"
+    local dockerfile="$2"
+    local build_args="--build-arg OVS_VERSION=$OVS_VERSION"
+    local cache_args=""
+    if $PUSH; then
+        cache_args="$cache_args --cache-to type=registry,ref=$image-cache:$BUILD_TAG,mode=max"
+    fi
+    if $NO_CACHE; then
+        cache_args="$cache_args --no-cache"
+    else
+        cache_args="$cache_args --cache-from type=registry,ref=$image-cache:$BUILD_TAG,mode=max"
+    fi
+    docker buildx build $PLATFORM_ARG -o type=docker -t $image:$BUILD_TAG $cache_args $build_args -f $dockerfile .
 
-if $PUSH; then
-    if [ "$DISTRO" == "ubuntu" ]; then
-        docker push antrea/openvswitch-debs:$BUILD_TAG
-        docker push antrea/openvswitch:$BUILD_TAG
-    elif [ "$DISTRO" == "ubi" ]; then
-        docker push antrea/openvswitch-rpms:$BUILD_TAG
-        docker push antrea/openvswitch-ubi:$BUILD_TAG
+    if $PUSH; then
+        docker push $image:$BUILD_TAG
     fi
+}
+
+if [ "$DISTRO" == "ubuntu" ]; then
+    docker_build_and_push "antrea/openvswitch" "Dockerfile"
+elif [ "$DISTRO" == "ubi" ]; then
+    docker_build_and_push "antrea/openvswitch-ubi" "Dockerfile.ubi"
 fi
 
 popd > /dev/null
diff --git a/cmd/antrea-agent/options.go b/cmd/antrea-agent/options.go
index a01150e16..732a6abfb 100644
--- a/cmd/antrea-agent/options.go
+++ b/cmd/antrea-agent/options.go
@@ -469,12 +469,7 @@ func (o *Options) setK8sNodeDefaultOptions() {
 	}
 
 	if o.config.NodePortLocal.Enable {
-		switch {
-		case o.config.NodePortLocal.PortRange != "":
-		case o.config.NPLPortRange != "":
-			klog.InfoS("The nplPortRange option is deprecated, please use nodePortLocal.portRange instead")
-			o.config.NodePortLocal.PortRange = o.config.NPLPortRange
-		default:
+		if o.config.NodePortLocal.PortRange == "" {
 			o.config.NodePortLocal.PortRange = defaultNPLPortRange
 		}
 	}
diff --git a/docs/antrea-l7-network-policy.md b/docs/antrea-l7-network-policy.md
index 3b1aaa2c9..958a655d7 100644
--- a/docs/antrea-l7-network-policy.md
+++ b/docs/antrea-l7-network-policy.md
@@ -119,7 +119,7 @@ OPTIONS, CONNECT and PATCH. If not set, the rule matches all methods.
 
 #### More examples
 
-The following NetworkPolicy grants access of privileged URLs to specific clients while make other URLs publicly
+The following NetworkPolicy grants access of privileged URLs to specific clients while making other URLs publicly
 accessible:
 
 ```yaml
@@ -146,7 +146,7 @@ spec:
             path: "/admin/*"
         - http:
             path: "/public/*"
-    - name: for-public   # Allow inbound HTTP GET requests to "/public" from Pods with label "app=client".
+    - name: for-public   # Allow inbound HTTP GET requests to "/public" from everyone.
       action: Allow      # All other inbound traffic will be automatically dropped.
       l7Protocols:
         - http:
@@ -292,9 +292,38 @@ spec:
 
 Layer 7 traffic that matches the NetworkPolicy will be logged in an event
 triggered log file (`/var/log/antrea/networkpolicy/l7engine/eve-YEAR-MONTH-DAY.json`).
-The event type for this log is `alert`. If `enableLogging` is set for the rule,
+Logs are categorized by **event_type**. The event type for allowed traffic is `http`,
+for dropped traffic it is `alert`. If `enableLogging` is set for the rule, dropped
 packets that match the rule will also be logged in addition to the event with
-event type `packet`. Below is an example of the two event types.
+event type `packet`. Below are examples for allow, drop, packet scenarios.
+
+Allow ingress from client (10.10.1.8) to web (10.10.1.7/public/*)
+
+```json
+{
+  "timestamp": "2024-02-22T21:26:07.074791+0000",
+  "flow_id": 757085628206447,
+  "in_iface": "antrea-l7-tap0",
+  "event_type": "http",
+  "vlan": [1],
+  "src_ip": "10.10.1.8",
+  "src_port": 44132,
+  "dest_ip": "10.10.1.7",
+  "dest_port": 80,
+  "proto": "TCP",
+  "tx_id": 0,
+  "http": {
+      "hostname": "10.10.1.7",
+      "url": "/public/main.html",
+      "http_user_agent": "Wget/1.21.1",
+      "http_content_type": "text/html",
+      "http_method": "GET",
+      "protocol": "HTTP/1.1",
+      "status": 404,
+      "length": 153
+  }
+}
+```
 
 Deny ingress from client (10.10.1.5) to web (10.10.1.4/admin)
 
@@ -341,6 +370,8 @@ Deny ingress from client (10.10.1.5) to web (10.10.1.4/admin)
 }
 ```
 
+Additional packet log when `enableLogging` is set
+
 ```json
 {
   "timestamp": "2023-03-09T20:00:28.225016+0000",
diff --git a/docs/node-port-local.md b/docs/node-port-local.md
index 89cf54c77..6a76cc296 100644
--- a/docs/node-port-local.md
+++ b/docs/node-port-local.md
@@ -38,9 +38,10 @@ From Antrea v1.14, NPL is GA.
 In addition to enabling the NodePortLocal feature gate (if needed), you need to
 ensure that the `nodePortLocal.enable` flag is set to true in the Antrea Agent
 configuration. The `nodePortLocal.portRange` parameter can also be set to change
-the range from which Node ports will be allocated. Otherwise, the default range
-of `61000-62000` will be used by default. When using the NodePortLocal feature,
-your `antrea-agent` ConfigMap should look like this:
+the range from which Node ports will be allocated. Otherwise, the range
+of `61000-62000` will be used by default on Linux, and the range `40000-41000` will
+be used on Windows. When using the NodePortLocal feature, your `antrea-agent` ConfigMap
+should look like this:
 
 ```yaml
 kind: ConfigMap
@@ -116,7 +117,7 @@ metadata:
   labels:
     app: nginx
   annotations:
-    nodeportlocal.antrea.io: '[{"podPort":8080,"nodeIP":"10.10.10.10","nodePort":61002,"protocol":"tcp","protocols":["tcp"]}]'
+    nodeportlocal.antrea.io: '[{"podPort":8080,"nodeIP":"10.10.10.10","nodePort":61002,"protocol":"tcp"}]'
 ```
 
 This annotation indicates that port 8080 of the Pod can be reached through port
@@ -132,11 +133,7 @@ NodePortLocal can only be used with Services of type `ClusterIP` or
 Services of type `NodePort` or `ExternalName`. The annotation also has no effect
 for Services with an empty or missing Selector.
 
-Starting from the Antrea v1.7 minor release, the `protocols` field in the
-annotation is deprecated. The array contains a single member, equal to the
-`protocol` field.
-The `protocols` field will be removed from Antrea for minor releases post March 2023,
-as per our deprecation policy.
+Starting from Antrea v2.0, the `protocols` field is removed.
 
 ### Usage pre Antrea v1.7
 
@@ -168,7 +165,7 @@ for a given podPort, and the allocated nodePort may be different for each one.
 Prior to the Antrea v1.4 minor release, the `nodePortLocal` option group in the
 Antrea Agent configuration did not exist. To enable the NodePortLocal feature,
 one simply needed to enable the feature gate, and the port range could be
-configured using the (now deprecated) `nplPortRange` parameter.
+configured using the (now removed) `nplPortRange` parameter.
 
 ### Usage pre Antrea v1.2
 
diff --git a/go.mod b/go.mod
index cc9957da1..95977e605 100644
--- a/go.mod
+++ b/go.mod
@@ -54,7 +54,7 @@ require (
 	github.com/vishvananda/netlink v1.2.1-beta.2
 	github.com/vmware/go-ipfix v0.9.0
 	go.uber.org/mock v0.4.0
-	golang.org/x/crypto v0.19.0
+	golang.org/x/crypto v0.20.0
 	golang.org/x/mod v0.15.0
 	golang.org/x/net v0.21.0
 	golang.org/x/sync v0.6.0
diff --git a/go.sum b/go.sum
index 3acd32bdc..5b438c028 100644
--- a/go.sum
+++ b/go.sum
@@ -853,8 +853,8 @@ golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
+golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
diff --git a/hack/build-antrea-linux-all.sh b/hack/build-antrea-linux-all.sh
index 8098d67d4..24894912a 100755
--- a/hack/build-antrea-linux-all.sh
+++ b/hack/build-antrea-linux-all.sh
@@ -16,9 +16,9 @@
 
 set -eo pipefail
 
-function echoerr {
-    >&2 echo "$@"
-}
+THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+source $THIS_DIR/../build/images/build-utils.sh
 
 _usage="Usage: $0 [--pull] [--push-base-images] [--coverage] [--platform <PLATFORM>] [--distro [ubuntu|ubi]]
 Build the antrea image, as well as all the base images in the build chain. This is typically used in
@@ -28,14 +28,12 @@ all Dockerfiles.
         --push-base-images      Push built images to the registry. Only base images will be pushed.
         --coverage              Build the image with support for code coverage.
         --platform <PLATFORM>   Target platform for the images if server is multi-platform capable.
-        --distro <distro>       Target Linux distribution."
-
-function print_usage {
-    echoerr "$_usage"
-}
+        --distro <distro>       Target Linux distribution.
+        --no-cache              Do not use the local build cache nor the cached image from the registry."
 
 PULL=false
 PUSH=false
+NO_CACHE=false
 COVERAGE=false
 PLATFORM=""
 DISTRO="ubuntu"
@@ -65,6 +63,10 @@ case $key in
     DISTRO="$2"
     shift 2
     ;;
+    --no-cache)
+    NO_CACHE=true
+    shift
+    ;;
     -h|--help)
     print_usage
     exit 0
@@ -76,7 +78,14 @@ case $key in
 esac
 done
 
-THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+# To support Docker versions where buildx is not the default build client (i.e.,
+# versions prior to Docker Engine 23.0 and Docker Desktop 4.19).
+export DOCKER_CLI_EXPERIMENTAL=enabled
+
+if ! check_for_buildx; then
+    echoerr "Buildx is required to execute this script"
+    exit 1
+fi
 
 pushd "$THIS_DIR/.." > /dev/null
 
@@ -85,6 +94,9 @@ PLATFORM_ARG=""
 if $PUSH; then
    ARGS="$ARGS --push"
 fi
+if $NO_CACHE; then
+    ARGS="$ARGS --no-cache"
+fi
 if [ "$PLATFORM" != "" ]; then
     ARGS="$ARGS --platform $PLATFORM"
     PLATFORM_ARG="--platform $PLATFORM"
@@ -110,8 +122,7 @@ echo "BUILD_TAG: $BUILD_TAG"
 # We pull all images ahead of time, instead of calling the independent build.sh
 # scripts with "--pull". We do not want to overwrite the antrea/openvswitch
 # image we just built when calling build.sh to build the antrea/base-ubuntu
-# image! This is a bit more inconvenient to maintain, but we rarely introduce
-# new base images in the build chain.
+# image!
 if $PULL; then
     if [[ ${DOCKER_REGISTRY} == "" ]]; then
         docker pull $PLATFORM_ARG ubuntu:22.04
@@ -122,32 +133,10 @@ if $PULL; then
         docker pull ${DOCKER_REGISTRY}/antrea/golang:$GO_VERSION
         docker tag ${DOCKER_REGISTRY}/antrea/golang:$GO_VERSION golang:$GO_VERSION
     fi
-    if [ "$DISTRO" == "ubuntu" ]; then
-        IMAGES_LIST=(
-            "antrea/openvswitch-debs:$BUILD_TAG"
-            "antrea/openvswitch:$BUILD_TAG"
-            "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubuntu:$BUILD_TAG"
-        )
-    elif [ "$DISTRO" == "ubi" ]; then
-        IMAGES_LIST=(
-            "antrea/openvswitch-rpms:$BUILD_TAG"
-            "antrea/openvswitch-ubi:$BUILD_TAG"
-            "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubi:$BUILD_TAG"
-        )
+    if [ "$DISTRO" == "ubi" ]; then
+        docker pull $PLATFORM_ARG centos:centos7
+        docker pull $PLATFORM_ARG registry.access.redhat.com/ubi8
     fi
-    for image in "${IMAGES_LIST[@]}"; do
-        if [[ ${DOCKER_REGISTRY} == "" ]]; then
-            docker pull $PLATFORM_ARG "${image}" || true
-        else
-            rc=0
-            docker pull "${DOCKER_REGISTRY}/${image}" || rc=$?
-            if [[ $rc -eq 0 ]]; then
-                docker tag "${DOCKER_REGISTRY}/${image}" "${image}"
-            fi
-        fi
-    done
 fi
 
 cd build/images/ovs
@@ -158,7 +147,15 @@ cd build/images/base
 ./build.sh $ARGS
 cd -
 
+if $NO_CACHE; then
+    export NO_CACHE=1
+fi
 export NO_PULL=1
+# To support Docker versions where buildx is not the default build client (i.e.,
+# versions prior to Docker Engine 23.0 and Docker Desktop 4.19).
+# This can be removed when the Makefile is updated to use "docker buildx"
+# explicitly (note that we already set DOCKER_CLI_EXPERIMENTAL=enabled at the
+# beginning of the script).
 export DOCKER_BUILDKIT=1
 if [ "$DISTRO" == "ubuntu" ]; then
     if $COVERAGE; then
diff --git a/pkg/agent/controller/networkpolicy/l7engine/reconciler.go b/pkg/agent/controller/networkpolicy/l7engine/reconciler.go
index 2ca942055..1eb6b132f 100644
--- a/pkg/agent/controller/networkpolicy/l7engine/reconciler.go
+++ b/pkg/agent/controller/networkpolicy/l7engine/reconciler.go
@@ -76,6 +76,11 @@ outputs:
       types:
         - alert:
             tagged-packets: yes
+        - http:
+            extended: yes
+            tagged-packets: yes
+        - tls:
+            extended: yes
   - eve-log:
       enabled: yes
       filetype: unix_stream
diff --git a/pkg/agent/nodeportlocal/k8s/npl_controller.go b/pkg/agent/nodeportlocal/k8s/npl_controller.go
index f0a17ab5d..592098e7a 100644
--- a/pkg/agent/nodeportlocal/k8s/npl_controller.go
+++ b/pkg/agent/nodeportlocal/k8s/npl_controller.go
@@ -512,11 +512,10 @@ func (c *NPLController) handleAddUpdatePod(key string, obj interface{}) error {
 		}
 		if _, ok := nplAnnotationsRequiredMap[portcache.NodePortProtoFormat(nodePort, protocol)]; !ok {
 			nplAnnotationsRequiredMap[portcache.NodePortProtoFormat(nodePort, protocol)] = types.NPLAnnotation{
-				PodPort:   port,
-				NodeIP:    pod.Status.HostIP,
-				NodePort:  nodePort,
-				Protocol:  protocol,
-				Protocols: []string{protocol},
+				PodPort:  port,
+				NodeIP:   pod.Status.HostIP,
+				NodePort: nodePort,
+				Protocol: protocol,
 			}
 		}
 	}
@@ -598,11 +597,10 @@ func (c *NPLController) waitForRulesInitialization() {
 				continue
 			}
 			allNPLPorts = append(allNPLPorts, rules.PodNodePort{
-				NodePort:  npl.NodePort,
-				PodPort:   npl.PodPort,
-				PodIP:     pod.Status.PodIP,
-				Protocol:  npl.Protocol,
-				Protocols: npl.Protocols,
+				NodePort: npl.NodePort,
+				PodPort:  npl.PodPort,
+				PodIP:    pod.Status.PodIP,
+				Protocol: npl.Protocol,
 			})
 		}
 	}
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_others.go b/pkg/agent/nodeportlocal/portcache/port_table_others.go
index 5e9d7b890..30ec417e7 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_others.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_others.go
@@ -183,11 +183,10 @@ func (pt *PortTable) syncRules() error {
 			protocols = append(protocols, protocol.Protocol)
 		}
 		nplPorts = append(nplPorts, rules.PodNodePort{
-			NodePort:  npData.NodePort,
-			PodPort:   npData.PodPort,
-			PodIP:     npData.PodIP,
-			Protocol:  protocols[0],
-			Protocols: protocols,
+			NodePort: npData.NodePort,
+			PodPort:  npData.PodPort,
+			PodIP:    npData.PodIP,
+			Protocol: protocols[0],
 		})
 	}
 	if err := pt.PodPortRules.AddAllRules(nplPorts); err != nil {
diff --git a/pkg/agent/nodeportlocal/rules/iptable_rule.go b/pkg/agent/nodeportlocal/rules/iptable_rule.go
index e65d7a128..767ad3c09 100644
--- a/pkg/agent/nodeportlocal/rules/iptable_rule.go
+++ b/pkg/agent/nodeportlocal/rules/iptable_rule.go
@@ -103,11 +103,9 @@ func (ipt *iptablesRules) AddAllRules(nplList []PodNodePort) error {
 	writeLine(iptablesData, "*nat")
 	writeLine(iptablesData, iptables.MakeChainLine(NodePortLocalChain))
 	for _, nplData := range nplList {
-		for _, protocol := range nplData.Protocols {
-			destination := nplData.PodIP + ":" + fmt.Sprint(nplData.PodPort)
-			rule := buildRuleForPod(nplData.NodePort, destination, protocol)
-			writeLine(iptablesData, append([]string{"-A", NodePortLocalChain}, rule...)...)
-		}
+		destination := nplData.PodIP + ":" + fmt.Sprint(nplData.PodPort)
+		rule := buildRuleForPod(nplData.NodePort, destination, nplData.Protocol)
+		writeLine(iptablesData, append([]string{"-A", NodePortLocalChain}, rule...)...)
 	}
 	writeLine(iptablesData, "COMMIT")
 	if err := ipt.table.Restore(iptablesData.String(), false, false); err != nil {
diff --git a/pkg/agent/nodeportlocal/rules/types.go b/pkg/agent/nodeportlocal/rules/types.go
index 21b89e5eb..29768957d 100644
--- a/pkg/agent/nodeportlocal/rules/types.go
+++ b/pkg/agent/nodeportlocal/rules/types.go
@@ -16,9 +16,8 @@ package rules
 
 // PodNodePort contains the Node Port, Pod IP, Pod Port and Protocols for NodePortLocal.
 type PodNodePort struct {
-	NodePort  int
-	PodPort   int
-	PodIP     string
-	Protocol  string
-	Protocols []string
+	NodePort int
+	PodPort  int
+	PodIP    string
+	Protocol string
 }
diff --git a/pkg/agent/nodeportlocal/types/types.go b/pkg/agent/nodeportlocal/types/types.go
index b876242b3..9cce1e9c5 100644
--- a/pkg/agent/nodeportlocal/types/types.go
+++ b/pkg/agent/nodeportlocal/types/types.go
@@ -21,9 +21,8 @@ const (
 
 // NPLAnnotation is the structure used for setting NodePortLocal annotation on the Pods.
 type NPLAnnotation struct {
-	PodPort   int      `json:"podPort"`
-	NodeIP    string   `json:"nodeIP"`
-	NodePort  int      `json:"nodePort"`
-	Protocol  string   `json:"protocol"`
-	Protocols []string `json:"protocols"` // deprecated, array with a single member which is equal to the Protocol field
+	PodPort  int    `json:"podPort"`
+	NodeIP   string `json:"nodeIP"`
+	NodePort int    `json:"nodePort"`
+	Protocol string `json:"protocol"`
 }
diff --git a/pkg/config/agent/config.go b/pkg/config/agent/config.go
index ed57bd00d..21d3ac50c 100644
--- a/pkg/config/agent/config.go
+++ b/pkg/config/agent/config.go
@@ -141,8 +141,6 @@ type AgentConfig struct {
 	ActiveFlowExportTimeout string `yaml:"activeFlowExportTimeout,omitempty"`
 	// Deprecated. Use the FlowExporter config options instead.
 	IdleFlowExportTimeout string `yaml:"idleFlowExportTimeout,omitempty"`
-	// Deprecated. Use the NodePortLocal config options instead.
-	NPLPortRange string `yaml:"nplPortRange,omitempty"`
 	// NodePortLocal (NPL) configuration options.
 	NodePortLocal NodePortLocalConfig `yaml:"nodePortLocal,omitempty"`
 	// FlowExporter configuration options.


Test patch:
diff --git a/ci/test-conformance-aks.sh b/ci/test-conformance-aks.sh
index c428ad5ba..4e692a4ff 100755
--- a/ci/test-conformance-aks.sh
+++ b/ci/test-conformance-aks.sh
@@ -305,6 +305,8 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 GIT_CHECKOUT_DIR=${THIS_DIR}/..
 pushd "$THIS_DIR" > /dev/null
 
+source ${THIS_DIR}/jenkins/utils.sh
+
 if [[ "$RUN_ALL" == true || "$RUN_SETUP_ONLY" == true ]]; then
     setup_aks
     deliver_antrea_to_aks
diff --git a/ci/test-conformance-eks.sh b/ci/test-conformance-eks.sh
index 59802a36e..2059bb3f6 100755
--- a/ci/test-conformance-eks.sh
+++ b/ci/test-conformance-eks.sh
@@ -359,6 +359,8 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 GIT_CHECKOUT_DIR=${THIS_DIR}/..
 pushd "$THIS_DIR" > /dev/null
 
+source ${THIS_DIR}/jenkins/utils.sh
+
 if [[ "$RUN_ALL" == true || "$RUN_SETUP_ONLY" == true ]]; then
     setup_eks
     deliver_antrea_to_eks
diff --git a/ci/test-conformance-gke.sh b/ci/test-conformance-gke.sh
index a422864d5..6f303626a 100755
--- a/ci/test-conformance-gke.sh
+++ b/ci/test-conformance-gke.sh
@@ -148,6 +148,8 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 GIT_CHECKOUT_DIR=${THIS_DIR}/..
 pushd "$THIS_DIR" > /dev/null
 
+source ${THIS_DIR}/jenkins/utils.sh
+
 # disable gcloud prompts, e.g., when deleting resources
 export CLOUDSDK_CORE_DISABLE_PROMPTS=1
 
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_others_test.go b/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
index 25231c326..2ac40b34e 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
@@ -35,25 +35,22 @@ func TestRestoreRules(t *testing.T) {
 	portTable := newPortTable(mockIPTables, mockPortOpener)
 	allNPLPorts := []rules.PodNodePort{
 		{
-			NodePort:  nodePort1,
-			PodPort:   1001,
-			PodIP:     podIP,
-			Protocol:  "tcp",
-			Protocols: []string{"tcp"},
+			NodePort: nodePort1,
+			PodPort:  1001,
+			PodIP:    podIP,
+			Protocol: "tcp",
 		},
 		{
-			NodePort:  nodePort1,
-			PodPort:   1001,
-			PodIP:     podIP,
-			Protocol:  "udp",
-			Protocols: []string{"udp"},
+			NodePort: nodePort1,
+			PodPort:  1001,
+			PodIP:    podIP,
+			Protocol: "udp",
 		},
 		{
-			NodePort:  nodePort2,
-			PodPort:   1002,
-			PodIP:     podIP,
-			Protocol:  "udp",
-			Protocols: []string{"udp"},
+			NodePort: nodePort2,
+			PodPort:  1002,
+			PodIP:    podIP,
+			Protocol: "udp",
 		},
 	}
 
diff --git a/pkg/agent/nodeportlocal/rules/iptable_rule_test.go b/pkg/agent/nodeportlocal/rules/iptable_rule_test.go
index 4915d7495..e9887496c 100644
--- a/pkg/agent/nodeportlocal/rules/iptable_rule_test.go
+++ b/pkg/agent/nodeportlocal/rules/iptable_rule_test.go
@@ -114,6 +114,8 @@ func TestAddAndDeleteAllRules(t *testing.T) {
 				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.NATTable, iptables.OutputChain, []string{"-p", "all", "-m", "addrtype", "--dst-type", "LOCAL", "-j", NodePortLocalChain})
 				mockIPTables.Restore(`*nat
 :ANTREA-NODE-PORT-LOCAL - [0:0]
+-A ANTREA-NODE-PORT-LOCAL -p udp -m udp --dport 8227 -j DNAT --to-destination 10.10.10.11:7237
+-A ANTREA-NODE-PORT-LOCAL -p tcp -m tcp --dport 8247 -j DNAT --to-destination 10.11.10.12:7257
 COMMIT
 `, false, false)
 				// Set the expectation for ChainExists to return true
@@ -151,6 +153,9 @@ COMMIT
 				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.NATTable, iptables.OutputChain, []string{"-p", "all", "-m", "addrtype", "--dst-type", "LOCAL", "-j", NodePortLocalChain})
 				mockIPTables.Restore(`*nat
 :ANTREA-NODE-PORT-LOCAL - [0:0]
+-A ANTREA-NODE-PORT-LOCAL -p tcp -m tcp --dport 7 -j DNAT --to-destination 10.10.10.10:17
+-A ANTREA-NODE-PORT-LOCAL -p udp -m udp --dport 27 -j DNAT --to-destination 10.10.10.11:37
+-A ANTREA-NODE-PORT-LOCAL -p tcp -m tcp --dport 47 -j DNAT --to-destination 10.11.10.12:57
 COMMIT
 `, false, false)
 				// Set the expectation for ChainExists to return true
diff --git a/pkg/agent/nodeportlocal/testing/annotations.go b/pkg/agent/nodeportlocal/testing/annotations.go
index ba479b980..e98559dca 100644
--- a/pkg/agent/nodeportlocal/testing/annotations.go
+++ b/pkg/agent/nodeportlocal/testing/annotations.go
@@ -47,8 +47,7 @@ func (a *ExpectedNPLAnnotations) find(podPort int, protocol string) *types.NPLAn
 }
 
 func (a *ExpectedNPLAnnotations) Add(nodePort *int, podPort int, protocol string) *ExpectedNPLAnnotations {
-	protocols := []string{protocol}
-	annotation := types.NPLAnnotation{PodPort: podPort, Protocol: protocol, Protocols: protocols}
+	annotation := types.NPLAnnotation{PodPort: podPort, Protocol: protocol}
 	if nodePort != nil {
 		annotation.NodePort = *nodePort
 	}
@@ -66,7 +65,6 @@ func (a *ExpectedNPLAnnotations) Check(t *testing.T, nplValue []types.NPLAnnotat
 		if !assert.NotNilf(t, expectedAnnotation, "Unexpected annotation with PodPort %d", nplAnnotation.PodPort) {
 			continue
 		}
-		assert.ElementsMatch(t, expectedAnnotation.Protocols, nplAnnotation.Protocols, "Protocols mismatch in annotation")
 		if expectedAnnotation.NodeIP != "" {
 			assert.Equal(t, expectedAnnotation.NodeIP, nplAnnotation.NodeIP, "NodeIP mismatch in annotation")
 		}
diff --git a/test/e2e/nodeportlocal_test.go b/test/e2e/nodeportlocal_test.go
index fee779211..3a95ed3bf 100644
--- a/test/e2e/nodeportlocal_test.go
+++ b/test/e2e/nodeportlocal_test.go
@@ -164,16 +164,14 @@ func checkNPLRules(t *testing.T, data *TestData, r *require.Assertions, nplAnnot
 func checkNPLRulesForPod(t *testing.T, data *TestData, r *require.Assertions, nplAnnotations []types.NPLAnnotation, antreaPod, podIP string, present bool) {
 	var rules []nplRuleData
 	for _, ann := range nplAnnotations {
-		for _, protocol := range ann.Protocols {
-			rule := nplRuleData{
-				nodeIP:   ann.NodeIP,
-				nodePort: ann.NodePort,
-				podIP:    podIP,
-				podPort:  ann.PodPort,
-				protocol: protocol,
-			}
-			rules = append(rules, rule)
+		rule := nplRuleData{
+			nodeIP:   ann.NodeIP,
+			nodePort: ann.NodePort,
+			podIP:    podIP,
+			podPort:  ann.PodPort,
+			protocol: ann.Protocol,
 		}
+		rules = append(rules, rule)
 	}
 	checkForNPLRuleInIPTables(t, data, r, antreaPod, rules, present)
 	checkForNPLListeningSockets(t, data, r, antreaPod, rules, present)
@@ -182,16 +180,14 @@ func checkNPLRulesForPod(t *testing.T, data *TestData, r *require.Assertions, np
 func checkNPLRulesForWindowsPod(t *testing.T, data *TestData, r *require.Assertions, nplAnnotations []types.NPLAnnotation, antreaPod, podIP string, nodeName string, present bool) {
 	var rules []nplRuleData
 	for _, ann := range nplAnnotations {
-		for _, protocol := range ann.Protocols {
-			rule := nplRuleData{
-				nodeIP:   ann.NodeIP,
-				nodePort: ann.NodePort,
-				podIP:    podIP,
-				podPort:  ann.PodPort,
-				protocol: protocol,
-			}
-			rules = append(rules, rule)
+		rule := nplRuleData{
+			nodeIP:   ann.NodeIP,
+			nodePort: ann.NodePort,
+			podIP:    podIP,
+			podPort:  ann.PodPort,
+			protocol: ann.Protocol,
 		}
+		rules = append(rules, rule)
 	}
 	checkForNPLRuleInNetNat(t, data, r, antreaPod, nodeName, rules, present)
 }
@@ -345,10 +341,8 @@ func deleteNPLRuleFromNetNat(t *testing.T, data *TestData, r *require.Assertions
 
 func checkTrafficForNPL(data *TestData, r *require.Assertions, nplAnnotations []types.NPLAnnotation, clientName string) {
 	for i := range nplAnnotations {
-		for j := range nplAnnotations[i].Protocols {
-			err := data.runNetcatCommandFromTestPodWithProtocol(clientName, data.testNamespace, "agnhost", nplAnnotations[i].NodeIP, int32(nplAnnotations[i].NodePort), nplAnnotations[i].Protocols[j])
-			r.NoError(err, "Traffic test failed for NodeIP: %s, NodePort: %d, Protocol: %s", nplAnnotations[i].NodeIP, nplAnnotations[i].NodePort, nplAnnotations[i].Protocols[j])
-		}
+		err := data.runNetcatCommandFromTestPodWithProtocol(clientName, data.testNamespace, "agnhost", nplAnnotations[i].NodeIP, int32(nplAnnotations[i].NodePort), nplAnnotations[i].Protocol)
+		r.NoError(err, "Traffic test failed for NodeIP: %s, NodePort: %d, Protocol: %s", nplAnnotations[i].NodeIP, nplAnnotations[i].NodePort, nplAnnotations[i].Protocol)
 	}
 }
 
@@ -645,7 +639,7 @@ func testNPLMultiplePodsAgentRestart(t *testing.T, data *TestData) {
 
 // testNPLChangePortRangeAgentRestart tests NodePortLocal functionalities after changing port range.
 // - Create multiple Nginx Pods.
-// - Change nplPortRange.
+// - Change the PortRange.
 // - Restart Antrea Agent Pods.
 // - Verify that updated port range is being used for NPL.
 func testNPLChangePortRangeAgentRestart(t *testing.T, data *TestData) {
@@ -677,15 +671,13 @@ func testNPLChangePortRangeAgentRestart(t *testing.T, data *TestData) {
 	for _, testPodName := range testPods {
 		nplAnnotations, testPodIP := getNPLAnnotations(t, data, r, testPodName, nil)
 		for i := range nplAnnotations {
-			for j := range nplAnnotations[i].Protocols {
-				rule := nplRuleData{
-					nodePort: nplAnnotations[i].NodePort,
-					podIP:    testPodIP,
-					podPort:  nplAnnotations[i].PodPort,
-					protocol: nplAnnotations[i].Protocols[j],
-				}
-				rules = append(rules, rule)
+			rule := nplRuleData{
+				nodePort: nplAnnotations[i].NodePort,
+				podIP:    testPodIP,
+				podPort:  nplAnnotations[i].PodPort,
+				protocol: nplAnnotations[i].Protocol,
 			}
+			rules = append(rules, rule)
 		}
 	}
 	configureNPLForAgent(t, data, updatedStartPort, updatedEndPort)