Skip to content

Commit 0348638

Browse files
committed
cilium-cli: skip identity-less endpoints in policy-revision wait
After applying a policy, the connectivity test waits for every endpoint on each Cilium node to realize the new revision before running the scenario. The wait polls "cilium endpoint list" and counts an endpoint as not-ready whenever its realized policy revision is missing or below the target. That count includes every endpoint on the node, not just the ones the policy was applied to. A pod that was just scheduled onto the node shows up in the list before it has been assigned a security identity. The agent reports such an endpoint with a nil policy status (GetPolicyModel returns nil while SecurityIdentity is nil) and a nil identity, so it is counted as "not realized" even though it has computed no policy at all and is enforcing no revision. Under --test-concurrency>1 a sibling test namespace is frequently creating a pod on the shared node, so there is almost always such an endpoint in flight and the wait can never reach zero: it burns the entire PolicyWaitTimeout and then fails setup, either with "N endpoints have not yet realized policy revision R" or, when the deadline lands during the endpoint-list exec, with an opaque "context deadline exceeded". On AKS with IPsec the extra regeneration latency and high concurrency make a fresh uninitialized endpoint almost always present during the poll. This is what cilium#46877 was chasing when it dropped the node-wide Ready requirement, but the residual uninitialized case remained. Skip endpoints that have no security identity yet (nil Status.Identity). An endpoint without an identity has computed no policy, so skipping it cannot let the test run against an endpoint still enforcing an old policy; once it gets an identity, regeneration computes policy at the current revision and it is counted again. Endpoints that do have an identity are counted as before, so a genuinely lagging endpoint still holds the wait. This corrects what is counted rather than bumping the timeout, which would only hide a truly stuck endpoint. Add a table-driven unit test over the counting logic covering the nil-identity skip, the identity-with-stale-revision count, and the identity-at-revision no-count cases. Seen in https://github.com/cilium/cilium/actions/runs/29349504650 AIL:3 Signed-off-by: André Martins <andre@cilium.io>
1 parent ce46fbf commit 0348638

2 files changed

Lines changed: 132 additions & 4 deletions

File tree

cilium-cli/connectivity/check/policy.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
policyv1alpha2 "sigs.k8s.io/network-policy-api/apis/v1alpha2"
2222

2323
flowpb "github.com/cilium/cilium/api/v1/flow"
24+
"github.com/cilium/cilium/api/v1/models"
2425
"github.com/cilium/cilium/cilium-cli/defaults"
2526
"github.com/cilium/cilium/cilium-cli/k8s"
2627
"github.com/cilium/cilium/cilium-cli/utils/features"
@@ -129,23 +130,34 @@ func waitCiliumPolicyRevision(ctx context.Context, pod Pod, rev int, timeout tim
129130
}
130131

131132
// endpointsBelowPolicyRevision returns how many endpoints managed by the given
132-
// Cilium pod have not yet realized the given policy revision. Endpoints with an
133-
// unknown realized revision are counted as not ready.
133+
// Cilium pod have not yet realized the given policy revision.
134134
func endpointsBelowPolicyRevision(ctx context.Context, pod Pod, rev int) (int, error) {
135135
eps, err := pod.K8sClient.CiliumDbgEndpoints(ctx, pod.Pod.Namespace, pod.Pod.Name)
136136
if err != nil {
137137
return 0, err
138138
}
139+
return countEndpointsBelowPolicyRevision(eps, rev), nil
140+
}
139141

142+
// countEndpointsBelowPolicyRevision counts endpoints that have not yet realized
143+
// the given policy revision. Endpoints without a security identity yet are
144+
// skipped: they have computed no policy at all, so they enforce no revision we
145+
// could be waiting on, and once they get an identity regeneration computes
146+
// policy at the current revision. Endpoints that do have an identity but a
147+
// missing or older realized revision are counted as not ready.
148+
func countEndpointsBelowPolicyRevision(eps []*models.Endpoint, rev int) int {
140149
notReady := 0
141150
for _, ep := range eps {
142-
if ep.Status == nil || ep.Status.Policy == nil ||
151+
if ep.Status == nil || ep.Status.Identity == nil {
152+
continue
153+
}
154+
if ep.Status.Policy == nil ||
143155
ep.Status.Policy.Realized == nil ||
144156
ep.Status.Policy.Realized.PolicyRevision < int64(rev) {
145157
notReady++
146158
}
147159
}
148-
return notReady, nil
160+
return notReady
149161
}
150162

151163
type policy interface {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Cilium
3+
4+
package check
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
11+
"github.com/cilium/cilium/api/v1/models"
12+
)
13+
14+
func TestCountEndpointsBelowPolicyRevision(t *testing.T) {
15+
identity := &models.Identity{ID: 12345}
16+
17+
realized := func(rev int64) *models.EndpointStatus {
18+
return &models.EndpointStatus{
19+
Identity: identity,
20+
Policy: &models.EndpointPolicyStatus{
21+
Realized: &models.EndpointPolicy{PolicyRevision: rev},
22+
},
23+
}
24+
}
25+
26+
tests := []struct {
27+
name string
28+
eps []*models.Endpoint
29+
rev int
30+
want int
31+
}{
32+
{
33+
name: "no endpoints",
34+
eps: nil,
35+
rev: 10,
36+
want: 0,
37+
},
38+
{
39+
name: "identity-less endpoint is skipped",
40+
eps: []*models.Endpoint{
41+
{Status: &models.EndpointStatus{Identity: nil}},
42+
},
43+
rev: 10,
44+
want: 0,
45+
},
46+
{
47+
name: "nil status is skipped",
48+
eps: []*models.Endpoint{
49+
{Status: nil},
50+
},
51+
rev: 10,
52+
want: 0,
53+
},
54+
{
55+
name: "identity endpoint with stale realized revision is counted",
56+
eps: []*models.Endpoint{
57+
{Status: realized(9)},
58+
},
59+
rev: 10,
60+
want: 1,
61+
},
62+
{
63+
name: "identity endpoint with nil policy is counted",
64+
eps: []*models.Endpoint{
65+
{Status: &models.EndpointStatus{Identity: identity, Policy: nil}},
66+
},
67+
rev: 10,
68+
want: 1,
69+
},
70+
{
71+
name: "identity endpoint with nil realized is counted",
72+
eps: []*models.Endpoint{
73+
{Status: &models.EndpointStatus{
74+
Identity: identity,
75+
Policy: &models.EndpointPolicyStatus{Realized: nil},
76+
}},
77+
},
78+
rev: 10,
79+
want: 1,
80+
},
81+
{
82+
name: "identity endpoint at target revision is not counted",
83+
eps: []*models.Endpoint{
84+
{Status: realized(10)},
85+
},
86+
rev: 10,
87+
want: 0,
88+
},
89+
{
90+
name: "identity endpoint ahead of target revision is not counted",
91+
eps: []*models.Endpoint{
92+
{Status: realized(11)},
93+
},
94+
rev: 10,
95+
want: 0,
96+
},
97+
{
98+
name: "mix: only identity-bearing lagging endpoints are counted",
99+
eps: []*models.Endpoint{
100+
{Status: realized(11)}, // ahead, not counted
101+
{Status: realized(9)}, // stale, counted
102+
{Status: &models.EndpointStatus{Identity: nil}}, // no identity, skipped
103+
{Status: nil}, // no status, skipped
104+
{Status: &models.EndpointStatus{Identity: identity}}, // identity, nil policy, counted
105+
},
106+
rev: 10,
107+
want: 2,
108+
},
109+
}
110+
111+
for _, tt := range tests {
112+
t.Run(tt.name, func(t *testing.T) {
113+
assert.Equal(t, tt.want, countEndpointsBelowPolicyRevision(tt.eps, tt.rev))
114+
})
115+
}
116+
}

0 commit comments

Comments
 (0)