Repository: spidernet-io/spiderpool
Pull request #1148 (2022-12-07 07:29:38)

test: Add unit tests for pkg namespacemanager
Signed-off-by: iiiceoo <ziqian.xue@daocloud.io>

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=spidernet-io_spiderpool&pullRequest=1148)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1148&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=1148) No Coverage information  
[![1.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '1.3%')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=1148&metric=new_duplicated_lines_density&view=list) [1.3% Duplication](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=1148&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/pkg/gcmanager/scanAll_IPPool.go b/pkg/gcmanager/scanAll_IPPool.go
index 6d70cf757..79639c782 100644
--- a/pkg/gcmanager/scanAll_IPPool.go
+++ b/pkg/gcmanager/scanAll_IPPool.go
@@ -193,7 +193,7 @@ func (s *SpiderGC) releaseSingleIPAndRemoveWEPFinalizer(ctx context.Context, poo
 	metrics.IPGCTotalCounts.Add(ctx, 1)
 	log.Sugar().Infof("release ip '%s' successfully", poolIP)
 
-	err = s.wepMgr.RemoveFinalizer(ctx, nil, poolIPAllocation.Namespace, poolIPAllocation.Pod)
+	err = s.wepMgr.RemoveFinalizer(ctx, poolIPAllocation.Namespace, poolIPAllocation.Pod)
 	if nil != err {
 		if apierrors.IsNotFound(err) {
 			log.Sugar().Debugf("SpiderEndpoint '%s/%s' is already cleaned up", poolIPAllocation.Namespace, poolIPAllocation.Pod)
diff --git a/pkg/gcmanager/tracePod_worker.go b/pkg/gcmanager/tracePod_worker.go
index 65cd5db58..34bc8232c 100644
--- a/pkg/gcmanager/tracePod_worker.go
+++ b/pkg/gcmanager/tracePod_worker.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/spidernet-io/spiderpool/pkg/constant"
 	metrics "github.com/spidernet-io/spiderpool/pkg/metric"
-	"github.com/spidernet-io/spiderpool/pkg/workloadendpointmanager"
 )
 
 // tracePodWorker will circle traverse PodEntry database
@@ -69,29 +68,46 @@ func (s *SpiderGC) releaseIPPoolIPExecutor(ctx context.Context, workerIndex int)
 	for {
 		select {
 		case podCache := <-s.gcIPPoolIPSignal:
-			endpoint, err := s.wepMgr.GetEndpointByName(ctx, podCache.Namespace, podCache.PodName)
+			wep, err := s.wepMgr.GetEndpointByName(ctx, podCache.Namespace, podCache.PodName)
 			if nil != err {
 				if apierrors.IsNotFound(err) {
-					loggerReleaseIP.Sugar().Infof("SpiderEndpoint '%s/%s' not found, maybe already cleaned by ScanAll", podCache.Namespace, podCache.PodName)
+					loggerReleaseIP.Sugar().Infof("wep '%s/%s' not found, maybe already cleaned", podCache.Namespace, podCache.PodName)
 					continue
 				}
 
-				loggerReleaseIP.Sugar().Errorf("failed to get SpiderEndpoint '%s/%s', error: %v", podCache.Namespace, podCache.PodName, err)
+				loggerReleaseIP.Sugar().Errorf("failed to get wep '%s/%s', error: %v", podCache.Namespace, podCache.PodName, err)
 				continue
 			}
 
-			// we need to gather the pod corresponding SpiderEndpoint to get the used history IPs.
-			podUsedIPs := workloadendpointmanager.ListAllHistoricalIPs(endpoint)
+			// when we received one gc signal which contains a pod name and its namespace,
+			// we need to search the pod corresponding SpiderEndpoint to get the used history IPs.
+			podUsedIPs, err := s.wepMgr.ListAllHistoricalIPs(ctx, podCache.Namespace, podCache.PodName)
+			if apierrors.IsNotFound(err) {
+				loggerReleaseIP.Sugar().Warnf("wep '%s/%s' not found, maybe already cleaned by execScanAll",
+					podCache.Namespace, podCache.PodName)
+				continue
+			}
+
+			if nil != err {
+				loggerReleaseIP.Sugar().Errorf("failed to list wep '%s/%s' all historical IPs: %v",
+					podCache.Namespace, podCache.PodName, err)
+				continue
+			}
 
 			// release pod used history IPs
 			for poolName, ips := range podUsedIPs {
-				loggerReleaseIP.Sugar().Infof("pod '%s/%s used IPs '%+v' from pool '%s', begin to release",
+				loggerReleaseIP.Sugar().Debugf("pod '%s/%s used IPs '%+v' from pool '%s', begin to release",
 					podCache.Namespace, podCache.PodName, ips, poolName)
 
 				err = s.ippoolMgr.ReleaseIP(ctx, poolName, ips)
+				if apierrors.IsNotFound(err) {
+					// If we can not find the IP object, which means cmdDel already released the IP
+					continue
+				}
+
 				if nil != err {
 					metrics.IPGCFailureCounts.Add(ctx, 1)
-					loggerReleaseIP.Sugar().Errorf("failed to release pool '%s' IPs '%+v' in wep '%s/%s', error: %v",
+					logger.Sugar().Errorf("failed to release pool '%s' IPs '%+v' in wep '%s/%s', error: %v",
 						poolName, ips, podCache.Namespace, podCache.PodName, err)
 					continue
 				}
@@ -102,9 +118,17 @@ func (s *SpiderGC) releaseIPPoolIPExecutor(ctx context.Context, workerIndex int)
 
 			loggerReleaseIP.Sugar().Infof("release IPPoolIP task '%+v' successfully", *podCache)
 
+			// remove wep finalizer
+			err = s.wepMgr.RemoveFinalizer(ctx, podCache.Namespace, podCache.PodName)
+			if nil != err {
+				loggerReleaseIP.Sugar().Errorf("failed to remove wep '%s/%s' finalizer, error: '%v'",
+					podCache.Namespace, podCache.PodName, err)
+				continue
+			}
+
 			// delete StatefulSet wep (other controller wep has OwnerReference, its lifecycle is same with pod)
-			if endpoint.Status.OwnerControllerType == constant.OwnerStatefulSet {
-				err = s.wepMgr.Delete(ctx, endpoint)
+			if wep.Status.OwnerControllerType == constant.OwnerStatefulSet {
+				err = s.wepMgr.Delete(ctx, wep)
 				if nil != err {
 					loggerReleaseIP.Sugar().Errorf("failed to delete StatefulSet wep '%s/%s', error: '%v'",
 						podCache.Namespace, podCache.PodName, err)
@@ -112,14 +136,7 @@ func (s *SpiderGC) releaseIPPoolIPExecutor(ctx context.Context, workerIndex int)
 				}
 			}
 
-			err = s.wepMgr.RemoveFinalizer(ctx, endpoint, podCache.Namespace, podCache.PodName)
-			if nil != err {
-				loggerReleaseIP.Sugar().Errorf("failed to remove wep '%s/%s' finalizer, error: '%v'",
-					podCache.Namespace, podCache.PodName, err)
-				continue
-			}
-
-			loggerReleaseIP.Sugar().Infof("remove wep '%s/%s' finalizer '%s' successfully",
+			loggerReleaseIP.Sugar().Debugf("remove wep '%s/%s' finalizer '%s' successfully",
 				podCache.Namespace, podCache.PodName, constant.SpiderFinalizer)
 
 		case <-ctx.Done():
diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go
index 0dd3166f4..04ee93f29 100644
--- a/pkg/ipam/ipam.go
+++ b/pkg/ipam/ipam.go
@@ -539,7 +539,7 @@ func (i *ipam) getPoolFromNS(ctx context.Context, namespace, nic string, cleanGa
 	if err != nil {
 		return nil, err
 	}
-	nsDefaultV4Pools, nsDefaultV6Pools, err := i.nsManager.GetNSDefaultPools(ctx, ns)
+	nsDefaultV4Pools, nsDefaultV6Pools, err := namespacemanager.GetNSDefaultPools(ns)
 	if err != nil {
 		return nil, err
 	}
@@ -675,6 +675,11 @@ func (i *ipam) Release(ctx context.Context, delArgs *models.IpamDelArgs) error {
 	logger := logutils.FromContext(ctx)
 	logger.Info("Start to release")
 
+	pod, err := i.podManager.GetPodByName(ctx, *delArgs.PodNamespace, *delArgs.PodName)
+	if err != nil {
+		return fmt.Errorf("failed to get Pod %s/%s: %v", *delArgs.PodNamespace, *delArgs.PodName, err)
+	}
+
 	endpoint, err := i.weManager.GetEndpointByName(ctx, *delArgs.PodNamespace, *delArgs.PodName)
 	if client.IgnoreNotFound(err) != nil {
 		return fmt.Errorf("failed to get Endpoint %s/%s: %v", *delArgs.PodNamespace, *delArgs.PodName, err)
@@ -690,8 +695,9 @@ func (i *ipam) Release(ctx context.Context, delArgs *models.IpamDelArgs) error {
 
 	// check whether the StatefulSet pod need to release
 	// ref: https://github.com/spidernet-io/spiderpool/issues/1045
-	if i.config.EnableStatefulSet && endpoint.Status.OwnerControllerType == constant.OwnerStatefulSet {
-		shouldCleanSts, err := i.shouldReleaseStatefulSet(ctx, endpoint.Namespace, endpoint.Name, allocation, currently)
+	ownerControllerType, _ := podmanager.GetOwnerControllerType(pod)
+	if i.config.EnableStatefulSet && ownerControllerType == constant.OwnerStatefulSet {
+		shouldCleanSts, err := i.shouldReleaseStatefulSet(ctx, pod, allocation, currently)
 		if nil != err {
 			return err
 		}
@@ -715,10 +721,10 @@ func (i *ipam) Release(ctx context.Context, delArgs *models.IpamDelArgs) error {
 // shouldReleaseStatefulSet checks whether the StatefulSet pod need to be released, if the StatefulSet object was deleted or decreased its replicas.
 // And we'll also check whether the StatefulSet pod's last ipam allocation is invalid or not,
 // if we set dual stack but only get one IP allocation, we should clean up it.
-func (i *ipam) shouldReleaseStatefulSet(ctx context.Context, podNamespace, podName string, allocation *spiderpoolv1.PodIPAllocation, currently bool) (bool, error) {
+func (i *ipam) shouldReleaseStatefulSet(ctx context.Context, pod *corev1.Pod, allocation *spiderpoolv1.PodIPAllocation, currently bool) (bool, error) {
 	log := logutils.FromContext(ctx)
 
-	isValidStsPod, err := i.stsManager.IsValidStatefulSetPod(ctx, podNamespace, podName, constant.OwnerStatefulSet)
+	isValidStsPod, err := i.stsManager.IsValidStatefulSetPod(ctx, pod.Namespace, pod.Name, constant.OwnerStatefulSet)
 	if err != nil {
 		return false, err
 	}
diff --git a/pkg/namespacemanager/namespace_manager.go b/pkg/namespacemanager/namespace_manager.go
index 52b300d56..b17433e21 100644
--- a/pkg/namespacemanager/namespace_manager.go
+++ b/pkg/namespacemanager/namespace_manager.go
@@ -5,8 +5,7 @@ package namespacemanager
 
 import (
 	"context"
-	"encoding/json"
-	"errors"
+	"fmt"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -14,13 +13,11 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/spidernet-io/spiderpool/pkg/constant"
-	"github.com/spidernet-io/spiderpool/pkg/types"
 )
 
 type NamespaceManager interface {
 	GetNamespaceByName(ctx context.Context, nsName string) (*corev1.Namespace, error)
 	ListNamespaces(ctx context.Context, opts ...client.ListOption) (*corev1.NamespaceList, error)
-	GetNSDefaultPools(ctx context.Context, ns *corev1.Namespace) ([]string, []string, error)
 	MatchLabelSelector(ctx context.Context, nsName string, labelSelector *metav1.LabelSelector) (bool, error)
 }
 
@@ -30,7 +27,7 @@ type namespaceManager struct {
 
 func NewNamespaceManager(client client.Client) (NamespaceManager, error) {
 	if client == nil {
-		return nil, errors.New("k8s client must be specified")
+		return nil, fmt.Errorf("k8s client %w", constant.ErrMissingRequiredParam)
 	}
 
 	return &namespaceManager{
@@ -56,24 +53,6 @@ func (nm *namespaceManager) ListNamespaces(ctx context.Context, opts ...client.L
 	return &nsList, nil
 }
 
-func (nm *namespaceManager) GetNSDefaultPools(ctx context.Context, ns *corev1.Namespace) ([]string, []string, error) {
-	var nsDefaultV4Pool types.AnnoNSDefautlV4PoolValue
-	var nsDefaultV6Pool types.AnnoNSDefautlV6PoolValue
-	if v, ok := ns.Annotations[constant.AnnoNSDefautlV4Pool]; ok {
-		if err := json.Unmarshal([]byte(v), &nsDefaultV4Pool); err != nil {
-			return nil, nil, err
-		}
-	}
-
-	if v, ok := ns.Annotations[constant.AnnoNSDefautlV6Pool]; ok {
-		if err := json.Unmarshal([]byte(v), &nsDefaultV6Pool); err != nil {
-			return nil, nil, err
-		}
-	}
-
-	return nsDefaultV4Pool, nsDefaultV6Pool, nil
-}
-
 func (nm *namespaceManager) MatchLabelSelector(ctx context.Context, nsName string, labelSelector *metav1.LabelSelector) (bool, error) {
 	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
 	if err != nil {
diff --git a/pkg/namespacemanager/utils.go b/pkg/namespacemanager/utils.go
new file mode 100644
index 000000000..348bc6f8f
--- /dev/null
+++ b/pkg/namespacemanager/utils.go
@@ -0,0 +1,31 @@
+// Copyright 2022 Authors of spidernet-io
+// SPDX-License-Identifier: Apache-2.0
+
+package namespacemanager
+
+import (
+	"encoding/json"
+
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/spidernet-io/spiderpool/pkg/constant"
+	"github.com/spidernet-io/spiderpool/pkg/types"
+)
+
+func GetNSDefaultPools(ns *corev1.Namespace) ([]string, []string, error) {
+	var nsDefaultV4Pool types.AnnoNSDefautlV4PoolValue
+	var nsDefaultV6Pool types.AnnoNSDefautlV6PoolValue
+	if v, ok := ns.Annotations[constant.AnnoNSDefautlV4Pool]; ok {
+		if err := json.Unmarshal([]byte(v), &nsDefaultV4Pool); err != nil {
+			return nil, nil, err
+		}
+	}
+
+	if v, ok := ns.Annotations[constant.AnnoNSDefautlV6Pool]; ok {
+		if err := json.Unmarshal([]byte(v), &nsDefaultV6Pool); err != nil {
+			return nil, nil, err
+		}
+	}
+
+	return nsDefaultV4Pool, nsDefaultV6Pool, nil
+}
diff --git a/pkg/workloadendpointmanager/utils.go b/pkg/workloadendpointmanager/utils.go
index a4109fb8c..fb6945782 100644
--- a/pkg/workloadendpointmanager/utils.go
+++ b/pkg/workloadendpointmanager/utils.go
@@ -3,13 +3,7 @@
 
 package workloadendpointmanager
 
-import (
-	"strings"
-
-	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/spiderpool.spidernet.io/v1"
-	"github.com/spidernet-io/spiderpool/pkg/logutils"
-	"github.com/spidernet-io/spiderpool/pkg/types"
-)
+import spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/spiderpool.spidernet.io/v1"
 
 func RetrieveIPAllocation(containerID, nic string, includeHistory bool, we *spiderpoolv1.SpiderEndpoint) (*spiderpoolv1.PodIPAllocation, bool) {
 	if we == nil || we.Status.Current == nil {
@@ -73,45 +67,3 @@ func mergeIPDetails(target, delta *spiderpoolv1.IPAllocationDetail) {
 
 	target.Routes = append(target.Routes, delta.Routes...)
 }
-
-// ListAllHistoricalIPs collect wep history IPs and classify them with each pool name.
-func ListAllHistoricalIPs(se *spiderpoolv1.SpiderEndpoint) map[string][]types.IPAndCID {
-	// key: IPPool name
-	// value: usedIP and container ID
-	wepHistoryIPs := make(map[string][]types.IPAndCID)
-
-	recordHistoryIPs := func(poolName, ipAndCIDR *string, containerID string) {
-		if poolName != nil {
-			if ipAndCIDR == nil {
-				logutils.Logger.Sugar().Errorf("SpiderEndpoint data broken, pod '%s/%s' containerID '%s' used ippool '%s' with no ip",
-					se.Namespace, se.Name, containerID, *poolName)
-
-				return
-			}
-
-			ip, _, _ := strings.Cut(*ipAndCIDR, "/")
-
-			ips, ok := wepHistoryIPs[*poolName]
-			if !ok {
-				ips = []types.IPAndCID{{IP: ip, ContainerID: containerID}}
-			} else {
-				ips = append(ips, types.IPAndCID{IP: ip, ContainerID: containerID})
-			}
-			wepHistoryIPs[*poolName] = ips
-		}
-	}
-
-	// circle to traverse each allocation
-	for _, PodIPAllocation := range se.Status.History {
-		// circle to traverse each NIC
-		for _, ipAllocationDetail := range PodIPAllocation.IPs {
-			// collect IPv4
-			recordHistoryIPs(ipAllocationDetail.IPv4Pool, ipAllocationDetail.IPv4, PodIPAllocation.ContainerID)
-
-			// collect IPv6
-			recordHistoryIPs(ipAllocationDetail.IPv6Pool, ipAllocationDetail.IPv6, PodIPAllocation.ContainerID)
-		}
-	}
-
-	return wepHistoryIPs
-}
diff --git a/pkg/workloadendpointmanager/workloadendpoint_manager.go b/pkg/workloadendpointmanager/workloadendpoint_manager.go
index 549e966f3..66f262508 100644
--- a/pkg/workloadendpointmanager/workloadendpoint_manager.go
+++ b/pkg/workloadendpointmanager/workloadendpoint_manager.go
@@ -23,6 +23,7 @@ import (
 	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/spiderpool.spidernet.io/v1"
 	"github.com/spidernet-io/spiderpool/pkg/logutils"
 	"github.com/spidernet-io/spiderpool/pkg/podmanager"
+	"github.com/spidernet-io/spiderpool/pkg/types"
 )
 
 type WorkloadEndpointManager interface {
@@ -33,7 +34,8 @@ type WorkloadEndpointManager interface {
 	MarkIPAllocation(ctx context.Context, containerID string, we *spiderpoolv1.SpiderEndpoint, pod *corev1.Pod) (*spiderpoolv1.SpiderEndpoint, error)
 	PatchIPAllocation(ctx context.Context, allocation *spiderpoolv1.PodIPAllocation, we *spiderpoolv1.SpiderEndpoint) error
 	ClearCurrentIPAllocation(ctx context.Context, containerID string, we *spiderpoolv1.SpiderEndpoint) error
-	RemoveFinalizer(ctx context.Context, se *spiderpoolv1.SpiderEndpoint, namespace, podName string) error
+	RemoveFinalizer(ctx context.Context, namespace, podName string) error
+	ListAllHistoricalIPs(ctx context.Context, namespace, podName string) (map[string][]types.IPAndCID, error)
 	IsIPBelongWEPCurrent(ctx context.Context, namespace, podName, poolIP string) (bool, error)
 	CheckCurrentContainerID(ctx context.Context, namespace, podName, containerID string) (bool, error)
 	UpdateCurrentStatus(ctx context.Context, containerID string, pod *corev1.Pod) error
@@ -187,22 +189,19 @@ func (em *workloadEndpointManager) ClearCurrentIPAllocation(ctx context.Context,
 }
 
 // RemoveFinalizer removes a specific finalizer field in finalizers string array.
-func (em *workloadEndpointManager) RemoveFinalizer(ctx context.Context, se *spiderpoolv1.SpiderEndpoint, namespace, podName string) error {
-	var err error
+func (em *workloadEndpointManager) RemoveFinalizer(ctx context.Context, namespace, podName string) error {
 	for i := 0; i <= em.config.MaxConflictRetries; i++ {
-		if se == nil {
-			se, err = em.GetEndpointByName(ctx, namespace, podName)
-			if err != nil {
-				return err
-			}
+		we, err := em.GetEndpointByName(ctx, namespace, podName)
+		if err != nil {
+			return err
 		}
 
-		if !controllerutil.ContainsFinalizer(se, constant.SpiderFinalizer) {
+		if !controllerutil.ContainsFinalizer(we, constant.SpiderFinalizer) {
 			return nil
 		}
 
-		controllerutil.RemoveFinalizer(se, constant.SpiderFinalizer)
-		if err := em.client.Update(ctx, se); err != nil {
+		controllerutil.RemoveFinalizer(we, constant.SpiderFinalizer)
+		if err := em.client.Update(ctx, we); err != nil {
 			if !apierrors.IsConflict(err) {
 				return err
 			}
@@ -218,6 +217,49 @@ func (em *workloadEndpointManager) RemoveFinalizer(ctx context.Context, se *spid
 	return nil
 }
 
+// ListAllHistoricalIPs collect wep history IPs and classify them with each pool name.
+func (em *workloadEndpointManager) ListAllHistoricalIPs(ctx context.Context, namespace, podName string) (map[string][]types.IPAndCID, error) {
+	wep, err := em.GetEndpointByName(ctx, namespace, podName)
+	if err != nil {
+		return nil, err
+	}
+
+	recordHistoryIPs := func(historyIPs map[string][]types.IPAndCID, poolName, ipAndCIDR *string, podName, podNS, containerID string) {
+		if poolName != nil {
+			if ipAndCIDR == nil {
+				logutils.Logger.Sugar().Errorf("WEP data broken, pod '%s/%s' containerID '%s' used ippool '%s' with no ip", podNS, podName, containerID, *poolName)
+				return
+			}
+
+			ip, _, _ := strings.Cut(*ipAndCIDR, "/")
+
+			ips, ok := historyIPs[*poolName]
+			if !ok {
+				ips = []types.IPAndCID{{IP: ip, ContainerID: containerID}}
+			} else {
+				ips = append(ips, types.IPAndCID{IP: ip, ContainerID: containerID})
+			}
+			historyIPs[*poolName] = ips
+		}
+	}
+
+	wepHistoryIPs := make(map[string][]types.IPAndCID)
+
+	// circle to traverse each allocation
+	for _, PodIPAllocation := range wep.Status.History {
+		// circle to traverse each NIC
+		for _, ipAllocationDetail := range PodIPAllocation.IPs {
+			// collect IPv4
+			recordHistoryIPs(wepHistoryIPs, ipAllocationDetail.IPv4Pool, ipAllocationDetail.IPv4, wep.Name, wep.Namespace, PodIPAllocation.ContainerID)
+
+			// collect IPv6
+			recordHistoryIPs(wepHistoryIPs, ipAllocationDetail.IPv6Pool, ipAllocationDetail.IPv6, wep.Name, wep.Namespace, PodIPAllocation.ContainerID)
+		}
+	}
+
+	return wepHistoryIPs, nil
+}
+
 // IsIPBelongWEPCurrent will check the given IP whether belong to the wep current IPs.
 func (em *workloadEndpointManager) IsIPBelongWEPCurrent(ctx context.Context, namespace, podName, poolIP string) (bool, error) {
 	wep, err := em.GetEndpointByName(ctx, namespace, podName)


Test patch:
diff --git a/pkg/namespacemanager/namespace_manager_suite_test.go b/pkg/namespacemanager/namespace_manager_suite_test.go
new file mode 100644
index 000000000..36b4baf80
--- /dev/null
+++ b/pkg/namespacemanager/namespace_manager_suite_test.go
@@ -0,0 +1,39 @@
+// Copyright 2022 Authors of spidernet-io
+// SPDX-License-Identifier: Apache-2.0
+
+package namespacemanager_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/spidernet-io/spiderpool/pkg/namespacemanager"
+)
+
+func TestNamespaceManager(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "NamespaceManager Suite", Label("namespacemanager", "unitest"))
+}
+
+var scheme *runtime.Scheme
+var fakeClient client.Client
+var nsManager namespacemanager.NamespaceManager
+
+var _ = BeforeSuite(func() {
+	scheme = runtime.NewScheme()
+	err := corev1.AddToScheme(scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	fakeClient = fake.NewClientBuilder().
+		WithScheme(scheme).
+		Build()
+
+	nsManager, err = namespacemanager.NewNamespaceManager(fakeClient)
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/pkg/namespacemanager/namespace_manager_test.go b/pkg/namespacemanager/namespace_manager_test.go
index 5f13e08d0..a20c24e3c 100644
--- a/pkg/namespacemanager/namespace_manager_test.go
+++ b/pkg/namespacemanager/namespace_manager_test.go
@@ -4,9 +4,217 @@
 package namespacemanager_test
 
 import (
+	"context"
+	"fmt"
+	"sync/atomic"
+
+	"github.com/agiledragon/gomonkey/v2"
 	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/spidernet-io/spiderpool/pkg/constant"
+	"github.com/spidernet-io/spiderpool/pkg/namespacemanager"
 )
 
-var _ = Describe("Namespacemanager", Label("unittest", "namespacemanager_test"), func() {
+var _ = Describe("NamespaceManager", Label("namespace_manager_test"), func() {
+	Describe("New NamespaceManager", func() {
+		It("inputs nil client", func() {
+			manager, err := namespacemanager.NewNamespaceManager(nil)
+			Expect(err).To(MatchError(constant.ErrMissingRequiredParam))
+			Expect(manager).To(BeNil())
+		})
+	})
+
+	Describe("Test NamespaceManager's method", func() {
+		var count uint64
+		var nsName string
+		var labels map[string]string
+		var nsT *corev1.Namespace
+
+		BeforeEach(func() {
+			atomic.AddUint64(&count, 1)
+			nsName = fmt.Sprintf("namespace-%v", count)
+			labels = map[string]string{"foo": fmt.Sprintf("bar-%v", count)}
+			nsT = &corev1.Namespace{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Namespace",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:   nsName,
+					Labels: labels,
+				},
+				Spec: corev1.NamespaceSpec{},
+			}
+		})
+
+		var deleteOption *client.DeleteOptions
+
+		AfterEach(func() {
+			zero := int64(0)
+			policy := metav1.DeletePropagationForeground
+			deleteOption = &client.DeleteOptions{
+				GracePeriodSeconds: &zero,
+				PropagationPolicy:  &policy,
+			}
+
+			ctx := context.TODO()
+			err := fakeClient.Delete(ctx, nsT, deleteOption)
+			Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred())
+		})
+
+		Describe("GetNamespaceByName", func() {
+			It("gets non-existent Namespace", func() {
+				ctx := context.TODO()
+				ns, err := nsManager.GetNamespaceByName(ctx, nsName)
+				Expect(apierrors.IsNotFound(err)).To(BeTrue())
+				Expect(ns).To(BeNil())
+			})
+
+			It("gets an existing Namespace", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				ns, err := nsManager.GetNamespaceByName(ctx, nsName)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(ns).NotTo(BeNil())
+
+				Expect(ns).To(Equal(nsT))
+			})
+		})
+
+		Describe("ListNamespaces", func() {
+			It("failed to list Namespaces due to some unknown errors", func() {
+				patches := gomonkey.ApplyMethodReturn(fakeClient, "List", constant.ErrUnknown)
+				defer patches.Reset()
+
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nsList, err := nsManager.ListNamespaces(ctx)
+				Expect(err).To(MatchError(constant.ErrUnknown))
+				Expect(nsList).To(BeNil())
+			})
+
+			It("lists all Namespaces", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nsList, err := nsManager.ListNamespaces(ctx)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(nsList.Items).NotTo(BeEmpty())
+
+				hasNS := false
+				for _, ns := range nsList.Items {
+					if ns.Name == nsName {
+						hasNS = true
+						break
+					}
+				}
+				Expect(hasNS).To(BeTrue())
+			})
+
+			It("filters results by label selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nsList, err := nsManager.ListNamespaces(ctx, client.MatchingLabels(labels))
+				Expect(err).NotTo(HaveOccurred())
+				Expect(nsList.Items).NotTo(BeEmpty())
+
+				hasNS := false
+				for _, ns := range nsList.Items {
+					if ns.Name == nsName {
+						hasNS = true
+						break
+					}
+				}
+				Expect(hasNS).To(BeTrue())
+			})
+
+			It("filters results by field selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nsList, err := nsManager.ListNamespaces(ctx, client.MatchingFields{metav1.ObjectNameField: nsName})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(nsList.Items).NotTo(BeEmpty())
+
+				hasNS := false
+				for _, ns := range nsList.Items {
+					if ns.Name == nsName {
+						hasNS = true
+						break
+					}
+				}
+				Expect(hasNS).To(BeTrue())
+			})
+		})
+
+		Describe("MatchLabelSelector", func() {
+			It("failed to list Namespace due to some unknown errors", func() {
+				patches := gomonkey.ApplyMethodReturn(fakeClient, "List", constant.ErrUnknown)
+				defer patches.Reset()
+
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				match, err := nsManager.MatchLabelSelector(ctx, nsName, &metav1.LabelSelector{MatchLabels: labels})
+				Expect(err).To(MatchError(constant.ErrUnknown))
+				Expect(match).To(BeFalse())
+			})
+
+			It("checks non-existent Namespace", func() {
+				ctx := context.TODO()
+				match, err := nsManager.MatchLabelSelector(ctx, nsName, &metav1.LabelSelector{MatchLabels: labels})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(match).To(BeFalse())
+			})
+
+			It("matches invalid label selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				invalidSelector := &metav1.LabelSelector{
+					MatchLabels: map[string]string{
+						"": "",
+					},
+				}
+				match, err := nsManager.MatchLabelSelector(ctx, nsName, invalidSelector)
+				Expect(err).To(HaveOccurred())
+				Expect(match).To(BeFalse())
+			})
+
+			It("matches nothing", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
+
+				match, err := nsManager.MatchLabelSelector(ctx, nsName, nil)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(match).To(BeFalse())
+			})
+
+			It("matches the label selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nsT)
+				Expect(err).NotTo(HaveOccurred())
 
+				match, err := nsManager.MatchLabelSelector(ctx, nsName, &metav1.LabelSelector{MatchLabels: labels})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(match).To(BeTrue())
+			})
+		})
+	})
 })
diff --git a/pkg/namespacemanager/namespacemanager_suite_test.go b/pkg/namespacemanager/namespacemanager_suite_test.go
deleted file mode 100644
index 847e998be..000000000
--- a/pkg/namespacemanager/namespacemanager_suite_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2022 Authors of spidernet-io
-// SPDX-License-Identifier: Apache-2.0
-
-package namespacemanager_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-)
-
-func TestNamespacemanager(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Namespacemanager Suite")
-}
diff --git a/pkg/namespacemanager/utils_test.go b/pkg/namespacemanager/utils_test.go
new file mode 100644
index 000000000..c1af36b1d
--- /dev/null
+++ b/pkg/namespacemanager/utils_test.go
@@ -0,0 +1,87 @@
+// Copyright 2022 Authors of spidernet-io
+// SPDX-License-Identifier: Apache-2.0
+
+package namespacemanager_test
+
+import (
+	"fmt"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/spidernet-io/spiderpool/pkg/constant"
+	"github.com/spidernet-io/spiderpool/pkg/namespacemanager"
+)
+
+var _ = Describe("NamespaceManager utils", Label("namespace_manager_utils_test"), func() {
+	Describe("Test GetNSDefaultPools", func() {
+		var v4Pool1, v4Pool2 string
+		var v6Pool1, v6Pool2 string
+		var nsT *corev1.Namespace
+
+		BeforeEach(func() {
+			v4Pool1, v4Pool2 = "ns-default-ipv4-ippool1", "ns-default-ipv4-ippool2"
+			v6Pool1, v6Pool2 = "ns-default-ipv6-ippool1", "ns-default-ipv6-ippool2"
+
+			nsT = &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "namespace",
+				},
+				Spec: corev1.NamespaceSpec{},
+			}
+		})
+
+		It("inputs invalid annotation ipam.spidernet.io/default-ipv4-ippool", func() {
+			nsT.SetAnnotations(map[string]string{
+				constant.AnnoNSDefautlV4Pool: "invalid value",
+			})
+			nsDefaultV4Pools, nsDefaultV6Pools, err := namespacemanager.GetNSDefaultPools(nsT)
+			Expect(err).To(HaveOccurred())
+			Expect(nsDefaultV4Pools).To(BeEmpty())
+			Expect(nsDefaultV6Pools).To(BeEmpty())
+		})
+
+		It("inputs invalid annotation ipam.spidernet.io/default-ipv6-ippool", func() {
+			nsT.SetAnnotations(map[string]string{
+				constant.AnnoNSDefautlV6Pool: "invalid value",
+			})
+			nsDefaultV4Pools, nsDefaultV6Pools, err := namespacemanager.GetNSDefaultPools(nsT)
+			Expect(err).To(HaveOccurred())
+			Expect(nsDefaultV4Pools).To(BeEmpty())
+			Expect(nsDefaultV6Pools).To(BeEmpty())
+		})
+
+		It("sets the IPv4 namespace default pools", func() {
+			nsT.SetAnnotations(map[string]string{
+				constant.AnnoNSDefautlV4Pool: fmt.Sprintf(`["%s", "%s"]`, v4Pool1, v4Pool2),
+			})
+			nsDefaultV4Pools, nsDefaultV6Pools, err := namespacemanager.GetNSDefaultPools(nsT)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(nsDefaultV4Pools).To(Equal([]string{v4Pool1, v4Pool2}))
+			Expect(nsDefaultV6Pools).To(BeEmpty())
+		})
+
+		It("sets the IPv6 namespace default pools", func() {
+			nsT.SetAnnotations(map[string]string{
+				constant.AnnoNSDefautlV6Pool: fmt.Sprintf(`["%s", "%s"]`, v6Pool1, v6Pool2),
+			})
+			nsDefaultV4Pools, nsDefaultV6Pools, err := namespacemanager.GetNSDefaultPools(nsT)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(nsDefaultV4Pools).To(BeEmpty())
+			Expect(nsDefaultV6Pools).To(Equal([]string{v6Pool1, v6Pool2}))
+		})
+
+		It("sets the dual-stack namespace default pools", func() {
+			nsT.SetAnnotations(map[string]string{
+				constant.AnnoNSDefautlV4Pool: fmt.Sprintf(`["%s", "%s"]`, v4Pool1, v4Pool2),
+				constant.AnnoNSDefautlV6Pool: fmt.Sprintf(`["%s"]`, v6Pool1),
+			})
+			nsDefaultV4Pools, nsDefaultV6Pools, err := namespacemanager.GetNSDefaultPools(nsT)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(nsDefaultV4Pools).To(Equal([]string{v4Pool1, v4Pool2}))
+			Expect(nsDefaultV6Pools).To(Equal([]string{v6Pool1}))
+		})
+	})
+})