Files

334 lines
12 KiB
Go

/*
Copyright 2021 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 ingress
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/equality"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
"github.com/kserve/kserve/pkg/constants"
"github.com/kserve/kserve/pkg/utils"
)
// RawIngressReconciler reconciles the kubernetes ingress
type RawIngressReconciler struct {
client client.Client
scheme *runtime.Scheme
ingressConfig *v1beta1.IngressConfig
isvcConfig *v1beta1.InferenceServicesConfig
}
func NewRawIngressReconciler(client client.Client,
scheme *runtime.Scheme,
ingressConfig *v1beta1.IngressConfig,
isvcConfig *v1beta1.InferenceServicesConfig,
) (*RawIngressReconciler, error) {
return &RawIngressReconciler{
client: client,
scheme: scheme,
ingressConfig: ingressConfig,
isvcConfig: isvcConfig,
}, nil
}
func (r *RawIngressReconciler) Reconcile(ctx context.Context, isvc *v1beta1.InferenceService) (ctrl.Result, error) {
var err error
isInternal := false
// disable ingress creation if service is labelled with cluster local or kserve domain is cluster local
if val, ok := isvc.Labels[constants.NetworkVisibility]; ok && val == constants.ClusterLocalVisibility {
isInternal = true
}
if r.ingressConfig.IngressDomain == constants.ClusterLocalDomain {
isInternal = true
}
existingIngress := &netv1.Ingress{}
getExistingErr := r.client.Get(ctx, types.NamespacedName{
Namespace: isvc.Namespace,
Name: isvc.Name,
}, existingIngress)
ingressIsNotFound := apierr.IsNotFound(getExistingErr)
if getExistingErr != nil && !ingressIsNotFound {
return ctrl.Result{}, fmt.Errorf("failed to get existing ingress: %w", getExistingErr)
}
// ISVC is stopped, delete the ingress if it exists, otherwise, do nothing
forceStopRuntime := utils.GetForceStopRuntime(isvc)
if (getExistingErr != nil && ingressIsNotFound) && forceStopRuntime {
return ctrl.Result{}, nil
}
if forceStopRuntime {
if controller := metav1.GetControllerOf(existingIngress); controller != nil && controller.UID == isvc.UID {
log.Info("The InferenceService is marked as stopped — deleting its associated ingress", "name", isvc.Name)
if err := r.client.Delete(ctx, existingIngress); err != nil {
return ctrl.Result{}, err
}
}
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
Type: v1beta1.IngressReady,
Status: corev1.ConditionFalse,
Reason: v1beta1.StoppedISVCReason,
})
return ctrl.Result{}, nil
}
// Create or update ingress to match the desired state
if !isInternal && !r.ingressConfig.DisableIngressCreation {
ingress, err := createRawIngress(r.scheme, isvc, r.ingressConfig, r.isvcConfig)
if err != nil {
return ctrl.Result{}, err
}
if ingress == nil {
return ctrl.Result{}, nil
}
if getExistingErr != nil && ingressIsNotFound {
log.Info("creating ingress", "ingressName", isvc.Name)
if err := r.client.Create(ctx, ingress); err != nil {
log.Error(err, "Failed to create ingress", "name", ingress.Name)
return ctrl.Result{}, err
}
} else if !semanticIngressEquals(ingress, existingIngress) {
log.Info("updating ingress", "ingressName", isvc.Name)
if err := r.client.Update(ctx, ingress); err != nil {
log.Error(err, "Failed to update ingress", "name", ingress.Name)
return ctrl.Result{}, err
}
}
}
isvc.Status.URL, err = createRawURL(isvc, r.ingressConfig)
if err != nil {
return ctrl.Result{}, err
}
isvc.Status.Address, err = createAddress(ctx, r.client, isvc)
if err != nil {
return ctrl.Result{}, err
}
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
Type: v1beta1.IngressReady,
Status: corev1.ConditionTrue,
})
return ctrl.Result{}, nil
}
func createAddress(ctx context.Context, cl client.Client, isvc *v1beta1.InferenceService) (*duckv1.Addressable, error) {
host := getRawServiceHost(isvc)
// Determine the entry point service name.
// If a transformer exists, it becomes the entry point; otherwise, the predictor is.
entryPointSvcName := constants.PredictorServiceName(isvc.Name)
if isvc.Spec.Transformer != nil {
entryPointSvcName = constants.TransformerServiceName(isvc.Name)
}
// Check if the entry point service is headless
entryPointSvc := &corev1.Service{}
if err := cl.Get(ctx, types.NamespacedName{
Namespace: isvc.Namespace,
Name: entryPointSvcName,
}, entryPointSvc); err != nil {
return nil, fmt.Errorf("failed to get entry point service %s: %w", entryPointSvcName, err)
}
if entryPointSvc.Spec.ClusterIP == corev1.ClusterIPNone {
host = host + ":" + constants.InferenceServiceDefaultHttpPort
}
return &duckv1.Addressable{
URL: &apis.URL{
Host: host,
Scheme: "http",
Path: "",
},
}, nil
}
func generateRule(ingressHost string, componentName string, path string, port int32) netv1.IngressRule { //nolint:unparam
pathType := netv1.PathTypePrefix
rule := netv1.IngressRule{
Host: ingressHost,
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: path,
PathType: &pathType,
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: componentName,
Port: netv1.ServiceBackendPort{
Number: port,
},
},
},
},
},
},
},
}
return rule
}
func generateMetadata(isvc *v1beta1.InferenceService,
componentType constants.InferenceServiceComponent, name string,
isvcConfig *v1beta1.InferenceServicesConfig,
) metav1.ObjectMeta {
// get annotations from isvc
annotations := utils.Filter(isvc.Annotations, func(key string) bool {
return !utils.Includes(isvcConfig.ServiceAnnotationDisallowedList, key)
})
objectMeta := metav1.ObjectMeta{
Name: name,
Namespace: isvc.Namespace,
Labels: utils.Union(isvc.Labels, map[string]string{
constants.InferenceServicePodLabelKey: isvc.Name,
constants.KServiceComponentLabel: string(componentType),
}),
Annotations: annotations,
}
return objectMeta
}
// generateIngressHost return the config domain in configmap.IngressDomain
func generateIngressHost(ingressConfig *v1beta1.IngressConfig,
isvc *v1beta1.InferenceService,
isvcConfig *v1beta1.InferenceServicesConfig,
componentType string,
topLevelFlag bool,
name string,
) (string, error) {
metadata := generateMetadata(isvc, constants.InferenceServiceComponent(componentType), name, isvcConfig)
if !topLevelFlag {
return GenerateDomainName(metadata.Name, isvc.ObjectMeta, ingressConfig)
} else {
return GenerateDomainName(isvc.Name, isvc.ObjectMeta, ingressConfig)
}
}
func createRawIngress(scheme *runtime.Scheme, isvc *v1beta1.InferenceService,
ingressConfig *v1beta1.IngressConfig, isvcConfig *v1beta1.InferenceServicesConfig,
) (*netv1.Ingress, error) {
if !isvc.Status.IsConditionReady(v1beta1.PredictorReady) {
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
Type: v1beta1.IngressReady,
Status: corev1.ConditionFalse,
Reason: "Predictor ingress not created",
})
return nil, nil
}
var rules []netv1.IngressRule
predictorName := constants.PredictorServiceName(isvc.Name)
switch {
case isvc.Spec.Transformer != nil:
if !isvc.Status.IsConditionReady(v1beta1.TransformerReady) {
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
Type: v1beta1.IngressReady,
Status: corev1.ConditionFalse,
Reason: "Transformer ingress not created",
})
return nil, nil
}
transformerName := constants.TransformerServiceName(isvc.Name)
explainerName := constants.ExplainerServiceName(isvc.Name)
host, err := generateIngressHost(ingressConfig, isvc, isvcConfig, string(constants.Transformer), true, transformerName)
if err != nil {
return nil, fmt.Errorf("failed creating top level transformer ingress host: %w", err)
}
transformerHost, err := generateIngressHost(ingressConfig, isvc, isvcConfig, string(constants.Transformer), false, transformerName)
if err != nil {
return nil, fmt.Errorf("failed creating transformer ingress host: %w", err)
}
if isvc.Spec.Explainer != nil {
explainerHost, err := generateIngressHost(ingressConfig, isvc, isvcConfig, string(constants.Explainer), false, transformerName)
if err != nil {
return nil, fmt.Errorf("failed creating explainer ingress host: %w", err)
}
rules = append(rules, generateRule(explainerHost, explainerName, "/", constants.CommonDefaultHttpPort))
}
// :predict routes to the transformer when there are both predictor and transformer
rules = append(rules, generateRule(host, transformerName, "/", constants.CommonDefaultHttpPort))
rules = append(rules, generateRule(transformerHost, predictorName, "/", constants.CommonDefaultHttpPort))
case isvc.Spec.Explainer != nil:
if !isvc.Status.IsConditionReady(v1beta1.ExplainerReady) {
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
Type: v1beta1.IngressReady,
Status: corev1.ConditionFalse,
Reason: "Explainer ingress not created",
})
return nil, nil
}
explainerName := constants.ExplainerServiceName(isvc.Name)
host, err := generateIngressHost(ingressConfig, isvc, isvcConfig, string(constants.Explainer), true, explainerName)
if err != nil {
return nil, fmt.Errorf("failed creating top level explainer ingress host: %w", err)
}
explainerHost, err := generateIngressHost(ingressConfig, isvc, isvcConfig, string(constants.Explainer), false, explainerName)
if err != nil {
return nil, fmt.Errorf("failed creating explainer ingress host: %w", err)
}
// :predict routes to the predictor when there is only predictor and explainer
rules = append(rules, generateRule(host, predictorName, "/", constants.CommonDefaultHttpPort))
rules = append(rules, generateRule(explainerHost, explainerName, "/", constants.CommonDefaultHttpPort))
default:
host, err := generateIngressHost(ingressConfig, isvc, isvcConfig, string(constants.Predictor), true, predictorName)
if err != nil {
return nil, fmt.Errorf("failed creating top level predictor ingress host: %w", err)
}
rules = append(rules, generateRule(host, predictorName, "/", constants.CommonDefaultHttpPort))
}
// add predictor rule
predictorHost, err := generateIngressHost(ingressConfig, isvc, isvcConfig, string(constants.Predictor), false, predictorName)
if err != nil {
return nil, fmt.Errorf("failed creating predictor ingress host: %w", err)
}
rules = append(rules, generateRule(predictorHost, predictorName, "/", constants.CommonDefaultHttpPort))
ingress := &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: isvc.Name,
Namespace: isvc.Namespace,
Annotations: isvc.Annotations,
},
Spec: netv1.IngressSpec{
IngressClassName: ingressConfig.IngressClassName,
Rules: rules,
},
}
if err := controllerutil.SetControllerReference(isvc, ingress, scheme); err != nil {
return nil, err
}
return ingress, nil
}
func semanticIngressEquals(desired, existing *netv1.Ingress) bool {
return equality.Semantic.DeepEqual(desired.Spec, existing.Spec)
}