From 8d6fb5860ab7066b2a22b6849f5028de6a5d06f2 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Mon, 9 Jun 2025 23:32:12 +0000 Subject: [PATCH 01/19] Add gateway_following_epp_routing test. --- .../basic/gateway_following_epp_routing.go | 63 +++++++++++++++++++ .../basic/gateway_following_epp_routing.yaml | 0 2 files changed, 63 insertions(+) create mode 100644 conformance/tests/basic/gateway_following_epp_routing.go create mode 100644 conformance/tests/basic/gateway_following_epp_routing.yaml diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go new file mode 100644 index 000000000..a7944b38e --- /dev/null +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -0,0 +1,63 @@ +/* +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 ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" // For standard condition types + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/pkg/features" // For standard feature names + + // Import the tests package to append to ConformanceTests + "sigs.k8s.io/gateway-api-inference-extension/conformance/tests" + k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" +) + +func init() { + // Register the InferencePoolAccepted test case with the conformance suite. + // This ensures it will be discovered and run by the test runner. + tests.ConformanceTests = append(tests.ConformanceTests, GatwayFollowingEPPRouting) +} + +// InferencePoolAccepted defines the test case for verifying basic InferencePool acceptance. +var GatwayFollowingEPPRouting = suite.ConformanceTest{ + ShortName: "GatwayFollowingEPPRouting", + Description: "A minimal InferencePool resource should be accepted by the controller and report an Accepted condition", + Manifests: []string{"tests/basic/inferencepool_accepted.yaml"}, + Features: []features.FeatureName{ + features.FeatureName("SupportInferencePool"), + features.SupportGateway, + }, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + // created by the associated manifest file. + poolNN := types.NamespacedName{Name: "inferencepool-basic-accepted", Namespace: "gateway-conformance-app-backend"} + + t.Run("InferencePool should have Accepted condition set to True", func(t *testing.T) { + // Define the expected status condition. We use the standard "Accepted" + // condition type from the Gateway API for consistency. + acceptedCondition := metav1.Condition{ + Type: string(gatewayv1.GatewayConditionAccepted), // Standard condition type + Status: metav1.ConditionTrue, + Reason: "", // "" means we don't strictly check the Reason for this basic test. + } + k8sutils.InferencePoolMustHaveCondition(t, s.Client, poolNN, acceptedCondition) + }) + }, +} diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml new file mode 100644 index 000000000..e69de29bb From f879558324b1d96b3b17e4afea7cf93dcaeba164 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Tue, 10 Jun 2025 20:23:20 +0000 Subject: [PATCH 02/19] One working version. --- conformance/conformance.go | 5 +- .../basic/gateway_following_epp_routing.go | 79 +++++-- .../basic/gateway_following_epp_routing.yaml | 205 ++++++++++++++++++ conformance/utils/kubernetes/helpers.go | 34 +++ conformance/utils/traffic/traffic.go | 51 ++++- 5 files changed, 353 insertions(+), 21 deletions(-) diff --git a/conformance/conformance.go b/conformance/conformance.go index 66b8c0969..7585e74ce 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -25,6 +25,7 @@ import ( "io/fs" "os" "testing" + "time" "github.com/stretchr/testify/require" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -153,6 +154,8 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { baseManifestsValue := "resources/manifests/manifests.yaml" + config := confconfig.DefaultTimeoutConfig() + config.HTTPRouteMustHaveCondition = 300 * time.Second opts := confsuite.ConformanceOptions{ Client: c, ClientOptions: clientOptions, @@ -163,7 +166,7 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { Debug: *confflags.ShowDebug, CleanupBaseResources: *confflags.CleanupBaseResources, SupportedFeatures: sets.New[features.FeatureName](), - TimeoutConfig: confconfig.DefaultTimeoutConfig(), + TimeoutConfig: config, SkipTests: skipTests, ExemptFeatures: exemptFeatures, RunTest: *confflags.RunTest, diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index a7944b38e..2145b9141 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -18,16 +18,17 @@ package basic import ( "testing" + "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" // For standard condition types + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" // For standard condition types "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api/pkg/features" // For standard feature names // Import the tests package to append to ConformanceTests "sigs.k8s.io/gateway-api-inference-extension/conformance/tests" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" + trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" ) func init() { @@ -39,25 +40,75 @@ func init() { // InferencePoolAccepted defines the test case for verifying basic InferencePool acceptance. var GatwayFollowingEPPRouting = suite.ConformanceTest{ ShortName: "GatwayFollowingEPPRouting", - Description: "A minimal InferencePool resource should be accepted by the controller and report an Accepted condition", - Manifests: []string{"tests/basic/inferencepool_accepted.yaml"}, + Description: "Inference gateway should redirect traffic to an endpoints belonging to what EPP respond endpoints list.", // TODO + 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) { - // created by the associated manifest file. - poolNN := types.NamespacedName{Name: "inferencepool-basic-accepted", Namespace: "gateway-conformance-app-backend"} + const ( + appBackendNamespace = "gateway-conformance-app-backend" + infraNamespace = "gateway-conformance-infra" + poolName = "normal-gateway-pool" + sharedPrimaryGatewayName = "conformance-gateway" + httpRoutePrimaryName = "httproute-for-primary-gw" + hostnamePrimaryGw = "primary.example.com" + pathPrimaryGw = "/primary-gateway-test" + backendServicePodName = "infra-backend-deployment" + ) + + poolNN := types.NamespacedName{Name: poolName, Namespace: appBackendNamespace} + httpRoutePrimaryNN := types.NamespacedName{Name: httpRoutePrimaryName, Namespace: appBackendNamespace} + gatewayPrimaryNN := types.NamespacedName{Name: sharedPrimaryGatewayName, Namespace: infraNamespace} + + // inferenceTimeoutConfig := config.DefaultInferenceExtensionTimeoutConfig() + + k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, httpRoutePrimaryNN, gatewayPrimaryNN) + gwPrimaryAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gatewayPrimaryNN) + time.Sleep(300 * time.Second) t.Run("InferencePool should have Accepted condition set to True", func(t *testing.T) { - // Define the expected status condition. We use the standard "Accepted" - // condition type from the Gateway API for consistency. - acceptedCondition := metav1.Condition{ - Type: string(gatewayv1.GatewayConditionAccepted), // Standard condition type - Status: metav1.ConditionTrue, - Reason: "", // "" means we don't strictly check the Reason for this basic test. + t.Logf("InferencePool %s has parent status Accepted:True as expected with one references.", poolNN.String()) + ipAddress, err := k8sutils.GetPodIPByLabelWithControllerRuntime(t, s.Client, appBackendNamespace, map[string]string{"app": "infra-backend"}) + if err != nil { + require.NoErrorf(t, err, "error getting podIpAdress") } - k8sutils.InferencePoolMustHaveCondition(t, s.Client, poolNN, acceptedCondition) + t.Logf("Getting IPAddress is %v.", ipAddress) + + trafficutils.MakeRequestAndExpectSuccessV2( + t, + s.RoundTripper, + s.TimeoutConfig, + gwPrimaryAddr, + hostnamePrimaryGw, + pathPrimaryGw, + backendServicePodName, + appBackendNamespace, + map[string]string{"Test-Epp-Endpoint-Selection": ipAddress}, + ) + }) + + t.Run("InferencePool should have Accepted condition set to True", func(t *testing.T) { + t.Logf("InferencePool %s has parent status Accepted:True as expected with one references.", poolNN.String()) + ipAddress, err := k8sutils.GetPodIPByLabelWithControllerRuntime(t, s.Client, appBackendNamespace, map[string]string{"app": "infra-backend"}) + if err != nil { + require.NoErrorf(t, err, "error getting podIpAdress") + } + t.Logf("Getting IPAddress is %v.", ipAddress) + + wrongAddress := "10.0.0.17" + trafficutils.MakeRequestAndExpectSuccessV2( + t, + s.RoundTripper, + s.TimeoutConfig, + gwPrimaryAddr, + hostnamePrimaryGw, + pathPrimaryGw, + backendServicePodName, + appBackendNamespace, + map[string]string{"test-epp-endpoint-selection": wrongAddress}, + ) }) }, } diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index e69de29bb..bc48c1fb4 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -0,0 +1,205 @@ +# conformance/tests/basic/gateway_following_epp_routing.yaml + +# This manifest defines the initial resources for the +# gateway_following_epp_routing.go conformance test. + +# --- Backend Deployment (using standard Gateway API echoserver) --- +# This Deployment provides Pods for the InferencePool to select. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: infra-backend-deployment + namespace: gateway-conformance-app-backend + labels: + app: infra-backend +spec: + selector: + matchLabels: + app: infra-backend + template: + metadata: + labels: + ai.gke.io/inference-server: vllm + ai.gke.io/model: gemma-2-2b-it + app: infra-backend + spec: + containers: + - name: echoserver + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + ports: + - containerPort: 3000 + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 3 + periodSeconds: 5 + failureThreshold: 2 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP +--- +# --- Backend Service --- +# Service for the infra-backend-deployment. +apiVersion: inference.networking.x-k8s.io/v1alpha2 +kind: InferenceModel +metadata: + name: gemma2b + namespace: gateway-conformance-app-backend +spec: + modelName: google/gemma-2-2b-it + criticality: Critical # TODO mark it as critical to by pass + poolRef: + name: normal-gateway-pool +--- +# --- InferencePool Definition --- +apiVersion: inference.networking.x-k8s.io/v1alpha2 +kind: InferencePool +metadata: + name: normal-gateway-pool + namespace: gateway-conformance-app-backend +spec: + selector: + app: "infra-backend" + targetPortNumber: 3000 + extensionRef: + name: infra-backend-endpoint-picker +--- +# --- 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-gateway + namespace: gateway-conformance-infra + sectionName: http + hostnames: + - "primary.example.com" + rules: + - backendRefs: + - group: inference.networking.x-k8s.io + kind: InferencePool + name: normal-gateway-pool + matches: + - path: + type: PathPrefix + value: /primary-gateway-test +--- +# --- Conformance EPP service Definition --- +apiVersion: v1 +kind: Service +metadata: + name: infra-backend-endpoint-picker + namespace: gateway-conformance-app-backend +spec: + selector: + app: infra-backend-epp + ports: + - protocol: TCP + port: 9002 + targetPort: 9002 + appProtocol: http2 + type: ClusterIP +--- +# --- Conformance EPP Deployment --- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: infra-backend-epp + namespace: gateway-conformance-app-backend + labels: + app: infra-backend-epp +spec: + replicas: 1 + selector: + matchLabels: + app: infra-backend-epp + template: + metadata: + labels: + app: infra-backend-epp + spec: + # Conservatively, this timeout should mirror the longest grace period of the pods within the pool + terminationGracePeriodSeconds: 130 + containers: + - name: epp + image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main + imagePullPolicy: Always + args: + - -poolName + - "normal-gateway-pool" + - -poolNamespace + - "gateway-conformance-app-backend" + - -v + - "4" + - --zap-encoder + - "json" + - -grpcPort + - "9002" + - -grpcHealthPort + - "9003" + env: + - name: USE_STREAMING + value: "true" + - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING # Used for conformance test. + value: "true" + ports: + - containerPort: 9002 + - containerPort: 9003 + - name: metrics + containerPort: 9090 + livenessProbe: + grpc: + port: 9003 + service: inference-extension + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + grpc: + port: 9003 + service: inference-extension + initialDelaySeconds: 5 + periodSeconds: 10 +--- +# --- Conformance EPP Requried Role and RoleBindings --- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: inference-model-reader + namespace: gateway-conformance-app-backend +rules: +- apiGroups: ["inference.networking.x-k8s.io"] + resources: ["inferencemodels", "inferencepools"] + verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: epp-to-inference-model-reader + namespace: gateway-conformance-app-backend +subjects: +- kind: ServiceAccount + name: default + namespace: gateway-conformance-app-backend +roleRef: + kind: Role + name: inference-model-reader + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 2e866ca62..b5494465e 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -34,8 +34,10 @@ import ( // Import the Inference Extension API types inferenceapi "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2" // Adjust if your API version is different + corev1 "k8s.io/api/core/v1" // Import local config for Inference Extension "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/config" + // Import necessary utilities from the core Gateway API conformance suite gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiconfig "sigs.k8s.io/gateway-api/conformance/utils/config" @@ -271,3 +273,35 @@ func GetGatewayEndpoint(t *testing.T, k8sClient client.Client, timeoutConfig gat t.Logf("Gateway %s/%s has address: %s", gatewayNN.Namespace, gatewayNN.Name, gwAddr) return gwAddr } + +// GetPodIPByLabelWithControllerRuntime uses the controller-runtime client to find a pod +// by a label in a specific namespace and returns its IP address. +func GetPodIPByLabelWithControllerRuntime(t *testing.T, c client.Client, namespace string, labels map[string]string) (string, error) { + t.Helper() + // Create a PodList object to store the results of the query. + podList := &corev1.PodList{} + + // Define the options for the list query. + listOptions := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels(labels), + } + + // List the pods that match the specified options. + if err := c.List(context.Background(), podList, listOptions...); err != nil { + return "", fmt.Errorf("failed to list pods with labels '%v' in namespace '%s': %w", labels, namespace, err) + } + + // Check if any pods were found. + if len(podList.Items) == 0 { + return "", fmt.Errorf("no pods found with labels '%v' in namespace '%s'", labels, namespace) + } + + // Return the IP address of the first pod in the list. + podIP := podList.Items[0].Status.PodIP + if podIP == "" { + return "", fmt.Errorf("pod %s found, but it does not have an IP address yet", podList.Items[0].Name) + } + + return podIP, nil +} diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go index e65f45fb9..1e0df244e 100644 --- a/conformance/utils/traffic/traffic.go +++ b/conformance/utils/traffic/traffic.go @@ -34,12 +34,24 @@ func BuildExpectedHTTPResponse( expectedStatusCode int, backendName string, backendNamespace string, +) gwhttp.ExpectedResponse { + return BuildExpectedHTTPResponseWithHeaders(requestHost, requestPath, expectedStatusCode, backendName, backendNamespace, nil) +} + +func BuildExpectedHTTPResponseWithHeaders( + requestHost string, + requestPath string, + expectedStatusCode int, + backendName string, + backendNamespace string, + headers map[string]string, ) gwhttp.ExpectedResponse { resp := gwhttp.ExpectedResponse{ Request: gwhttp.Request{ - Host: requestHost, - Path: requestPath, - Method: "GET", + Host: requestHost, + Path: requestPath, + Method: "GET", + Headers: headers, }, Response: gwhttp.Response{ StatusCode: expectedStatusCode, @@ -51,9 +63,10 @@ func BuildExpectedHTTPResponse( if expectedStatusCode == http.StatusOK { resp.ExpectedRequest = &gwhttp.ExpectedRequest{ Request: gwhttp.Request{ - Host: requestHost, - Path: requestPath, - Method: "GET", + Host: requestHost, + Path: requestPath, + Headers: headers, + Method: "GET", }, } } @@ -83,6 +96,32 @@ func MakeRequestAndExpectSuccess( gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) } +// MakeRequestAndExpectSuccess is a helper function that builds an expected success (200 OK) response +// and then calls MakeRequestAndExpectEventuallyConsistentResponse. +func MakeRequestAndExpectSuccessV2( + t *testing.T, + r roundtripper.RoundTripper, + timeoutConfig gwconfig.TimeoutConfig, + gatewayAddress string, + requestHost string, + requestPath string, + backendName string, + backendNamespace string, + headers map[string]string, +) { + t.Helper() + expectedResponse := BuildExpectedHTTPResponseWithHeaders( + requestHost, + requestPath, + http.StatusOK, + backendName, + backendNamespace, + headers, + ) + expectedResponse.BackendSetResponseHeaders = map[string]string{"Hello": "world!"} + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) +} + // MakeRequestAndExpectNotFound is a helper function that builds an expected not found (404) response // and then calls MakeRequestAndExpectEventuallyConsistentResponse. func MakeRequestAndExpectNotFound( From 9f3e38202bf1b8c95d97109e4194dc8bc9f6e996 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Wed, 11 Jun 2025 03:49:28 +0000 Subject: [PATCH 03/19] Okay version of GatwayFollowingEPPRouting conformance test. --- .../basic/gateway_following_epp_routing.go | 132 ++++--- .../basic/gateway_following_epp_routing.yaml | 8 +- conformance/utils/config/timing.go | 14 +- conformance/utils/kubernetes/helpers.go | 12 +- conformance/utils/traffic/traffic.go | 325 +++++++++++++++++- 5 files changed, 417 insertions(+), 74 deletions(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 2145b9141..f2428f64c 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -27,6 +27,7 @@ import ( // Import the tests package to append to ConformanceTests "sigs.k8s.io/gateway-api-inference-extension/conformance/tests" + "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" ) @@ -40,7 +41,7 @@ func init() { // InferencePoolAccepted defines the test case for verifying basic InferencePool acceptance. var GatwayFollowingEPPRouting = suite.ConformanceTest{ ShortName: "GatwayFollowingEPPRouting", - Description: "Inference gateway should redirect traffic to an endpoints belonging to what EPP respond endpoints list.", // TODO + Description: "Inference gateway should redirect traffic to an endpoints belonging to what EPP respond endpoints list", Manifests: []string{"tests/basic/gateway_following_epp_routing.yaml"}, Features: []features.FeatureName{ features.FeatureName("SupportInferencePool"), @@ -48,66 +49,113 @@ var GatwayFollowingEPPRouting = suite.ConformanceTest{ }, Test: func(t *testing.T, s *suite.ConformanceTestSuite) { const ( - appBackendNamespace = "gateway-conformance-app-backend" - infraNamespace = "gateway-conformance-infra" - poolName = "normal-gateway-pool" - sharedPrimaryGatewayName = "conformance-gateway" - httpRoutePrimaryName = "httproute-for-primary-gw" - hostnamePrimaryGw = "primary.example.com" - pathPrimaryGw = "/primary-gateway-test" - backendServicePodName = "infra-backend-deployment" + appBackendNamespace = "gateway-conformance-app-backend" + infraNamespace = "gateway-conformance-infra" + hostname = "primary.example.com" + path = "/primary-gateway-test" ) - poolNN := types.NamespacedName{Name: poolName, Namespace: appBackendNamespace} - httpRoutePrimaryNN := types.NamespacedName{Name: httpRoutePrimaryName, Namespace: appBackendNamespace} - gatewayPrimaryNN := types.NamespacedName{Name: sharedPrimaryGatewayName, Namespace: infraNamespace} + httpRouteNN := types.NamespacedName{Name: "httproute-for-primary-gw", Namespace: appBackendNamespace} + gatewayNN := types.NamespacedName{Name: "conformance-gateway", Namespace: infraNamespace} + poolNN := types.NamespacedName{Name: "normal-gateway-pool", Namespace: appBackendNamespace} + backendPodLabels := map[string]string{"app": "infra-backend"} - // inferenceTimeoutConfig := config.DefaultInferenceExtensionTimeoutConfig() + k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, httpRouteNN, gatewayNN) + k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN) + gwAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gatewayNN) - k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, httpRoutePrimaryNN, gatewayPrimaryNN) - gwPrimaryAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gatewayPrimaryNN) - time.Sleep(300 * time.Second) + backendPodIP, err := k8sutils.GetOnePodIPWithLabel(t, s.Client, appBackendNamespace, backendPodLabels) + require.NoError(t, err, "Failed to get backend Pod IP address") - t.Run("InferencePool should have Accepted condition set to True", func(t *testing.T) { - t.Logf("InferencePool %s has parent status Accepted:True as expected with one references.", poolNN.String()) - ipAddress, err := k8sutils.GetPodIPByLabelWithControllerRuntime(t, s.Client, appBackendNamespace, map[string]string{"app": "infra-backend"}) - if err != nil { - require.NoErrorf(t, err, "error getting podIpAdress") - } - t.Logf("Getting IPAddress is %v.", ipAddress) + inferenceTimeoutConfig := config.DefaultInferenceExtensionTimeoutConfig() + // TODO: replace this with a poll and check. + t.Log("Waiting for the httpRoute and inferecePool ready to serve traffic.") + time.Sleep(inferenceTimeoutConfig.WaitForHttpRouteAndInferencePoolReadyTimeout) + + correctRequestBody := `{ + "model": "conformance-fake-model", + "prompt": "Write as if you were a critic: San Francisc" + }` + + t.Run("Gateway should route traffic to a valid endpoint specified by EPP", func(t *testing.T) { + t.Logf("Sending request to %s with EPP header routing to valid IP %s", gwAddr, backendPodIP) + eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} trafficutils.MakeRequestAndExpectSuccessV2( t, s.RoundTripper, s.TimeoutConfig, - gwPrimaryAddr, - hostnamePrimaryGw, - pathPrimaryGw, - backendServicePodName, + gwAddr, + hostname, + path, + "infra-backend-deployment", // This might be better as a constant if used often appBackendNamespace, - map[string]string{"Test-Epp-Endpoint-Selection": ipAddress}, + eppHeader, + correctRequestBody, + "POST", ) }) - t.Run("InferencePool should have Accepted condition set to True", func(t *testing.T) { - t.Logf("InferencePool %s has parent status Accepted:True as expected with one references.", poolNN.String()) - ipAddress, err := k8sutils.GetPodIPByLabelWithControllerRuntime(t, s.Client, appBackendNamespace, map[string]string{"app": "infra-backend"}) - if err != nil { - require.NoErrorf(t, err, "error getting podIpAdress") - } - t.Logf("Getting IPAddress is %v.", ipAddress) + t.Run("Gateway should route traffic specified by EPP even an invalidIP and should get response with error code 429", func(t *testing.T) { + invalidIP := "256.256.256.256" // An IP that cannot be a real endpoint + t.Logf("Sending request to %s with EPP header routing to invalid IP %s", gwAddr, invalidIP) + eppHeader := map[string]string{"test-epp-endpoint-selection": invalidIP} - wrongAddress := "10.0.0.17" - trafficutils.MakeRequestAndExpectSuccessV2( + trafficutils.MakeRequestAndExpectTooManyRequest( + t, + s.RoundTripper, + s.TimeoutConfig, + gwAddr, + hostname, + path, + "infra-backend-deployment", + appBackendNamespace, + eppHeader, + correctRequestBody, + "POST", + ) + }) + + t.Run("Gateway should reject request that is missing the model name and return 400 response", func(t *testing.T) { + requestBodyWithoutModel := `{"prompt": "Write as if you were a critic: San Francisc"}` + eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} + t.Logf("Sending request to %s with a malformed body (missing model)", gwAddr) + + trafficutils.MakeRequestAndExpectBadRequest( + t, + s.RoundTripper, + s.TimeoutConfig, + gwAddr, + hostname, + path, + "infra-backend-deployment", + appBackendNamespace, + eppHeader, + requestBodyWithoutModel, + "POST", + ) + }) + + t.Run("Gateway should reject request that is with a nonexist model name and return 404 response", func(t *testing.T) { + requestBodyNonExistModel := `{ + "model": "non-exist-model", + "prompt": "Write as if you were a critic: San Francisc" + }` + eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} + t.Logf("Sending request to %s with a malformed body (nonexist model)", gwAddr) + + trafficutils.MakeRequestAndExpectNotFoundV2( t, s.RoundTripper, s.TimeoutConfig, - gwPrimaryAddr, - hostnamePrimaryGw, - pathPrimaryGw, - backendServicePodName, + gwAddr, + hostname, + path, + "infra-backend-deployment", appBackendNamespace, - map[string]string{"test-epp-endpoint-selection": wrongAddress}, + eppHeader, + requestBodyNonExistModel, + "POST", ) }) }, diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index bc48c1fb4..fe96cd981 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -19,8 +19,6 @@ spec: template: metadata: labels: - ai.gke.io/inference-server: vllm - ai.gke.io/model: gemma-2-2b-it app: infra-backend spec: containers: @@ -54,11 +52,11 @@ spec: apiVersion: inference.networking.x-k8s.io/v1alpha2 kind: InferenceModel metadata: - name: gemma2b + name: conformance-fake-model-server namespace: gateway-conformance-app-backend spec: - modelName: google/gemma-2-2b-it - criticality: Critical # TODO mark it as critical to by pass + 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: normal-gateway-pool --- diff --git a/conformance/utils/config/timing.go b/conformance/utils/config/timing.go index f5d4eeb52..888a0c74a 100644 --- a/conformance/utils/config/timing.go +++ b/conformance/utils/config/timing.go @@ -41,15 +41,19 @@ type InferenceExtensionTimeoutConfig struct { // HTTPRouteDeletionReconciliationTimeout is the time to wait for controllers to reconcile // state after an HTTPRoute is deleted, before checking dependent resources or traffic. HTTPRouteDeletionReconciliationTimeout time.Duration + + // WaitForHttpRouteAndInferencePoolReadyTimeout is the time to wait for httpRoute and inferencePool ready to server the traffic. + WaitForHttpRouteAndInferencePoolReadyTimeout time.Duration } // DefaultInferenceExtensionTimeoutConfig returns a new InferenceExtensionTimeoutConfig with default values. func DefaultInferenceExtensionTimeoutConfig() InferenceExtensionTimeoutConfig { return InferenceExtensionTimeoutConfig{ - TimeoutConfig: gatewayconfig.DefaultTimeoutConfig(), // Initialize embedded struct - InferencePoolMustHaveConditionTimeout: 300 * time.Second, - InferencePoolMustHaveConditionInterval: 10 * time.Second, - GatewayObjectPollInterval: 5 * time.Second, - HTTPRouteDeletionReconciliationTimeout: 5 * time.Second, + TimeoutConfig: gatewayconfig.DefaultTimeoutConfig(), // Initialize embedded struct + InferencePoolMustHaveConditionTimeout: 300 * time.Second, + InferencePoolMustHaveConditionInterval: 10 * time.Second, + GatewayObjectPollInterval: 5 * time.Second, + HTTPRouteDeletionReconciliationTimeout: 5 * time.Second, + WaitForHttpRouteAndInferencePoolReadyTimeout: 100 * time.Second, } } diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index b5494465e..81d04cfe1 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -274,30 +274,26 @@ func GetGatewayEndpoint(t *testing.T, k8sClient client.Client, timeoutConfig gat return gwAddr } -// GetPodIPByLabelWithControllerRuntime uses the controller-runtime client to find a pod -// by a label in a specific namespace and returns its IP address. -func GetPodIPByLabelWithControllerRuntime(t *testing.T, c client.Client, namespace string, labels map[string]string) (string, error) { +// GetOnePodIPWithLabel finds a pod with labels in a specific namespace and returns its IP address. +func GetOnePodIPWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string) (string, error) { t.Helper() - // Create a PodList object to store the results of the query. + podList := &corev1.PodList{} - // Define the options for the list query. listOptions := []client.ListOption{ client.InNamespace(namespace), client.MatchingLabels(labels), } - // List the pods that match the specified options. + t.Logf("Retrieving backend Pod IP address in namespace %s", namespace) if err := c.List(context.Background(), podList, listOptions...); err != nil { return "", fmt.Errorf("failed to list pods with labels '%v' in namespace '%s': %w", labels, namespace, err) } - // Check if any pods were found. if len(podList.Items) == 0 { return "", fmt.Errorf("no pods found with labels '%v' in namespace '%s'", labels, namespace) } - // Return the IP address of the first pod in the list. podIP := podList.Items[0].Status.PodIP if podIP == "" { return "", fmt.Errorf("pod %s found, but it does not have an IP address yet", podList.Items[0].Name) diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go index 1e0df244e..0e0c1989f 100644 --- a/conformance/utils/traffic/traffic.go +++ b/conformance/utils/traffic/traffic.go @@ -17,12 +17,21 @@ 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" ) // BuildExpectedHTTPResponse constructs a gwhttp.ExpectedResponse for common test scenarios. @@ -35,7 +44,7 @@ func BuildExpectedHTTPResponse( backendName string, backendNamespace string, ) gwhttp.ExpectedResponse { - return BuildExpectedHTTPResponseWithHeaders(requestHost, requestPath, expectedStatusCode, backendName, backendNamespace, nil) + return BuildExpectedHTTPResponseWithHeaders(requestHost, requestPath, expectedStatusCode, backendName, backendNamespace, nil, "GET") } func BuildExpectedHTTPResponseWithHeaders( @@ -45,12 +54,13 @@ func BuildExpectedHTTPResponseWithHeaders( backendName string, backendNamespace string, headers map[string]string, + method string, ) gwhttp.ExpectedResponse { resp := gwhttp.ExpectedResponse{ Request: gwhttp.Request{ Host: requestHost, Path: requestPath, - Method: "GET", + Method: method, Headers: headers, }, Response: gwhttp.Response{ @@ -66,7 +76,7 @@ func BuildExpectedHTTPResponseWithHeaders( Host: requestHost, Path: requestPath, Headers: headers, - Method: "GET", + Method: method, }, } } @@ -96,8 +106,29 @@ func MakeRequestAndExpectSuccess( gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) } -// MakeRequestAndExpectSuccess is a helper function that builds an expected success (200 OK) response +// MakeRequestAndExpectNotFound is a helper function that builds an expected not found (404) response // and then calls MakeRequestAndExpectEventuallyConsistentResponse. +func MakeRequestAndExpectNotFound( + t *testing.T, + r roundtripper.RoundTripper, + timeoutConfig gwconfig.TimeoutConfig, + gatewayAddress string, + requestHost string, + requestPath string, +) { + t.Helper() + expectedResponse := BuildExpectedHTTPResponse( + requestHost, + requestPath, + http.StatusNotFound, + "", // Backend name not relevant for 404 + "", // Backend namespace not relevant for 404 + ) + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) +} + +// MakeRequestAndExpectSuccessV2 is a helper function that builds an expected success (200 OK) response. +// And then make the request and waiting for the response to be expected. func MakeRequestAndExpectSuccessV2( t *testing.T, r roundtripper.RoundTripper, @@ -108,37 +139,303 @@ func MakeRequestAndExpectSuccessV2( backendName string, backendNamespace string, headers map[string]string, + reqBody string, + method string, ) { t.Helper() - expectedResponse := BuildExpectedHTTPResponseWithHeaders( + t.Helper() + makeRequestAndExpectRequestInternal( + t, + r, + timeoutConfig, + gatewayAddress, requestHost, requestPath, + backendName, + backendNamespace, + headers, + reqBody, + method, http.StatusOK, + ) +} + +// MakeRequestAndExpectTooManyRequest is a helper function that builds an expected too many request (429) response. +// And then make the request and waiting for the response to be expected. +func MakeRequestAndExpectTooManyRequest( + t *testing.T, + r roundtripper.RoundTripper, + timeoutConfig gwconfig.TimeoutConfig, + gatewayAddress string, + requestHost string, + requestPath string, + backendName string, + backendNamespace string, + headers map[string]string, + reqBody string, + method string, +) { + t.Helper() + makeRequestAndExpectRequestInternal( + t, + r, + timeoutConfig, + gatewayAddress, + requestHost, + requestPath, backendName, backendNamespace, headers, + reqBody, + method, + http.StatusTooManyRequests, ) - expectedResponse.BackendSetResponseHeaders = map[string]string{"Hello": "world!"} - gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) } -// MakeRequestAndExpectNotFound is a helper function that builds an expected not found (404) response -// and then calls MakeRequestAndExpectEventuallyConsistentResponse. -func MakeRequestAndExpectNotFound( +// MakeRequestAndExpectBadRequest is a helper function that builds an expected bad request (400) response. +// And then make the request and waiting for the response to be expected. +func MakeRequestAndExpectBadRequest( t *testing.T, r roundtripper.RoundTripper, timeoutConfig gwconfig.TimeoutConfig, gatewayAddress string, requestHost string, requestPath string, + backendName string, + backendNamespace string, + headers map[string]string, + reqBody string, + method string, ) { t.Helper() - expectedResponse := BuildExpectedHTTPResponse( + makeRequestAndExpectRequestInternal( + t, + r, + timeoutConfig, + gatewayAddress, + requestHost, + requestPath, + backendName, + backendNamespace, + headers, + reqBody, + method, + http.StatusBadRequest, + ) +} + +// MakeRequestAndExpectBadRequest is a helper function that builds an expected not found (404) response. +// And then make the request and waiting for the response to be expected. +func MakeRequestAndExpectNotFoundV2( + t *testing.T, + r roundtripper.RoundTripper, + timeoutConfig gwconfig.TimeoutConfig, + gatewayAddress string, + requestHost string, + requestPath string, + backendName string, + backendNamespace string, + headers map[string]string, + reqBody string, + method string, +) { + t.Helper() + makeRequestAndExpectRequestInternal( + t, + r, + timeoutConfig, + gatewayAddress, requestHost, requestPath, + backendName, + backendNamespace, + headers, + reqBody, + method, http.StatusNotFound, - "", // Backend name not relevant for 404 - "", // Backend namespace not relevant for 404 ) - gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) +} + +func makeRequestAndExpectRequestInternal(t *testing.T, + r roundtripper.RoundTripper, + timeoutConfig gwconfig.TimeoutConfig, + gatewayAddress string, + requestHost string, + requestPath string, + backendName string, + backendNamespace string, + headers map[string]string, + reqBody string, + method string, + expectedHttpCode int, +) { + t.Helper() + expectedResponse := BuildExpectedHTTPResponseWithHeaders( + requestHost, + requestPath, + expectedHttpCode, + backendName, + backendNamespace, + headers, + method, + ) + waitForConvergeToExpected(t, r, timeoutConfig, gatewayAddress, reqBody, expectedResponse) +} + +// TODO: 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, Body: strings.NewReader(requestBody)} + defaultRoundTripper, ok := r.(*roundtripper.DefaultRoundTripper) + if !ok { + t.Fatalf("Unsupported RoundTripper type: %T", r) + } + cReq, cRes, err := makeCallRoundTripper(defaultRoundTripper, request) + if err != nil { + tlog.Logf(t, "Request failed, not ready yet: %v (after %v)", err.Error(), elapsed) + return false + } + + if err := gwhttp.CompareRequest(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") +} + +// RequestWithBody extends roundtripper.Request to include a request body. +type RequestWithBody struct { + roundtripper.Request + Body io.Reader +} + +// makeCallRoundTripper executes an HTTP request using the provided RoundTripper and captures the request and response. +func makeCallRoundTripper(rt *roundtripper.DefaultRoundTripper, request *RequestWithBody) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { + client := &http.Client{} + + 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 b83290c385c3de9e1f8abf499967d60f233c094c Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Wed, 11 Jun 2025 18:28:50 +0000 Subject: [PATCH 04/19] fix typos and formats. --- .../basic/gateway_following_epp_routing.go | 22 +++++++++---------- .../basic/gateway_following_epp_routing.yaml | 3 ++- conformance/utils/traffic/traffic.go | 1 - 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index f2428f64c..fb0737dc4 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -33,15 +33,15 @@ import ( ) func init() { - // Register the InferencePoolAccepted test case with the conformance suite. + // 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, GatwayFollowingEPPRouting) + tests.ConformanceTests = append(tests.ConformanceTests, GatewayFollowingEPPRouting) } -// InferencePoolAccepted defines the test case for verifying basic InferencePool acceptance. -var GatwayFollowingEPPRouting = suite.ConformanceTest{ - ShortName: "GatwayFollowingEPPRouting", - Description: "Inference gateway should redirect traffic to an endpoints belonging to what EPP respond endpoints list", +// 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"), @@ -74,10 +74,10 @@ var GatwayFollowingEPPRouting = suite.ConformanceTest{ correctRequestBody := `{ "model": "conformance-fake-model", - "prompt": "Write as if you were a critic: San Francisc" + "prompt": "Write as if you were a critic: San Francisco" }` - t.Run("Gateway should route traffic to a valid endpoint specified by EPP", func(t *testing.T) { + t.Run("Gateway should send traffic to a valid endpoint specified by EPP", func(t *testing.T) { t.Logf("Sending request to %s with EPP header routing to valid IP %s", gwAddr, backendPodIP) eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} @@ -96,7 +96,7 @@ var GatwayFollowingEPPRouting = suite.ConformanceTest{ ) }) - t.Run("Gateway should route traffic specified by EPP even an invalidIP and should get response with error code 429", func(t *testing.T) { + t.Run("Gateway should send traffic specified by EPP even an invalidIP and should get response with error code 429", func(t *testing.T) { invalidIP := "256.256.256.256" // An IP that cannot be a real endpoint t.Logf("Sending request to %s with EPP header routing to invalid IP %s", gwAddr, invalidIP) eppHeader := map[string]string{"test-epp-endpoint-selection": invalidIP} @@ -117,7 +117,7 @@ var GatwayFollowingEPPRouting = suite.ConformanceTest{ }) t.Run("Gateway should reject request that is missing the model name and return 400 response", func(t *testing.T) { - requestBodyWithoutModel := `{"prompt": "Write as if you were a critic: San Francisc"}` + requestBodyWithoutModel := `{"prompt": "Write as if you were a critic: San Francisco"}` eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} t.Logf("Sending request to %s with a malformed body (missing model)", gwAddr) @@ -139,7 +139,7 @@ var GatwayFollowingEPPRouting = suite.ConformanceTest{ t.Run("Gateway should reject request that is with a nonexist model name and return 404 response", func(t *testing.T) { requestBodyNonExistModel := `{ "model": "non-exist-model", - "prompt": "Write as if you were a critic: San Francisc" + "prompt": "Write as if you were a critic: San Francisco" }` eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} t.Logf("Sending request to %s with a malformed body (nonexist model)", gwAddr) diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index fe96cd981..d48521cc9 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -200,4 +200,5 @@ subjects: roleRef: kind: Role name: inference-model-reader - apiGroup: rbac.authorization.k8s.io \ No newline at end of file + apiGroup: rbac.authorization.k8s.io + \ No newline at end of file diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go index 0e0c1989f..08b260dd6 100644 --- a/conformance/utils/traffic/traffic.go +++ b/conformance/utils/traffic/traffic.go @@ -142,7 +142,6 @@ func MakeRequestAndExpectSuccessV2( reqBody string, method string, ) { - t.Helper() t.Helper() makeRequestAndExpectRequestInternal( t, From ff3086a31f5d4051d58fc30633f5037da2afadcc Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Wed, 11 Jun 2025 22:19:22 +0000 Subject: [PATCH 05/19] upgrader gateway-api versino to use the updated conformance testutils and small refactor. --- .../basic/gateway_following_epp_routing.go | 76 ++-- conformance/utils/kubernetes/helpers.go | 40 +- conformance/utils/traffic/traffic.go | 397 ++++-------------- go.mod | 2 +- go.sum | 4 +- 5 files changed, 152 insertions(+), 367 deletions(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index fb0737dc4..93ad84568 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -17,6 +17,7 @@ limitations under the License. package basic import ( + "net/http" "testing" "time" @@ -53,6 +54,7 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ infraNamespace = "gateway-conformance-infra" hostname = "primary.example.com" path = "/primary-gateway-test" + backendName = "infra-backend-deployment" ) httpRouteNN := types.NamespacedName{Name: "httproute-for-primary-gw", Namespace: appBackendNamespace} @@ -86,13 +88,15 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ s.RoundTripper, s.TimeoutConfig, gwAddr, - hostname, - path, - "infra-backend-deployment", // This might be better as a constant if used often - appBackendNamespace, - eppHeader, - correctRequestBody, - "POST", + trafficutils.Request{ + Host: hostname, + Path: path, + Headers: eppHeader, + Method: http.MethodPost, + Body: correctRequestBody, + Backend: backendName, + Namespace: appBackendNamespace, + }, ) }) @@ -101,18 +105,21 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ t.Logf("Sending request to %s with EPP header routing to invalid IP %s", gwAddr, invalidIP) eppHeader := map[string]string{"test-epp-endpoint-selection": invalidIP} - trafficutils.MakeRequestAndExpectTooManyRequest( + trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwAddr, - hostname, - path, - "infra-backend-deployment", - appBackendNamespace, - eppHeader, - correctRequestBody, - "POST", + trafficutils.Request{ + Host: hostname, + Path: path, + Headers: eppHeader, + Method: http.MethodPost, + Body: correctRequestBody, + Namespace: appBackendNamespace, + + ExpectedStatusCode: http.StatusTooManyRequests, + }, ) }) @@ -121,18 +128,21 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} t.Logf("Sending request to %s with a malformed body (missing model)", gwAddr) - trafficutils.MakeRequestAndExpectBadRequest( + trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwAddr, - hostname, - path, - "infra-backend-deployment", - appBackendNamespace, - eppHeader, - requestBodyWithoutModel, - "POST", + trafficutils.Request{ + Host: hostname, + Path: path, + Headers: eppHeader, + Method: http.MethodPost, + Body: requestBodyWithoutModel, + Namespace: appBackendNamespace, + + ExpectedStatusCode: http.StatusBadRequest, + }, ) }) @@ -144,19 +154,23 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} t.Logf("Sending request to %s with a malformed body (nonexist model)", gwAddr) - trafficutils.MakeRequestAndExpectNotFoundV2( + trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( t, s.RoundTripper, s.TimeoutConfig, gwAddr, - hostname, - path, - "infra-backend-deployment", - appBackendNamespace, - eppHeader, - requestBodyNonExistModel, - "POST", + trafficutils.Request{ + Host: hostname, + Path: path, + Headers: eppHeader, + Method: http.MethodPost, + Body: requestBodyNonExistModel, + Namespace: appBackendNamespace, + + ExpectedStatusCode: http.StatusNotFound, + }, ) + }) }, } diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 81d04cfe1..253d49952 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -274,30 +274,50 @@ func GetGatewayEndpoint(t *testing.T, k8sClient client.Client, timeoutConfig gat return gwAddr } -// GetOnePodIPWithLabel finds a pod with labels in a specific namespace and returns its IP address. -func GetOnePodIPWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string) (string, error) { +// GetPodIPsWithLabel retrieves a list of Pod IP addresses. +// It finds pods matching the given labels in a specific namespace. +// The 'count' parameter specifies the maximum number of IPs to return. +// If 'count' is 0, it will return all found IP addresses. +func GetPodIPsWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string, count int) ([]string, error) { t.Helper() podList := &corev1.PodList{} - listOptions := []client.ListOption{ client.InNamespace(namespace), client.MatchingLabels(labels), } - t.Logf("Retrieving backend Pod IP address in namespace %s", namespace) + t.Logf("Searching for Pods with labels %v in namespace %s", labels, namespace) if err := c.List(context.Background(), podList, listOptions...); err != nil { - return "", fmt.Errorf("failed to list pods with labels '%v' in namespace '%s': %w", labels, namespace, err) + return nil, fmt.Errorf("failed to list pods with labels '%v' in namespace '%s': %w", labels, namespace, err) } if len(podList.Items) == 0 { - return "", fmt.Errorf("no pods found with labels '%v' in namespace '%s'", labels, namespace) + return nil, fmt.Errorf("no pods found with labels '%v' in namespace '%s'", labels, namespace) + } + + var podIPs []string + for _, pod := range podList.Items { + podIPs = append(podIPs, pod.Status.PodIP) + if count > 0 && len(podIPs) == count { + break + } } + return podIPs, nil +} - podIP := podList.Items[0].Status.PodIP - if podIP == "" { - return "", fmt.Errorf("pod %s found, but it does not have an IP address yet", podList.Items[0].Name) +// GetOnePodIPWithLabel finds a single pod with labels in a specific namespace and returns its IP address. +// This function is a wrapper around GetPodIPsWithLabel for convenience. +func GetOnePodIPWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string) (string, error) { + t.Helper() + + podIPs, err := GetPodIPsWithLabel(t, c, namespace, labels, 1) + if err != nil { + return "", nil } - return podIP, nil + if len(podIPs) == 0 { + return "", fmt.Errorf("no pods were found with labels '%v' in namespace '%s'", labels, namespace) + } + return podIPs[0], nil } diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go index 08b260dd6..e0bfae885 100644 --- a/conformance/utils/traffic/traffic.go +++ b/conformance/utils/traffic/traffic.go @@ -17,23 +17,37 @@ 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 +} + +// Deprecated: this will be replaced by letting caller to construct the Request type. // BuildExpectedHTTPResponse constructs a gwhttp.ExpectedResponse for common test scenarios. // For 200 OK responses, it sets up ExpectedRequest to check Host and Path. // For other status codes (like 404), ExpectedRequest is nil as detailed backend checks are usually skipped by CompareRequest. @@ -43,25 +57,12 @@ func BuildExpectedHTTPResponse( expectedStatusCode int, backendName string, backendNamespace string, -) gwhttp.ExpectedResponse { - return BuildExpectedHTTPResponseWithHeaders(requestHost, requestPath, expectedStatusCode, backendName, backendNamespace, nil, "GET") -} - -func BuildExpectedHTTPResponseWithHeaders( - requestHost string, - requestPath string, - expectedStatusCode int, - backendName string, - backendNamespace string, - headers map[string]string, - method string, ) gwhttp.ExpectedResponse { resp := gwhttp.ExpectedResponse{ Request: gwhttp.Request{ - Host: requestHost, - Path: requestPath, - Method: method, - Headers: headers, + Host: requestHost, + Path: requestPath, + Method: "GET", }, Response: gwhttp.Response{ StatusCode: expectedStatusCode, @@ -73,16 +74,16 @@ func BuildExpectedHTTPResponseWithHeaders( if expectedStatusCode == http.StatusOK { resp.ExpectedRequest = &gwhttp.ExpectedRequest{ Request: gwhttp.Request{ - Host: requestHost, - Path: requestPath, - Headers: headers, - Method: method, + Host: requestHost, + Path: requestPath, + Method: "GET", }, } } return resp } +// Deprecated: please use MakeRequestAndExpectSuccessV2 instead. // MakeRequestAndExpectSuccess is a helper function that builds an expected success (200 OK) response // and then calls MakeRequestAndExpectEventuallyConsistentResponse. func MakeRequestAndExpectSuccess( @@ -106,6 +107,7 @@ func MakeRequestAndExpectSuccess( gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) } +// Deprecated: please use MakeRequestAndExpectEventuallyConsistentResponse instead and specify the ExpectedStatusCode in Request. // MakeRequestAndExpectNotFound is a helper function that builds an expected not found (404) response // and then calls MakeRequestAndExpectEventuallyConsistentResponse. func MakeRequestAndExpectNotFound( @@ -127,314 +129,63 @@ func MakeRequestAndExpectNotFound( gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) } -// MakeRequestAndExpectSuccessV2 is a helper function that builds an expected success (200 OK) response. -// And then make the request and waiting for the response to be expected. -func MakeRequestAndExpectSuccessV2( +// 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, - requestHost string, - requestPath string, - backendName string, - backendNamespace string, - headers map[string]string, - reqBody string, - method string, + req Request, ) { t.Helper() - makeRequestAndExpectRequestInternal( - t, - r, - timeoutConfig, - gatewayAddress, - requestHost, - requestPath, - backendName, - backendNamespace, - headers, - reqBody, - method, - http.StatusOK, - ) -} -// MakeRequestAndExpectTooManyRequest is a helper function that builds an expected too many request (429) response. -// And then make the request and waiting for the response to be expected. -func MakeRequestAndExpectTooManyRequest( - t *testing.T, - r roundtripper.RoundTripper, - timeoutConfig gwconfig.TimeoutConfig, - gatewayAddress string, - requestHost string, - requestPath string, - backendName string, - backendNamespace string, - headers map[string]string, - reqBody string, - method string, -) { - t.Helper() - makeRequestAndExpectRequestInternal( - t, - r, - timeoutConfig, - gatewayAddress, - requestHost, - requestPath, - backendName, - backendNamespace, - headers, - reqBody, - method, - http.StatusTooManyRequests, - ) -} - -// MakeRequestAndExpectBadRequest is a helper function that builds an expected bad request (400) response. -// And then make the request and waiting for the response to be expected. -func MakeRequestAndExpectBadRequest( - t *testing.T, - r roundtripper.RoundTripper, - timeoutConfig gwconfig.TimeoutConfig, - gatewayAddress string, - requestHost string, - requestPath string, - backendName string, - backendNamespace string, - headers map[string]string, - reqBody string, - method string, -) { - t.Helper() - makeRequestAndExpectRequestInternal( - t, - r, - timeoutConfig, - gatewayAddress, - requestHost, - requestPath, - backendName, - backendNamespace, - headers, - reqBody, - method, - http.StatusBadRequest, - ) -} + method := http.MethodGet + if req.Method != "" { + method = req.Method + } -// MakeRequestAndExpectBadRequest is a helper function that builds an expected not found (404) response. -// And then make the request and waiting for the response to be expected. -func MakeRequestAndExpectNotFoundV2( - t *testing.T, - r roundtripper.RoundTripper, - timeoutConfig gwconfig.TimeoutConfig, - gatewayAddress string, - requestHost string, - requestPath string, - backendName string, - backendNamespace string, - headers map[string]string, - reqBody string, - method string, -) { - t.Helper() - makeRequestAndExpectRequestInternal( - t, - r, - timeoutConfig, - gatewayAddress, - requestHost, - requestPath, - backendName, - backendNamespace, - headers, - reqBody, - method, - http.StatusNotFound, - ) -} + expectedResponse := gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: req.Host, + Path: req.Path, + Method: method, + Headers: req.Headers, + Body: req.Body, + }, + Response: gwhttp.Response{ + StatusCode: req.ExpectedStatusCode, + }, + Backend: req.Backend, + Namespace: req.Namespace, + } -func makeRequestAndExpectRequestInternal(t *testing.T, - r roundtripper.RoundTripper, - timeoutConfig gwconfig.TimeoutConfig, - gatewayAddress string, - requestHost string, - requestPath string, - backendName string, - backendNamespace string, - headers map[string]string, - reqBody string, - method string, - expectedHttpCode int, -) { - t.Helper() - expectedResponse := BuildExpectedHTTPResponseWithHeaders( - requestHost, - requestPath, - expectedHttpCode, - backendName, - backendNamespace, - headers, - method, - ) - waitForConvergeToExpected(t, r, timeoutConfig, gatewayAddress, reqBody, expectedResponse) + // 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, + }, + } + } + gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) } -// TODO: replace the following method when sigs.k8s.io/gateway-api/conformance/utils/roundtripper is able to send request with body. -func waitForConvergeToExpected( +// MakeRequestAndExpectSuccessV2 is a convenience wrapper for requests that are +// expected to succeed with a 200 OK status. +func MakeRequestAndExpectSuccessV2( t *testing.T, r roundtripper.RoundTripper, timeoutConfig gwconfig.TimeoutConfig, gatewayAddress string, - requestBody string, - expectedResponse gwhttp.ExpectedResponse, + req Request, ) { - gwhttp.AwaitConvergence(t, timeoutConfig.RequiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency, func(elapsed time.Duration) bool { - req := gwhttp.MakeRequest(t, &expectedResponse, gatewayAddress, "HTTP", "http") - request := &RequestWithBody{Request: req, Body: strings.NewReader(requestBody)} - defaultRoundTripper, ok := r.(*roundtripper.DefaultRoundTripper) - if !ok { - t.Fatalf("Unsupported RoundTripper type: %T", r) - } - cReq, cRes, err := makeCallRoundTripper(defaultRoundTripper, request) - if err != nil { - tlog.Logf(t, "Request failed, not ready yet: %v (after %v)", err.Error(), elapsed) - return false - } - - if err := gwhttp.CompareRequest(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") -} - -// RequestWithBody extends roundtripper.Request to include a request body. -type RequestWithBody struct { - roundtripper.Request - Body io.Reader -} - -// makeCallRoundTripper executes an HTTP request using the provided RoundTripper and captures the request and response. -func makeCallRoundTripper(rt *roundtripper.DefaultRoundTripper, request *RequestWithBody) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) { - client := &http.Client{} - - 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) + t.Helper() + req.ExpectedStatusCode = http.StatusOK + MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, req) } diff --git a/go.mod b/go.mod index 192773cc6..6209c3983 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( k8s.io/component-base v0.33.1 k8s.io/utils v0.0.0-20241210054802-24370beab758 sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/gateway-api v1.3.0 + sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6 sigs.k8s.io/structured-merge-diff/v4 v4.7.0 sigs.k8s.io/yaml v1.4.0 ) diff --git a/go.sum b/go.sum index 7733d5555..e0803dd2f 100644 --- a/go.sum +++ b/go.sum @@ -321,8 +321,8 @@ sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytI sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3EwfNw= sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI= -sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M= -sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk= +sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6 h1:/9+O2vVgnZz1BbJXIMtvAT+YaajmFzBIZzyku8VwMB4= +sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6/go.mod h1:YpiVaiQMBZtoEGeoCEz2vMBYpJLkvAv2kqtiMvtjnrQ= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= From 5b725d0794e6e217c4512e58efea498dbf0be95a Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Wed, 11 Jun 2025 23:56:28 +0000 Subject: [PATCH 06/19] use AllowCRDsMismatch to bypass. --- conformance/conformance.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conformance/conformance.go b/conformance/conformance.go index 7585e74ce..367ea4227 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -176,6 +176,8 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { ManifestFS: []fs.FS{&Manifests}, ReportOutputPath: *confflags.ReportOutput, SkipProvisionalTests: *confflags.SkipProvisionalTests, + // TODO: make AllowCRDsMismatch to false when the gateway-api new version is released. + AllowCRDsMismatch: true, // TODO: Add the inference extension specific fields to ConformanceOptions struct if needed, // or handle them during report generation. // GatewayAPIInferenceExtensionChannel: inferenceExtensionChannel, From 838b5359266bae389f94c7e0fa813808881fdfa5 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 13 Jun 2025 19:03:55 +0000 Subject: [PATCH 07/19] format. --- conformance/tests/basic/gateway_following_epp_routing.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index d48521cc9..dea753137 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -201,4 +201,3 @@ roleRef: kind: Role name: inference-model-reader apiGroup: rbac.authorization.k8s.io - \ No newline at end of file From f313e580cce25545c99ce71803e71d488c7f2495 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 13 Jun 2025 22:54:29 +0000 Subject: [PATCH 08/19] refactor more. --- conformance/conformance.go | 2 - .../basic/gateway_following_epp_routing.go | 180 +++++++----------- .../basic/gateway_following_epp_routing.yaml | 1 + conformance/utils/kubernetes/helpers.go | 32 +--- 4 files changed, 72 insertions(+), 143 deletions(-) diff --git a/conformance/conformance.go b/conformance/conformance.go index 367ea4227..7585e74ce 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -176,8 +176,6 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { ManifestFS: []fs.FS{&Manifests}, ReportOutputPath: *confflags.ReportOutput, SkipProvisionalTests: *confflags.SkipProvisionalTests, - // TODO: make AllowCRDsMismatch to false when the gateway-api new version is released. - AllowCRDsMismatch: true, // TODO: Add the inference extension specific fields to ConformanceOptions struct if needed, // or handle them during report generation. // GatewayAPIInferenceExtensionChannel: inferenceExtensionChannel, diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 93ad84568..10c5404e8 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -17,18 +17,18 @@ limitations under the License. package basic import ( + "fmt" "net/http" + "strings" "testing" "time" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/types" // For standard condition types + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/gateway-api/conformance/utils/suite" - "sigs.k8s.io/gateway-api/pkg/features" // For standard feature names + "sigs.k8s.io/gateway-api/pkg/features" - // Import the tests package to append to ConformanceTests "sigs.k8s.io/gateway-api-inference-extension/conformance/tests" - "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" ) @@ -50,11 +50,14 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ }, 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" - backendName = "infra-backend-deployment" + 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" ) httpRouteNN := types.NamespacedName{Name: "httproute-for-primary-gw", Namespace: appBackendNamespace} @@ -62,115 +65,68 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ poolNN := types.NamespacedName{Name: "normal-gateway-pool", Namespace: appBackendNamespace} backendPodLabels := map[string]string{"app": "infra-backend"} + 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) - backendPodIP, err := k8sutils.GetOnePodIPWithLabel(t, s.Client, appBackendNamespace, backendPodLabels) - require.NoError(t, err, "Failed to get backend Pod IP address") + 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)) - inferenceTimeoutConfig := config.DefaultInferenceExtensionTimeoutConfig() - // TODO: replace this with a poll and check. - t.Log("Waiting for the httpRoute and inferecePool ready to serve traffic.") - time.Sleep(inferenceTimeoutConfig.WaitForHttpRouteAndInferencePoolReadyTimeout) + podIPs := make([]string, len(pods)) + for i, pod := range pods { + podIPs[i] = pod.Status.PodIP + } - correctRequestBody := `{ + requestBody := `{ "model": "conformance-fake-model", - "prompt": "Write as if you were a critic: San Francisco" + "prompt": "Write as if you were a critic: San Francisco" }` - t.Run("Gateway should send traffic to a valid endpoint specified by EPP", func(t *testing.T) { - t.Logf("Sending request to %s with EPP header routing to valid IP %s", gwAddr, backendPodIP) - eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} - - trafficutils.MakeRequestAndExpectSuccessV2( - t, - s.RoundTripper, - s.TimeoutConfig, - gwAddr, - trafficutils.Request{ - Host: hostname, - Path: path, - Headers: eppHeader, - Method: http.MethodPost, - Body: correctRequestBody, - Backend: backendName, - Namespace: appBackendNamespace, - }, - ) - }) - - t.Run("Gateway should send traffic specified by EPP even an invalidIP and should get response with error code 429", func(t *testing.T) { - invalidIP := "256.256.256.256" // An IP that cannot be a real endpoint - t.Logf("Sending request to %s with EPP header routing to invalid IP %s", gwAddr, invalidIP) - eppHeader := map[string]string{"test-epp-endpoint-selection": invalidIP} - - trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( - t, - s.RoundTripper, - s.TimeoutConfig, - gwAddr, - trafficutils.Request{ - Host: hostname, - Path: path, - Headers: eppHeader, - Method: http.MethodPost, - Body: correctRequestBody, - Namespace: appBackendNamespace, - - ExpectedStatusCode: http.StatusTooManyRequests, - }, - ) - }) - - t.Run("Gateway should reject request that is missing the model name and return 400 response", func(t *testing.T) { - requestBodyWithoutModel := `{"prompt": "Write as if you were a critic: San Francisco"}` - eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} - t.Logf("Sending request to %s with a malformed body (missing model)", gwAddr) - - trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( - t, - s.RoundTripper, - s.TimeoutConfig, - gwAddr, - trafficutils.Request{ - Host: hostname, - Path: path, - Headers: eppHeader, - Method: http.MethodPost, - Body: requestBodyWithoutModel, - Namespace: appBackendNamespace, - - ExpectedStatusCode: http.StatusBadRequest, - }, - ) - }) - - t.Run("Gateway should reject request that is with a nonexist model name and return 404 response", func(t *testing.T) { - requestBodyNonExistModel := `{ - "model": "non-exist-model", - "prompt": "Write as if you were a critic: San Francisco" - }` - eppHeader := map[string]string{"test-epp-endpoint-selection": backendPodIP} - t.Logf("Sending request to %s with a malformed body (nonexist model)", gwAddr) - - trafficutils.MakeRequestAndExpectEventuallyConsistentResponse( - t, - s.RoundTripper, - s.TimeoutConfig, - gwAddr, - trafficutils.Request{ - Host: hostname, - Path: path, - Headers: eppHeader, - Method: http.MethodPost, - Body: requestBodyNonExistModel, - Namespace: appBackendNamespace, - - ExpectedStatusCode: http.StatusNotFound, - }, - ) - - }) - }, -} + testCases := []struct { + name string + podOrder []string + expectedBackendPodIndex int + }{ + { + name: fmt.Sprintf("should route to first pod in list: %s", pods[0].Name), + podOrder: []string{podIPs[0], podIPs[1], podIPs[2]}, + expectedBackendPodIndex: 0, + }, + { + name: fmt.Sprintf("should route to new first pod after reordering: %s", pods[2].Name), + podOrder: []string{podIPs[2], podIPs[1], podIPs[0]}, + expectedBackendPodIndex: 2, + }, + } + + s.TimeoutConfig.MaxTimeToConsistency = 200 * time.Second + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + eppHeaderValue := strings.Join(tc.podOrder, ",") + headers := map[string]string{eppSelectionHeaderName: eppHeaderValue} + expectedBackendPod := pods[tc.expectedBackendPodIndex] + + t.Logf("Sending request to %s with EPP header '%s: %s'", gwAddr, eppSelectionHeaderName, eppHeaderValue) + t.Logf("Expecting traffic to be routed to pod %s (%s)", expectedBackendPod.Name, expectedBackendPod.Status.PodIP) + + trafficutils.MakeRequestAndExpectSuccessV2( + t, + s.RoundTripper, + s.TimeoutConfig, + gwAddr, + trafficutils.Request{ + Host: hostname, + Path: path, + Headers: headers, + Method: http.MethodPost, + Body: requestBody, + Backend: expectedBackendPod.Name, + Namespace: appBackendNamespace, + }, + ) + }) + } + }, \ No newline at end of file diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index dea753137..3f31b8e07 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -13,6 +13,7 @@ metadata: labels: app: infra-backend spec: + replicas: 3 selector: matchLabels: app: infra-backend diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 253d49952..1a896257a 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -274,11 +274,9 @@ func GetGatewayEndpoint(t *testing.T, k8sClient client.Client, timeoutConfig gat return gwAddr } -// GetPodIPsWithLabel retrieves a list of Pod IP addresses. +// GetPodsWithLabel retrieves a list of Pods. // It finds pods matching the given labels in a specific namespace. -// The 'count' parameter specifies the maximum number of IPs to return. -// If 'count' is 0, it will return all found IP addresses. -func GetPodIPsWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string, count int) ([]string, error) { +func GetPodsWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string) ([]corev1.Pod, error) { t.Helper() podList := &corev1.PodList{} @@ -295,29 +293,5 @@ func GetPodIPsWithLabel(t *testing.T, c client.Client, namespace string, labels if len(podList.Items) == 0 { return nil, fmt.Errorf("no pods found with labels '%v' in namespace '%s'", labels, namespace) } - - var podIPs []string - for _, pod := range podList.Items { - podIPs = append(podIPs, pod.Status.PodIP) - if count > 0 && len(podIPs) == count { - break - } - } - return podIPs, nil -} - -// GetOnePodIPWithLabel finds a single pod with labels in a specific namespace and returns its IP address. -// This function is a wrapper around GetPodIPsWithLabel for convenience. -func GetOnePodIPWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string) (string, error) { - t.Helper() - - podIPs, err := GetPodIPsWithLabel(t, c, namespace, labels, 1) - if err != nil { - return "", nil - } - - if len(podIPs) == 0 { - return "", fmt.Errorf("no pods were found with labels '%v' in namespace '%s'", labels, namespace) - } - return podIPs[0], nil + return podList.Items, nil } From bf95ea9a80ed71be3787b3f80e0e4ba97206f6ec Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 13 Jun 2025 23:05:44 +0000 Subject: [PATCH 09/19] wire up the flag. --- conformance/conformance.go | 1 + .../basic/gateway_following_epp_routing.go | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/conformance/conformance.go b/conformance/conformance.go index 7585e74ce..d52eadfc2 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -176,6 +176,7 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { ManifestFS: []fs.FS{&Manifests}, ReportOutputPath: *confflags.ReportOutput, SkipProvisionalTests: *confflags.SkipProvisionalTests, + AllowCRDsMismatch: *confflags.AllowCRDsMismatch, // TODO: Add the inference extension specific fields to ConformanceOptions struct if needed, // or handle them during report generation. // GatewayAPIInferenceExtensionChannel: inferenceExtensionChannel, diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 10c5404e8..34d00ce65 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -50,11 +50,11 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ }, 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 + 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" @@ -86,22 +86,22 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ }` testCases := []struct { - name string - podOrder []string + name string + podOrder []string expectedBackendPodIndex int }{ { - name: fmt.Sprintf("should route to first pod in list: %s", pods[0].Name), - podOrder: []string{podIPs[0], podIPs[1], podIPs[2]}, + name: fmt.Sprintf("should route to first pod in list: %s", pods[0].Name), + podOrder: []string{podIPs[0], podIPs[1], podIPs[2]}, expectedBackendPodIndex: 0, }, { - name: fmt.Sprintf("should route to new first pod after reordering: %s", pods[2].Name), - podOrder: []string{podIPs[2], podIPs[1], podIPs[0]}, + name: fmt.Sprintf("should route to new first pod after reordering: %s", pods[2].Name), + podOrder: []string{podIPs[2], podIPs[1], podIPs[0]}, expectedBackendPodIndex: 2, }, } - + s.TimeoutConfig.MaxTimeToConsistency = 200 * time.Second for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -129,4 +129,5 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ ) }) } - }, \ No newline at end of file + }, +} From 215b7ce376d992cfe36dc24902585fc3e6bc8cd6 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Mon, 16 Jun 2025 06:56:43 +0000 Subject: [PATCH 10/19] Refine test cases. --- conformance/conformance.go | 6 +- .../basic/gateway_following_epp_routing.go | 140 ++++++++++++++---- .../basic/gateway_following_epp_routing.yaml | 2 +- conformance/utils/config/timing.go | 18 +-- conformance/utils/traffic/traffic.go | 8 +- 5 files changed, 127 insertions(+), 47 deletions(-) diff --git a/conformance/conformance.go b/conformance/conformance.go index d52eadfc2..b41d31a78 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -25,7 +25,6 @@ import ( "io/fs" "os" "testing" - "time" "github.com/stretchr/testify/require" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -46,7 +45,6 @@ import ( // Import necessary types and utilities from the core Gateway API conformance suite. gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" // Import core Gateway API types confapis "sigs.k8s.io/gateway-api/conformance/apis/v1" // Report struct definition - confconfig "sigs.k8s.io/gateway-api/conformance/utils/config" confflags "sigs.k8s.io/gateway-api/conformance/utils/flags" apikubernetes "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" confsuite "sigs.k8s.io/gateway-api/conformance/utils/suite" @@ -154,8 +152,6 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { baseManifestsValue := "resources/manifests/manifests.yaml" - config := confconfig.DefaultTimeoutConfig() - config.HTTPRouteMustHaveCondition = 300 * time.Second opts := confsuite.ConformanceOptions{ Client: c, ClientOptions: clientOptions, @@ -166,7 +162,7 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { Debug: *confflags.ShowDebug, CleanupBaseResources: *confflags.CleanupBaseResources, SupportedFeatures: sets.New[features.FeatureName](), - TimeoutConfig: config, + TimeoutConfig: inferenceconfig.DefaultInferenceExtensionTimeoutConfig().TimeoutConfig, SkipTests: skipTests, ExemptFeatures: exemptFeatures, RunTest: *confflags.RunTest, diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 34d00ce65..10f52e40c 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -20,10 +20,11 @@ import ( "fmt" "net/http" "strings" + "sync" "testing" - "time" "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" @@ -31,6 +32,7 @@ import ( "sigs.k8s.io/gateway-api-inference-extension/conformance/tests" k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes" trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic" + gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" ) func init() { @@ -58,6 +60,7 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ // eppSelectionHeaderName is the custom header used by the testing-EPP service // to determine which endpoint to select. eppSelectionHeaderName = "test-epp-endpoint-selection" + appPodBackendPrefix = "infra-backend-deployment" ) httpRouteNN := types.NamespacedName{Name: "httproute-for-primary-gw", Namespace: appBackendNamespace} @@ -76,8 +79,10 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ 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 := `{ @@ -85,49 +90,126 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ "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 - podOrder []string - expectedBackendPodIndex int + name string + podIPsForHeader []string + expectedPodNames []string }{ { - name: fmt.Sprintf("should route to first pod in list: %s", pods[0].Name), - podOrder: []string{podIPs[0], podIPs[1], podIPs[2]}, - expectedBackendPodIndex: 0, + name: "should route traffic to a single designated pod", + podIPsForHeader: []string{podIPs[2]}, + expectedPodNames: []string{podNames[2]}, }, { - name: fmt.Sprintf("should route to new first pod after reordering: %s", pods[2].Name), - podOrder: []string{podIPs[2], podIPs[1], podIPs[0]}, - expectedBackendPodIndex: 2, + name: "should route traffic to two designated pods", + podIPsForHeader: []string{podIPs[0], podIPs[1]}, + expectedPodNames: []string{podNames[0], podNames[1]}, + }, + { + name: "should route traffic to all available pods", + podIPsForHeader: []string{podIPs[0], podIPs[1], podIPs[2]}, + expectedPodNames: []string{podNames[0], podNames[1], podNames[2]}, }, } - s.TimeoutConfig.MaxTimeToConsistency = 200 * time.Second for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - eppHeaderValue := strings.Join(tc.podOrder, ",") + eppHeaderValue := strings.Join(tc.podIPsForHeader, ",") headers := map[string]string{eppSelectionHeaderName: eppHeaderValue} - expectedBackendPod := pods[tc.expectedBackendPodIndex] t.Logf("Sending request to %s with EPP header '%s: %s'", gwAddr, eppSelectionHeaderName, eppHeaderValue) - t.Logf("Expecting traffic to be routed to pod %s (%s)", expectedBackendPod.Name, expectedBackendPod.Status.PodIP) - - trafficutils.MakeRequestAndExpectSuccessV2( - t, - s.RoundTripper, - s.TimeoutConfig, - gwAddr, - trafficutils.Request{ - Host: hostname, - Path: path, - Headers: headers, - Method: http.MethodPost, - Body: requestBody, - Backend: expectedBackendPod.Name, - Namespace: appBackendNamespace, + t.Logf("Expecting traffic to be routed to pod: %v", tc.expectedPodNames) + + assertTrafficReachesPods(t, s, gwAddr, gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Host: hostname, + Path: path, + Method: http.MethodPost, + Headers: headers, + Body: requestBody, + }, + Response: gwhttp.Response{ + StatusCode: http.StatusOK, }, - ) + Backend: appPodBackendPrefix, + Namespace: appBackendNamespace, + }, tc.expectedPodNames) }) } }, } + +func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expected gwhttp.ExpectedResponse, expectedPodNames []string) { + t.Helper() + const ( + concurrentRequests = 10 + totalRequests = 100 + ) + var ( + roundTripper = suite.RoundTripper + + g errgroup.Group + seenMutex sync.Mutex + seen = make(map[string]int) + req = gwhttp.MakeRequest(t, &expected, gwAddr, "HTTP", "http") + ) + g.SetLimit(concurrentRequests) + for i := 0; i < totalRequests; i++ { + g.Go(func() error { + cReq, cRes, err := roundTripper.CaptureRoundTrip(req) + 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) + } + + seenMutex.Lock() + defer seenMutex.Unlock() + + for _, expectedBackend := range expectedPodNames { + if cReq.Pod == expectedBackend { + seen[expectedBackend]++ + return nil + } + } + return fmt.Errorf("request was handled by an unexpected pod %q", cReq.Pod) + }) + } + if err := g.Wait(); err != nil { + t.Errorf("Not all the requests are sent successfully, err: %v", err) + } + + if len(seen) != len(expectedPodNames) { + missedPods := []string{} + for _, pod := range expectedPodNames { + if _, ok := seen[pod]; !ok { + missedPods = append(missedPods, pod) + } + } + t.Fatalf("Traffic did not reach all expected pods. Expected %d, but only %d were seen.\nMissing: %v\nReached pods with request counts: %v", + len(expectedPodNames), len(seen), missedPods, seen) + } + + t.Logf("Traffic successfully reached all %d expected pods with the following request counts: %v", len(expectedPodNames), seen) +} diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index 3f31b8e07..040e599c0 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -48,7 +48,7 @@ spec: fieldRef: fieldPath: status.podIP --- -# --- Backend Service --- +# --- InferenceModel Definition --- # Service for the infra-backend-deployment. apiVersion: inference.networking.x-k8s.io/v1alpha2 kind: InferenceModel diff --git a/conformance/utils/config/timing.go b/conformance/utils/config/timing.go index 888a0c74a..44e404da1 100644 --- a/conformance/utils/config/timing.go +++ b/conformance/utils/config/timing.go @@ -20,6 +20,7 @@ import ( "time" // Import the upstream Gateway API timeout config + gatewayconfig "sigs.k8s.io/gateway-api/conformance/utils/config" ) @@ -41,19 +42,18 @@ type InferenceExtensionTimeoutConfig struct { // HTTPRouteDeletionReconciliationTimeout is the time to wait for controllers to reconcile // state after an HTTPRoute is deleted, before checking dependent resources or traffic. HTTPRouteDeletionReconciliationTimeout time.Duration - - // WaitForHttpRouteAndInferencePoolReadyTimeout is the time to wait for httpRoute and inferencePool ready to server the traffic. - WaitForHttpRouteAndInferencePoolReadyTimeout time.Duration } // DefaultInferenceExtensionTimeoutConfig returns a new InferenceExtensionTimeoutConfig with default values. func DefaultInferenceExtensionTimeoutConfig() InferenceExtensionTimeoutConfig { + config := gatewayconfig.DefaultTimeoutConfig() + config.HTTPRouteMustHaveCondition = 300 * time.Second + config.MaxTimeToConsistency = 200 * time.Second return InferenceExtensionTimeoutConfig{ - TimeoutConfig: gatewayconfig.DefaultTimeoutConfig(), // Initialize embedded struct - InferencePoolMustHaveConditionTimeout: 300 * time.Second, - InferencePoolMustHaveConditionInterval: 10 * time.Second, - GatewayObjectPollInterval: 5 * time.Second, - HTTPRouteDeletionReconciliationTimeout: 5 * time.Second, - WaitForHttpRouteAndInferencePoolReadyTimeout: 100 * time.Second, + TimeoutConfig: config, // Initialize embedded struct + InferencePoolMustHaveConditionTimeout: 300 * time.Second, + InferencePoolMustHaveConditionInterval: 10 * time.Second, + GatewayObjectPollInterval: 5 * time.Second, + HTTPRouteDeletionReconciliationTimeout: 5 * time.Second, } } diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go index e0bfae885..959d7d622 100644 --- a/conformance/utils/traffic/traffic.go +++ b/conformance/utils/traffic/traffic.go @@ -83,7 +83,8 @@ func BuildExpectedHTTPResponse( return resp } -// Deprecated: please use MakeRequestAndExpectSuccessV2 instead. +// Deprecated: please use MakeRequestWithRequestParamAndExpectSuccess instead. +// https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/983 // MakeRequestAndExpectSuccess is a helper function that builds an expected success (200 OK) response // and then calls MakeRequestAndExpectEventuallyConsistentResponse. func MakeRequestAndExpectSuccess( @@ -108,6 +109,7 @@ func MakeRequestAndExpectSuccess( } // Deprecated: please use MakeRequestAndExpectEventuallyConsistentResponse instead and specify the ExpectedStatusCode in Request. +// https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/983 // MakeRequestAndExpectNotFound is a helper function that builds an expected not found (404) response // and then calls MakeRequestAndExpectEventuallyConsistentResponse. func MakeRequestAndExpectNotFound( @@ -176,9 +178,9 @@ func MakeRequestAndExpectEventuallyConsistentResponse( gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) } -// MakeRequestAndExpectSuccessV2 is a convenience wrapper for requests that are +// MakeRequestWithRequestParamAndExpectSuccess is a convenience wrapper for requests that are // expected to succeed with a 200 OK status. -func MakeRequestAndExpectSuccessV2( +func MakeRequestWithRequestParamAndExpectSuccess( t *testing.T, r roundtripper.RoundTripper, timeoutConfig gwconfig.TimeoutConfig, From 417cb4415c586311d8db7c5e0dbd3064e71003c7 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Mon, 16 Jun 2025 18:19:33 +0000 Subject: [PATCH 11/19] Refine log error info. --- .../tests/basic/gateway_following_epp_routing.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 10f52e40c..d0ca87a01 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -19,6 +19,7 @@ package basic import ( "fmt" "net/http" + "slices" "strings" "sync" "testing" @@ -187,17 +188,15 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g seenMutex.Lock() defer seenMutex.Unlock() - for _, expectedBackend := range expectedPodNames { - if cReq.Pod == expectedBackend { - seen[expectedBackend]++ - return nil - } + seen[cReq.Pod]++ + 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.Errorf("Not all the requests are sent successfully, err: %v", err) + t.Fatalf("Not all the requests are sent to the expectedPods successfully, err: %v, Reached pods with request counts: %v", err, seen) } if len(seen) != len(expectedPodNames) { From 3809e50d7d464ddf1773600fc775948ae9494734 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Wed, 18 Jun 2025 23:44:38 +0000 Subject: [PATCH 12/19] small timeout twek. --- conformance/conformance.go | 1 + conformance/utils/config/timing.go | 1 + 2 files changed, 2 insertions(+) diff --git a/conformance/conformance.go b/conformance/conformance.go index b41d31a78..ad9bca1a0 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -87,6 +87,7 @@ const SupportInferencePool features.FeatureName = "SupportInferencePool" // of the "Gateway" profile for the Inference Extension MUST support. var InferenceCoreFeatures = sets.New( features.SupportGateway, // This is needed to ensure manifest gets applied during setup. + features.SupportHTTPRoute, SupportInferencePool, ) diff --git a/conformance/utils/config/timing.go b/conformance/utils/config/timing.go index c99549159..861eb0f90 100644 --- a/conformance/utils/config/timing.go +++ b/conformance/utils/config/timing.go @@ -48,6 +48,7 @@ func DefaultInferenceExtensionTimeoutConfig() InferenceExtensionTimeoutConfig { config := gatewayconfig.DefaultTimeoutConfig() config.HTTPRouteMustHaveCondition = 300 * time.Second config.MaxTimeToConsistency = 200 * time.Second + config.DefaultTestTimeout = 600 * time.Second return InferenceExtensionTimeoutConfig{ TimeoutConfig: config, // Initialize embedded struct GeneralMustHaveConditionTimeout: 300 * time.Second, From d99f0961b34b93e0d7acb55cd989ae5275e3f757 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Thu, 19 Jun 2025 00:03:21 +0000 Subject: [PATCH 13/19] use common resource. --- conformance/conformance.go | 3 - .../resources/manifests/manifests.yaml | 1 + .../basic/gateway_following_epp_routing.go | 8 +- .../basic/gateway_following_epp_routing.yaml | 175 +----------------- 4 files changed, 9 insertions(+), 178 deletions(-) diff --git a/conformance/conformance.go b/conformance/conformance.go index e87fbf3c3..406124bdd 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -176,12 +176,9 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions { ManifestFS: []fs.FS{&Manifests}, ReportOutputPath: *confflags.ReportOutput, SkipProvisionalTests: *confflags.SkipProvisionalTests, -<<<<<<< HEAD AllowCRDsMismatch: *confflags.AllowCRDsMismatch, -======= NamespaceLabels: namespaceLabels, NamespaceAnnotations: namespaceAnnotations, ->>>>>>> main // TODO: Add the inference extension specific fields to ConformanceOptions struct if needed, // or handle them during report generation. // GatewayAPIInferenceExtensionChannel: inferenceExtensionChannel, diff --git a/conformance/resources/manifests/manifests.yaml b/conformance/resources/manifests/manifests.yaml index c4f9b1f14..a62275f50 100644 --- a/conformance/resources/manifests/manifests.yaml +++ b/conformance/resources/manifests/manifests.yaml @@ -66,6 +66,7 @@ metadata: labels: app: primary-inference-model-server spec: + replicas: 3 selector: matchLabels: app: primary-inference-model-server diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index d0ca87a01..6494c0787 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -61,13 +61,13 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ // eppSelectionHeaderName is the custom header used by the testing-EPP service // to determine which endpoint to select. eppSelectionHeaderName = "test-epp-endpoint-selection" - appPodBackendPrefix = "infra-backend-deployment" + appPodBackendPrefix = "primary-inference-model-server" ) httpRouteNN := types.NamespacedName{Name: "httproute-for-primary-gw", Namespace: appBackendNamespace} - gatewayNN := types.NamespacedName{Name: "conformance-gateway", Namespace: infraNamespace} - poolNN := types.NamespacedName{Name: "normal-gateway-pool", Namespace: appBackendNamespace} - backendPodLabels := map[string]string{"app": "infra-backend"} + 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) diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index 040e599c0..552820c14 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -1,53 +1,3 @@ -# conformance/tests/basic/gateway_following_epp_routing.yaml - -# This manifest defines the initial resources for the -# gateway_following_epp_routing.go conformance test. - -# --- Backend Deployment (using standard Gateway API echoserver) --- -# This Deployment provides Pods for the InferencePool to select. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: infra-backend-deployment - namespace: gateway-conformance-app-backend - labels: - app: infra-backend -spec: - replicas: 3 - selector: - matchLabels: - app: infra-backend - template: - metadata: - labels: - app: infra-backend - spec: - containers: - - name: echoserver - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd - ports: - - containerPort: 3000 - readinessProbe: - httpGet: - path: / - port: 3000 - initialDelaySeconds: 3 - periodSeconds: 5 - failureThreshold: 2 - env: - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP ---- # --- InferenceModel Definition --- # Service for the infra-backend-deployment. apiVersion: inference.networking.x-k8s.io/v1alpha2 @@ -59,20 +9,7 @@ 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: normal-gateway-pool ---- -# --- InferencePool Definition --- -apiVersion: inference.networking.x-k8s.io/v1alpha2 -kind: InferencePool -metadata: - name: normal-gateway-pool - namespace: gateway-conformance-app-backend -spec: - selector: - app: "infra-backend" - targetPortNumber: 3000 - extensionRef: - name: infra-backend-endpoint-picker + name: primary-inference-pool --- # --- HTTPRoute for Primary Gateway (conformance-gateway) --- apiVersion: gateway.networking.k8s.io/v1 @@ -84,7 +21,7 @@ spec: parentRefs: - group: gateway.networking.k8s.io kind: Gateway - name: conformance-gateway + name: conformance-primary-gateway namespace: gateway-conformance-infra sectionName: http hostnames: @@ -93,112 +30,8 @@ spec: - backendRefs: - group: inference.networking.x-k8s.io kind: InferencePool - name: normal-gateway-pool + name: primary-inference-pool matches: - path: type: PathPrefix - value: /primary-gateway-test ---- -# --- Conformance EPP service Definition --- -apiVersion: v1 -kind: Service -metadata: - name: infra-backend-endpoint-picker - namespace: gateway-conformance-app-backend -spec: - selector: - app: infra-backend-epp - ports: - - protocol: TCP - port: 9002 - targetPort: 9002 - appProtocol: http2 - type: ClusterIP ---- -# --- Conformance EPP Deployment --- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: infra-backend-epp - namespace: gateway-conformance-app-backend - labels: - app: infra-backend-epp -spec: - replicas: 1 - selector: - matchLabels: - app: infra-backend-epp - template: - metadata: - labels: - app: infra-backend-epp - spec: - # Conservatively, this timeout should mirror the longest grace period of the pods within the pool - terminationGracePeriodSeconds: 130 - containers: - - name: epp - image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main - imagePullPolicy: Always - args: - - -poolName - - "normal-gateway-pool" - - -poolNamespace - - "gateway-conformance-app-backend" - - -v - - "4" - - --zap-encoder - - "json" - - -grpcPort - - "9002" - - -grpcHealthPort - - "9003" - env: - - name: USE_STREAMING - value: "true" - - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING # Used for conformance test. - value: "true" - ports: - - containerPort: 9002 - - containerPort: 9003 - - name: metrics - containerPort: 9090 - livenessProbe: - grpc: - port: 9003 - service: inference-extension - initialDelaySeconds: 5 - periodSeconds: 10 - readinessProbe: - grpc: - port: 9003 - service: inference-extension - initialDelaySeconds: 5 - periodSeconds: 10 ---- -# --- Conformance EPP Requried Role and RoleBindings --- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: inference-model-reader - namespace: gateway-conformance-app-backend -rules: -- apiGroups: ["inference.networking.x-k8s.io"] - resources: ["inferencemodels", "inferencepools"] - verbs: ["get", "list", "watch"] -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: epp-to-inference-model-reader - namespace: gateway-conformance-app-backend -subjects: -- kind: ServiceAccount - name: default - namespace: gateway-conformance-app-backend -roleRef: - kind: Role - name: inference-model-reader - apiGroup: rbac.authorization.k8s.io + value: /primary-gateway-test \ No newline at end of file From 5b4a86c8244884beee9f938c8248239838df39b4 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 20 Jun 2025 20:00:20 +0000 Subject: [PATCH 14/19] back to depend on gateway-api 1.30. --- .../basic/gateway_following_epp_routing.go | 7 +- conformance/utils/traffic/traffic.go | 233 +++++++++++++++--- go.mod | 2 +- go.sum | 2 + 4 files changed, 212 insertions(+), 32 deletions(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 6494c0787..89d0683b0 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -32,6 +32,7 @@ import ( "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" ) @@ -147,20 +148,19 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ Path: path, Method: http.MethodPost, Headers: headers, - Body: requestBody, }, Response: gwhttp.Response{ StatusCode: http.StatusOK, }, Backend: appPodBackendPrefix, Namespace: appBackendNamespace, - }, tc.expectedPodNames) + }, requestBody, tc.expectedPodNames) }) } }, } -func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expected gwhttp.ExpectedResponse, expectedPodNames []string) { +func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expected gwhttp.ExpectedResponse, requestBody string, expectedPodNames []string) { t.Helper() const ( concurrentRequests = 10 @@ -177,6 +177,7 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g g.SetLimit(concurrentRequests) for i := 0; i < totalRequests; i++ { g.Go(func() error { + traffic.MakeCallRoundTripper(t, roundTripper, &traffic.RequestWithBody{req, 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/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go index 15bf917ba..8173a92d2 100644 --- a/conformance/utils/traffic/traffic.go +++ b/conformance/utils/traffic/traffic.go @@ -17,14 +17,22 @@ limitations under the License. package traffic import ( + "context" + "encoding/json" "fmt" + "io" "net/http" + "net/http/httputil" + "regexp" + "strings" "testing" + "time" corev1 "k8s.io/api/core/v1" 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. @@ -144,6 +152,52 @@ func MakeRequestAndExpectEventuallyConsistentResponse( ) { t.Helper() + expectedResponse := makeExpectedResponse(t, req) + waitForConvergeToExpected(t, r, timeoutConfig, gatewayAddress, req.Body, expectedResponse) +} + +// MakeRequestWithRequestParamAndExpectSuccess is a convenience wrapper for requests that are +// expected to succeed with a 200 OK status. +func MakeRequestWithRequestParamAndExpectSuccess( + 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) +} + +// MakeRequestAndExpectResponseFromPod sends a request to the specified path by IP address and +// uses a special "test-epp-endpoint-selection" header to target a specific backend Pod. +// It then verifies that the response was served by that Pod. +func MakeRequestAndExpectResponseFromPod(t *testing.T, r roundtripper.RoundTripper, timeoutConfig gwconfig.TimeoutConfig, gwAddr, path string, targetPod *corev1.Pod) { + t.Helper() + + const ( + eppSelectionHeader = "test-epp-endpoint-selection" + backendPort = 3000 + ) + + expectedResponse := gwhttp.ExpectedResponse{ + Request: gwhttp.Request{ + Path: path, + Headers: map[string]string{ + eppSelectionHeader: fmt.Sprintf("%s:%d", targetPod.Status.PodIP, backendPort), + }, + }, + Backend: targetPod.Name, + Namespace: targetPod.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 @@ -155,7 +209,6 @@ func MakeRequestAndExpectEventuallyConsistentResponse( Path: req.Path, Method: method, Headers: req.Headers, - Body: req.Body, }, Response: gwhttp.Response{ StatusCode: req.ExpectedStatusCode, @@ -177,44 +230,168 @@ func MakeRequestAndExpectEventuallyConsistentResponse( }, } } - gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, expectedResponse) + return expectedResponse } -// MakeRequestWithRequestParamAndExpectSuccess is a convenience wrapper for requests that are -// expected to succeed with a 200 OK status. -func MakeRequestWithRequestParamAndExpectSuccess( +// TODO: replace the following method when sigs.k8s.io/gateway-api/conformance/utils/roundtripper is able to send request with body. +// https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1031 +func waitForConvergeToExpected( t *testing.T, r roundtripper.RoundTripper, timeoutConfig gwconfig.TimeoutConfig, gatewayAddress string, - req Request, + requestBody string, + expectedResponse gwhttp.ExpectedResponse, ) { - t.Helper() - req.ExpectedStatusCode = http.StatusOK - MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gatewayAddress, req) + 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 := gwhttp.CompareRequest(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") } -// MakeRequestAndExpectResponseFromPod sends a request to the specified path by IP address and -// uses a special "test-epp-endpoint-selection" header to target a specific backend Pod. -// It then verifies that the response was served by that Pod. -func MakeRequestAndExpectResponseFromPod(t *testing.T, r roundtripper.RoundTripper, timeoutConfig gwconfig.TimeoutConfig, gwAddr, path string, targetPod *corev1.Pod) { - t.Helper() +// RequestWithBody extends roundtripper.Request to include a request body. +type RequestWithBody struct { + roundtripper.Request + Body io.Reader +} - const ( - eppSelectionHeader = "test-epp-endpoint-selection" - backendPort = 3000 - ) +// 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{} - expectedResponse := gwhttp.ExpectedResponse{ - Request: gwhttp.Request{ - Path: path, - Headers: map[string]string{ - eppSelectionHeader: fmt.Sprintf("%s:%d", targetPod.Status.PodIP, backendPort), - }, - }, - Backend: targetPod.Name, - Namespace: targetPod.Namespace, + 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 + } } - gwhttp.MakeRequestAndExpectEventuallyConsistentResponse(t, r, timeoutConfig, gwAddr, expectedResponse) + 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) } diff --git a/go.mod b/go.mod index 879562a1e..0c02daccc 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( k8s.io/component-base v0.33.1 k8s.io/utils v0.0.0-20241210054802-24370beab758 sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6 + sigs.k8s.io/gateway-api v1.3.0 sigs.k8s.io/structured-merge-diff/v4 v4.7.0 sigs.k8s.io/yaml v1.4.0 ) diff --git a/go.sum b/go.sum index 162e01699..fab871255 100644 --- a/go.sum +++ b/go.sum @@ -323,6 +323,8 @@ sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytI sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3EwfNw= sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI= +sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M= +sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk= sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6 h1:/9+O2vVgnZz1BbJXIMtvAT+YaajmFzBIZzyku8VwMB4= sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6/go.mod h1:YpiVaiQMBZtoEGeoCEz2vMBYpJLkvAv2kqtiMvtjnrQ= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= From 9c7788fb03bd36d9eadf6bd337181adbffb806f5 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 20 Jun 2025 20:02:51 +0000 Subject: [PATCH 15/19] update go.sum. --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index fab871255..2d45d351f 100644 --- a/go.sum +++ b/go.sum @@ -325,8 +325,6 @@ sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3Ew sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI= sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M= sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk= -sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6 h1:/9+O2vVgnZz1BbJXIMtvAT+YaajmFzBIZzyku8VwMB4= -sigs.k8s.io/gateway-api v1.3.1-0.20250611170256-6cd1558a9ed6/go.mod h1:YpiVaiQMBZtoEGeoCEz2vMBYpJLkvAv2kqtiMvtjnrQ= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= From 2627f00ba65b1eb83b7620518b71bad8ab6d30ec Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 20 Jun 2025 20:15:43 +0000 Subject: [PATCH 16/19] format. --- conformance/tests/basic/gateway_following_epp_routing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 89d0683b0..93d5e29ce 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -177,7 +177,7 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g g.SetLimit(concurrentRequests) for i := 0; i < totalRequests; i++ { g.Go(func() error { - traffic.MakeCallRoundTripper(t, roundTripper, &traffic.RequestWithBody{req, strings.NewReader(requestBody)}) + 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) From 72c4ec1351db7e19324b4d3e3b35105cfc5afc2b Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 20 Jun 2025 21:23:14 +0000 Subject: [PATCH 17/19] resolve minor comments. --- .../basic/gateway_following_epp_routing.go | 45 +++++++------------ .../basic/gateway_following_epp_routing.yaml | 2 +- conformance/utils/traffic/traffic.go | 8 +++- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 93d5e29ce..90987ffd6 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -113,34 +113,34 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ } testCases := []struct { - name string - podIPsForHeader []string - expectedPodNames []string + name string + podIPsToBeReturnedByEPP []string + expectAllRequestsRoutedWithinPodNames []string }{ { - name: "should route traffic to a single designated pod", - podIPsForHeader: []string{podIPs[2]}, - expectedPodNames: []string{podNames[2]}, + 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", - podIPsForHeader: []string{podIPs[0], podIPs[1]}, - expectedPodNames: []string{podNames[0], podNames[1]}, + 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", - podIPsForHeader: []string{podIPs[0], podIPs[1], podIPs[2]}, - expectedPodNames: []string{podNames[0], podNames[1], podNames[2]}, + 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.podIPsForHeader, ",") + 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.expectedPodNames) + t.Logf("Expecting traffic to be routed to pod: %v", tc.expectAllRequestsRoutedWithinPodNames) assertTrafficReachesPods(t, s, gwAddr, gwhttp.ExpectedResponse{ Request: gwhttp.Request{ @@ -154,7 +154,7 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ }, Backend: appPodBackendPrefix, Namespace: appBackendNamespace, - }, requestBody, tc.expectedPodNames) + }, requestBody, tc.expectAllRequestsRoutedWithinPodNames) }) } }, @@ -177,8 +177,7 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g g.SetLimit(concurrentRequests) for i := 0; i < totalRequests; i++ { g.Go(func() error { - traffic.MakeCallRoundTripper(t, roundTripper, &traffic.RequestWithBody{Request: req, Body: strings.NewReader(requestBody)}) - cReq, cRes, err := roundTripper.CaptureRoundTrip(req) + 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) } @@ -199,17 +198,5 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g if err := g.Wait(); err != nil { t.Fatalf("Not all the requests are sent to the expectedPods successfully, err: %v, Reached pods with request counts: %v", err, seen) } - - if len(seen) != len(expectedPodNames) { - missedPods := []string{} - for _, pod := range expectedPodNames { - if _, ok := seen[pod]; !ok { - missedPods = append(missedPods, pod) - } - } - t.Fatalf("Traffic did not reach all expected pods. Expected %d, but only %d were seen.\nMissing: %v\nReached pods with request counts: %v", - len(expectedPodNames), len(seen), missedPods, seen) - } - t.Logf("Traffic successfully reached all %d expected pods with the following request counts: %v", len(expectedPodNames), seen) } diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index 552820c14..43cfa04ae 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -34,4 +34,4 @@ spec: matches: - path: type: PathPrefix - value: /primary-gateway-test \ No newline at end of file + value: /primary-gateway-test diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go index 8173a92d2..e27722349 100644 --- a/conformance/utils/traffic/traffic.go +++ b/conformance/utils/traffic/traffic.go @@ -233,8 +233,8 @@ func makeExpectedResponse(t *testing.T, req Request) gwhttp.ExpectedResponse { return expectedResponse } -// TODO: replace the following method when sigs.k8s.io/gateway-api/conformance/utils/roundtripper is able to send request with body. -// https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1031 +// 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, @@ -265,12 +265,16 @@ func waitForConvergeToExpected( tlog.Logf(t, "Request passed") } +// 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{} From fa47b154f447a94036e9cb36c994c1d08e78f218 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 20 Jun 2025 21:31:30 +0000 Subject: [PATCH 18/19] remove seen logic. --- .../basic/gateway_following_epp_routing.go | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/conformance/tests/basic/gateway_following_epp_routing.go b/conformance/tests/basic/gateway_following_epp_routing.go index 90987ffd6..987799977 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.go +++ b/conformance/tests/basic/gateway_following_epp_routing.go @@ -21,7 +21,6 @@ import ( "net/http" "slices" "strings" - "sync" "testing" "github.com/stretchr/testify/require" @@ -142,7 +141,7 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ 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) - assertTrafficReachesPods(t, s, gwAddr, gwhttp.ExpectedResponse{ + assertTrafficOnlyReachesToExpectedPods(t, s, gwAddr, gwhttp.ExpectedResponse{ Request: gwhttp.Request{ Host: hostname, Path: path, @@ -160,7 +159,7 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{ }, } -func assertTrafficReachesPods(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, requestBody string, expectedPodNames []string) { t.Helper() const ( concurrentRequests = 10 @@ -168,11 +167,8 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g ) var ( roundTripper = suite.RoundTripper - - g errgroup.Group - seenMutex sync.Mutex - seen = make(map[string]int) - req = gwhttp.MakeRequest(t, &expected, gwAddr, "HTTP", "http") + g errgroup.Group + req = gwhttp.MakeRequest(t, &expected, gwAddr, "HTTP", "http") ) g.SetLimit(concurrentRequests) for i := 0; i < totalRequests; i++ { @@ -185,10 +181,6 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g return fmt.Errorf("response expectation failed for request: %w", err) } - seenMutex.Lock() - defer seenMutex.Unlock() - - seen[cReq.Pod]++ if slices.Contains(expectedPodNames, cReq.Pod) { return nil } @@ -196,7 +188,7 @@ func assertTrafficReachesPods(t *testing.T, suite *suite.ConformanceTestSuite, g }) } if err := g.Wait(); err != nil { - t.Fatalf("Not all the requests are sent to the expectedPods successfully, err: %v, Reached pods with request counts: %v", err, seen) + t.Fatalf("Not all the requests are sent to the expectedPods successfully, err: %v", err) } - t.Logf("Traffic successfully reached all %d expected pods with the following request counts: %v", len(expectedPodNames), seen) + t.Logf("Traffic successfully reached only to expected pods: %v", expectedPodNames) } From 1a3daae2d7ec1f934bd1c65fe4248fdc2a03fb32 Mon Sep 17 00:00:00 2001 From: Bob Tian Date: Fri, 20 Jun 2025 21:40:12 +0000 Subject: [PATCH 19/19] trailing new line. --- conformance/tests/basic/gateway_following_epp_routing.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conformance/tests/basic/gateway_following_epp_routing.yaml b/conformance/tests/basic/gateway_following_epp_routing.yaml index 43cfa04ae..d290b7541 100644 --- a/conformance/tests/basic/gateway_following_epp_routing.yaml +++ b/conformance/tests/basic/gateway_following_epp_routing.yaml @@ -35,3 +35,4 @@ spec: - path: type: PathPrefix value: /primary-gateway-test + \ No newline at end of file