From dcfde57dcc14c6266c751dc0c6f9f50a7c641fdb Mon Sep 17 00:00:00 2001 From: bobzetian Date: Sun, 12 Oct 2025 06:12:05 +0000 Subject: [PATCH 1/2] conformance:use the updated gateway api utility functions and fix some typos. --- conformance/conformance.go | 4 +- .../tests/epp_unavailable_fail_open.go | 44 ++- .../tests/gateway_following_epp_routing.go | 30 +- ...route_multiple_gateways_different_pools.go | 38 +- ...inferencepool_httproute_port_validation.go | 42 ++- .../inferencepool_invalid_epp_service.go | 24 +- .../inferencepool_resolvedrefs_condition.go | 73 ++-- conformance/utils/traffic/traffic.go | 357 ------------------ 8 files changed, 164 insertions(+), 448 deletions(-) delete mode 100644 conformance/utils/traffic/traffic.go diff --git a/conformance/conformance.go b/conformance/conformance.go index 5d94197a8..7b3c32cfd 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -208,7 +208,7 @@ func RunConformanceWithOptions(t *testing.T, opts confsuite.ConformanceOptions) installedCRDs := &apiextensionsv1.CustomResourceDefinitionList{} err = opts.Client.List(ctx, installedCRDs) require.NoError(t, err, "error getting installedCRDs") - apiVersion, err := getGatewayInferenceExtentionVersion(installedCRDs.Items) + apiVersion, err := getGatewayInferenceExtensionVersion(installedCRDs.Items) if err != nil { if opts.AllowCRDsMismatch { apiVersion = "UNDEFINED" @@ -266,7 +266,7 @@ func SetupConformanceTestSuite(ctx context.Context, t *testing.T, suite *confsui ensureGatewayAvailableAndReady(ctx, t, suite.Client, opts, resources.SecondaryGatewayNN) } -func getGatewayInferenceExtentionVersion(crds []apiextensionsv1.CustomResourceDefinition) (string, error) { +func getGatewayInferenceExtensionVersion(crds []apiextensionsv1.CustomResourceDefinition) (string, error) { var inferenceVersion string for _, crd := range crds { v, okv := crd.Annotations[version.BundleVersionAnnotation] diff --git a/conformance/tests/epp_unavailable_fail_open.go b/conformance/tests/epp_unavailable_fail_open.go index 9a831b11b..7f9089abf 100644 --- a/conformance/tests/epp_unavailable_fail_open.go +++ b/conformance/tests/epp_unavailable_fail_open.go @@ -22,13 +22,13 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/types" + gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api/pkg/features" "sigs.k8s.io/gateway-api-inference-extension/conformance/resources" "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/config" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" - trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test" ) @@ -69,19 +69,24 @@ var EppUnAvailableFailOpen = suite.ConformanceTest{ targetPodIP := pods[0].Status.PodIP t.Run("Phase 1: Verify baseline connectivity with EPP available", func(t *testing.T) { t.Log("Sending request to ensure the Gateway and EPP are working correctly...") - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwAddr, - trafficutils.Request{ - Host: hostname, - Path: path, - Headers: map[string]string{ - test.HeaderTestEppEndPointSelectionKey: targetPodIP, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + Headers: map[string]string{ + test.HeaderTestEppEndPointSelectionKey: targetPodIP, + }, + Method: http.MethodPost, + Body: requestBody, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, }, - Method: http.MethodPost, - Body: requestBody, Backend: pods[0].Name, // Make sure the request is from the targetPod when the EPP is alive. Namespace: resources.AppBackendNamespace, }, @@ -96,19 +101,24 @@ var EppUnAvailableFailOpen = suite.ConformanceTest{ require.NoError(t, err, "Failed to make the EPP service %v unavailable", resources.PrimaryEppServiceNN) t.Log("Sending request again, expecting success to verify fail-open...") - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwAddr, - trafficutils.Request{ - Host: hostname, - Path: path, - Headers: map[string]string{ - test.HeaderTestEppEndPointSelectionKey: targetPodIP, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + Headers: map[string]string{ + test.HeaderTestEppEndPointSelectionKey: targetPodIP, + }, + Method: http.MethodPost, + Body: requestBody, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, }, - Method: http.MethodPost, - Body: requestBody, Backend: appPodBackendPrefix, // Only checks the prefix since the EPP is not alive and the response can return from any Pod. Namespace: resources.AppBackendNamespace, }, diff --git a/conformance/tests/gateway_following_epp_routing.go b/conformance/tests/gateway_following_epp_routing.go index eaa618141..df8af8ca5 100644 --- a/conformance/tests/gateway_following_epp_routing.go +++ b/conformance/tests/gateway_following_epp_routing.go @@ -32,7 +32,6 @@ import ( "sigs.k8s.io/gateway-api-inference-extension/conformance/resources" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" - "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test" ) @@ -86,19 +85,24 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ 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. - traffic.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwAddr, - traffic.Request{ - Host: hostname, - Path: path, - Headers: map[string]string{ - test.HeaderTestEppEndPointSelectionKey: podIPs[i], + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + Method: http.MethodPost, + Body: requestBody, + Headers: map[string]string{ + test.HeaderTestEppEndPointSelectionKey: podIPs[i], + }, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, }, - Method: http.MethodPost, - Body: requestBody, Backend: podNames[i], Namespace: resources.AppBackendNamespace, }, @@ -142,21 +146,21 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ Host: hostname, Path: path, Method: http.MethodPost, + Body: requestBody, Headers: headers, }, Response: gwhttp.Response{ StatusCode: http.StatusOK, }, - // DO NOT SUBMIT Backend: appPodBackendPrefix, Namespace: resources.AppBackendNamespace, - }, requestBody, tc.expectAllRequestsRoutedWithinPodNames) + }, tc.expectAllRequestsRoutedWithinPodNames) }) } }, } -func assertTrafficOnlyReachesToExpectedPods(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expected gwhttp.ExpectedResponse, requestBody string, expectedPodNames []string) { +func assertTrafficOnlyReachesToExpectedPods(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expected gwhttp.ExpectedResponse, expectedPodNames []string) { t.Helper() const ( concurrentRequests = 10 @@ -170,7 +174,7 @@ func assertTrafficOnlyReachesToExpectedPods(t *testing.T, suite *suite.Conforman 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)}) + cReq, cRes, err := roundTripper.CaptureRoundTrip(req) if err != nil { return fmt.Errorf("failed to roundtrip request: %w", err) } diff --git a/conformance/tests/httproute_multiple_gateways_different_pools.go b/conformance/tests/httproute_multiple_gateways_different_pools.go index 19a3cbb23..04137ef92 100644 --- a/conformance/tests/httproute_multiple_gateways_different_pools.go +++ b/conformance/tests/httproute_multiple_gateways_different_pools.go @@ -21,11 +21,11 @@ import ( "testing" "k8s.io/apimachinery/pkg/types" + gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api-inference-extension/conformance/resources" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" - "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" ) func init() { @@ -65,17 +65,21 @@ var HTTPRouteMultipleGatewaysDifferentPools = suite.ConformanceTest{ primaryGwAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, primaryGatewayNN) - traffic.MakeRequestAndExpectEventuallyConsistentResponse( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, primaryGwAddr, - traffic.Request{ - Host: primaryRouteHostname, - Path: primaryRoutePath, - ExpectedStatusCode: http.StatusOK, - Backend: primaryBackendPodName, - Namespace: resources.AppBackendNamespace, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: primaryRouteHostname, + Path: primaryRoutePath, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, + }, + Backend: primaryBackendPodName, + Namespace: resources.AppBackendNamespace, }, ) }) @@ -91,17 +95,21 @@ var HTTPRouteMultipleGatewaysDifferentPools = suite.ConformanceTest{ secondaryGwAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, secondaryGatewayNN) - traffic.MakeRequestAndExpectEventuallyConsistentResponse( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, secondaryGwAddr, - traffic.Request{ - Host: secondaryRouteHostname, - Path: secondaryRoutePath, - ExpectedStatusCode: http.StatusOK, - Backend: secondaryBackendPodName, - Namespace: resources.AppBackendNamespace, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: secondaryRouteHostname, + Path: secondaryRoutePath, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, + }, + Backend: secondaryBackendPodName, + Namespace: resources.AppBackendNamespace, }, ) }) diff --git a/conformance/tests/inferencepool_httproute_port_validation.go b/conformance/tests/inferencepool_httproute_port_validation.go index dd3a938d4..35504c2a4 100644 --- a/conformance/tests/inferencepool_httproute_port_validation.go +++ b/conformance/tests/inferencepool_httproute_port_validation.go @@ -17,15 +17,16 @@ limitations under the License. package tests import ( + "net/http" "testing" "k8s.io/apimachinery/pkg/types" + gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api/pkg/features" "sigs.k8s.io/gateway-api-inference-extension/conformance/resources" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" - trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" ) func init() { @@ -54,14 +55,19 @@ var InferencePoolHTTPRoutePortValidation = suite.ConformanceTest{ k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, routeNN, gatewayNN) k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN, gatewayNN) - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gatewayAddr, - trafficutils.Request{ - Host: hostname, - Path: path, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, + }, Backend: resources.PrimaryModelServerDeploymentName, Namespace: resources.AppBackendNamespace, }, @@ -76,14 +82,19 @@ var InferencePoolHTTPRoutePortValidation = suite.ConformanceTest{ k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, routeNN, gatewayNN) k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN, gatewayNN) - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gatewayAddr, - trafficutils.Request{ - Host: hostname, - Path: path, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, + }, Backend: resources.PrimaryModelServerDeploymentName, Namespace: resources.AppBackendNamespace, }, @@ -99,14 +110,19 @@ var InferencePoolHTTPRoutePortValidation = suite.ConformanceTest{ k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, routeNN, gatewayNN) k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN, gatewayNN) - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gatewayAddr, - trafficutils.Request{ - Host: hostname, - Path: path, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, + }, Backend: resources.PrimaryModelServerDeploymentName, Namespace: resources.AppBackendNamespace, }, diff --git a/conformance/tests/inferencepool_invalid_epp_service.go b/conformance/tests/inferencepool_invalid_epp_service.go index ed282b1a7..ff5ce092c 100644 --- a/conformance/tests/inferencepool_invalid_epp_service.go +++ b/conformance/tests/inferencepool_invalid_epp_service.go @@ -17,11 +17,13 @@ limitations under the License. package tests import ( + "net/http" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api/pkg/features" @@ -29,7 +31,6 @@ import ( inferenceapi "sigs.k8s.io/gateway-api-inference-extension/api/v1" "sigs.k8s.io/gateway-api-inference-extension/conformance/resources" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" - trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" ) func init() { @@ -59,7 +60,7 @@ var InferencePoolInvalidEPPService = suite.ConformanceTest{ Reason: string(gatewayv1.RouteReasonAccepted), } kubernetes.HTTPRouteMustHaveCondition(t, s.Client, s.TimeoutConfig, routeNN, gwNN, acceptedCondition) - t.Run("InferecePool has a ResolvedRefs Condition with status False", func(t *testing.T) { + t.Run("InferencePool has a ResolvedRefs Condition with status False", func(t *testing.T) { acceptedCondition := metav1.Condition{ Type: string(inferenceapi.InferencePoolConditionResolvedRefs), // Standard condition type Status: metav1.ConditionFalse, @@ -69,10 +70,21 @@ var InferencePoolInvalidEPPService = suite.ConformanceTest{ }) t.Run("Request to a route with an invalid backend reference receives a 500 response", func(t *testing.T) { - trafficutils.MakeRequestAndExpectEventuallyConsistentResponse(t, s.RoundTripper, s.TimeoutConfig, gwAddr, trafficutils.Request{ - Path: routePath, - ExpectedStatusCode: 5, // Expecting response status code 5XX. - }) + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( + t, + s.RoundTripper, + s.TimeoutConfig, + gwAddr, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Path: routePath, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusInternalServerError, http.StatusNotImplemented, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout}, // Expecting response status code 5XX. + }, + Namespace: resources.AppBackendNamespace, + }, + ) }) }, } diff --git a/conformance/tests/inferencepool_resolvedrefs_condition.go b/conformance/tests/inferencepool_resolvedrefs_condition.go index 6ee182448..b8e02b2c6 100644 --- a/conformance/tests/inferencepool_resolvedrefs_condition.go +++ b/conformance/tests/inferencepool_resolvedrefs_condition.go @@ -26,13 +26,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api/pkg/features" "sigs.k8s.io/gateway-api-inference-extension/conformance/resources" "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/config" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" - trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" ) func init() { @@ -76,27 +76,37 @@ var InferencePoolParentStatus = suite.ConformanceTest{ k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN, gatewaySecondaryNN) t.Logf("InferencePool %s has parent status Accepted:True as expected with two references.", poolNN.String()) - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwPrimaryAddr, - trafficutils.Request{ - Host: hostnamePrimaryGw, - Path: pathPrimaryGw, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostnamePrimaryGw, + Path: pathPrimaryGw, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, + }, Backend: resources.PrimaryModelServerDeploymentName, Namespace: resources.AppBackendNamespace, }, ) - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, - gwSecondaryAddr, - trafficutils.Request{ - Host: hostnameSecondaryGw, - Path: pathSecondaryGw, + gwPrimaryAddr, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostnameSecondaryGw, + Path: pathSecondaryGw, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusNotFound}, + }, Backend: resources.PrimaryModelServerDeploymentName, Namespace: resources.AppBackendNamespace, }, @@ -116,28 +126,37 @@ var InferencePoolParentStatus = suite.ConformanceTest{ k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN, gatewaySecondaryNN) t.Logf("InferencePool %s still has parent status Accepted:True as expected with one reference remaining.", poolNN.String()) - trafficutils.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwSecondaryAddr, - trafficutils.Request{ - Host: hostnameSecondaryGw, - Path: pathSecondaryGw, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostnameSecondaryGw, + Path: pathSecondaryGw, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, + }, Backend: resources.PrimaryModelServerDeploymentName, Namespace: resources.AppBackendNamespace, }, ) - trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwPrimaryAddr, - trafficutils.Request{ - Host: hostnamePrimaryGw, - Path: pathPrimaryGw, - ExpectedStatusCode: http.StatusNotFound, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostnamePrimaryGw, + Path: pathPrimaryGw, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusNotFound}, + }, }, ) }) @@ -155,15 +174,19 @@ var InferencePoolParentStatus = suite.ConformanceTest{ k8sutils.InferencePoolMustHaveNoParents(t, s.Client, poolNN) t.Logf("InferencePool %s correctly shows no parent statuses, indicating it's no longer referenced.", poolNN.String()) - trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, - gwSecondaryAddr, - trafficutils.Request{ - Host: hostnameSecondaryGw, - Path: pathSecondaryGw, - ExpectedStatusCode: http.StatusNotFound, + gwPrimaryAddr, + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostnameSecondaryGw, + Path: pathSecondaryGw, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusNotFound}, + }, }, ) }) diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go deleted file mode 100644 index 8acaefad9..000000000 --- a/conformance/utils/traffic/traffic.go +++ /dev/null @@ -1,357 +0,0 @@ -/* -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 traffic - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httputil" - "regexp" - "strings" - "testing" - "time" - - gwconfig "sigs.k8s.io/gateway-api/conformance/utils/config" - gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" - "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" - "sigs.k8s.io/gateway-api/conformance/utils/tlog" -) - -// Request defines the parameters for a single HTTP test request and its expected outcome. -type Request struct { - // Host is the hostname to use in the HTTP request. - Host string - // Path is the path to request. - Path string - // Method is the HTTP method to use. Defaults to "GET" if empty. - Method string - // Headers are the HTTP headers to include in the request. - Headers map[string]string - // Body is the request body. - Body string - - // ExpectedStatusCode is the HTTP status code expected in the response. - ExpectedStatusCode int - // Backend is the name of the backend service expected to handle the request. - // This is not checked for non-200 responses. - Backend string - // Namespace is the namespace of the backend service. - Namespace string -} - -// MakeRequestAndExpectSuccess is a convenience wrapper for requests that are -// expected to succeed with a 200 OK status. -func MakeRequestAndExpectSuccess( - t *testing.T, - r roundtripper.RoundTripper, - timeoutConfig gwconfig.TimeoutConfig, - gatewayAddress string, - req Request, -) { - t.Helper() - req.ExpectedStatusCode = http.StatusOK - MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, req) -} - -// MakeRequestAndExpectEventuallyConsistentResponse makes a request using the parameters -// from the Request struct and waits for the response to consistently match the expectations. -func MakeRequestAndExpectEventuallyConsistentResponse( - t *testing.T, - r roundtripper.RoundTripper, - timeoutConfig gwconfig.TimeoutConfig, - gatewayAddress string, - req Request, -) { - t.Helper() - - expectedResponse := makeExpectedResponse(t, req) - waitForConvergeToExpected(t, r, timeoutConfig, gatewayAddress, req.Body, expectedResponse) -} - -// MakeRequestAndExpectResponseFromPod sends a request to the specified path -func MakeRequestAndExpectResponseFromPod(t *testing.T, r roundtripper.RoundTripper, timeoutConfig gwconfig.TimeoutConfig, gwAddr, path, podPrefix, nameSpace string) { - t.Helper() - expectedResponse := gwhttp.ExpectedResponse{ - Request: gwhttp.Request{ - Path: path, - }, - Backend: podPrefix, - Namespace: nameSpace, - } - - gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gwAddr, expectedResponse) -} - -func makeExpectedResponse(t *testing.T, req Request) gwhttp.ExpectedResponse { - t.Helper() - - method := http.MethodGet - if req.Method != "" { - method = req.Method - } - - expectedResponse := gwhttp.ExpectedResponse{ - Request: gwhttp.Request{ - Host: req.Host, - Path: req.Path, - Method: method, - Headers: req.Headers, - }, - Response: gwhttp.Response{ - StatusCode: req.ExpectedStatusCode, - }, - Backend: req.Backend, - Namespace: req.Namespace, - } - - // For successful responses (200 OK), we also verify that the backend - // received the request with the correct details (Host, Path, etc.). - // For other statuses (e.g., 404), this check is skipped. - if req.ExpectedStatusCode == http.StatusOK { - expectedResponse.ExpectedRequest = &gwhttp.ExpectedRequest{ - Request: gwhttp.Request{ - Host: req.Host, - Path: req.Path, - Headers: req.Headers, - Method: method, - }, - } - } - return expectedResponse -} - -// TODO: https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1031 -// replace the following method when sigs.k8s.io/gateway-api/conformance/utils/roundtripper is able to send request with body. -func waitForConvergeToExpected( - t *testing.T, - r roundtripper.RoundTripper, - timeoutConfig gwconfig.TimeoutConfig, - gatewayAddress string, - requestBody string, - expectedResponse gwhttp.ExpectedResponse, -) { - gwhttp.AwaitConvergence(t, timeoutConfig.RequiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency, func(elapsed time.Duration) bool { - req := gwhttp.MakeRequest(t, &expectedResponse, gatewayAddress, "HTTP", "http") - request := &RequestWithBody{Request: req} - if requestBody != "" { - request = &RequestWithBody{Request: req, Body: strings.NewReader(requestBody)} - } - cReq, cRes, err := MakeCallRoundTripper(t, r, request) - if err != nil { - tlog.Logf(t, "Request failed, not ready yet: %v (after %v)", err.Error(), elapsed) - return false - } - - if err := CompareRequestWithWildcardStatus(t, &request.Request, cReq, cRes, expectedResponse); err != nil { - tlog.Logf(t, "Response expectation failed for request: %+v not ready yet: %v (after %v)", request.Request, err, elapsed) - return false - } - - return true - }) - tlog.Logf(t, "Request passed") -} - -// CompareRequestWithWildcardStatus compares requests allowing single-digit -// status "classes" (e.g. 5 => any 5xx) via ExpectedResponse.Response.StatusCodes. -// If no class wildcards are present, it defers to CompareRoundTrip. -func CompareRequestWithWildcardStatus( - t *testing.T, - req *roundtripper.Request, - cReq *roundtripper.CapturedRequest, - cRes *roundtripper.CapturedResponse, - expected gwhttp.ExpectedResponse, -) error { - // Separate specific status codes (>=100) from wildcard classes (1..9). - var wildcardClasses []int - var specificCodes []int - seenClass := map[int]struct{}{} - seenCode := map[int]struct{}{} - - for _, sc := range expected.Response.StatusCodes { - switch { - case sc >= 100: - if _, ok := seenCode[sc]; !ok { - specificCodes = append(specificCodes, sc) - seenCode[sc] = struct{}{} - } - case sc >= 1 && sc <= 9: - if _, ok := seenClass[sc]; !ok { - wildcardClasses = append(wildcardClasses, sc) - seenClass[sc] = struct{}{} - } - } - } - - // No wildcard classes? Defer to standard comparator. - if len(wildcardClasses) == 0 { - return gwhttp.CompareRoundTrip(t, req, cReq, cRes, expected) - } - - // If the concrete status matches any wildcard class, materialize it and compare. - actualClass := cRes.StatusCode / 100 - for _, wc := range wildcardClasses { - if actualClass == wc { - modified := expected - // Keep any specific codes that were provided, but ensure the actual status is allowed. - modified.Response.StatusCodes = append(append([]int(nil), specificCodes...), cRes.StatusCode) - return gwhttp.CompareRoundTrip(t, req, cReq, cRes, modified) - } - } - - // Wildcards were provided but none matched the actual class—fail clearly. - return fmt.Errorf("expected status code class to be one of %vxx, got %d", - wildcardClasses, cRes.StatusCode) -} - -// TODO: https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1031 -// remove this when sigs.k8s.io/gateway-api/conformance/utils/roundtripper is able to send request with body. -// RequestWithBody extends roundtripper.Request to include a request body. -type RequestWithBody struct { - roundtripper.Request - Body io.Reader -} - -// TODO: https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1031 -// remove this when sigs.k8s.io/gateway-api/conformance/utils/roundtripper is able to send request with body. -// MakeCallRoundTripper executes an HTTP request using the provided RoundTripper and captures the request and response. -func MakeCallRoundTripper(t *testing.T, r roundtripper.RoundTripper, request *RequestWithBody) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { - client := &http.Client{} - - defaultRoundTripper, ok := r.(*roundtripper.DefaultRoundTripper) - if !ok { - t.Fatalf("Unsupported RoundTripper type: %T", r) - } - rt := defaultRoundTripper - if request.UnfollowRedirect { - client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { - return http.ErrUseLastResponse - } - } - - client.Transport = &http.Transport{ - DialContext: rt.CustomDialContext, - // We disable keep-alives so that we don't leak established TCP connections. - // Leaking TCP connections is bad because we could eventually hit the - // threshold of maximum number of open TCP connections to a specific - // destination. Keep-alives are not presently utilized so disabling this has - // no adverse affect. - // - // Ref. https://github.com/kubernetes-sigs/gateway-api/issues/2357 - DisableKeepAlives: true, - } - - method := "GET" - if request.Method != "" { - method = request.Method - } - ctx, cancel := context.WithTimeout(context.Background(), rt.TimeoutConfig.RequestTimeout) - defer cancel() - req, err := http.NewRequestWithContext(ctx, method, request.URL.String(), request.Body) - if err != nil { - return nil, nil, err - } - - if request.Host != "" { - req.Host = request.Host - } - - if request.Headers != nil { - for name, value := range request.Headers { - req.Header.Set(name, value[0]) - } - } - - if rt.Debug { - var dump []byte - dump, err = httputil.DumpRequestOut(req, true) - if err != nil { - return nil, nil, err - } - - tlog.Logf(request.T, "Sending Request:\n%s\n\n", formatDump(dump, "< ")) - } - - resp, err := client.Do(req) - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - - if rt.Debug { - var dump []byte - dump, err = httputil.DumpResponse(resp, true) - if err != nil { - return nil, nil, err - } - - tlog.Logf(request.T, "Received Response:\n%s\n\n", formatDump(dump, "< ")) - } - - cReq := &roundtripper.CapturedRequest{} - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, err - } - - // we cannot assume the response is JSON - if resp.Header.Get("Content-type") == "application/json" { - err = json.Unmarshal(body, cReq) - if err != nil { - return nil, nil, fmt.Errorf("unexpected error reading response: %w", err) - } - } else { - cReq.Method = method // assume it made the right request if the service being called isn't echoing - } - - cRes := &roundtripper.CapturedResponse{ - StatusCode: resp.StatusCode, - ContentLength: resp.ContentLength, - Protocol: resp.Proto, - Headers: resp.Header, - } - - if resp.TLS != nil { - cRes.PeerCertificates = resp.TLS.PeerCertificates - } - - if roundtripper.IsRedirect(resp.StatusCode) { - redirectURL, err := resp.Location() - if err != nil { - return nil, nil, err - } - cRes.RedirectRequest = &roundtripper.RedirectRequest{ - Scheme: redirectURL.Scheme, - Host: redirectURL.Hostname(), - Port: redirectURL.Port(), - Path: redirectURL.Path, - } - } - - return cReq, cRes, nil -} - -var startLineRegex = regexp.MustCompile(`(?m)^`) - -func formatDump(data []byte, prefix string) string { - data = startLineRegex.ReplaceAllLiteral(data, []byte(prefix)) - return string(data) -} From 752a67d47f03bf8ea936fc98ffc7c6801b495b05 Mon Sep 17 00:00:00 2001 From: bobzetian Date: Mon, 13 Oct 2025 20:46:44 +0000 Subject: [PATCH 2/2] modify newly added test. --- .../tests/gateway_weighted_two_pools.go | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/conformance/tests/gateway_weighted_two_pools.go b/conformance/tests/gateway_weighted_two_pools.go index bbe3cbb18..2414522a3 100644 --- a/conformance/tests/gateway_weighted_two_pools.go +++ b/conformance/tests/gateway_weighted_two_pools.go @@ -33,7 +33,6 @@ import ( "sigs.k8s.io/gateway-api-inference-extension/conformance/resources" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" - "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test" ) @@ -113,19 +112,24 @@ var GatewayWeightedAcrossTwoInferencePools = suite.ConformanceTest{ allIPs := append(append([]string{}, primaryPodIPs...), secondaryPodIPs...) allNames := append(append([]string{}, primaryPodNames...), secondaryPodNames...) for i := 0; i < len(allIPs); i++ { - traffic.MakeRequestAndExpectSuccess( + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwAddr, - traffic.Request{ - Host: hostname, - Path: path, - Headers: map[string]string{ - test.HeaderTestEppEndPointSelectionKey: allIPs[i], + gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + Method: http.MethodPost, + Body: `{"model":"conformance-fake-model","prompt":"Warmup"}`, + Headers: map[string]string{ + test.HeaderTestEppEndPointSelectionKey: allIPs[i], + }, + }, + Response: gwhttp.Response{ + StatusCodes: []int{http.StatusOK}, }, - Method: http.MethodPost, - Body: `{"model":"conformance-fake-model","prompt":"Warmup"}`, Backend: allNames[i], Namespace: resources.AppBackendNamespace, }, @@ -160,6 +164,7 @@ var GatewayWeightedAcrossTwoInferencePools = suite.ConformanceTest{ Path: path, Method: http.MethodPost, Headers: headers, + Body: requestBody, }, Response: gwhttp.Response{ StatusCode: http.StatusOK, @@ -174,10 +179,7 @@ var GatewayWeightedAcrossTwoInferencePools = suite.ConformanceTest{ for i := 0; i < totalRequests; i++ { g.Go(func() error { - cReq, cRes, err := traffic.MakeCallRoundTripper(t, s.RoundTripper, &traffic.RequestWithBody{ - Request: req, - Body: strings.NewReader(requestBody), - }) + cReq, cRes, err := s.RoundTripper.CaptureRoundTrip(req) if err != nil { return fmt.Errorf("failed to roundtrip request: %w", err) }