Skip to content

Commit 4aa36ae

Browse files
Copilotrejain456rbtr
authored
Fix CNS IP demand overcounting by filtering terminal Pods in IPAMv2 (#3697)
* Initial plan for issue * Fix CNS IP demand overcounting by filtering terminal Pods in IPAMv2 Co-authored-by: rejain456 <[email protected]> * Fix gocritic rangeValCopy performance issue in PodIPDemandListener Co-authored-by: rejain456 <[email protected]> * Implement server-side Pod filtering with indexer for status.phase Co-authored-by: rbtr <[email protected]> * Revert to client-side filtering strategy while keeping status.phase indexer Co-authored-by: rbtr <[email protected]> * Fix linting issues: remove trailing blank lines in modified files Co-authored-by: rbtr <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: rejain456 <[email protected]> Co-authored-by: rbtr <[email protected]>
1 parent 62c335a commit 4aa36ae

File tree

3 files changed

+121
-2
lines changed

3 files changed

+121
-2
lines changed

cns/ipampool/v2/adapter.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ func (m *adapter) GetStateSnapshot() cns.IpamPoolMonitorStateSnapshot {
3131

3232
func PodIPDemandListener(ch chan<- int) func([]v1.Pod) {
3333
return func(pods []v1.Pod) {
34-
ch <- len(pods)
34+
// Filter out Pods in terminal phases (Succeeded/Failed) since they no longer
35+
// have network sandboxes and don't contribute to IP demand
36+
activePods := 0
37+
for i := range pods {
38+
if pods[i].Status.Phase != v1.PodSucceeded && pods[i].Status.Phase != v1.PodFailed {
39+
activePods++
40+
}
41+
}
42+
ch <- activePods
3543
}
3644
}

cns/ipampool/v2/adapter_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package v2
2+
3+
import (
4+
"testing"
5+
6+
v1 "k8s.io/api/core/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
func TestPodIPDemandListener(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
pods []v1.Pod
14+
expected int
15+
}{
16+
{
17+
name: "empty pod list",
18+
pods: []v1.Pod{},
19+
expected: 0,
20+
},
21+
{
22+
name: "single running pod",
23+
pods: []v1.Pod{
24+
{
25+
ObjectMeta: metav1.ObjectMeta{Name: "pod1"},
26+
Status: v1.PodStatus{Phase: v1.PodRunning},
27+
},
28+
},
29+
expected: 1,
30+
},
31+
{
32+
name: "multiple running pods",
33+
pods: []v1.Pod{
34+
{
35+
ObjectMeta: metav1.ObjectMeta{Name: "pod1"},
36+
Status: v1.PodStatus{Phase: v1.PodRunning},
37+
},
38+
{
39+
ObjectMeta: metav1.ObjectMeta{Name: "pod2"},
40+
Status: v1.PodStatus{Phase: v1.PodPending},
41+
},
42+
},
43+
expected: 2,
44+
},
45+
{
46+
name: "mix of running and terminal pods - should exclude terminal",
47+
pods: []v1.Pod{
48+
{
49+
ObjectMeta: metav1.ObjectMeta{Name: "pod1"},
50+
Status: v1.PodStatus{Phase: v1.PodRunning},
51+
},
52+
{
53+
ObjectMeta: metav1.ObjectMeta{Name: "pod2"},
54+
Status: v1.PodStatus{Phase: v1.PodSucceeded},
55+
},
56+
{
57+
ObjectMeta: metav1.ObjectMeta{Name: "pod3"},
58+
Status: v1.PodStatus{Phase: v1.PodFailed},
59+
},
60+
{
61+
ObjectMeta: metav1.ObjectMeta{Name: "pod4"},
62+
Status: v1.PodStatus{Phase: v1.PodPending},
63+
},
64+
},
65+
expected: 2, // Only pod1 (Running) and pod4 (Pending) should be counted
66+
},
67+
{
68+
name: "only terminal pods - should count zero",
69+
pods: []v1.Pod{
70+
{
71+
ObjectMeta: metav1.ObjectMeta{Name: "pod1"},
72+
Status: v1.PodStatus{Phase: v1.PodSucceeded},
73+
},
74+
{
75+
ObjectMeta: metav1.ObjectMeta{Name: "pod2"},
76+
Status: v1.PodStatus{Phase: v1.PodFailed},
77+
},
78+
},
79+
expected: 0,
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
ch := make(chan int, 1)
86+
listener := PodIPDemandListener(ch)
87+
88+
listener(tt.pods)
89+
90+
select {
91+
case result := <-ch:
92+
if result != tt.expected {
93+
t.Errorf("expected %d, got %d", tt.expected, result)
94+
}
95+
default:
96+
t.Error("expected value in channel")
97+
}
98+
})
99+
}
100+
}

cns/kubecontroller/pod/reconciler.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type limiter interface {
5050
Allow() bool
5151
}
5252

53-
// NotifierFunc returns a reconcile.Func that lists Pods to get the latest
53+
// NewNotifierFunc returns a reconcile.Func that lists Pods to get the latest
5454
// state and notifies listeners of the resulting Pods.
5555
// listOpts are passed to the client.List call to filter the Pod list.
5656
// limiter is an optional rate limiter which may be used to limit the
@@ -88,12 +88,23 @@ var hostNetworkIndexer = client.IndexerFunc(func(o client.Object) []string {
8888
return []string{strconv.FormatBool(pod.Spec.HostNetwork)}
8989
})
9090

91+
var statusPhaseIndexer = client.IndexerFunc(func(o client.Object) []string {
92+
pod, ok := o.(*v1.Pod)
93+
if !ok {
94+
return nil
95+
}
96+
return []string{string(pod.Status.Phase)}
97+
})
98+
9199
// SetupWithManager Sets up the reconciler with a new manager, filtering using NodeNetworkConfigFilter on nodeName.
92100
func (p *watcher) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
93101
p.cli = mgr.GetClient()
94102
if err := mgr.GetFieldIndexer().IndexField(ctx, &v1.Pod{}, "spec.hostNetwork", hostNetworkIndexer); err != nil {
95103
return errors.Wrap(err, "failed to set up hostNetwork indexer")
96104
}
105+
if err := mgr.GetFieldIndexer().IndexField(ctx, &v1.Pod{}, "status.phase", statusPhaseIndexer); err != nil {
106+
return errors.Wrap(err, "failed to set up status.phase indexer")
107+
}
97108
if err := ctrl.NewControllerManagedBy(mgr).
98109
For(&v1.Pod{}).
99110
WithEventFilter(predicate.Funcs{ // we only want create/delete events

0 commit comments

Comments
 (0)