Files
kserve/pkg/controller/v1alpha2/llmisvc/router_gateway_conditions_test.go
2026-06-19 11:33:53 -04:00

780 lines
28 KiB
Go

/*
Copyright 2026 The KServe 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 llmisvc_test
import (
"testing"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
"knative.dev/pkg/apis"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
"github.com/kserve/kserve/pkg/apis/serving/v1alpha2"
"github.com/kserve/kserve/pkg/controller/v1alpha2/llmisvc"
. "github.com/kserve/kserve/pkg/controller/v1alpha2/llmisvc/fixture"
)
func TestGatewayConditionsEvaluation(t *testing.T) {
tests := []struct {
name string
llmSvc *v1alpha2.LLMInferenceService
gateways []*gwapiv1.Gateway
assertCondition func(routerCondition, gatewayCondition *apis.Condition) assertConditionsFunc
}{
{
name: "single ready gateway - router should be ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithGatewayRefs(v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{
Name: "ready-gateway",
Namespace: "test-ns",
}}),
),
gateways: []*gwapiv1.Gateway{
Gateway("ready-gateway",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
WithAddresses("203.0.113.1"),
WithProgrammedCondition(metav1.ConditionTrue, "Ready", "Gateway is ready"),
),
},
assertCondition: assertRouterReady,
},
{
name: "single not ready gateway - router should not be ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithGatewayRefs(v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{
Name: "not-ready-gateway",
Namespace: "test-ns",
}}),
),
gateways: []*gwapiv1.Gateway{
Gateway("not-ready-gateway",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
WithAddresses("203.0.113.1"),
WithProgrammedCondition(metav1.ConditionFalse, "NotReady", "Gateway is not ready"),
),
},
assertCondition: func(routerCondition, gatewayCondition *apis.Condition) assertConditionsFunc {
return assertRouterNotReadyWithReason(routerCondition, gatewayCondition, "GatewaysNotReady")
},
},
{
name: "multiple gateways - all ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithGatewayRefs(
v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{Name: "gateway-1", Namespace: "test-ns"}},
v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{Name: "gateway-2", Namespace: "test-ns"}},
),
),
gateways: []*gwapiv1.Gateway{
Gateway("gateway-1",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
WithProgrammedCondition(metav1.ConditionTrue, "Ready", "Gateway 1 is ready"),
),
Gateway("gateway-2",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
WithProgrammedCondition(metav1.ConditionTrue, "Ready", "Gateway 2 is ready"),
),
},
assertCondition: assertRouterReady,
},
{
name: "multiple gateways - some not ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithGatewayRefs(
v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{Name: "ready-gateway", Namespace: "test-ns"}},
v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{Name: "not-ready-gateway", Namespace: "test-ns"}},
),
),
gateways: []*gwapiv1.Gateway{
Gateway("ready-gateway",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
WithProgrammedCondition(metav1.ConditionTrue, "Ready", "Gateway is ready"),
),
Gateway("not-ready-gateway",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
WithProgrammedCondition(metav1.ConditionFalse, "NotReady", "Gateway is not ready"),
),
},
assertCondition: func(routerCondition, gatewayCondition *apis.Condition) assertConditionsFunc {
return assertRouterNotReadyWithReason(routerCondition, gatewayCondition, "GatewaysNotReady")
},
},
{
name: "gateway with no programmed condition - should be not ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithGatewayRefs(v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{
Name: "no-condition-gateway",
Namespace: "test-ns",
}}),
),
gateways: []*gwapiv1.Gateway{
Gateway("no-condition-gateway",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
// No programmed condition set
),
},
assertCondition: func(routerCondition, gatewayCondition *apis.Condition) assertConditionsFunc {
return assertRouterNotReadyWithReason(routerCondition, gatewayCondition, "GatewaysNotReady")
},
},
{
name: "no gateway refs - should skip evaluation",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
// No gateway refs
),
gateways: []*gwapiv1.Gateway{},
assertCondition: assertConditionUnset,
},
{
name: "gateway without namespace uses LLMInferenceService namespace",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithGatewayRefs(v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{
Name: "same-ns-gateway",
// Namespace omitted - should use test-ns
}}),
),
gateways: []*gwapiv1.Gateway{
Gateway("same-ns-gateway",
InNamespace[*gwapiv1.Gateway]("test-ns"),
WithListener(gwapiv1.HTTPProtocolType),
WithProgrammedCondition(metav1.ConditionTrue, "Ready", "Gateway is ready"),
),
},
assertCondition: assertRouterReady,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
ctx := t.Context()
scheme := runtime.NewScheme()
err := v1alpha2.AddToScheme(scheme)
g.Expect(err).ToNot(HaveOccurred())
err = gwapiv1.Install(scheme)
g.Expect(err).ToNot(HaveOccurred())
var objects []client.Object
objects = append(objects, tt.llmSvc)
for _, gw := range tt.gateways {
objects = append(objects, gw)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objects...).
Build()
reconciler := &llmisvc.LLMISVCReconciler{
Client: fakeClient,
}
resolved := make([]llmisvc.ResolvedGateway, len(tt.gateways))
for i, gw := range tt.gateways {
resolved[i] = llmisvc.ResolvedGateway{Gateway: gw}
}
err = reconciler.EvaluateGatewayConditions(ctx, tt.llmSvc, resolved)
g.Expect(err).ToNot(HaveOccurred())
tt.llmSvc.DetermineRouterReadiness()
routerCondition := tt.llmSvc.GetStatus().GetCondition(v1alpha2.RouterReady)
gatewayCondition := tt.llmSvc.GetStatus().GetCondition(v1alpha2.GatewaysReady)
if tt.assertCondition != nil {
tt.assertCondition(routerCondition, gatewayCondition)(g)
}
})
}
}
func TestIsGatewayReady(t *testing.T) {
tests := []struct {
name string
gateway *gwapiv1.Gateway
expected bool
}{
{
name: "gateway with programmed condition true - should be ready",
gateway: Gateway("test-gateway",
WithProgrammedCondition(metav1.ConditionTrue, "Ready", "Gateway is ready"),
),
expected: true,
},
{
name: "gateway with programmed condition false - should not be ready",
gateway: Gateway("test-gateway",
WithProgrammedCondition(metav1.ConditionFalse, "NotReady", "Gateway is not ready"),
),
expected: false,
},
{
name: "gateway with programmed condition unknown - should not be ready",
gateway: Gateway("test-gateway",
WithProgrammedCondition(metav1.ConditionUnknown, "Unknown", "Gateway status unknown"),
),
expected: false,
},
{
name: "gateway with no conditions - should not be ready",
gateway: Gateway("test-gateway",
WithListener(gwapiv1.HTTPProtocolType),
),
expected: false,
},
{
name: "gateway with other conditions but no programmed - should not be ready",
gateway: Gateway("test-gateway",
WithGatewayCondition("Accepted", metav1.ConditionTrue, "Accepted", "Gateway accepted"),
),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
result := llmisvc.IsGatewayReady(tt.gateway)
g.Expect(result).To(Equal(tt.expected))
})
}
}
func TestHTTPRouteConditionsEvaluation(t *testing.T) {
tests := []struct {
name string
llmSvc *v1alpha2.LLMInferenceService
httpRoutes []*gwapiv1.HTTPRoute
expectedErrorMsg string
createAssertion func(routerCondition, httpRouteCondition *apis.Condition) assertConditionsFunc
}{
{
name: "HTTPRoute with multiple controllers - should be ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("llm"),
WithModelURI("hf://facebook/opt-125m"),
WithHTTPRouteRefs(HTTPRouteRef("facebook-opt-125m-single-simulated-kserve-route")),
),
httpRoutes: []*gwapiv1.HTTPRoute{
HTTPRoute("facebook-opt-125m-single-simulated-kserve-route",
InNamespace[*gwapiv1.HTTPRoute]("llm"),
WithParentRefs(GatewayParentRef("openshift-ai-inference", "openshift-ingress")),
WithHTTPRule(
Matches(PathPrefixMatch("/llm/facebook-opt-125m-single-simulated")),
WithBackendRefs(ServiceRef("facebook-opt-125m-single-simulated-kserve-workload-svc", 8000, 1)),
Timeouts("0s", "0s"),
Filters(gwapiv1.HTTPRouteFilter{
Type: gwapiv1.HTTPRouteFilterURLRewrite,
URLRewrite: &gwapiv1.HTTPURLRewriteFilter{
Path: &gwapiv1.HTTPPathModifier{
Type: gwapiv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: ptr.To("/"),
},
},
}),
),
WithHTTPRouteMultipleControllerStatus(
GatewayParentRef("openshift-ai-inference", "openshift-ingress"),
KuadrantControllerStatus,
GatewayAPIControllerStatus,
),
),
},
createAssertion: assertRouterReady,
},
{
name: "HTTPRoute with standard controller only - should be ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithHTTPRouteRefs(HTTPRouteRef("test-route")),
),
httpRoutes: []*gwapiv1.HTTPRoute{
HTTPRoute("test-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("test-gateway", "test-ns")),
WithHTTPRouteReadyStatus("openshift.io/gateway-controller/v1"),
),
},
createAssertion: assertRouterReady,
},
{
name: "HTTPRoute not ready - should not be ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithHTTPRouteRefs(HTTPRouteRef("not-ready-route")),
),
httpRoutes: []*gwapiv1.HTTPRoute{
HTTPRoute("not-ready-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("test-gateway", "test-ns")),
WithHTTPRouteNotReadyStatus("openshift.io/gateway-controller/v1", "NotAccepted", "Route was not accepted"),
),
},
createAssertion: func(routerCondition, httpRouteCondition *apis.Condition) assertConditionsFunc {
return assertRouterNotReadyWithReason(routerCondition, httpRouteCondition, "HTTPRoutesNotReady")
},
},
{
name: "no HTTPRoute refs - should skip evaluation",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
// No HTTPRoute refs
),
httpRoutes: []*gwapiv1.HTTPRoute{},
createAssertion: assertHTTPRouteConditionUnset,
},
{
name: "HTTPRoute with only policy-controller status (no Accepted) - should not be ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithHTTPRouteRefs(HTTPRouteRef("policy-only-route")),
),
httpRoutes: []*gwapiv1.HTTPRoute{
HTTPRoute("policy-only-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("test-gateway", "test-ns")),
WithHTTPRouteMultipleControllerStatus(
GatewayParentRef("test-gateway", "test-ns"),
KuadrantControllerStatus, // Only policy controller, no gateway controller
),
),
},
createAssertion: func(routerCondition, httpRouteCondition *apis.Condition) assertConditionsFunc {
return assertRouterNotReadyWithReason(routerCondition, httpRouteCondition, "HTTPRoutesNotReady")
},
},
{
name: "HTTPRoute with NoMatchingParent - should surface raw Gateway API reason",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithHTTPRouteRefs(HTTPRouteRef("no-matching-parent-route")),
),
httpRoutes: []*gwapiv1.HTTPRoute{
HTTPRoute("no-matching-parent-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("test-gateway", "test-ns")),
WithHTTPRouteParentStatus(
GatewayParentRef("test-gateway", "test-ns"),
"gateway-controller/v1",
metav1.Condition{
Type: string(gwapiv1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
Reason: string(gwapiv1.RouteReasonNoMatchingParent),
Message: "No matching parent",
LastTransitionTime: metav1.Now(),
},
),
),
},
createAssertion: func(routerCondition, httpRouteCondition *apis.Condition) assertConditionsFunc {
return func(g *WithT) {
g.Expect(httpRouteCondition).ToNot(BeNil(), "HTTPRoute condition should be set")
g.Expect(httpRouteCondition.IsFalse()).To(BeTrue(), "HTTPRoutes should not be ready")
g.Expect(httpRouteCondition.Reason).To(Equal("HTTPRoutesNotReady"), "Top-level condition reason should be HTTPRoutesNotReady")
g.Expect(httpRouteCondition.Message).To(ContainSubstring(string(gwapiv1.RouteReasonNoMatchingParent)), "Embedded reason should preserve the raw Gateway API NoMatchingParent reason")
}
},
},
{
name: "HTTPRoute with Accepted=True but ResolvedRefs missing - should not be ready",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithModelURI("hf://test/model"),
WithHTTPRouteRefs(HTTPRouteRef("accepted-no-resolved-route")),
),
httpRoutes: []*gwapiv1.HTTPRoute{
HTTPRoute("accepted-no-resolved-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("test-gateway", "test-ns")),
WithHTTPRouteParentStatus(
GatewayParentRef("test-gateway", "test-ns"),
"openshift.io/gateway-controller/v1",
metav1.Condition{
Type: string(gwapiv1.RouteConditionAccepted),
Status: metav1.ConditionTrue,
Reason: "Accepted",
Message: "Route was valid",
LastTransitionTime: metav1.Now(),
},
// ResolvedRefs intentionally omitted
),
),
},
createAssertion: func(routerCondition, httpRouteCondition *apis.Condition) assertConditionsFunc {
return assertRouterNotReadyWithReason(routerCondition, httpRouteCondition, "HTTPRoutesNotReady")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
ctx := t.Context()
scheme := runtime.NewScheme()
err := v1alpha2.AddToScheme(scheme)
g.Expect(err).ToNot(HaveOccurred())
err = gwapiv1.Install(scheme)
g.Expect(err).ToNot(HaveOccurred())
var objects []client.Object
objects = append(objects, tt.llmSvc)
for _, route := range tt.httpRoutes {
objects = append(objects, route)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objects...).
Build()
reconciler := &llmisvc.LLMISVCReconciler{
Client: fakeClient,
}
err = reconciler.EvaluateHTTPRouteConditions(ctx, tt.llmSvc, &llmisvc.Config{})
if tt.expectedErrorMsg != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErrorMsg))
return
}
g.Expect(err).ToNot(HaveOccurred())
tt.llmSvc.DetermineRouterReadiness()
routerCondition := tt.llmSvc.GetStatus().GetCondition(v1alpha2.RouterReady)
httpRouteCondition := tt.llmSvc.GetStatus().GetCondition(v1alpha2.HTTPRoutesReady)
if tt.createAssertion != nil {
tt.createAssertion(routerCondition, httpRouteCondition)(g)
}
})
}
}
func TestIsHTTPRouteReady(t *testing.T) {
tests := []struct {
name string
route *gwapiv1.HTTPRoute
expected bool
}{
{
name: "nil route",
route: nil,
expected: false,
},
{
name: "no parent refs",
route: HTTPRoute("empty-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
),
expected: false,
},
{
name: "single parent ref - ready",
route: HTTPRoute("ready-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("gw", "test-ns")),
WithHTTPRouteReadyStatus("controller"),
),
expected: true,
},
{
name: "single parent ref with multiple controllers - ready",
route: HTTPRoute("multi-ctrl-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("gw", "test-ns")),
WithHTTPRouteMultipleControllerStatus(
GatewayParentRef("gw", "test-ns"),
KuadrantControllerStatus,
GatewayAPIControllerStatus,
),
),
expected: true,
},
{
name: "multiple parent refs - all have status - ready",
route: HTTPRoute("multi-parent-ready",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(
GatewayParentRef("gw-a", "test-ns"),
GatewayParentRef("gw-b", "test-ns"),
),
WithHTTPRouteParentStatus(
GatewayParentRef("gw-a", "test-ns"), "controller",
metav1.Condition{Type: string(gwapiv1.RouteConditionAccepted), Status: metav1.ConditionTrue, Reason: "Accepted", LastTransitionTime: metav1.Now()},
metav1.Condition{Type: string(gwapiv1.RouteConditionResolvedRefs), Status: metav1.ConditionTrue, Reason: "ResolvedRefs", LastTransitionTime: metav1.Now()},
),
WithHTTPRouteParentStatus(
GatewayParentRef("gw-b", "test-ns"), "controller",
metav1.Condition{Type: string(gwapiv1.RouteConditionAccepted), Status: metav1.ConditionTrue, Reason: "Accepted", LastTransitionTime: metav1.Now()},
metav1.Condition{Type: string(gwapiv1.RouteConditionResolvedRefs), Status: metav1.ConditionTrue, Reason: "ResolvedRefs", LastTransitionTime: metav1.Now()},
),
),
expected: true,
},
{
name: "single parent ref with Accepted=False - not ready",
route: HTTPRoute("rejected-route",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(GatewayParentRef("gw", "test-ns")),
WithHTTPRouteParentStatus(
GatewayParentRef("gw", "test-ns"), "controller",
metav1.Condition{Type: string(gwapiv1.RouteConditionAccepted), Status: metav1.ConditionFalse, Reason: "NotAllowed", LastTransitionTime: metav1.Now()},
),
),
expected: false,
},
{
name: "multiple parent refs - one has no status entry - not ready",
route: HTTPRoute("multi-parent-partial",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(
GatewayParentRef("gw-a", "test-ns"),
GatewayParentRef("gw-b", "test-ns"),
),
// Only gw-a has a status entry; gw-b has none
WithHTTPRouteParentStatus(
GatewayParentRef("gw-a", "test-ns"), "controller",
metav1.Condition{Type: string(gwapiv1.RouteConditionAccepted), Status: metav1.ConditionTrue, Reason: "Accepted", LastTransitionTime: metav1.Now()},
metav1.Condition{Type: string(gwapiv1.RouteConditionResolvedRefs), Status: metav1.ConditionTrue, Reason: "ResolvedRefs", LastTransitionTime: metav1.Now()},
),
),
expected: false,
},
{
name: "multiple parent refs - one has only policy controller status - not ready",
route: HTTPRoute("multi-parent-policy-only",
InNamespace[*gwapiv1.HTTPRoute]("test-ns"),
WithParentRefs(
GatewayParentRef("gw-a", "test-ns"),
GatewayParentRef("gw-b", "test-ns"),
),
// gw-a: gateway controller ready
WithHTTPRouteParentStatus(
GatewayParentRef("gw-a", "test-ns"), "controller",
metav1.Condition{Type: string(gwapiv1.RouteConditionAccepted), Status: metav1.ConditionTrue, Reason: "Accepted", LastTransitionTime: metav1.Now()},
metav1.Condition{Type: string(gwapiv1.RouteConditionResolvedRefs), Status: metav1.ConditionTrue, Reason: "ResolvedRefs", LastTransitionTime: metav1.Now()},
),
// gw-b: only a policy controller entry, no gateway controller status
WithHTTPRouteMultipleControllerStatus(
GatewayParentRef("gw-b", "test-ns"),
KuadrantControllerStatus,
),
),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
g.Expect(llmisvc.IsHTTPRouteReady(tt.route)).To(Equal(tt.expected))
})
}
}
func TestFetchReferencedGateways(t *testing.T) {
tests := []struct {
name string
llmSvc *v1alpha2.LLMInferenceService
gateways []*gwapiv1.Gateway
expectedCount int
expectedError string
}{
{
name: "fetch single gateway successfully",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithGatewayRefs(v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{
Name: "test-gateway",
Namespace: "test-ns",
}}),
),
gateways: []*gwapiv1.Gateway{
Gateway("test-gateway", InNamespace[*gwapiv1.Gateway]("test-ns")),
},
expectedCount: 1,
},
{
name: "fetch multiple gateways successfully",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithGatewayRefs(
v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{Name: "gateway-1", Namespace: "test-ns"}},
v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{Name: "gateway-2", Namespace: "other-ns"}},
),
),
gateways: []*gwapiv1.Gateway{
Gateway("gateway-1", InNamespace[*gwapiv1.Gateway]("test-ns")),
Gateway("gateway-2", InNamespace[*gwapiv1.Gateway]("other-ns")),
},
expectedCount: 2,
},
{
name: "gateway not found - should return error",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
WithGatewayRefs(v1alpha2.GatewayObjectReference{UntypedObjectReference: v1alpha2.UntypedObjectReference{
Name: "missing-gateway",
Namespace: "test-ns",
}}),
),
gateways: []*gwapiv1.Gateway{},
expectedCount: 0,
expectedError: "failed to get Gateway",
},
{
name: "no gateway refs - should return empty slice",
llmSvc: LLMInferenceService("test-llm",
InNamespace[*v1alpha2.LLMInferenceService]("test-ns"),
// No gateway refs
),
gateways: []*gwapiv1.Gateway{},
expectedCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
ctx := t.Context()
scheme := runtime.NewScheme()
err := v1alpha2.AddToScheme(scheme)
g.Expect(err).ToNot(HaveOccurred())
err = gwapiv1.Install(scheme)
g.Expect(err).ToNot(HaveOccurred())
var objects []client.Object
objects = append(objects, tt.llmSvc)
for _, gw := range tt.gateways {
objects = append(objects, gw)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objects...).
Build()
reconciler := &llmisvc.LLMISVCReconciler{
Client: fakeClient,
}
gateways, err := reconciler.CollectReferencedGateways(ctx, tt.llmSvc)
if tt.expectedError != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.expectedError))
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(gateways).To(HaveLen(tt.expectedCount))
})
}
}
// assertConditionsFunc defines the signature for condition assertion functions
type assertConditionsFunc func(g *WithT)
// assertConditionUnset returns a function that verifies the router is ready but gateway condition is not set
func assertConditionUnset(routerCondition, gatewayCondition *apis.Condition) assertConditionsFunc {
return func(g *WithT) {
g.Expect(routerCondition.IsTrue()).To(BeTrue(), "Router should be ready")
g.Expect(gatewayCondition).To(BeNil(), "Gateway condition should not be set when no gateway refs")
}
}
// assertRouterReady returns a function that verifies both router and gateway conditions are set and ready
func assertRouterReady(routerCondition, gatewayCondition *apis.Condition) assertConditionsFunc {
return func(g *WithT) {
g.Expect(routerCondition).ToNot(BeNil(), "Router condition should be set")
g.Expect(routerCondition.IsTrue()).To(BeTrue(), "Router should be ready")
g.Expect(gatewayCondition).ToNot(BeNil(), "Gateway condition should be set")
g.Expect(gatewayCondition.IsTrue()).To(BeTrue(), "Gateways should be ready")
}
}
// assertRouterNotReady returns a function that verifies both router and gateway conditions are set but not ready
func assertRouterNotReady(routerCondition, gatewayCondition *apis.Condition) assertConditionsFunc {
return func(g *WithT) {
g.Expect(routerCondition).ToNot(BeNil(), "Router condition should be set")
g.Expect(routerCondition.IsFalse()).To(BeTrue(), "Router should not be ready")
g.Expect(gatewayCondition).ToNot(BeNil(), "Gateway condition should be set")
g.Expect(gatewayCondition.IsFalse()).To(BeTrue(), "Gateways should not be ready")
}
}
// assertRouterNotReadyWithReason returns a function that verifies conditions are not ready and checks the reason
func assertRouterNotReadyWithReason(routerCondition, gatewayCondition *apis.Condition, expectedReason string) assertConditionsFunc {
return func(g *WithT) {
g.Expect(routerCondition).ToNot(BeNil(), "Router condition should be set")
g.Expect(routerCondition.IsFalse()).To(BeTrue(), "Router should not be ready")
g.Expect(gatewayCondition).ToNot(BeNil(), "Gateway condition should be set")
g.Expect(gatewayCondition.IsFalse()).To(BeTrue(), "Gateways should not be ready")
g.Expect(routerCondition.Reason).To(Equal(gatewayCondition.Reason))
g.Expect(routerCondition.Reason).To(Equal(expectedReason))
}
}
// assertHTTPRouteConditionUnset returns a function that verifies the router is ready and HTTPRoute condition is cleared
func assertHTTPRouteConditionUnset(routerCondition, httpRouteCondition *apis.Condition) assertConditionsFunc {
return func(g *WithT) {
g.Expect(routerCondition.IsTrue()).To(BeTrue(), "Router should be ready")
g.Expect(httpRouteCondition).To(BeNil(), "HTTPRoute condition should not be set when no HTTPRoute refs")
}
}