Skip to content

Commit d63225c

Browse files
committed
conformance tests for percentage-based request mirroring
1 parent 27b4aa8 commit d63225c

File tree

9 files changed

+431
-31
lines changed

9 files changed

+431
-31
lines changed

conformance/tests/httproute-request-mirror.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@ var HTTPRouteRequestMirror = suite.ConformanceTest{
5757
},
5858
},
5959
Backend: "infra-backend-v1",
60-
MirroredTo: []http.BackendRef{{
61-
Name: "infra-backend-v2",
62-
Namespace: ns,
63-
}},
60+
MirroredTo: []http.MirroredBackend{
61+
{
62+
BackendRef: http.BackendRef{
63+
Name: "infra-backend-v2",
64+
Namespace: ns,
65+
},
66+
}},
6467
Namespace: ns,
6568
},
6669
{
@@ -84,10 +87,13 @@ var HTTPRouteRequestMirror = suite.ConformanceTest{
8487
},
8588
Namespace: ns,
8689
Backend: "infra-backend-v1",
87-
MirroredTo: []http.BackendRef{{
88-
Name: "infra-backend-v2",
89-
Namespace: ns,
90-
}},
90+
MirroredTo: []http.MirroredBackend{
91+
{
92+
BackendRef: http.BackendRef{
93+
Name: "infra-backend-v2",
94+
Namespace: ns,
95+
},
96+
}},
9197
},
9298
}
9399
for i := range testCases {

conformance/tests/httproute-request-multiple-mirrors.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,18 @@ var HTTPRouteRequestMultipleMirrors = suite.ConformanceTest{
5858
},
5959
},
6060
Backend: "infra-backend-v1",
61-
MirroredTo: []http.BackendRef{
61+
MirroredTo: []http.MirroredBackend{
6262
{
63-
Name: "infra-backend-v2",
64-
Namespace: ns,
63+
BackendRef: http.BackendRef{
64+
Name: "infra-backend-v2",
65+
Namespace: ns,
66+
},
6567
},
6668
{
67-
Name: "infra-backend-v3",
68-
Namespace: ns,
69+
BackendRef: http.BackendRef{
70+
Name: "infra-backend-v3",
71+
Namespace: ns,
72+
},
6973
},
7074
},
7175
Namespace: ns,
@@ -90,14 +94,18 @@ var HTTPRouteRequestMultipleMirrors = suite.ConformanceTest{
9094
},
9195
Namespace: ns,
9296
Backend: "infra-backend-v1",
93-
MirroredTo: []http.BackendRef{
97+
MirroredTo: []http.MirroredBackend{
9498
{
95-
Name: "infra-backend-v2",
96-
Namespace: ns,
99+
BackendRef: http.BackendRef{
100+
Name: "infra-backend-v2",
101+
Namespace: ns,
102+
},
97103
},
98104
{
99-
Name: "infra-backend-v3",
100-
Namespace: ns,
105+
BackendRef: http.BackendRef{
106+
Name: "infra-backend-v3",
107+
Namespace: ns,
108+
},
101109
},
102110
},
103111
},
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tests
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"regexp"
23+
"sync"
24+
"testing"
25+
"time"
26+
27+
"github.com/stretchr/testify/require"
28+
"k8s.io/apimachinery/pkg/types"
29+
30+
"sigs.k8s.io/gateway-api/conformance/utils/config"
31+
"sigs.k8s.io/gateway-api/conformance/utils/http"
32+
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
33+
"sigs.k8s.io/gateway-api/conformance/utils/roundtripper"
34+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
35+
"sigs.k8s.io/gateway-api/conformance/utils/tlog"
36+
"sigs.k8s.io/gateway-api/pkg/features"
37+
)
38+
39+
const (
40+
concurrentRequests = 10
41+
tolerancePercentage = 15.0
42+
totalRequests = 500.0
43+
numDistributionChecks = 5
44+
)
45+
46+
func init() {
47+
ConformanceTests = append(ConformanceTests, HTTPRouteRequestPercentageMirror)
48+
}
49+
50+
var HTTPRouteRequestPercentageMirror = suite.ConformanceTest{
51+
ShortName: "HTTPRouteRequestPercentageMirror",
52+
Description: "An HTTPRoute with percentage based request mirroring",
53+
Manifests: []string{"tests/httproute-request-percentage-mirror.yaml"},
54+
Features: []features.FeatureName{
55+
features.SupportGateway,
56+
features.SupportHTTPRoute,
57+
features.SupportHTTPRouteRequestPercentageMirror,
58+
},
59+
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
60+
var (
61+
ns = "gateway-conformance-infra"
62+
routeNN = types.NamespacedName{Name: "request-percentage-mirror", Namespace: ns}
63+
gwNN = types.NamespacedName{Name: "same-namespace", Namespace: ns}
64+
gwAddr = kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
65+
)
66+
67+
kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN)
68+
69+
// TODO(liorlieberman) add another test to show fraction takes precedence over percent
70+
testCases := []http.ExpectedResponse{
71+
{
72+
Request: http.Request{Path: "/percent-mirror"},
73+
Namespace: ns,
74+
ExpectedRequest: &http.ExpectedRequest{
75+
Request: http.Request{
76+
Path: "/percent-mirror",
77+
},
78+
},
79+
Backend: "infra-backend-v1",
80+
MirroredTo: []http.MirroredBackend{
81+
{
82+
BackendRef: http.BackendRef{
83+
Name: "infra-backend-v2",
84+
Namespace: ns,
85+
},
86+
Percent: ptrTo(int32(20)),
87+
}},
88+
}, {
89+
Request: http.Request{Path: "/percent-mirror-fraction"},
90+
Namespace: ns,
91+
ExpectedRequest: &http.ExpectedRequest{
92+
Request: http.Request{
93+
Path: "/percent-mirror-fraction",
94+
},
95+
},
96+
Backend: "infra-backend-v1",
97+
MirroredTo: []http.MirroredBackend{
98+
{
99+
BackendRef: http.BackendRef{
100+
Name: "infra-backend-v2",
101+
Namespace: ns,
102+
},
103+
Percent: ptrTo(int32(50)), // 1000/2000
104+
}},
105+
}, {
106+
Request: http.Request{
107+
Path: "/percent-multi-mirror-and-modify-headers",
108+
Headers: map[string]string{
109+
"X-Header-Remove": "remove-val",
110+
"X-Header-Add-Append": "append-val-1",
111+
},
112+
},
113+
ExpectedRequest: &http.ExpectedRequest{
114+
Request: http.Request{
115+
Path: "/percent-multi-mirror-and-modify-headers",
116+
Headers: map[string]string{
117+
"X-Header-Add": "header-val-1",
118+
"X-Header-Add-Append": "append-val-1,header-val-2",
119+
"X-Header-Set": "set-overwrites-values",
120+
},
121+
},
122+
AbsentHeaders: []string{"X-Header-Remove"},
123+
},
124+
Namespace: ns,
125+
Backend: "infra-backend-v1",
126+
MirroredTo: []http.MirroredBackend{
127+
{
128+
BackendRef: http.BackendRef{
129+
Name: "infra-backend-v2",
130+
Namespace: ns,
131+
},
132+
Percent: ptrTo(int32(35)),
133+
},
134+
{
135+
BackendRef: http.BackendRef{
136+
Name: "infra-backend-v3",
137+
Namespace: ns,
138+
},
139+
Percent: ptrTo(int32(50)),
140+
},
141+
},
142+
},
143+
}
144+
145+
for i := range testCases {
146+
// Declare tc here to avoid loop variable
147+
// reuse issues across parallel tests.
148+
expected := testCases[i]
149+
t.Run(expected.GetTestCaseName(i), func(t *testing.T) {
150+
// Assert request succeeds before doing our distribution check
151+
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expected)
152+
153+
// Override to not have more requests than expected
154+
suite.TimeoutConfig.RequiredConsecutiveSuccesses = 1
155+
// used to limit number of parallel go routines
156+
semaphore := make(chan struct{}, concurrentRequests)
157+
var wg sync.WaitGroup
158+
159+
for i := 0; i < numDistributionChecks; i++ {
160+
time := time.Now()
161+
for i := 0; i < totalRequests; i++ {
162+
wg.Add(1)
163+
semaphore <- struct{}{}
164+
go func(t *testing.T, r roundtripper.RoundTripper, timeoutConfig config.TimeoutConfig, gwAddr string, expected http.ExpectedResponse) {
165+
defer wg.Done()
166+
defer func() { <-semaphore }()
167+
http.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gwAddr, expected)
168+
169+
}(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expected)
170+
}
171+
wg.Wait()
172+
173+
if err := testMirroredRequestsDistribution(t, suite, expected, time); err != nil {
174+
t.Logf("Traffic distribution test failed (%d/%d): %s", i+1, numDistributionChecks, err)
175+
} else {
176+
return
177+
}
178+
}
179+
t.Fatal("Percentage based mirror distribution tests failed")
180+
181+
})
182+
}
183+
},
184+
}
185+
186+
func testMirroredRequestsDistribution(t *testing.T, suite *suite.ConformanceTestSuite, expected http.ExpectedResponse, timeVal time.Time) error {
187+
mirrorPods := expected.MirroredTo
188+
for i, mirrorPod := range mirrorPods {
189+
if mirrorPod.Name == "" {
190+
tlog.Fatalf(t, "Mirrored BackendRef[%d].Name wasn't provided in the testcase, this test should only check http request mirror.", i)
191+
}
192+
}
193+
194+
var mu sync.Mutex
195+
mirroredCounts := make(map[string]int)
196+
197+
for _, mirrorPod := range mirrorPods {
198+
require.Eventually(t, func() bool {
199+
mirrorLogRegexp := regexp.MustCompile(fmt.Sprintf("Echoing back request made to \\%s to client", expected.Request.Path))
200+
201+
tlog.Log(t, "Searching for the mirrored request log")
202+
tlog.Logf(t, `Reading "%s/%s" logs`, mirrorPod.Namespace, mirrorPod.Name)
203+
logs, err := kubernetes.DumpEchoLogs(mirrorPod.Namespace, mirrorPod.Name, suite.Client, suite.Clientset, timeVal)
204+
if err != nil {
205+
tlog.Logf(t, `Couldn't read "%s/%s" logs: %v`, mirrorPod.Namespace, mirrorPod.Name, err)
206+
return false
207+
}
208+
209+
count := 0
210+
for _, log := range logs {
211+
if mirrorLogRegexp.MatchString(log) {
212+
count++
213+
}
214+
}
215+
mu.Lock()
216+
mirroredCounts[mirrorPod.Name] += count
217+
mu.Unlock()
218+
219+
return true
220+
}, 60*time.Second, time.Millisecond*100, fmt.Sprintf(`Couldn't verify the logs for "%s/%s"`, mirrorPod.Namespace, mirrorPod.Name))
221+
222+
}
223+
224+
var errs []error
225+
226+
for _, mirrorPod := range mirrorPods {
227+
expected := float64(totalRequests) * float64(*mirrorPod.Percent) / 100.0
228+
minExpected := expected * (1 - tolerancePercentage/100)
229+
maxExpected := expected * (1 + tolerancePercentage/100)
230+
231+
actual := float64(mirroredCounts[mirrorPod.Name])
232+
tlog.Logf(t, "Pod: %s, Expected: %f (min: %f, max: %f), Actual: %f", mirrorPod.Name, expected, minExpected, maxExpected, actual)
233+
234+
if actual < minExpected || actual > maxExpected {
235+
errs = append(errs, fmt.Errorf("Pod %s did not meet the mirroring percentage within tolerance. Expected between %f and %f, but got %f", mirrorPod.Name, minExpected, maxExpected, actual))
236+
}
237+
}
238+
if len(errs) > 0 {
239+
return errors.Join(errs...)
240+
}
241+
tlog.Log(t, "Validated mirrored request logs across all desired backends within the given tolerance")
242+
return nil
243+
}
244+
245+
func ptrTo[T any](a T) *T {
246+
return &a
247+
}

0 commit comments

Comments
 (0)