mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
760 lines
25 KiB
Go
760 lines
25 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"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
istiov1beta1 "istio.io/api/networking/v1beta1"
|
|
istioclientv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
|
|
corev1 "k8s.io/api/core/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"
|
|
"k8s.io/client-go/kubernetes"
|
|
"knative.dev/pkg/apis"
|
|
duckv1 "knative.dev/pkg/apis/duck/v1"
|
|
"knative.dev/pkg/kmp"
|
|
"knative.dev/pkg/network"
|
|
"knative.dev/pkg/system"
|
|
"knative.dev/serving/pkg/reconciler/route/config"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
|
|
"github.com/kserve/kserve/pkg/constants"
|
|
"github.com/kserve/kserve/pkg/utils"
|
|
)
|
|
|
|
var log = logf.Log.WithName("IngressReconciler")
|
|
|
|
type IngressReconciler struct {
|
|
// client is the client that is used to access the custom resources
|
|
client client.Client
|
|
// clientset is the client that allows us to talk to the k8s for core APIs
|
|
clientset kubernetes.Interface
|
|
scheme *runtime.Scheme
|
|
ingressConfig *v1beta1.IngressConfig
|
|
isvcConfig *v1beta1.InferenceServicesConfig
|
|
// isVirtualServiceAvailable indicates whether Istio VirtualService CRD
|
|
// exists in the cluster. If false, VirtualService reconciliation is skipped.
|
|
isVirtualServiceAvailable bool
|
|
}
|
|
|
|
func NewIngressReconciler(client client.Client, clientset kubernetes.Interface, scheme *runtime.Scheme,
|
|
ingressConfig *v1beta1.IngressConfig, isvcConfig *v1beta1.InferenceServicesConfig,
|
|
isVirtualServiceAvailable bool,
|
|
) *IngressReconciler {
|
|
return &IngressReconciler{
|
|
client: client,
|
|
clientset: clientset,
|
|
scheme: scheme,
|
|
ingressConfig: ingressConfig,
|
|
isvcConfig: isvcConfig,
|
|
isVirtualServiceAvailable: isVirtualServiceAvailable,
|
|
}
|
|
}
|
|
|
|
func (ir *IngressReconciler) Reconcile(ctx context.Context, isvc *v1beta1.InferenceService) (ctrl.Result, error) {
|
|
disableIstioVirtualHost := ir.ingressConfig.DisableIstioVirtualHost
|
|
|
|
if err := ir.reconcileVirtualService(ctx, isvc); err != nil {
|
|
return ctrl.Result{}, errors.Wrapf(err, "fails to reconcile virtual service")
|
|
}
|
|
// Create external service which points to local gateway
|
|
if err := ir.reconcileExternalService(ctx, isvc, ir.ingressConfig); err != nil {
|
|
return ctrl.Result{}, errors.Wrapf(err, "fails to reconcile external name service")
|
|
}
|
|
|
|
if utils.GetForceStopRuntime(isvc) {
|
|
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
|
|
Type: v1beta1.IngressReady,
|
|
Status: corev1.ConditionFalse,
|
|
Reason: v1beta1.StoppedISVCReason,
|
|
})
|
|
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
serviceHost := getServiceHost(isvc)
|
|
serviceUrl := getServiceUrl(isvc, ir.ingressConfig)
|
|
if serviceHost == "" || serviceUrl == "" {
|
|
log.Info("service host and serviceurl are empty, skipping updating the inference service")
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
if url, err := apis.ParseURL(serviceUrl); err == nil {
|
|
isvc.Status.URL = url
|
|
hostPrefix := getHostPrefix(isvc, disableIstioVirtualHost)
|
|
isvc.Status.Address = &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Host: network.GetServiceHostname(hostPrefix, isvc.Namespace),
|
|
Scheme: "http",
|
|
},
|
|
}
|
|
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
|
|
Type: v1beta1.IngressReady,
|
|
Status: corev1.ConditionTrue,
|
|
})
|
|
return ctrl.Result{}, nil
|
|
} else {
|
|
return ctrl.Result{}, errors.Wrapf(err, "fails to parse service url")
|
|
}
|
|
}
|
|
|
|
func getServiceHost(isvc *v1beta1.InferenceService) string {
|
|
if isvc.Status.Components == nil {
|
|
return ""
|
|
}
|
|
// Derive the ingress service host from underlying service url
|
|
if isvc.Spec.Transformer != nil {
|
|
if transformerStatus, ok := isvc.Status.Components[v1beta1.TransformerComponent]; !ok {
|
|
return ""
|
|
} else if transformerStatus.URL == nil {
|
|
return ""
|
|
} else {
|
|
if strings.Contains(transformerStatus.URL.Host, "-default") {
|
|
return strings.Replace(transformerStatus.URL.Host, fmt.Sprintf("-%s-default", string(constants.Transformer)), "",
|
|
1)
|
|
} else {
|
|
return strings.Replace(transformerStatus.URL.Host, "-"+string(constants.Transformer), "",
|
|
1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if predictorStatus, ok := isvc.Status.Components[v1beta1.PredictorComponent]; !ok {
|
|
return ""
|
|
} else if predictorStatus.URL == nil {
|
|
return ""
|
|
} else {
|
|
if strings.Contains(predictorStatus.URL.Host, "-default") {
|
|
return strings.Replace(predictorStatus.URL.Host, fmt.Sprintf("-%s-default", string(constants.Predictor)), "",
|
|
1)
|
|
} else {
|
|
return strings.Replace(predictorStatus.URL.Host, "-"+string(constants.Predictor), "",
|
|
1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getServiceUrl(isvc *v1beta1.InferenceService, config *v1beta1.IngressConfig) string {
|
|
url := getHostBasedServiceUrl(isvc, config)
|
|
if url == "" {
|
|
return ""
|
|
}
|
|
if config.PathTemplate == "" {
|
|
return url
|
|
} else {
|
|
return getPathBasedServiceUrl(isvc, config)
|
|
}
|
|
}
|
|
|
|
func getPathBasedServiceUrl(isvc *v1beta1.InferenceService, config *v1beta1.IngressConfig) string {
|
|
path, err := GenerateUrlPath(isvc.Name, isvc.Namespace, config)
|
|
if err != nil {
|
|
log.Error(err, "Failed to generate URL path from pathTemplate")
|
|
return ""
|
|
}
|
|
url := &apis.URL{}
|
|
url.Scheme = config.UrlScheme
|
|
url.Host = config.IngressDomain
|
|
url.Path = path
|
|
|
|
return url.String()
|
|
}
|
|
|
|
func getHostBasedServiceUrl(isvc *v1beta1.InferenceService, config *v1beta1.IngressConfig) string {
|
|
urlScheme := config.UrlScheme
|
|
disableIstioVirtualHost := config.DisableIstioVirtualHost
|
|
if isvc.Status.Components == nil {
|
|
return ""
|
|
}
|
|
// Derive the ingress url from underlying service url
|
|
if isvc.Spec.Transformer != nil {
|
|
if transformerStatus, ok := isvc.Status.Components[v1beta1.TransformerComponent]; !ok {
|
|
return ""
|
|
} else if transformerStatus.URL == nil {
|
|
return ""
|
|
} else {
|
|
url := transformerStatus.URL
|
|
url.Scheme = urlScheme
|
|
urlString := url.String()
|
|
if !disableIstioVirtualHost {
|
|
if strings.Contains(urlString, "-default") {
|
|
return strings.Replace(urlString, fmt.Sprintf("-%s-default", string(constants.Transformer)), "", 1)
|
|
} else {
|
|
return strings.Replace(urlString, "-"+string(constants.Transformer), "", 1)
|
|
}
|
|
}
|
|
return urlString
|
|
}
|
|
}
|
|
|
|
if predictorStatus, ok := isvc.Status.Components[v1beta1.PredictorComponent]; !ok {
|
|
return ""
|
|
} else if predictorStatus.URL == nil {
|
|
return ""
|
|
} else {
|
|
url := predictorStatus.URL
|
|
url.Scheme = urlScheme
|
|
urlString := url.String()
|
|
if !disableIstioVirtualHost {
|
|
if strings.Contains(urlString, "-default") {
|
|
return strings.Replace(urlString, fmt.Sprintf("-%s-default", string(constants.Predictor)), "", 1)
|
|
} else {
|
|
return strings.Replace(urlString, "-"+string(constants.Predictor), "", 1)
|
|
}
|
|
}
|
|
return urlString
|
|
}
|
|
}
|
|
|
|
func (ir *IngressReconciler) reconcileVirtualService(ctx context.Context, isvc *v1beta1.InferenceService) error {
|
|
disableIstioVirtualHost := ir.ingressConfig.DisableIstioVirtualHost
|
|
|
|
// If Istio VirtualService CRD is not present in the cluster, gracefully skip VirtualService reconciliation.
|
|
// This allows KServe to operate with non-Istio network layers (e.g., Kourier) without failing.
|
|
if !ir.isVirtualServiceAvailable {
|
|
return nil
|
|
}
|
|
|
|
domainList := getDomainList(ctx, ir.clientset)
|
|
desiredIngress := createIngress(isvc, ir.ingressConfig, domainList, ir.isvcConfig) // actually the virtual service
|
|
|
|
existing := &istioclientv1beta1.VirtualService{}
|
|
getExistingErr := ir.client.Get(ctx, types.NamespacedName{Name: isvc.Name, Namespace: isvc.Namespace}, existing)
|
|
|
|
if !utils.GetForceStopRuntime(isvc) {
|
|
// When Istio virtual host is disabled, we return the underlying component url.
|
|
// When Istio virtual host is enabled. we return the url using inference service virtual host name and redirect to the corresponding transformer, predictor or explainer url.
|
|
if !disableIstioVirtualHost {
|
|
if desiredIngress == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := controllerutil.SetControllerReference(isvc, desiredIngress, ir.scheme); err != nil {
|
|
return errors.Wrapf(err, "fails to set owner reference for ingress")
|
|
}
|
|
|
|
if getExistingErr != nil {
|
|
if apierr.IsNotFound(getExistingErr) {
|
|
log.Info("Creating Ingress for isvc", "namespace", desiredIngress.Namespace, "name", desiredIngress.Name)
|
|
if err := ir.client.Create(ctx, desiredIngress); err != nil {
|
|
log.Error(err, "Failed to create ingress", "namespace", desiredIngress.Namespace, "name", desiredIngress.Name)
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
if !routeSemanticEquals(desiredIngress, existing) {
|
|
deepCopy := existing.DeepCopy()
|
|
deepCopy.Spec = *desiredIngress.Spec.DeepCopy()
|
|
deepCopy.Annotations = desiredIngress.Annotations
|
|
deepCopy.Labels = desiredIngress.Labels
|
|
log.Info("Update Ingress for isvc", "namespace", desiredIngress.Namespace, "name", desiredIngress.Name)
|
|
if err := ir.client.Update(ctx, deepCopy); err != nil {
|
|
log.Error(err, "Failed to update ingress", "namespace", desiredIngress.Namespace, "name", desiredIngress.Name)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Delete the virtualservice
|
|
if getExistingErr == nil {
|
|
// Make sure that we only delete virtual services owned by the isvc
|
|
if existing.OwnerReferences[0].UID == isvc.UID {
|
|
log.Info("The InferenceService ", isvc.Name, " is marked as stopped — delete its associated VirtualService")
|
|
if err := ir.client.Delete(ctx, existing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if !apierr.IsNotFound(getExistingErr) {
|
|
return getExistingErr
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ir *IngressReconciler) reconcileExternalService(ctx context.Context, isvc *v1beta1.InferenceService, config *v1beta1.IngressConfig) error {
|
|
disableIstioVirtualHost := ir.ingressConfig.DisableIstioVirtualHost
|
|
|
|
desired := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvc.Name,
|
|
Namespace: isvc.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
ExternalName: config.LocalGatewayServiceName,
|
|
Type: corev1.ServiceTypeExternalName,
|
|
SessionAffinity: corev1.ServiceAffinityNone,
|
|
},
|
|
}
|
|
|
|
existing := &corev1.Service{}
|
|
getExistingErr := ir.client.Get(ctx, types.NamespacedName{Name: isvc.Name, Namespace: isvc.Namespace}, existing)
|
|
|
|
if err := controllerutil.SetControllerReference(isvc, desired, ir.scheme); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !utils.GetForceStopRuntime(isvc) {
|
|
// When Istio virtual host is disabled, we return the underlying component url.
|
|
// When Istio virtual host is enabled. we return the url using inference service virtual host name and redirect to the corresponding transformer, predictor or explainer url.
|
|
if !disableIstioVirtualHost {
|
|
// Create service if does not exist
|
|
if getExistingErr != nil {
|
|
if apierr.IsNotFound(getExistingErr) {
|
|
log.Info("Creating external name service", "namespace", desired.Namespace, "name", desired.Name)
|
|
return ir.client.Create(ctx, desired)
|
|
}
|
|
return getExistingErr
|
|
}
|
|
|
|
// Return if no differences to reconcile.
|
|
// DeepDerivative treats zero/nil fields in desired as "don't care",
|
|
// so server-populated metadata fields on existing don't cause false diffs.
|
|
if equality.Semantic.DeepDerivative(desired, existing) {
|
|
return nil
|
|
}
|
|
|
|
// Reconcile differences and update
|
|
diff, err := kmp.SafeDiff(desired.Spec, existing.Spec)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to diff external name service")
|
|
}
|
|
log.Info("Reconciling external service diff (-desired, +observed):", "diff", diff)
|
|
log.Info("Updating external service", "namespace", existing.Namespace, "name", existing.Name)
|
|
existing.Spec = desired.Spec
|
|
if desired.Labels != nil {
|
|
if existing.Labels == nil {
|
|
existing.Labels = make(map[string]string)
|
|
}
|
|
for k, v := range desired.Labels {
|
|
existing.Labels[k] = v
|
|
}
|
|
}
|
|
if desired.Annotations != nil {
|
|
if existing.Annotations == nil {
|
|
existing.Annotations = make(map[string]string)
|
|
}
|
|
for k, v := range desired.Annotations {
|
|
existing.Annotations[k] = v
|
|
}
|
|
}
|
|
err = ir.client.Update(ctx, existing)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "fails to update external name service")
|
|
}
|
|
}
|
|
} else {
|
|
// Delete the service
|
|
if getExistingErr == nil {
|
|
// Make sure that we only delete services owned by the isvc
|
|
if ctrl := metav1.GetControllerOf(existing); ctrl != nil && ctrl.UID == isvc.UID {
|
|
log.Info("The InferenceService ", isvc.Name, " is marked as stopped — delete its associated Service")
|
|
if err := ir.client.Delete(ctx, existing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if !apierr.IsNotFound(getExistingErr) {
|
|
return getExistingErr
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createHTTPRouteDestination(gatewayService string) *istiov1beta1.HTTPRouteDestination {
|
|
httpRouteDestination := &istiov1beta1.HTTPRouteDestination{
|
|
Destination: &istiov1beta1.Destination{
|
|
Host: gatewayService,
|
|
Port: &istiov1beta1.PortSelector{
|
|
Number: constants.CommonDefaultHttpPort,
|
|
},
|
|
},
|
|
Weight: 100,
|
|
}
|
|
return httpRouteDestination
|
|
}
|
|
|
|
func createHTTPMatchRequest(prefix, targetHost, internalHost string, additionalHosts *[]string, isInternal bool, config *v1beta1.IngressConfig) []*istiov1beta1.HTTPMatchRequest {
|
|
var uri *istiov1beta1.StringMatch
|
|
if prefix != "" {
|
|
uri = &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: prefix,
|
|
},
|
|
}
|
|
}
|
|
matchRequests := []*istiov1beta1.HTTPMatchRequest{
|
|
{
|
|
Uri: uri,
|
|
Authority: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: constants.HostRegExp(internalHost),
|
|
},
|
|
},
|
|
Gateways: []string{config.LocalGateway, constants.IstioMeshGateway},
|
|
},
|
|
}
|
|
if !isInternal {
|
|
// We only create the HTTPMatchRequest for the targetHost and the additional hosts, when the ingress is not internal.
|
|
matchRequests = append(matchRequests,
|
|
&istiov1beta1.HTTPMatchRequest{
|
|
Uri: uri,
|
|
Authority: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: constants.HostRegExp(targetHost),
|
|
},
|
|
},
|
|
Gateways: []string{config.IngressGateway},
|
|
})
|
|
|
|
if additionalHosts != nil && len(*additionalHosts) != 0 {
|
|
for _, host := range *additionalHosts {
|
|
matchRequest := &istiov1beta1.HTTPMatchRequest{
|
|
Uri: uri,
|
|
Authority: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: constants.HostRegExp(host),
|
|
},
|
|
},
|
|
Gateways: []string{config.IngressGateway},
|
|
}
|
|
if !containsHTTPMatchRequest(matchRequest, matchRequests) {
|
|
matchRequests = append(matchRequests, matchRequest)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return matchRequests
|
|
}
|
|
|
|
func containsHTTPMatchRequest(matchRequest *istiov1beta1.HTTPMatchRequest, matchRequests []*istiov1beta1.HTTPMatchRequest) bool {
|
|
for _, matchRequestEle := range matchRequests {
|
|
// If authority, gateways and uri are all equal, two HTTPMatchRequests will be equal.
|
|
if stringMatchEqual(matchRequest.GetAuthority(), matchRequestEle.GetAuthority()) && gatewaysEqual(matchRequest, matchRequestEle) &&
|
|
stringMatchEqual(matchRequest.GetUri(), matchRequestEle.GetUri()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func stringMatchEqual(stringMatch, stringMatchDest *istiov1beta1.StringMatch) bool {
|
|
if stringMatch != nil && stringMatchDest != nil {
|
|
return equality.Semantic.DeepEqual(stringMatch.GetMatchType(), stringMatchDest.GetMatchType())
|
|
}
|
|
if stringMatch == nil && stringMatchDest == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func gatewaysEqual(matchRequest, matchRequestDest *istiov1beta1.HTTPMatchRequest) bool {
|
|
return equality.Semantic.DeepEqual(matchRequest.GetGateways(), matchRequestDest.GetGateways())
|
|
}
|
|
|
|
func createIngress(isvc *v1beta1.InferenceService, config *v1beta1.IngressConfig,
|
|
domainList *[]string, isvcConfig *v1beta1.InferenceServicesConfig,
|
|
) *istioclientv1beta1.VirtualService {
|
|
if !isvc.Status.IsConditionReady(v1beta1.PredictorReady) {
|
|
status := corev1.ConditionFalse
|
|
if isvc.Status.IsConditionUnknown(v1beta1.PredictorReady) {
|
|
status = corev1.ConditionUnknown
|
|
}
|
|
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
|
|
Type: v1beta1.IngressReady,
|
|
Status: status,
|
|
Reason: "Predictor ingress not created",
|
|
})
|
|
return nil
|
|
}
|
|
backend := constants.PredictorServiceName(isvc.Name)
|
|
|
|
if isvc.Spec.Transformer != nil {
|
|
backend = constants.TransformerServiceName(isvc.Name)
|
|
if !isvc.Status.IsConditionReady(v1beta1.TransformerReady) {
|
|
status := corev1.ConditionFalse
|
|
if isvc.Status.IsConditionUnknown(v1beta1.TransformerReady) {
|
|
status = corev1.ConditionUnknown
|
|
}
|
|
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
|
|
Type: v1beta1.IngressReady,
|
|
Status: status,
|
|
Reason: "Transformer ingress not created",
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
isInternal := false
|
|
serviceHost := getServiceHost(isvc)
|
|
// if service is labelled with cluster local or knative domain is configured as internal
|
|
if val, ok := isvc.Labels[constants.VisibilityLabel]; ok && val == constants.ClusterLocalVisibility {
|
|
isInternal = true
|
|
}
|
|
serviceInternalHostName := network.GetServiceHostname(isvc.Name, isvc.Namespace)
|
|
if serviceHost == serviceInternalHostName {
|
|
isInternal = true
|
|
}
|
|
httpRoutes := []*istiov1beta1.HTTPRoute{}
|
|
// Build explain route
|
|
expBackend := constants.ExplainerServiceName(isvc.Name)
|
|
|
|
var additionalHosts *[]string
|
|
hosts := []string{
|
|
network.GetServiceHostname(isvc.Name, isvc.Namespace),
|
|
}
|
|
if !isInternal {
|
|
additionalHosts = GetAdditionalHosts(domainList, serviceHost, config)
|
|
}
|
|
|
|
if isvc.Spec.Explainer != nil {
|
|
if !isvc.Status.IsConditionReady(v1beta1.ExplainerReady) {
|
|
status := corev1.ConditionFalse
|
|
if isvc.Status.IsConditionUnknown(v1beta1.ExplainerReady) {
|
|
status = corev1.ConditionUnknown
|
|
}
|
|
isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{
|
|
Type: v1beta1.IngressReady,
|
|
Status: status,
|
|
Reason: "Explainer ingress not created",
|
|
})
|
|
return nil
|
|
}
|
|
explainerRouter := istiov1beta1.HTTPRoute{
|
|
Match: createHTTPMatchRequest(constants.ExplainPrefix(), serviceHost,
|
|
network.GetServiceHostname(isvc.Name, isvc.Namespace), additionalHosts, isInternal, config),
|
|
Route: []*istiov1beta1.HTTPRouteDestination{
|
|
createHTTPRouteDestination(config.KnativeLocalGatewayService),
|
|
},
|
|
Headers: &istiov1beta1.Headers{
|
|
Request: &istiov1beta1.Headers_HeaderOperations{
|
|
Set: map[string]string{
|
|
"Host": network.GetServiceHostname(expBackend, isvc.Namespace),
|
|
constants.IsvcNameHeader: isvc.Name,
|
|
constants.IsvcNamespaceHeader: isvc.Namespace,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
httpRoutes = append(httpRoutes, &explainerRouter)
|
|
}
|
|
// Add predict route
|
|
httpRoutes = append(httpRoutes, &istiov1beta1.HTTPRoute{
|
|
Match: createHTTPMatchRequest("", serviceHost,
|
|
network.GetServiceHostname(isvc.Name, isvc.Namespace), additionalHosts, isInternal, config),
|
|
Route: []*istiov1beta1.HTTPRouteDestination{
|
|
createHTTPRouteDestination(config.KnativeLocalGatewayService),
|
|
},
|
|
Headers: &istiov1beta1.Headers{
|
|
Request: &istiov1beta1.Headers_HeaderOperations{
|
|
Set: map[string]string{
|
|
"Host": network.GetServiceHostname(backend, isvc.Namespace),
|
|
constants.IsvcNameHeader: isvc.Name,
|
|
constants.IsvcNamespaceHeader: isvc.Namespace,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
gateways := []string{
|
|
config.LocalGateway,
|
|
constants.IstioMeshGateway,
|
|
}
|
|
if !isInternal {
|
|
hosts = append(hosts, serviceHost)
|
|
gateways = append(gateways, config.IngressGateway)
|
|
}
|
|
|
|
if config.PathTemplate != "" {
|
|
path, err := GenerateUrlPath(isvc.Name, isvc.Namespace, config)
|
|
if err != nil {
|
|
log.Error(err, "Failed to generate URL from pathTemplate")
|
|
return nil
|
|
}
|
|
url := &apis.URL{}
|
|
url.Path = strings.TrimSuffix(path, "/") // remove trailing "/" if present
|
|
url.Host = config.IngressDomain
|
|
// In this case, we have a path-based URL so we add a path-based rule
|
|
if isvc.Spec.Explainer != nil {
|
|
httpRoutes = append(httpRoutes, &istiov1beta1.HTTPRoute{
|
|
Match: []*istiov1beta1.HTTPMatchRequest{
|
|
{
|
|
Uri: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: url.Path + constants.PathBasedExplainPrefix(),
|
|
},
|
|
},
|
|
Authority: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: constants.HostRegExp(url.Host),
|
|
},
|
|
},
|
|
Gateways: []string{config.IngressGateway},
|
|
},
|
|
},
|
|
Rewrite: &istiov1beta1.HTTPRewrite{
|
|
UriRegexRewrite: &istiov1beta1.RegexRewrite{
|
|
Match: url.Path + constants.PathBasedExplainPrefix(),
|
|
Rewrite: `\1`,
|
|
},
|
|
},
|
|
Route: []*istiov1beta1.HTTPRouteDestination{
|
|
createHTTPRouteDestination(config.KnativeLocalGatewayService),
|
|
},
|
|
Headers: &istiov1beta1.Headers{
|
|
Request: &istiov1beta1.Headers_HeaderOperations{
|
|
Set: map[string]string{
|
|
"Host": network.GetServiceHostname(expBackend, isvc.Namespace),
|
|
constants.IsvcNameHeader: isvc.Name,
|
|
constants.IsvcNamespaceHeader: isvc.Namespace,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
httpRoutes = append(httpRoutes, &istiov1beta1.HTTPRoute{
|
|
Match: []*istiov1beta1.HTTPMatchRequest{
|
|
{
|
|
Uri: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Prefix{
|
|
Prefix: url.Path + "/",
|
|
},
|
|
},
|
|
Authority: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: constants.HostRegExp(url.Host),
|
|
},
|
|
},
|
|
Gateways: []string{config.IngressGateway},
|
|
},
|
|
{
|
|
Uri: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Exact{
|
|
Exact: url.Path,
|
|
},
|
|
},
|
|
Authority: &istiov1beta1.StringMatch{
|
|
MatchType: &istiov1beta1.StringMatch_Regex{
|
|
Regex: constants.HostRegExp(url.Host),
|
|
},
|
|
},
|
|
Gateways: []string{config.IngressGateway},
|
|
},
|
|
},
|
|
Rewrite: &istiov1beta1.HTTPRewrite{
|
|
Uri: "/",
|
|
},
|
|
Route: []*istiov1beta1.HTTPRouteDestination{
|
|
createHTTPRouteDestination(config.KnativeLocalGatewayService),
|
|
},
|
|
Headers: &istiov1beta1.Headers{
|
|
Request: &istiov1beta1.Headers_HeaderOperations{
|
|
Set: map[string]string{
|
|
"Host": network.GetServiceHostname(backend, isvc.Namespace),
|
|
constants.IsvcNameHeader: isvc.Name,
|
|
constants.IsvcNamespaceHeader: isvc.Namespace,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
// Include ingressDomain to the domains (both internal and external) derived by Knative
|
|
hosts = append(hosts, url.Host)
|
|
}
|
|
|
|
if !isInternal {
|
|
// We only append the additional hosts, when the ingress is not internal.
|
|
hostMap := make(map[string]bool, len(hosts))
|
|
for _, host := range hosts {
|
|
hostMap[host] = true
|
|
}
|
|
if additionalHosts != nil && len(*additionalHosts) != 0 {
|
|
for _, additionalHost := range *additionalHosts {
|
|
if !hostMap[additionalHost] {
|
|
hosts = append(hosts, additionalHost)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
annotations := utils.Filter(isvc.Annotations, func(key string) bool {
|
|
return !utils.Includes(isvcConfig.ServiceAnnotationDisallowedList, key)
|
|
})
|
|
desiredIngress := &istioclientv1beta1.VirtualService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvc.Name,
|
|
Namespace: isvc.Namespace,
|
|
Annotations: annotations,
|
|
Labels: isvc.Labels,
|
|
},
|
|
Spec: istiov1beta1.VirtualService{
|
|
Hosts: hosts,
|
|
Gateways: gateways,
|
|
Http: httpRoutes,
|
|
},
|
|
}
|
|
return desiredIngress
|
|
}
|
|
|
|
// getDomainList gets all the available domain names available with Knative Serving.
|
|
func getDomainList(ctx context.Context, clientset kubernetes.Interface) *[]string {
|
|
res := new([]string)
|
|
ns := constants.DefaultNSKnativeServing
|
|
if namespace := os.Getenv(system.NamespaceEnvKey); namespace != "" {
|
|
ns = namespace
|
|
}
|
|
|
|
// Leverage the clientset to access the configMap to get all the available domain names
|
|
configMap, err := clientset.CoreV1().ConfigMaps(ns).Get(ctx,
|
|
config.DomainConfigName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return res
|
|
}
|
|
for domain := range configMap.Data {
|
|
*res = append(*res, domain)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func routeSemanticEquals(desired, existing *istioclientv1beta1.VirtualService) bool {
|
|
return cmp.Equal(desired.Spec.DeepCopy(), existing.Spec.DeepCopy(), protocmp.Transform()) &&
|
|
equality.Semantic.DeepEqual(desired.Labels, existing.Labels) &&
|
|
equality.Semantic.DeepEqual(desired.Annotations, existing.Annotations)
|
|
}
|
|
|
|
func getHostPrefix(isvc *v1beta1.InferenceService, disableIstioVirtualHost bool) string {
|
|
if disableIstioVirtualHost {
|
|
if isvc.Spec.Transformer != nil {
|
|
return constants.TransformerServiceName(isvc.Name)
|
|
}
|
|
return constants.PredictorServiceName(isvc.Name)
|
|
}
|
|
return isvc.Name
|
|
}
|