mirror of
https://github.com/alibaba/higress.git
synced 2026-04-22 04:27:26 +08:00
795 lines
31 KiB
Go
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
|
|
}
|