Skip to content

Commit 69a4a5f

Browse files
add httproute weight based routing mesh conformance tests (#3827)
* add httproute weights mesh conformance tests * gofmt
1 parent cdcca5f commit 69a4a5f

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
Copyright 2025 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 meshtests
18+
19+
import (
20+
"cmp"
21+
"errors"
22+
"fmt"
23+
"math"
24+
"slices"
25+
"strings"
26+
"sync"
27+
"testing"
28+
29+
"golang.org/x/sync/errgroup"
30+
31+
"sigs.k8s.io/gateway-api/conformance/utils/echo"
32+
"sigs.k8s.io/gateway-api/conformance/utils/http"
33+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
34+
"sigs.k8s.io/gateway-api/pkg/features"
35+
)
36+
37+
func init() {
38+
MeshConformanceTests = append(MeshConformanceTests, MeshHTTPRouteWeight)
39+
}
40+
41+
var MeshHTTPRouteWeight = suite.ConformanceTest{
42+
ShortName: "MeshHTTPRouteWeight",
43+
Description: "An HTTPRoute with weighted backends",
44+
Manifests: []string{"tests/mesh/httproute-weight.yaml"},
45+
Features: []features.FeatureName{
46+
features.SupportMesh,
47+
features.SupportHTTPRoute,
48+
},
49+
Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
50+
client := echo.ConnectToApp(t, s, echo.MeshAppEchoV1)
51+
52+
t.Run("Requests should have a distribution that matches the weight", func(t *testing.T) {
53+
host := "echo"
54+
expected := http.ExpectedResponse{
55+
Request: http.Request{Path: "/", Host: host},
56+
Response: http.Response{StatusCode: 200},
57+
Namespace: "gateway-conformance-mesh",
58+
}
59+
60+
// Assert request succeeds before doing our distribution check
61+
client.MakeRequestAndExpectEventuallyConsistentResponse(t, expected, s.TimeoutConfig)
62+
for i := 0; i < 10; i++ {
63+
if err := testDistribution(t, client, expected); err != nil {
64+
t.Logf("Traffic distribution test failed (%d/10): %s", i+1, err)
65+
} else {
66+
return
67+
}
68+
}
69+
t.Fatal("Weighted distribution tests failed")
70+
})
71+
},
72+
}
73+
74+
func testDistribution(t *testing.T, client echo.MeshPod, expected http.ExpectedResponse) error {
75+
const (
76+
concurrentRequests = 10
77+
tolerancePercentage = 0.05
78+
totalRequests = 500.0
79+
)
80+
var (
81+
g errgroup.Group
82+
seenMutex sync.Mutex
83+
seen = make(map[string]float64, 2 /* number of backends */)
84+
expectedWeights = map[string]float64{
85+
"echo-v1": 0.7,
86+
"echo-v2": 0.3,
87+
}
88+
)
89+
g.SetLimit(concurrentRequests)
90+
for i := 0.0; i < totalRequests; i++ {
91+
g.Go(func() error {
92+
_, cRes, err := client.CaptureRequestResponseAndCompare(t, expected)
93+
if err != nil {
94+
return fmt.Errorf("failed: %w", err)
95+
}
96+
97+
seenMutex.Lock()
98+
defer seenMutex.Unlock()
99+
100+
for expectedBackend := range expectedWeights {
101+
if strings.HasPrefix(cRes.Hostname, expectedBackend) {
102+
seen[expectedBackend]++
103+
return nil
104+
}
105+
}
106+
107+
return fmt.Errorf("request was handled by an unexpected pod %q", cRes.Hostname)
108+
})
109+
}
110+
111+
if err := g.Wait(); err != nil {
112+
return fmt.Errorf("error while sending requests: %w", err)
113+
}
114+
115+
var errs []error
116+
if len(seen) != 2 {
117+
errs = append(errs, fmt.Errorf("expected only two backends to receive traffic"))
118+
}
119+
120+
for wantBackend, wantPercent := range expectedWeights {
121+
gotCount, ok := seen[wantBackend]
122+
123+
if !ok && wantPercent != 0.0 {
124+
errs = append(errs, fmt.Errorf("expect traffic to hit backend %q - but none was received", wantBackend))
125+
continue
126+
}
127+
128+
gotPercent := gotCount / totalRequests
129+
130+
if math.Abs(gotPercent-wantPercent) > tolerancePercentage {
131+
errs = append(errs, fmt.Errorf("backend %q weighted traffic of %v not within tolerance %v (+/-%f)",
132+
wantBackend,
133+
gotPercent,
134+
wantPercent,
135+
tolerancePercentage,
136+
))
137+
}
138+
}
139+
slices.SortFunc(errs, func(a, b error) int {
140+
return cmp.Compare(a.Error(), b.Error())
141+
})
142+
return errors.Join(errs...)
143+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: gateway.networking.k8s.io/v1
2+
kind: HTTPRoute
3+
metadata:
4+
name: mesh-weighted-backends
5+
namespace: gateway-conformance-mesh
6+
spec:
7+
parentRefs:
8+
- group: ""
9+
kind: Service
10+
name: echo
11+
port: 80
12+
rules:
13+
- backendRefs:
14+
- name: echo-v1
15+
port: 8080
16+
weight: 70
17+
- name: echo-v2
18+
port: 8080
19+
weight: 30

conformance/utils/echo/pod.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ func makeRequest(t *testing.T, exp *http.ExpectedResponse) []string {
105105
}
106106

107107
func compareRequest(exp http.ExpectedResponse, resp Response) error {
108+
if exp.ExpectedRequest == nil {
109+
exp.ExpectedRequest = &http.ExpectedRequest{}
110+
}
108111
wantReq := exp.ExpectedRequest
109112
wantResp := exp.Response
110113
if fmt.Sprint(wantResp.StatusCode) != resp.Code {
@@ -220,3 +223,19 @@ func ConnectToAppInNamespace(t *testing.T, s *suite.ConformanceTestSuite, app Me
220223
rcfg: s.RestConfig,
221224
}
222225
}
226+
227+
func (m *MeshPod) CaptureRequestResponseAndCompare(t *testing.T, exp http.ExpectedResponse) ([]string, Response, error) {
228+
req := makeRequest(t, &exp)
229+
230+
resp, err := m.request(req)
231+
if err != nil {
232+
tlog.Logf(t, "Request %v failed, not ready yet: %v", req, err.Error())
233+
return []string{}, Response{}, err
234+
}
235+
tlog.Logf(t, "Got resp %v", resp)
236+
if err := compareRequest(exp, resp); err != nil {
237+
tlog.Logf(t, "Response expectation failed for request: %v not ready yet: %v", req, err)
238+
return []string{}, Response{}, err
239+
}
240+
return req, resp, nil
241+
}

0 commit comments

Comments
 (0)