Files
2026-03-02 16:46:53 -05:00

270 lines
9.0 KiB
Go

/*
Copyright 2025 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
import (
"context"
"errors"
"fmt"
"strings"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/log"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
"github.com/kserve/kserve/pkg/apis/serving/v1alpha2"
)
const (
RefsInvalidReason = "RefsInvalid"
)
// ValidationError represents a validation failure that should be reported via conditions
// rather than causing reconciliation to fail completely
type ValidationError struct {
message string
}
func (e *ValidationError) Error() string {
return e.message
}
func NewValidationError(msg string) *ValidationError {
return &ValidationError{
message: msg,
}
}
func IsValidationError(err error) bool {
var validationError *ValidationError
return errors.As(err, &validationError)
}
// validateRouterReferences performs comprehensive validation of all router-related references
// including gateway references, HTTPRoute references, managed HTTPRoute specs, and route targets.
// It handles condition marking internally and returns validation or unexpected errors.
func (r *LLMISVCReconciler) validateRouterReferences(ctx context.Context, llmSvc *v1alpha2.LLMInferenceService) error {
logger := log.FromContext(ctx).WithName("validateRouterReferences")
if err := r.validateGatewayReferences(ctx, llmSvc); err != nil {
if IsValidationError(err) {
llmSvc.MarkGatewaysNotReady(RefsInvalidReason, err.Error())
logger.Info("Gateway reference validation failed", "error", err)
return err
}
return fmt.Errorf("gateway reference validation failed: %w", err)
}
// Clear stale GatewaysReady condition from previous validation failures
// The actual readiness will be evaluated later in EvaluateGatewayConditions
llmSvc.MarkGatewaysReadyUnset()
if err := r.validateHTTPRouteReferences(ctx, llmSvc); err != nil {
if IsValidationError(err) {
llmSvc.MarkHTTPRoutesNotReady(RefsInvalidReason, err.Error())
logger.Info("HTTPRoute reference validation failed", "error", err)
return err
}
return fmt.Errorf("HTTPRoute reference validation failed: %w", err)
}
// Clear stale HTTPRoutesReady condition from previous validation failures
llmSvc.MarkHTTPRoutesReadyUnset()
if err := r.validateManagedHTTPRouteSpec(ctx, llmSvc); err != nil {
if IsValidationError(err) {
llmSvc.MarkHTTPRoutesNotReady(RefsInvalidReason, err.Error())
logger.Info("Managed HTTPRoute spec validation failed", "error", err)
return err
}
return fmt.Errorf("managed HTTPRoute spec validation failed: %w", err)
}
if llmSvc.Spec.Router != nil && llmSvc.Spec.Router.Route != nil && llmSvc.Spec.Router.Route.HTTP.HasRefs() {
referencedRoutes, err := r.collectReferencedRoutes(ctx, llmSvc)
if err != nil {
return fmt.Errorf("failed to collect referenced routes for target validation: %w", err)
}
if err := r.validateHTTPRouteTargets(ctx, referencedRoutes); err != nil {
if IsValidationError(err) {
llmSvc.MarkHTTPRoutesNotReady(RefsInvalidReason, err.Error())
logger.Info("HTTPRoute target validation failed", "error", err)
return err
}
return fmt.Errorf("HTTPRoute target validation failed: %w", err)
}
}
return nil
}
// validateGatewayReferences checks if all referenced gateways exist
func (r *LLMISVCReconciler) validateGatewayReferences(ctx context.Context, llmSvc *v1alpha2.LLMInferenceService) error {
logger := log.FromContext(ctx).WithName("validateGatewayReferences")
// If no router or gateway configuration, skip validation
if llmSvc.Spec.Router == nil || !llmSvc.Spec.Router.Gateway.HasRefs() {
return nil
}
var missingGateways []string
for _, ref := range llmSvc.Spec.Router.Gateway.Refs {
gateway := &gwapiv1.Gateway{}
gatewayKey := types.NamespacedName{
Name: string(ref.Name),
Namespace: string(ref.Namespace),
}
if gatewayKey.Namespace == "" {
gatewayKey.Namespace = llmSvc.GetNamespace()
}
err := r.Get(ctx, gatewayKey, gateway)
if err != nil {
if apierrors.IsNotFound(err) {
missingGateways = append(missingGateways, fmt.Sprintf("Gateway %s/%s does not exist", gatewayKey.Namespace, gatewayKey.Name))
continue
}
logger.Error(err, "Error fetching Gateway", "name", gatewayKey.Name, "namespace", gatewayKey.Namespace)
return fmt.Errorf("failed to get Gateway %s: %w", gatewayKey, err)
}
}
if len(missingGateways) > 0 {
message := strings.Join(missingGateways, "; ")
return NewValidationError(message)
}
return nil
}
// validateHTTPRouteReferences checks if all referenced HTTPRoutes exist
func (r *LLMISVCReconciler) validateHTTPRouteReferences(ctx context.Context, llmSvc *v1alpha2.LLMInferenceService) error {
// If no router or route configuration, skip validation
if llmSvc.Spec.Router == nil || llmSvc.Spec.Router.Route == nil || !llmSvc.Spec.Router.Route.HTTP.HasRefs() {
return nil
}
var missingRoutes []string
for _, routeRef := range llmSvc.Spec.Router.Route.HTTP.Refs {
route := &gwapiv1.HTTPRoute{}
if err := r.Get(ctx, types.NamespacedName{Namespace: llmSvc.GetNamespace(), Name: routeRef.Name}, route); err != nil {
if apierrors.IsNotFound(err) {
missingRoutes = append(missingRoutes, fmt.Sprintf("HTTPRoute %s/%s does not exist", llmSvc.GetNamespace(), routeRef.Name))
continue
}
return fmt.Errorf("failed to get HTTPRoute %s/%s: %w", llmSvc.GetNamespace(), routeRef.Name, err)
}
}
if len(missingRoutes) > 0 {
message := strings.Join(missingRoutes, "; ")
return NewValidationError(message)
}
return nil
}
// validateHTTPRouteTargets checks if referenced HTTPRoutes properly target the inference service
func (r *LLMISVCReconciler) validateHTTPRouteTargets(ctx context.Context, routes []*gwapiv1.HTTPRoute) error {
logger := log.FromContext(ctx).WithName("validateHTTPRouteTargets")
var targetErrors []string
for _, route := range routes {
if len(route.Spec.ParentRefs) == 0 {
targetErrors = append(targetErrors, fmt.Sprintf("HTTPRoute %s/%s has no parent gateway references", route.Namespace, route.Name))
continue
}
for _, parentRef := range route.Spec.ParentRefs {
gatewayKey := types.NamespacedName{
Name: string(parentRef.Name),
}
if parentRef.Namespace != nil {
gatewayKey.Namespace = string(*parentRef.Namespace)
} else {
gatewayKey.Namespace = route.Namespace
}
gateway := &gwapiv1.Gateway{}
err := r.Get(ctx, gatewayKey, gateway)
if err != nil {
if apierrors.IsNotFound(err) {
targetErrors = append(targetErrors, fmt.Sprintf("HTTPRoute %s/%s references non-existent Gateway %s/%s", route.Namespace, route.Name, gatewayKey.Namespace, gatewayKey.Name))
} else {
logger.Error(err, "Error fetching parent Gateway", "route", route.Name, "gateway", gatewayKey)
targetErrors = append(targetErrors, fmt.Sprintf("HTTPRoute %s/%s failed to validate parent Gateway %s/%s: %v", route.Namespace, route.Name, gatewayKey.Namespace, gatewayKey.Name, err))
}
}
}
}
if len(targetErrors) > 0 {
message := strings.Join(targetErrors, "; ")
return NewValidationError("HTTPRoute target validation failed: " + message)
}
return nil
}
// validateManagedHTTPRouteSpec checks if managed HTTPRoute spec has valid parent gateway references
func (r *LLMISVCReconciler) validateManagedHTTPRouteSpec(ctx context.Context, llmSvc *v1alpha2.LLMInferenceService) error {
logger := log.FromContext(ctx).WithName("validateManagedHTTPRouteSpec")
// Only validate if there's a managed HTTPRoute spec
if llmSvc.Spec.Router == nil || llmSvc.Spec.Router.Route == nil || !llmSvc.Spec.Router.Route.HTTP.HasSpec() {
return nil
}
var targetErrors []string
httpSpec := llmSvc.Spec.Router.Route.HTTP.Spec
for _, parentRef := range httpSpec.ParentRefs {
gatewayKey := types.NamespacedName{
Name: string(parentRef.Name),
}
if parentRef.Namespace != nil {
gatewayKey.Namespace = string(*parentRef.Namespace)
} else {
gatewayKey.Namespace = llmSvc.GetNamespace()
}
gateway := &gwapiv1.Gateway{}
err := r.Get(ctx, gatewayKey, gateway)
if err != nil {
if apierrors.IsNotFound(err) {
targetErrors = append(targetErrors, fmt.Sprintf("Managed HTTPRoute references non-existent Gateway %s/%s", gatewayKey.Namespace, gatewayKey.Name))
} else {
logger.Error(err, "Error fetching parent Gateway for managed route", "gateway", gatewayKey)
return fmt.Errorf("failed to validate parent Gateway %s/%s: %w", gatewayKey.Namespace, gatewayKey.Name, err)
}
}
}
if len(targetErrors) > 0 {
message := strings.Join(targetErrors, "; ")
return NewValidationError(message)
}
return nil
}