generated from kubernetes/kubernetes-template-project
-
Notifications
You must be signed in to change notification settings - Fork 179
feat(conformance): Add EPP conformance test for Gateway routing #961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
8d6fb58
Add gateway_following_epp_routing test.
zetxqx f879558
One working version.
zetxqx 9f3e382
Okay version of GatwayFollowingEPPRouting conformance test.
zetxqx b83290c
fix typos and formats.
zetxqx a680c76
Merge branch 'main' into epptest
zetxqx ff3086a
upgrader gateway-api versino to use the updated conformance testutils…
zetxqx 5b725d0
use AllowCRDsMismatch to bypass.
zetxqx 838b535
format.
zetxqx f313e58
refactor more.
zetxqx bf95ea9
wire up the flag.
zetxqx 215b7ce
Refine test cases.
zetxqx 417cb44
Refine log error info.
zetxqx be9c779
Merge branch 'main' into epptest
zetxqx 3809e50
small timeout twek.
zetxqx a2ba595
Merge branch 'main' into epptest
zetxqx d99f096
use common resource.
zetxqx 5b4a86c
back to depend on gateway-api 1.30.
zetxqx 9c7788f
update go.sum.
zetxqx 2627f00
format.
zetxqx 72c4ec1
resolve minor comments.
zetxqx fa47b15
remove seen logic.
zetxqx 1a3daae
trailing new line.
zetxqx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
194 changes: 194 additions & 0 deletions
194
conformance/tests/basic/gateway_following_epp_routing.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
/* | ||
Copyright 2025 The Kubernetes Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package basic | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"slices" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"golang.org/x/sync/errgroup" | ||
"k8s.io/apimachinery/pkg/types" | ||
"sigs.k8s.io/gateway-api/conformance/utils/suite" | ||
"sigs.k8s.io/gateway-api/pkg/features" | ||
|
||
"sigs.k8s.io/gateway-api-inference-extension/conformance/tests" | ||
k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" | ||
"sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" | ||
trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" | ||
gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" | ||
) | ||
|
||
func init() { | ||
// Register the GatewayFollowingEPPRouting test case with the conformance suite. | ||
// This ensures it will be discovered and run by the test runner. | ||
tests.ConformanceTests = append(tests.ConformanceTests, GatewayFollowingEPPRouting) | ||
} | ||
|
||
// GatewayFollowingEPPRouting defines the test case for verifying gateway should send traffic to an endpoint in the list returned by EPP. | ||
var GatewayFollowingEPPRouting = suite.ConformanceTest{ | ||
ShortName: "GatewayFollowingEPPRouting", | ||
Description: "Inference gateway should send traffic to an endpoint in the list returned by EPP", | ||
Manifests: []string{"tests/basic/gateway_following_epp_routing.yaml"}, | ||
Features: []features.FeatureName{ | ||
features.FeatureName("SupportInferencePool"), | ||
features.SupportGateway, | ||
}, | ||
Test: func(t *testing.T, s *suite.ConformanceTestSuite) { | ||
const ( | ||
appBackendNamespace = "gateway-conformance-app-backend" | ||
infraNamespace = "gateway-conformance-infra" | ||
hostname = "primary.example.com" | ||
path = "/primary-gateway-test" | ||
expectedPodReplicas = 3 | ||
// eppSelectionHeaderName is the custom header used by the testing-EPP service | ||
// to determine which endpoint to select. | ||
eppSelectionHeaderName = "test-epp-endpoint-selection" | ||
appPodBackendPrefix = "primary-inference-model-server" | ||
) | ||
|
||
httpRouteNN := types.NamespacedName{Name: "httproute-for-primary-gw", Namespace: appBackendNamespace} | ||
gatewayNN := types.NamespacedName{Name: "conformance-primary-gateway", Namespace: infraNamespace} | ||
poolNN := types.NamespacedName{Name: "primary-inference-pool", Namespace: appBackendNamespace} | ||
backendPodLabels := map[string]string{"app": "primary-inference-model-server"} | ||
|
||
t.Log("Verifying HTTPRoute and InferencePool are accepted and the Gateway has an address.") | ||
k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, httpRouteNN, gatewayNN) | ||
k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN) | ||
gwAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gatewayNN) | ||
|
||
t.Logf("Fetching backend pods with labels: %v", backendPodLabels) | ||
pods, err := k8sutils.GetPodsWithLabel(t, s.Client, appBackendNamespace, backendPodLabels) | ||
require.NoError(t, err, "Failed to get backend pods") | ||
require.Len(t, pods, expectedPodReplicas, "Expected to find %d backend pods, but found %d.", expectedPodReplicas, len(pods)) | ||
|
||
podIPs := make([]string, len(pods)) | ||
podNames := make([]string, len(pods)) | ||
for i, pod := range pods { | ||
podIPs[i] = pod.Status.PodIP | ||
podNames[i] = pod.Name | ||
} | ||
|
||
requestBody := `{ | ||
"model": "conformance-fake-model", | ||
"prompt": "Write as if you were a critic: San Francisco" | ||
}` | ||
|
||
for i := 0; i < len(pods); i++ { | ||
// Send an initial request targeting a single pod and wait for it to be successful to ensure the Gateway and EPP | ||
// are functioning correctly before running the main test cases. | ||
trafficutils.MakeRequestWithRequestParamAndExpectSuccess( | ||
t, | ||
s.RoundTripper, | ||
s.TimeoutConfig, | ||
gwAddr, | ||
trafficutils.Request{ | ||
Host: hostname, | ||
Path: path, | ||
Headers: map[string]string{eppSelectionHeaderName: podIPs[i]}, | ||
Method: http.MethodPost, | ||
Body: requestBody, | ||
Backend: podNames[i], | ||
Namespace: appBackendNamespace, | ||
}, | ||
) | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
podIPsToBeReturnedByEPP []string | ||
expectAllRequestsRoutedWithinPodNames []string | ||
}{ | ||
{ | ||
name: "should route traffic to a single designated pod", | ||
podIPsToBeReturnedByEPP: []string{podIPs[2]}, | ||
expectAllRequestsRoutedWithinPodNames: []string{podNames[2]}, | ||
}, | ||
{ | ||
name: "should route traffic to two designated pods", | ||
podIPsToBeReturnedByEPP: []string{podIPs[0], podIPs[1]}, | ||
expectAllRequestsRoutedWithinPodNames: []string{podNames[0], podNames[1]}, | ||
}, | ||
{ | ||
name: "should route traffic to all available pods", | ||
podIPsToBeReturnedByEPP: []string{podIPs[0], podIPs[1], podIPs[2]}, | ||
expectAllRequestsRoutedWithinPodNames: []string{podNames[0], podNames[1], podNames[2]}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
eppHeaderValue := strings.Join(tc.podIPsToBeReturnedByEPP, ",") | ||
headers := map[string]string{eppSelectionHeaderName: eppHeaderValue} | ||
|
||
t.Logf("Sending request to %s with EPP header '%s: %s'", gwAddr, eppSelectionHeaderName, eppHeaderValue) | ||
t.Logf("Expecting traffic to be routed to pod: %v", tc.expectAllRequestsRoutedWithinPodNames) | ||
|
||
assertTrafficOnlyReachesToExpectedPods(t, s, gwAddr, gwhttp.ExpectedResponse{ | ||
Request: gwhttp.Request{ | ||
Host: hostname, | ||
Path: path, | ||
Method: http.MethodPost, | ||
Headers: headers, | ||
}, | ||
Response: gwhttp.Response{ | ||
StatusCode: http.StatusOK, | ||
}, | ||
Backend: appPodBackendPrefix, | ||
Namespace: appBackendNamespace, | ||
}, requestBody, tc.expectAllRequestsRoutedWithinPodNames) | ||
}) | ||
} | ||
}, | ||
} | ||
|
||
func assertTrafficOnlyReachesToExpectedPods(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expected gwhttp.ExpectedResponse, requestBody string, expectedPodNames []string) { | ||
t.Helper() | ||
const ( | ||
concurrentRequests = 10 | ||
totalRequests = 100 | ||
) | ||
var ( | ||
roundTripper = suite.RoundTripper | ||
g errgroup.Group | ||
req = gwhttp.MakeRequest(t, &expected, gwAddr, "HTTP", "http") | ||
) | ||
g.SetLimit(concurrentRequests) | ||
for i := 0; i < totalRequests; i++ { | ||
g.Go(func() error { | ||
cReq, cRes, err := traffic.MakeCallRoundTripper(t, roundTripper, &traffic.RequestWithBody{Request: req, Body: strings.NewReader(requestBody)}) | ||
if err != nil { | ||
return fmt.Errorf("failed to roundtrip request: %w", err) | ||
} | ||
if err := gwhttp.CompareRequest(t, &req, cReq, cRes, expected); err != nil { | ||
return fmt.Errorf("response expectation failed for request: %w", err) | ||
} | ||
|
||
if slices.Contains(expectedPodNames, cReq.Pod) { | ||
return nil | ||
} | ||
return fmt.Errorf("request was handled by an unexpected pod %q", cReq.Pod) | ||
}) | ||
} | ||
if err := g.Wait(); err != nil { | ||
t.Fatalf("Not all the requests are sent to the expectedPods successfully, err: %v", err) | ||
} | ||
t.Logf("Traffic successfully reached only to expected pods: %v", expectedPodNames) | ||
} |
38 changes: 38 additions & 0 deletions
38
conformance/tests/basic/gateway_following_epp_routing.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# --- InferenceModel Definition --- | ||
# Service for the infra-backend-deployment. | ||
apiVersion: inference.networking.x-k8s.io/v1alpha2 | ||
kind: InferenceModel | ||
metadata: | ||
name: conformance-fake-model-server | ||
namespace: gateway-conformance-app-backend | ||
spec: | ||
modelName: conformance-fake-model | ||
criticality: Critical # Mark it as critical to bypass the saturation check since the model server is fake and don't have such metrics. | ||
poolRef: | ||
name: primary-inference-pool | ||
--- | ||
# --- HTTPRoute for Primary Gateway (conformance-gateway) --- | ||
apiVersion: gateway.networking.k8s.io/v1 | ||
kind: HTTPRoute | ||
metadata: | ||
name: httproute-for-primary-gw | ||
namespace: gateway-conformance-app-backend | ||
spec: | ||
parentRefs: | ||
- group: gateway.networking.k8s.io | ||
kind: Gateway | ||
name: conformance-primary-gateway | ||
namespace: gateway-conformance-infra | ||
sectionName: http | ||
hostnames: | ||
- "primary.example.com" | ||
rules: | ||
- backendRefs: | ||
- group: inference.networking.x-k8s.io | ||
kind: InferencePool | ||
name: primary-inference-pool | ||
matches: | ||
- path: | ||
type: PathPrefix | ||
value: /primary-gateway-test | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.