Skip to content

Commit 7f6c983

Browse files
committed
cilium-cli/connectivity: don't retry expected-drop actions
Commit 39c5e16 attached WithRetryCondition(WithRetryAll()) to echo-ingress-l7-named-port so a transient first-packet loss or Envoy warm-up on the allowed requests no longer fails the test on the first attempt. Its message assumed the expected-drop actions were unaffected beyond "only adding retry delay", but that delay is the whole cost: the scenario's denied actions are silently dropped by policy, so their curl hits --connect-timeout and exits 28, and --retry re-issues each one three times with --retry-delay in between. curl has no zero-delay retry mode, so every denied action pays the full retry budget re-sending a request that is meant to fail. On the v1.18 ci-ipsec-upgrade runs this inflated echo-ingress-l7-named-port from ~52s to ~450s in the concurrent phase and, once the cli bumped to the release vendoring 39c5e16, pushed the job past its 45 minute limit. Gate retry-option generation on the action's expected result in the shared retryCondition.CurlOptions: retrying an expected drop can never turn it into the expected result, so return no retry options when a drop is expected. The expectation is resolved in NewAction before the action's Run closure runs, so a.ExpectingSuccess() is already known at the call sites. This covers every retry user, not just echo-ingress-l7-named-port: client-egress-l7-set-header (also PodToPodWithEndpoints + WithRetryAll) and client-egress-tls-sni (PodToWorld + WithRetryAll) likewise stopped retrying their denied actions. The scoped conditions (WithRetryDestIP/DestPort/PodLabel) already matched only the allowed destinations, so their behavior is unchanged. The allowed requests keep their retries, so the flake 39c5e16 fixed stays fixed. While here, clone the shared base curl options per action in podToPodWithEndpoints so a drop action never inherits retry flags appended by a sibling success action in the same scenario. This commit was prepared with AIL:3. Signed-off-by: André Martins <andre@cilium.io>
1 parent 4ebb834 commit 7f6c983

6 files changed

Lines changed: 83 additions & 15 deletions

File tree

cilium-cli/connectivity/check/action.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,14 +1197,16 @@ func (a *Action) validateMetric(ctx context.Context, node string, result Metrics
11971197
}
11981198
}
11991199

1200-
func (a *Action) expectingSuccess() bool {
1200+
// ExpectingSuccess reports whether the action's registered expectation is a
1201+
// successful command (exit code 0), as opposed to an expected drop.
1202+
func (a *Action) ExpectingSuccess() bool {
12011203
return a.expectedExitCode() == ExitCode(0)
12021204
}
12031205

12041206
func (a *Action) CurlCommandWithOutput(peer TestPeer, opts ...string) []string {
1205-
return a.test.ctx.CurlCommandWithOutput(peer, a.IPFamily(), a.expectingSuccess(), opts)
1207+
return a.test.ctx.CurlCommandWithOutput(peer, a.IPFamily(), a.ExpectingSuccess(), opts)
12061208
}
12071209

12081210
func (a *Action) CurlCommand(peer TestPeer, opts ...string) []string {
1209-
return a.test.ctx.CurlCommand(peer, a.IPFamily(), a.expectingSuccess(), opts)
1211+
return a.test.ctx.CurlCommand(peer, a.IPFamily(), a.ExpectingSuccess(), opts)
12101212
}

cilium-cli/connectivity/tests/common.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,18 @@ type retryCondition struct {
7171
}
7272

7373
// CurlOptions returns curl retry option or empty slice depending on retry conditions
74-
func (rc *retryCondition) CurlOptions(peer check.TestPeer, ipFam features.IPFamily, pod check.Pod, params check.Parameters) []string {
74+
func (rc *retryCondition) CurlOptions(peer check.TestPeer, ipFam features.IPFamily, pod check.Pod, params check.Parameters, expectSuccess bool) []string {
7575
if params.Retry == 0 {
7676
return []string{}
7777
}
78+
// Never retry an action that is expected to be dropped. curl cannot retry
79+
// without waiting between attempts, so retrying a denied request just adds
80+
// retry delay while it re-issues traffic meant to fail; the retry condition
81+
// exists to paper over transient failures of the allowed requests, not the
82+
// denied ones.
83+
if !expectSuccess {
84+
return []string{}
85+
}
7886
if !rc.all && rc.destIP == "" && rc.destPort == 0 {
7987
return []string{}
8088
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Cilium
3+
4+
package tests
5+
6+
import (
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
corev1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
14+
"github.com/cilium/cilium/cilium-cli/connectivity/check"
15+
"github.com/cilium/cilium/cilium-cli/utils/features"
16+
)
17+
18+
func TestRetryConditionCurlOptions(t *testing.T) {
19+
params := check.Parameters{Retry: 3, RetryDelay: 3 * time.Second}
20+
ep := check.HTTPEndpoint("ep", "http://192.0.2.1:80/public")
21+
pod := check.Pod{Pod: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"kind": "client"}}}}
22+
23+
newRC := func(opts ...RetryOption) *retryCondition {
24+
rc := &retryCondition{}
25+
for _, o := range opts {
26+
o(rc)
27+
}
28+
return rc
29+
}
30+
31+
retryOpts := []string{"--retry", "3", "--retry-all-errors", "--retry-delay", "3"}
32+
33+
// A drop is expected: no retry flags regardless of the retry condition,
34+
// because curl always waits between retries and retrying a request meant to
35+
// fail only adds retry delay.
36+
assert.Empty(t, newRC(WithRetryAll()).CurlOptions(ep, features.IPFamilyV4, pod, params, false),
37+
"WithRetryAll must not emit retry options when a drop is expected")
38+
assert.Empty(t, newRC(WithRetryDestPort(80)).CurlOptions(ep, features.IPFamilyV4, pod, params, false),
39+
"scoped condition must not emit retry options when a drop is expected")
40+
41+
// Success expected + WithRetryAll: emit the retry flags.
42+
assert.Equal(t, retryOpts, newRC(WithRetryAll()).CurlOptions(ep, features.IPFamilyV4, pod, params, true))
43+
44+
// Retry disabled globally: never emit, even when success is expected.
45+
assert.Empty(t, newRC(WithRetryAll()).CurlOptions(ep, features.IPFamilyV4, pod,
46+
check.Parameters{Retry: 0}, true))
47+
48+
// Success expected but no retry condition set: nothing to emit.
49+
assert.Empty(t, newRC().CurlOptions(ep, features.IPFamilyV4, pod, params, true))
50+
51+
// Matching dest-port condition + success: emit.
52+
assert.Equal(t, retryOpts, newRC(WithRetryDestPort(80)).CurlOptions(ep, features.IPFamilyV4, pod, params, true))
53+
54+
// Non-matching dest-port condition: no retry even on success.
55+
assert.Empty(t, newRC(WithRetryDestPort(8080)).CurlOptions(ep, features.IPFamilyV4, pod, params, true))
56+
}

cilium-cli/connectivity/tests/pod.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net"
1111
"net/netip"
1212
"regexp"
13+
"slices"
1314
"strconv"
1415
"strings"
1516

@@ -164,8 +165,9 @@ func (s *podToPodWithEndpoints) curlEndpoints(ctx context.Context, t *check.Test
164165
ep := check.HTTPEndpointWithLabels(epName, url, echo.Labels())
165166

166167
t.NewAction(s, epName, client, ep, ipFam).Run(func(a *check.Action) {
167-
curlOpts = append(curlOpts, s.retryCondition.CurlOptions(ep, ipFam, *client, ct.Params())...)
168-
a.ExecInPod(ctx, a.CurlCommand(ep, curlOpts...))
168+
opts := slices.Clone(curlOpts)
169+
opts = append(opts, s.retryCondition.CurlOptions(ep, ipFam, *client, ct.Params(), a.ExpectingSuccess())...)
170+
a.ExecInPod(ctx, a.CurlCommand(ep, opts...))
169171

170172
a.ValidateFlows(ctx, client, a.GetEgressRequirements(check.FlowParameters{}))
171173
a.ValidateFlows(ctx, ep, a.GetIngressRequirements(check.FlowParameters{}))
@@ -178,8 +180,8 @@ func (s *podToPodWithEndpoints) curlEndpoints(ctx context.Context, t *check.Test
178180
labels["X-Very-Secret-Token"] = "42"
179181
ep = check.HTTPEndpointWithLabels(epName, url, labels)
180182
t.NewAction(s, epName, client, ep, ipFam).Run(func(a *check.Action) {
181-
opts := make([]string, 0, len(curlOpts)+2)
182-
opts = append(opts, curlOpts...)
183+
opts := slices.Clone(curlOpts)
184+
opts = append(opts, s.retryCondition.CurlOptions(ep, ipFam, *client, ct.Params(), a.ExpectingSuccess())...)
183185
opts = append(opts, "-H", "X-Very-Secret-Token: 42")
184186

185187
a.ExecInPod(ctx, a.CurlCommand(ep, opts...))

cilium-cli/connectivity/tests/to-cidr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (s *podToCIDR) Run(ctx context.Context, t *check.Test) {
5454
var i int
5555
for _, src := range ct.ClientPods() {
5656
t.NewAction(s, fmt.Sprintf("%s-%d", ep.Name(), i), &src, ep, features.GetIPFamily(ip)).Run(func(a *check.Action) {
57-
opts := s.rc.CurlOptions(ep, features.GetIPFamily(ip), src, ct.Params())
57+
opts := s.rc.CurlOptions(ep, features.GetIPFamily(ip), src, ct.Params(), a.ExpectingSuccess())
5858
a.ExecInPod(ctx, a.CurlCommand(ep, opts...))
5959

6060
a.ValidateFlows(ctx, src, a.GetEgressRequirements(check.FlowParameters{

cilium-cli/connectivity/tests/world.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,25 +103,25 @@ func (s *podToWorld) Run(ctx context.Context, t *check.Test) {
103103
}
104104

105105
// With http, over port 80.
106-
httpOpts := s.rc.CurlOptions(http, ipFam, client, ct.Params())
107-
httpOpts = append(httpOpts, s.curlOptionalFakeDNS(extTarget, ipFam, t.Context().Params())...)
108106
t.NewAction(s, fmt.Sprintf("http-to-%s-%s-%d", extTarget, ipFam, i), &client, http, ipFam).Run(func(a *check.Action) {
107+
httpOpts := s.rc.CurlOptions(http, ipFam, client, ct.Params(), a.ExpectingSuccess())
108+
httpOpts = append(httpOpts, s.curlOptionalFakeDNS(extTarget, ipFam, t.Context().Params())...)
109109
a.ExecInPod(ctx, a.CurlCommand(http, httpOpts...))
110110
a.ValidateFlows(ctx, client, a.GetEgressRequirements(fp))
111111
})
112112

113113
// With https, over port 443.
114-
httpsOpts := s.rc.CurlOptions(https, ipFam, client, ct.Params())
115-
httpsOpts = append(httpsOpts, s.curlOptionalFakeDNS(extTarget, ipFam, t.Context().Params())...)
116114
t.NewAction(s, fmt.Sprintf("https-to-%s-%s-%d", extTarget, ipFam, i), &client, https, ipFam).Run(func(a *check.Action) {
115+
httpsOpts := s.rc.CurlOptions(https, ipFam, client, ct.Params(), a.ExpectingSuccess())
116+
httpsOpts = append(httpsOpts, s.curlOptionalFakeDNS(extTarget, ipFam, t.Context().Params())...)
117117
a.ExecInPod(ctx, a.CurlCommand(https, httpsOpts...))
118118
a.ValidateFlows(ctx, client, a.GetEgressRequirements(fp))
119119
})
120120

121121
// With https, over port 443, index.html.
122-
httpsindexOpts := s.rc.CurlOptions(httpsindex, ipFam, client, ct.Params())
123-
httpsindexOpts = append(httpsindexOpts, s.curlOptionalFakeDNS(extTarget, ipFam, t.Context().Params())...)
124122
t.NewAction(s, fmt.Sprintf("https-to-%s-index-%s-%d", extTarget, ipFam, i), &client, httpsindex, ipFam).Run(func(a *check.Action) {
123+
httpsindexOpts := s.rc.CurlOptions(httpsindex, ipFam, client, ct.Params(), a.ExpectingSuccess())
124+
httpsindexOpts = append(httpsindexOpts, s.curlOptionalFakeDNS(extTarget, ipFam, t.Context().Params())...)
125125
a.ExecInPod(ctx, a.CurlCommand(httpsindex, httpsindexOpts...))
126126
a.ValidateFlows(ctx, client, a.GetEgressRequirements(fp))
127127
})

0 commit comments

Comments
 (0)