Files
higress/pkg/ingress/kube/gateway/istio/inferencepool_status_test.go
2025-11-26 10:15:00 +08:00

795 lines
31 KiB
Go

// Copyright Istio 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 istio
import (
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
"istio.io/istio/pilot/pkg/features"
"istio.io/istio/pilot/pkg/status"
"istio.io/istio/pkg/config/schema/gvk"
"istio.io/istio/pkg/kube/krt"
"istio.io/istio/pkg/test"
)
const (
IstioController = "higress.io/gateway-controller"
DefaultTestNS = "default"
GatewayTestNS = "gateway-ns"
AppTestNS = "app-ns"
EmptyTestNS = ""
infPoolPending = "Pending"
)
func TestInferencePoolStatusReconciliation(t *testing.T) {
test.SetForTest(t, &features.EnableGatewayAPIInferenceExtension, true)
testCases := []struct {
name string
givens []runtime.Object // Objects to create before the test
targetPool *inferencev1.InferencePool // The InferencePool to check
expectations func(t *testing.T, pool *inferencev1.InferencePoolStatus)
}{
//
// Positive Test Scenarios
//
{
name: "should add gateway parentRef to inferencepool status",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
WithRouteParentCondition(string(gatewayv1.RouteConditionAccepted), metav1.ConditionTrue, "Accepted", "Accepted"),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
Type: string(inferencev1.InferencePoolConditionAccepted),
Status: metav1.ConditionTrue,
Reason: string(inferencev1.InferencePoolReasonAccepted),
Message: "Referenced by an HTTPRoute",
}, "Expected condition with Accepted")
},
},
{
name: "should add only 1 gateway parentRef to status for multiple routes on different gateways with different controllers",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("other")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithParentRefAndStatus("gateway-2", DefaultTestNS, "other-controller"),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
},
},
{
name: "should keep the status of the gateway parentRefs from another controller",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("other-class")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
NewHTTPRoute("route-2", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-2", DefaultTestNS, "other-class"),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithParentStatus("gateway-2", DefaultTestNS, WithAcceptedConditions())),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 2, "Expected two parent references")
assert.ElementsMatch(t,
[]string{"gateway-1", "gateway-2"},
[]string{string(status.Parents[0].ParentRef.Name), string(status.Parents[1].ParentRef.Name)},
)
},
},
{
name: "should add multiple gateway parentRefs to status for multiple routes",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
NewHTTPRoute("route-2", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-2", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 2, "Expected two parent references")
assert.ElementsMatch(t,
[]string{"gateway-1", "gateway-2"},
[]string{string(status.Parents[0].ParentRef.Name), string(status.Parents[1].ParentRef.Name)},
)
},
},
{
name: "should remove our status from previous reconciliation that is no longer referenced by any HTTPRoute",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS),
WithParentStatus("gateway-2", DefaultTestNS,
WithAcceptedConditions(),
)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
},
},
{
name: "should update/recreate our status from previous reconciliation",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS),
WithParentStatus("gateway-1", DefaultTestNS,
WithAcceptedConditions(),
)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
require.Len(t, status.Parents[0].Conditions, 2, "Expected two conditions")
},
},
{
name: "should keep others status from previous reconciliation",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("other-class")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithParentStatus("gateway-2", DefaultTestNS, WithAcceptedConditions())),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 2, "Expected two parent references")
assert.ElementsMatch(t,
[]string{"gateway-1", "gateway-2"},
[]string{string(status.Parents[0].ParentRef.Name), string(status.Parents[1].ParentRef.Name)},
)
},
},
{
name: "should remove default parent 'waiting for controller' status",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithParentStatus("default", DefaultTestNS, AsDefaultStatus())),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected two parent references")
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
},
},
{
name: "should remove unknown condition types from controlled parents",
givens: []runtime.Object{
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS),
WithParentStatus("gateway-1", DefaultTestNS,
WithAcceptedConditions(),
WithConditions(metav1.ConditionUnknown, "X", "Y", "Dummy"),
)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected two parent references")
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
require.Len(t, status.Parents[0].Conditions, 2, "Expected two conditions")
assert.ElementsMatch(t,
[]string{string(inferencev1.InferencePoolConditionAccepted), string(inferencev1.InferencePoolConditionResolvedRefs)},
[]string{status.Parents[0].Conditions[0].Type, status.Parents[0].Conditions[1].Type},
)
},
},
{
name: "should handle cross-namespace gateway references correctly",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(AppTestNS),
WithParentRefAndStatus("main-gateway", GatewayTestNS, IstioController),
WithBackendRef("test-pool", AppTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(AppTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, GatewayTestNS, string(status.Parents[0].ParentRef.Namespace))
},
},
{
name: "should handle cross-namespace httproute references correctly",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(AppTestNS),
WithParentRefAndStatus("main-gateway", GatewayTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, GatewayTestNS, string(status.Parents[0].ParentRef.Namespace))
},
},
{
name: "should handle HTTPRoute in same namespace (empty)",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(AppTestNS),
WithParentRefAndStatus("main-gateway", GatewayTestNS, IstioController),
WithBackendRef("test-pool", EmptyTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(AppTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, GatewayTestNS, string(status.Parents[0].ParentRef.Namespace))
},
},
{
name: "should handle Gateway in same namespace (empty)",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(AppTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(AppTestNS),
WithParentRefAndStatus("main-gateway", EmptyTestNS, IstioController),
WithBackendRef("test-pool", AppTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(AppTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, AppTestNS, string(status.Parents[0].ParentRef.Namespace))
},
},
{
name: "should add only one parentRef for multiple routes on same gateway",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("route-a", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
NewHTTPRoute("route-b", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected only one parent reference for the same gateway")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
},
},
{
name: "should report ResolvedRef true when ExtensioNRef found",
givens: []runtime.Object{
NewService("test-epp", InNamespace(DefaultTestNS)),
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithExtensionRef("Service", "test-epp")),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
require.Len(t, status.Parents[0].Conditions, 2, "Expected two condition")
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
Type: string(inferencev1.InferencePoolConditionResolvedRefs),
Status: metav1.ConditionTrue,
Reason: string(inferencev1.InferencePoolReasonResolvedRefs),
Message: "Referenced ExtensionRef resolved",
}, "Expected condition with InvalidExtensionRef")
},
},
{
name: "should report HTTPRoute not accepted when parent gateway rejects HTTPRoute",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
WithRouteParentCondition(string(gatewayv1.RouteConditionAccepted), metav1.ConditionFalse, "GatewayNotReady", "Gateway not ready"),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
Type: string(inferencev1.InferencePoolConditionAccepted),
Status: metav1.ConditionFalse,
Reason: string(inferencev1.InferencePoolReasonHTTPRouteNotAccepted),
Message: "Referenced HTTPRoute default/test-route not accepted by Gateway default/main-gateway",
}, "Expected condition with HTTPRouteNotAccepted")
},
},
{
name: "should report unknown status when HTTPRoute parent status has no Accepted condition",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
// Note: No WithRouteParentCondition for Accepted - parent is listed but has no conditions
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
Type: string(inferencev1.InferencePoolConditionAccepted),
Status: metav1.ConditionUnknown,
Reason: string(inferencev1.InferencePoolReasonAccepted),
Message: "Referenced by an HTTPRoute unknown parentRef Gateway status",
}, "Expected condition with ConditionUnknown")
},
},
//
// Negative Test Scenarios
//
{
name: "should not add parentRef for gatewayclass not controlled by us",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("other")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, "other-controller"),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
assert.Empty(t, status.Parents, "ParentRefs should be empty")
},
},
{
name: "should not add parentRef if httproute has no backendref",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController)), // No BackendRef
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
assert.Empty(t, status.Parents, "ParentRefs should be empty")
},
},
{
name: "should not add parentRef if httproute has no parentref",
givens: []runtime.Object{
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithBackendRef("test-pool", DefaultTestNS)), // No ParentRef
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
assert.Empty(t, status.Parents, "ParentRefs should be empty")
},
},
{
name: "should report ExtensionRef not found if no matching service found",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
require.Len(t, status.Parents[0].Conditions, 2, "Expected two condition")
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
Type: string(inferencev1.InferencePoolConditionResolvedRefs),
Status: metav1.ConditionFalse,
Reason: string(inferencev1.InferencePoolReasonInvalidExtensionRef),
Message: "Referenced ExtensionRef not found",
}, "Expected condition with InvalidExtensionRef")
},
},
{
name: "should report unsupported ExtensionRef if kind is not service",
givens: []runtime.Object{
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
WithBackendRef("test-pool", DefaultTestNS)),
},
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithExtensionRef("Gateway", "main-gateway")),
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
require.Len(t, status.Parents, 1, "Expected one parent reference")
require.Len(t, status.Parents[0].Conditions, 2, "Expected two condition")
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
Type: string(inferencev1.InferencePoolConditionResolvedRefs),
Status: metav1.ConditionFalse,
Reason: string(inferencev1.InferencePoolReasonInvalidExtensionRef),
Message: "Unsupported ExtensionRef kind",
}, "Expected condition with InvalidExtensionRef")
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
stop := test.NewStop(t)
controller := setupController(t,
append(tc.givens, tc.targetPool)...,
)
sq := &TestStatusQueue{
state: map[status.Resource]any{},
}
statusSynced := controller.status.SetQueue(sq)
for _, st := range statusSynced {
st.WaitUntilSynced(stop)
}
dumpOnFailure(t, krt.GlobalDebugHandler)
getInferencePoolStatus := func() *inferencev1.InferencePoolStatus {
statuses := sq.Statuses()
for _, status := range statuses {
if pool, ok := status.(*inferencev1.InferencePoolStatus); ok {
return pool
}
}
return nil
}
poolStatus := getInferencePoolStatus()
assert.NotNil(t, poolStatus)
tc.expectations(t, poolStatus)
})
}
}
func assertConditionContains(t *testing.T, conditions []metav1.Condition, expected metav1.Condition, msgAndArgs ...interface{}) {
t.Helper()
for _, condition := range conditions {
if (expected.Type == "" || condition.Type == expected.Type) &&
(expected.Status == "" || condition.Status == expected.Status) &&
(expected.Reason == "" || condition.Reason == expected.Reason) &&
(expected.Message == "" || strings.HasPrefix(condition.Message, expected.Message)) {
return // Found matching condition
}
}
// If we get here, no matching condition was found
assert.Fail(t, fmt.Sprintf("Expected condition with Type=%s, Status=%s, Reason=%s not found in conditions. Available conditions: %+v",
expected.Type, expected.Status, expected.Reason, conditions), msgAndArgs...)
}
// --- Mock Objects ---
// Option is a function that mutates an object.
type Option func(client.Object)
type ParentOption func(*inferencev1.ParentStatus)
// --- Helper functions to mutate objects ---
func InNamespace(namespace string) Option {
return func(obj client.Object) {
obj.SetNamespace(namespace)
}
}
func WithController(name string) Option {
return func(obj client.Object) {
gw, ok := obj.(*gateway.GatewayClass)
if ok {
gw.Spec.ControllerName = gateway.GatewayController(name)
}
}
}
func WithGatewayClass(name string) Option {
return func(obj client.Object) {
gw, ok := obj.(*gateway.Gateway)
if ok {
gw.Spec.GatewayClassName = gateway.ObjectName(name)
}
}
}
func WithParentRef(name, namespace string) Option {
return func(obj client.Object) {
hr, ok := obj.(*gateway.HTTPRoute)
if ok {
namespaceName := gateway.Namespace(namespace)
hr.Spec.ParentRefs = []gateway.ParentReference{
{
Name: gateway.ObjectName(name),
Namespace: &namespaceName,
},
}
}
}
}
func WithParentRefAndStatus(name, namespace, controllerName string) Option {
return func(obj client.Object) {
hr, ok := obj.(*gateway.HTTPRoute)
if ok {
namespaceName := gateway.Namespace(namespace)
if hr.Spec.ParentRefs == nil {
hr.Spec.ParentRefs = []gateway.ParentReference{}
}
hr.Spec.ParentRefs = append(hr.Spec.ParentRefs, gateway.ParentReference{
Name: gateway.ObjectName(name),
Namespace: &namespaceName,
})
if hr.Status.Parents == nil {
hr.Status.Parents = []gateway.RouteParentStatus{}
}
parentStatusRef := &gateway.RouteParentStatus{
ParentRef: gateway.ParentReference{
Name: gateway.ObjectName(name),
Namespace: &namespaceName,
},
ControllerName: gateway.GatewayController(controllerName),
}
hr.Status.Parents = append(hr.Status.Parents, *parentStatusRef)
}
}
}
func WithRouteParentCondition(conditionType string, status metav1.ConditionStatus, reason, message string) Option {
return func(obj client.Object) {
hr, ok := obj.(*gateway.HTTPRoute)
if ok && len(hr.Status.Parents) > 0 {
// Add condition to the last parent status (most recently added)
lastParentIdx := len(hr.Status.Parents) - 1
if hr.Status.Parents[lastParentIdx].Conditions == nil {
hr.Status.Parents[lastParentIdx].Conditions = []metav1.Condition{}
}
hr.Status.Parents[lastParentIdx].Conditions = append(hr.Status.Parents[lastParentIdx].Conditions,
metav1.Condition{
Type: conditionType,
Status: status,
Reason: reason,
Message: message,
ObservedGeneration: 1,
LastTransitionTime: metav1.NewTime(time.Now()),
},
)
}
}
}
func WithBackendRef(name, namespace string) Option {
return func(obj client.Object) {
hr, ok := obj.(*gateway.HTTPRoute)
if ok {
namespaceName := gateway.Namespace(namespace)
if hr.Spec.Rules == nil {
hr.Spec.Rules = []gateway.HTTPRouteRule{}
}
group := gateway.Group(gvk.InferencePool.Group)
kind := gateway.Kind(gvk.InferencePool.Kind)
hr.Spec.Rules = append(hr.Spec.Rules, gateway.HTTPRouteRule{
BackendRefs: []gateway.HTTPBackendRef{
{
BackendRef: gateway.BackendRef{
BackendObjectReference: gateway.BackendObjectReference{
Name: gateway.ObjectName(name),
Namespace: &namespaceName,
Kind: &kind,
Group: &group,
},
},
},
},
})
}
}
}
func WithParentStatus(gatewayName, namespace string, opt ...ParentOption) Option {
return func(obj client.Object) {
ip, ok := obj.(*inferencev1.InferencePool)
if ok {
if ip.Status.Parents == nil {
ip.Status.Parents = []inferencev1.ParentStatus{}
}
poolStatus := inferencev1.ParentStatus{
ParentRef: inferencev1.ParentReference{
Name: inferencev1.ObjectName(gatewayName),
Namespace: inferencev1.Namespace(namespace),
},
}
for _, opt := range opt {
opt(&poolStatus)
}
ip.Status.Parents = append(ip.Status.Parents, poolStatus)
}
}
}
func AsDefaultStatus() ParentOption {
return func(parentStatusRef *inferencev1.ParentStatus) {
dName := "default"
dKind := "Status"
parentStatusRef.ParentRef.Name = inferencev1.ObjectName(dName)
parentStatusRef.ParentRef.Kind = inferencev1.Kind(dKind)
WithConditions(
metav1.ConditionUnknown,
string(inferencev1.InferencePoolConditionAccepted),
infPoolPending,
"Waiting for controller",
)
}
}
func WithConditions(status metav1.ConditionStatus, conType, reason, message string) ParentOption {
return func(parentStatusRef *inferencev1.ParentStatus) {
if parentStatusRef.Conditions == nil {
parentStatusRef.Conditions = []metav1.Condition{}
}
parentStatusRef.Conditions = append(parentStatusRef.Conditions,
metav1.Condition{
Type: conType,
Status: status,
Reason: reason,
Message: message,
ObservedGeneration: 1,
LastTransitionTime: metav1.NewTime(time.Now()),
},
)
}
}
func WithAcceptedConditions() ParentOption {
return func(parentStatusRef *inferencev1.ParentStatus) {
WithConditions(
metav1.ConditionTrue,
string(inferencev1.InferencePoolConditionAccepted),
string(inferencev1.InferencePoolReasonAccepted),
"Accepted by the parentRef Gateway",
)(parentStatusRef)
WithConditions(
metav1.ConditionTrue,
string(inferencev1.InferencePoolConditionResolvedRefs),
string(inferencev1.InferencePoolReasonResolvedRefs),
"Resolved ExtensionRef",
)(parentStatusRef)
}
}
func WithExtensionRef(kind, name string) Option {
return func(obj client.Object) {
ip, ok := obj.(*inferencev1.InferencePool)
if ok {
typedKind := inferencev1.Kind(kind)
ip.Spec.EndpointPickerRef = inferencev1.EndpointPickerRef{
Name: inferencev1.ObjectName(name),
Kind: typedKind,
}
}
}
}
// --- Object Creation Functions ---
func NewGateway(name string, opts ...Option) *gateway.Gateway {
gw := &gateway.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: DefaultTestNS,
},
Spec: gateway.GatewaySpec{
GatewayClassName: "higress",
},
}
for _, opt := range opts {
opt(gw)
}
return gw
}
func NewHTTPRoute(name string, opts ...Option) *gateway.HTTPRoute {
hr := &gateway.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: DefaultTestNS,
},
}
for _, opt := range opts {
opt(hr)
}
return hr
}
func NewInferencePool(name string, opts ...Option) *inferencev1.InferencePool {
ip := &inferencev1.InferencePool{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: DefaultTestNS,
},
Spec: inferencev1.InferencePoolSpec{
Selector: inferencev1.LabelSelector{
MatchLabels: map[inferencev1.LabelKey]inferencev1.LabelValue{
"app": "test",
},
},
EndpointPickerRef: inferencev1.EndpointPickerRef{
Name: "endpoint-picker",
},
},
}
for _, opt := range opts {
opt(ip)
}
return ip
}
func NewService(name string, opts ...Option) *corev1.Service {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: DefaultTestNS,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "http",
Port: 80,
TargetPort: intstr.FromInt(9002),
},
},
},
}
for _, opt := range opts {
opt(svc)
}
return svc
}