Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions api/v1alpha2/inferencepool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ type Extension struct {
ExtensionConnection `json:",inline"`
}

// ExtensionReference is a reference to the extension deployment. When ExtensionReference is invalid,
// a 5XX status code MUST be returned for the request that would have otherwise been routed to the
// invalid backend.
// ExtensionReference is a reference to the extension service. An invalid reference
// MUST result in a 5XX status code for all affected requests. The implementation
// MUST also update the `ResolvedRefs` Condition on the InferencePool's status
// to `status: False`, providing a reason and message for the failure.
type ExtensionReference struct {
// Group is the group of the referent.
// The default value is "", representing the Core API group.
Expand Down Expand Up @@ -182,7 +183,7 @@ type PoolStatus struct {
// Known condition types are:
//
// * "Accepted"
// * "ResolvedRefs"
// * "ResolvedRefs": If a reference is invalid, the status of this condition will be "False".
//
// +optional
// +listType=map
Expand Down
29 changes: 13 additions & 16 deletions conformance/tests/basic/inferencepool_invalid_epp_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ limitations under the License.
package basic

import (
"net/http"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
inferenceapi "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/gateway-api/pkg/features"

"sigs.k8s.io/gateway-api-inference-extension/conformance/tests"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
conformancehttp "sigs.k8s.io/gateway-api/conformance/utils/http"
k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes"
trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic"
)

func init() {
Expand All @@ -49,30 +49,27 @@ var InferencePoolInvalidEPPService = suite.ConformanceTest{
routePath = "/invalid-epp-test"
infraNamespace = "gateway-conformance-infra"
appNamespace = "gateway-conformance-app-backend"
poolName = "primary-inference-pool"
)

routeNN := types.NamespacedName{Name: "httproute-for-invalid-epp-pool", Namespace: appNamespace}
gwNN := types.NamespacedName{Name: "conformance-primary-gateway", Namespace: infraNamespace}
poolNN := types.NamespacedName{Name: poolName, Namespace: appNamespace}

gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, s.Client, s.TimeoutConfig, s.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)

t.Run("HTTPRoute has a ResolvedRefs Condition with status False and Reason BackendNotFound", func(t *testing.T) {
resolvedRefsCond := metav1.Condition{
Type: string(gatewayv1.RouteConditionResolvedRefs),
t.Run("InferecePool has a ResolvedRefs Condition with status False", func(t *testing.T) {
acceptedCondition := metav1.Condition{
Type: string(inferenceapi.InferencePoolConditionResolvedRefs), // Standard condition type
Status: metav1.ConditionFalse,
Reason: string(gatewayv1.RouteReasonBackendNotFound),
Reason: "", // "" means we don't strictly check the Reason for this basic test.
}
kubernetes.HTTPRouteMustHaveCondition(t, s.Client, s.TimeoutConfig, routeNN, gwNN, resolvedRefsCond)
k8sutils.InferencePoolMustHaveCondition(t, s.Client, poolNN, acceptedCondition)
})

t.Run("Request to a route with an invalid backend reference receives a 500 response", func(t *testing.T) {
conformancehttp.MakeRequestAndExpectEventuallyConsistentResponse(t, s.RoundTripper, s.TimeoutConfig, gwAddr, conformancehttp.ExpectedResponse{
Request: conformancehttp.Request{
Path: routePath,
},
Response: conformancehttp.Response{
StatusCode: http.StatusInternalServerError,
},
trafficutils.MakeRequestAndExpectEventuallyConsistentResponse(t, s.RoundTripper, s.TimeoutConfig, gwAddr, trafficutils.Request{
Path: routePath,
ExpectedStatusCode: 5, // Expecting response status code 5XX.
})
})
},
Expand Down
1 change: 1 addition & 0 deletions conformance/utils/config/timing.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type InferenceExtensionTimeoutConfig struct {
func DefaultInferenceExtensionTimeoutConfig() InferenceExtensionTimeoutConfig {
config := gatewayconfig.DefaultTimeoutConfig()
config.HTTPRouteMustHaveCondition = 300 * time.Second
config.RouteMustHaveParents = 200 * time.Second
config.MaxTimeToConsistency = 200 * time.Second
config.DefaultTestTimeout = 600 * time.Second
return InferenceExtensionTimeoutConfig{
Expand Down
22 changes: 21 additions & 1 deletion conformance/utils/traffic/traffic.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func waitForConvergeToExpected(
return false
}

if err := gwhttp.CompareRequest(t, &request.Request, cReq, cRes, expectedResponse); err != nil {
if err := CompareRequestWithWildcardStatus(t, &request.Request, cReq, cRes, expectedResponse); err != nil {
tlog.Logf(t, "Response expectation failed for request: %+v not ready yet: %v (after %v)", request.Request, err, elapsed)
return false
}
Expand All @@ -265,6 +265,26 @@ func waitForConvergeToExpected(
tlog.Logf(t, "Request passed")
}

// CompareRequestWithWildcardStatus compares requests with wildcard status code support.
// It treats a single-digit expected code (e.g., 4) as a class wildcard (4xx),
// while standard 3-digit codes are matched exactly.
func CompareRequestWithWildcardStatus(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected gwhttp.ExpectedResponse) error {
if expected.Response.StatusCode < 1 || expected.Response.StatusCode >= 100 {
return gwhttp.CompareRequest(t, req, cReq, cRes, expected)
}

expectedClass := expected.Response.StatusCode
actualClass := cRes.StatusCode / 100
if expectedClass != actualClass {
return fmt.Errorf("expected status code class %dxx, but got %d", expectedClass, cRes.StatusCode)
}

// StatusCode Class matches; update status code on a copy to allow the standard comparator to pass.
modifiedExpected := expected
modifiedExpected.Response.StatusCode = cRes.StatusCode
return gwhttp.CompareRequest(t, req, cReq, cRes, modifiedExpected)
}

// 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.
Expand Down