Files
2026-03-10 19:32:56 -04:00

4908 lines
183 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 inferenceservice
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
istiov1beta1 "istio.io/api/networking/v1beta1"
istioclientv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
corev1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"k8s.io/utils/ptr"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
"knative.dev/pkg/network"
"knative.dev/serving/pkg/apis/autoscaling"
knservingv1 "knative.dev/serving/pkg/apis/serving/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/kserve/kserve/pkg/apis/serving/v1alpha1"
"github.com/kserve/kserve/pkg/apis/serving/v1beta1"
"github.com/kserve/kserve/pkg/constants"
"github.com/kserve/kserve/pkg/utils"
)
var _ = Describe("v1beta1 inference service controller", func() {
// Define utility constants for object names and testing timeouts/durations and intervals.
configs := getKnativeTestConfigs()
Context("with knative configured to not allow zero initial scale", func() {
When("a Knative InferenceService is created with an initial scale annotation and value of zero", func() {
It("should ignore the annotation", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create InferenceService
serviceName := "initialscale1"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
var minScale int32 = 2
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
autoscaling.InitialScaleAnnotationKey: "0",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: &minScale,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
Expect(actualService.Spec.Template.Annotations).NotTo(HaveKey(autoscaling.InitialScaleAnnotationKey))
})
})
When("a Knative InferenceService is created with an initial scale annotation and valid non-zero integer value", func() {
It("should override the default initial scale value with the annotation value", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create InferenceService
serviceName := "initialscale2"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
var minScale int32 = 2
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
autoscaling.InitialScaleAnnotationKey: "3",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: &minScale,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
Expect(actualService.Spec.Template.Annotations[autoscaling.InitialScaleAnnotationKey]).To(Equal("3"))
})
})
When("a Knative InferenceService is created with an initial scale annotation and invalid non-integer value", func() {
It("should ignore the annotation", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create InferenceService
serviceName := "initialscale3"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
var minScale int32 = 2
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
autoscaling.InitialScaleAnnotationKey: "non-integer",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: &minScale,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
Expect(actualService.Spec.Template.Annotations).NotTo(HaveKey(autoscaling.InitialScaleAnnotationKey))
})
})
})
Context("with knative configured to allow zero initial scale", func() {
BeforeEach(func() {
// Patch the existing config-autoscaler configmap to set allow-zero-initial-scale to true
configAutoscaler := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: constants.AutoscalerConfigmapName,
Namespace: constants.AutoscalerConfigmapNamespace,
},
}
configPatch := []byte(`{"data":{"allow-zero-initial-scale":"true"}}`)
Eventually(func() error {
return k8sClient.Patch(context.TODO(), configAutoscaler, client.RawPatch(types.StrategicMergePatchType, configPatch))
}, timeout, interval).Should(Succeed())
})
AfterEach(func() {
// Restore the default autoscaler configuration
configAutoscaler := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: constants.AutoscalerConfigmapName,
Namespace: constants.AutoscalerConfigmapNamespace,
},
}
configPatch := []byte(`{"data":{}}`)
Eventually(func() error {
return k8sClient.Patch(context.TODO(), configAutoscaler, client.RawPatch(types.StrategicMergePatchType, configPatch))
}, timeout, interval).Should(Succeed())
})
When("a Knative InferenceService is created with an initial scale annotation and value of zero", func() {
It("should override the default initial scale value with the annotation value", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create InferenceService
serviceName := "initialscale4"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
var minScale int32 = 2
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
autoscaling.InitialScaleAnnotationKey: "0",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: &minScale,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
Expect(actualService.Spec.Template.Annotations[autoscaling.InitialScaleAnnotationKey]).To(Equal("0"))
})
})
When("a Knative InferenceService is created with an initial scale annotation and valid non-zero integer value", func() {
It("should override the default initial scale value with the annotation value", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create InferenceService
serviceName := "initialscale5"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
var minScale int32 = 2
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
autoscaling.InitialScaleAnnotationKey: "3",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: &minScale,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
Expect(actualService.Spec.Template.Annotations[autoscaling.InitialScaleAnnotationKey]).To(Equal("3"))
})
})
When("a Knative InferenceService is created with an initial scale annotation and invalid non-integer value", func() {
It("should ignore the annotation", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create InferenceService
serviceName := "initialscale6"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
var minScale int32 = 2
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
autoscaling.InitialScaleAnnotationKey: "non-integer",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: &minScale,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
Expect(actualService.Spec.Template.Annotations).NotTo(HaveKey(autoscaling.InitialScaleAnnotationKey))
})
})
})
Context("When creating inference service with predictor", func() {
It("Should have knative service created", func() {
By("By creating a new InferenceService")
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create ServingRuntime
servingRuntime := &v1alpha1.ServingRuntime{
ObjectMeta: metav1.ObjectMeta{
Name: "tf-serving",
Namespace: "default",
},
Spec: v1alpha1.ServingRuntimeSpec{
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
{
Name: "tensorflow",
Version: ptr.To("1"),
AutoSelect: ptr.To(true),
},
},
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
Labels: map[string]string{
"key1": "val1FromSR",
"key2": "val2FromSR",
"key3": "val3FromSR",
},
Annotations: map[string]string{
"key1": "val1FromSR",
"key2": "val2FromSR",
"key3": "val3FromSR",
},
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Resources: defaultResource,
},
},
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: "sr-image-pull-secret"},
},
},
Disabled: ptr.To(false),
},
}
_ = k8sClient.Create(context.TODO(), servingRuntime)
defer k8sClient.Delete(context.TODO(), servingRuntime)
// Create InferenceService
serviceName := "foo"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Labels: map[string]string{
"key2": "val2FromISVC",
"key3": "val3FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"key2": "val2FromISVC",
"key3": "val3FromISVC",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
Labels: map[string]string{
"key3": "val3FromPredictor",
},
Annotations: map[string]string{
"key3": "val3FromPredictor",
},
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
actualService := &knservingv1.Service{}
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
expectedService := &knservingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: predictorServiceKey.Name,
Namespace: predictorServiceKey.Namespace,
},
Spec: knservingv1.ServiceSpec{
ConfigurationSpec: knservingv1.ConfigurationSpec{
Template: knservingv1.RevisionTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.KServiceComponentLabel: constants.Predictor.String(),
constants.InferenceServicePodLabelKey: serviceName,
"key1": "val1FromSR",
"key2": "val2FromISVC",
"key3": "val3FromPredictor",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
"autoscaling.knative.dev/max-scale": "3",
"autoscaling.knative.dev/min-scale": "1",
"autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev",
"key1": "val1FromSR",
"key2": "val2FromISVC",
"key3": "val3FromPredictor",
},
},
Spec: knservingv1.RevisionSpec{
ContainerConcurrency: isvc.Spec.Predictor.ContainerConcurrency,
TimeoutSeconds: isvc.Spec.Predictor.TimeoutSeconds,
PodSpec: corev1.PodSpec{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: "sr-image-pull-secret"},
},
Containers: []corev1.Container{
{
Image: "tensorflow/serving:" +
*isvc.Spec.Predictor.Model.RuntimeVersion,
Name: constants.InferenceServiceContainerName,
Command: []string{v1beta1.TensorflowEntrypointCommand},
Args: []string{
"--port=" + v1beta1.TensorflowServingGRPCPort,
"--rest_api_port=" + v1beta1.TensorflowServingRestPort,
"--model_base_path=" + constants.DefaultModelLocalMountPath,
"--rest_api_timeout_in_ms=60000",
},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
},
},
AutomountServiceAccountToken: ptr.To(false),
},
},
},
},
RouteSpec: knservingv1.RouteSpec{
Traffic: []knservingv1.TrafficTarget{{LatestRevision: ptr.To(true), Percent: ptr.To(int64(100))}},
},
},
}
// Set ResourceVersion which is required for update operation.
expectedService.ResourceVersion = actualService.ResourceVersion
// Do a dry-run update. This will populate our local knative service object with any default values
// that are present on the remote version.
err := k8sClient.Update(context.TODO(), expectedService, client.DryRunAll)
Expect(err).ShouldNot(HaveOccurred())
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor
{
updatedService := actualService.DeepCopy()
updatedService.Status.LatestCreatedRevisionName = "revision-v1"
updatedService.Status.LatestReadyRevisionName = "revision-v1"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(context.TODO(), updatedService)).NotTo(HaveOccurred())
}
// assert ingress
virtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, virtualService)
}, timeout, interval).Should(Succeed())
expectedVirtualService := &istioclientv1beta1.VirtualService{
Spec: istiov1beta1.VirtualService{
Gateways: []string{
constants.KnativeLocalGateway,
constants.IstioMeshGateway,
constants.KnativeIngressGateway,
},
Hosts: []string{
network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace),
constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain),
},
Http: []*istiov1beta1.HTTPRoute{
{
Match: []*istiov1beta1.HTTPMatchRequest{
{
Gateways: []string{constants.KnativeLocalGateway, constants.IstioMeshGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace)),
},
},
},
{
Gateways: []string{constants.KnativeIngressGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain)),
},
},
},
},
Route: []*istiov1beta1.HTTPRouteDestination{
{
Destination: &istiov1beta1.Destination{
Host: network.GetServiceHostname("knative-local-gateway", "istio-system"),
Port: &istiov1beta1.PortSelector{Number: constants.CommonDefaultHttpPort},
},
Weight: 100,
},
},
Headers: &istiov1beta1.Headers{
Request: &istiov1beta1.Headers_HeaderOperations{
Set: map[string]string{
"Host": network.GetServiceHostname(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace),
"KServe-Isvc-Name": serviceName,
"KServe-Isvc-Namespace": serviceKey.Namespace,
},
},
},
},
},
},
}
Expect(virtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
// get inference service
actualIsvc := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, expectedRequest.NamespacedName, actualIsvc)
return err == nil
}, timeout, interval).Should(BeTrue())
// update inference service with annotations and labels
annotations := map[string]string{"testAnnotation": "test"}
labels := map[string]string{"testLabel": "test"}
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations = annotations
updatedIsvc.Labels = labels
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
updatedVirtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, updatedVirtualService)
if err != nil {
return false
}
return reflect.DeepEqual(updatedVirtualService.Annotations, annotations) &&
reflect.DeepEqual(updatedVirtualService.Labels, labels)
}, timeout, interval).Should(BeTrue())
Expect(updatedVirtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
Expect(updatedVirtualService.Annotations).To(Equal(annotations))
Expect(updatedVirtualService.Labels).To(Equal(labels))
})
It("Should fail if Knative Serving is not installed", func() {
// Simulate Knative Serving is absent by setting to false the relevant item in utils.gvResourcesCache variable
servingResources, getServingResourcesErr := utils.GetAvailableResourcesForApi(cfg, knservingv1.SchemeGroupVersion.String())
Expect(getServingResourcesErr).ToNot(HaveOccurred())
defer utils.SetAvailableResourcesForApi(knservingv1.SchemeGroupVersion.String(), servingResources)
utils.SetAvailableResourcesForApi(knservingv1.SchemeGroupVersion.String(), nil)
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create InferenceService
serviceName := "serverless-isvc"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
ctx := context.Background()
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
Eventually(func() bool {
events := &corev1.EventList{}
err := k8sClient.List(ctx, events, client.InNamespace(serviceKey.Namespace))
if err != nil {
return false
}
for _, event := range events.Items {
if event.InvolvedObject.Kind == "InferenceService" &&
event.InvolvedObject.Name == serviceKey.Name &&
event.Reason == "ServerlessModeRejected" {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())
})
})
Context("When creating inference service with `serving.kserve.io/stop`", func() {
// --- Default values ---
defaultIsvc := func(namespace string, name string, storageUri string) *v1beta1.InferenceService {
predictor := v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
}
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: predictor,
},
}
return isvc
}
// --- Reusable Check Functions ---
// Wait for the Predictor's Knative Service to exist and for its status URL and conditions to be ready.
expectPredictorKsvcToBeReady := func(ctx context.Context, serviceKey types.NamespacedName, predictorServiceKey types.NamespacedName) {
// Predictor knative service
predictorService := &knservingv1.Service{}
Eventually(func() bool {
err := k8sClient.Get(ctx, predictorServiceKey, predictorService)
return err == nil
}, timeout, interval).Should(BeTrue())
// Add a url to the predictor knative service so the services can be made
updatedPredictorService := predictorService.DeepCopy()
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
updatedPredictorService.Status.URL = predictorUrl
updatedPredictorService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
updatedPredictorService.Status.LatestCreatedRevisionName = "revision-v1"
updatedPredictorService.Status.LatestReadyRevisionName = "revision-v1"
Expect(k8sClient.Status().Update(ctx, updatedPredictorService)).NotTo(HaveOccurred())
}
// Wait for the Transformer's Knative Service to exist and for its status URL and conditions to be ready.
expectTransformerKsvcToBeReady := func(ctx context.Context, serviceKey types.NamespacedName, transformerServiceKey types.NamespacedName) {
// Transformer knative service
transformerService := &knservingv1.Service{}
Eventually(func() bool {
err := k8sClient.Get(ctx, transformerServiceKey, transformerService)
return err == nil
}, timeout, interval).Should(BeTrue())
// Add a url to the transformer knative service so the services can be made
updatedTransformerService := transformerService.DeepCopy()
transformerUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.TransformerServiceName(serviceKey.Name), serviceKey.Namespace, domain))
updatedTransformerService.Status.URL = transformerUrl
updatedTransformerService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
updatedTransformerService.Status.LatestCreatedRevisionName = "t-revision-v1"
updatedTransformerService.Status.LatestReadyRevisionName = "t-revision-v1"
Expect(k8sClient.Status().Update(ctx, updatedTransformerService)).NotTo(HaveOccurred())
}
// Wait for the Explainer's Knative Service to exist and for its status URL and conditions to be ready.
expectExplainerKsvcToBeReady := func(ctx context.Context, serviceKey types.NamespacedName, explainerServiceKey types.NamespacedName) {
// Explainer knative service
explainerService := &knservingv1.Service{}
Eventually(func() bool {
err := k8sClient.Get(ctx, explainerServiceKey, explainerService)
return err == nil
}, timeout, interval).Should(BeTrue())
// Add a url to the explainer knative service so the services can be made
updatedExplainerService := explainerService.DeepCopy()
explainerUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.ExplainerServiceName(serviceKey.Name), serviceKey.Namespace, domain))
updatedExplainerService.Status.URL = explainerUrl
updatedExplainerService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
updatedExplainerService.Status.LatestCreatedRevisionName = "e-revision-v1"
updatedExplainerService.Status.LatestReadyRevisionName = "e-revision-v1"
Expect(k8sClient.Status().Update(ctx, updatedExplainerService)).NotTo(HaveOccurred())
}
// Wait for the ISVC to exist.
expectIsvcToExist := func(ctx context.Context, serviceKey types.NamespacedName) v1beta1.InferenceService {
actualIsvc := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, actualIsvc)
return err == nil
}, timeout, interval).Should(BeTrue())
return *actualIsvc
}
// Wait for a specific condition on an InferenceService to reach the desired status
expectIsvcConditionStatus := func(ctx context.Context, serviceKey types.NamespacedName, conditionType apis.ConditionType, expectedStatus corev1.ConditionStatus) {
message := fmt.Sprintf("The '%s' condition for InferenceService '%s' should be '%s'",
conditionType, serviceKey.Name, expectedStatus)
actualIsvc := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, actualIsvc)
if err == nil {
cond := actualIsvc.Status.GetCondition(conditionType)
if cond != nil && cond.Status == expectedStatus {
return true
}
}
return false
}, timeout, interval).Should(BeTrue(), message)
}
// Wait for the InferenceService's PredictorReady and IngressReady condition.
expectIsvcReadyStatus := func(ctx context.Context, serviceKey types.NamespacedName) {
updatedIsvc := &v1beta1.InferenceService{}
// Check that the inference service is ready
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
if err != nil {
return false
}
readyCond := updatedIsvc.Status.GetCondition(v1beta1.PredictorReady)
return readyCond != nil && readyCond.Status == corev1.ConditionTrue
}, timeout, interval).Should(BeTrue(), "The predictor should be ready")
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
if err != nil {
return false
}
readyCond := updatedIsvc.Status.GetCondition(v1beta1.IngressReady)
return readyCond != nil && readyCond.Status == corev1.ConditionTrue
}, timeout, interval).Should(BeTrue(), "The ingress should be ready")
}
// Wait for the InferenceService's TransformerReady condition.
expectIsvcTransformerReadyStatus := func(ctx context.Context, serviceKey types.NamespacedName) {
updatedIsvc := &v1beta1.InferenceService{}
// Check that the transformer is ready
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
if err != nil {
return false
}
readyCond := updatedIsvc.Status.GetCondition(v1beta1.TransformerReady)
return readyCond != nil && readyCond.Status == corev1.ConditionTrue
}, timeout, interval).Should(BeTrue(), "The transformer should be ready")
}
// Wait for the InferenceService's ExplainerReady condition.
expectIsvcExplainerReadyStatus := func(ctx context.Context, serviceKey types.NamespacedName) {
updatedIsvc := &v1beta1.InferenceService{}
// Check that the explainer is ready
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
if err != nil {
return false
}
readyCond := updatedIsvc.Status.GetCondition(v1beta1.ExplainerReady)
return readyCond != nil && readyCond.Status == corev1.ConditionTrue
}, timeout, interval).Should(BeTrue(), "The explainer should be ready")
}
// Waits for any Kubernetes object to be found
expectResourceToExist := func(ctx context.Context, obj client.Object, objKey types.NamespacedName) {
Eventually(func() bool {
err := k8sClient.Get(ctx, objKey, obj)
return err == nil
}, timeout, interval).Should(BeTrue(), "%T %s should exist", obj, objKey.Name)
}
// Checks that any Kubernetes object to be not found.
expectResourceIsDeleted := func(ctx context.Context, obj client.Object, objKey types.NamespacedName) {
Consistently(func() bool {
err := k8sClient.Get(ctx, objKey, obj)
return apierr.IsNotFound(err)
}, fastTimeout, interval).Should(BeTrue(), "%T %s should not be created", obj, objKey.Name)
}
// Wait for any Kubernetes object to be not found.
expectResourceToBeDeleted := func(ctx context.Context, obj client.Object, objKey types.NamespacedName) {
Eventually(func() bool {
err := k8sClient.Get(ctx, objKey, obj)
return apierr.IsNotFound(err)
}, timeout, interval).Should(BeTrue(), "%T %s should be deleted", obj, objKey.Name)
}
Describe("inference service only", func() {
It("Should keep the knative service/virtualService/service when the annotation is set to false", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-false-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Knative service
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
})
It("Should delete the knative service/virtualService/service when the annotation is set to true", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-true-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Check that the KSVC does not exist
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
// Check that the services were not created
expectResourceIsDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceIsDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
})
It("Should delete the knative service/virtualService/service when the annotation is updated to true on an existing ISVC", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-edit-true-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Knative service
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
// Stop the inference service
actualIsvc := expectIsvcToExist(ctx, serviceKey)
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
// Check that the KSVC was deleted
expectResourceToBeDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
// Check that the services are deleted
expectResourceToBeDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToBeDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
})
It("Should create the knative service/virtualService/service when the annotation is updated to false on an existing ISVC that is stopped", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-edit-false-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Check that the KSVC does not exist
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
// Check that the services were not created
expectResourceIsDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceIsDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
actualIsvc := expectIsvcToExist(ctx, serviceKey)
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
// Resume the inference service
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
// Knative service
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
})
})
Describe("inference service with a transformer", func() {
// --- Default values ---
defaultTransformerIsvc := func(namespace string, name string, storageUri string) *v1beta1.InferenceService {
predictor := v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
}
transformer := &v1beta1.TransformerSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
PodSpec: v1beta1.PodSpec{
Containers: []corev1.Container{
{
Image: "transformer:v1",
Resources: defaultResource,
},
},
},
}
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: predictor,
Transformer: transformer,
},
}
return isvc
}
It("Should keep the transformer knative service/virtualService/service when the annotation is set to false", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-transformer-false-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultTransformerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Predictor knative service
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
// Transformer knative service
transformerServiceKey := types.NamespacedName{
Name: constants.TransformerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectTransformerKsvcToBeReady(context.Background(), serviceKey, transformerServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
expectIsvcTransformerReadyStatus(ctx, serviceKey)
})
It("Should delete the transformer knative service/virtualService/service when the annotation is set to true", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-transformer-true-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultTransformerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Check that the KSVC does not exist
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
// Transformer knative service
transformerServiceKey := types.NamespacedName{
Name: constants.TransformerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, transformerServiceKey)
// Check that the services were not created
expectResourceIsDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceIsDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
})
It("Should delete the transformer knative service/virtualService/service when the annotation is updated to true on an existing ISVC", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-transformer-edit-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultTransformerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Predictor knative service
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
// Transformer knative service
transformerServiceKey := types.NamespacedName{
Name: constants.TransformerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectTransformerKsvcToBeReady(context.Background(), serviceKey, transformerServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
expectIsvcTransformerReadyStatus(ctx, serviceKey)
// Stop the inference service
actualIsvc := expectIsvcToExist(ctx, serviceKey)
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
// Check that the KSVCs were deleted
expectResourceToBeDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
expectResourceToBeDeleted(context.Background(), &knservingv1.Service{}, transformerServiceKey)
// Check that the services were deleted
expectResourceToBeDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToBeDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
})
It("Should create the transformer knative service/virtualService/service when the annotation is updated to false on an existing ISVC that is stopped", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-transformer-edit-false-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultTransformerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Check that the KSVC does not exist
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
// Transformer knative service
transformerServiceKey := types.NamespacedName{
Name: constants.TransformerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, transformerServiceKey)
// Check that the services were not created
expectResourceIsDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceIsDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
actualIsvc := expectIsvcToExist(ctx, serviceKey)
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
// Resume the inference service
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
// Check the KSVCs
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
expectTransformerKsvcToBeReady(context.Background(), serviceKey, transformerServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
expectIsvcTransformerReadyStatus(ctx, serviceKey)
})
})
Describe("inference service with an explainer", func() {
// --- Default values ---
defaultExplainerIsvc := func(namespace string, name string, storageUri string) *v1beta1.InferenceService {
predictor := v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
}
explainer := &v1beta1.ExplainerSpec{
ART: &v1beta1.ARTExplainerSpec{
Type: v1beta1.ARTSquareAttackExplainer,
ExplainerExtensionSpec: v1beta1.ExplainerExtensionSpec{
Config: map[string]string{"nb_classes": "10"},
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
RuntimeVersion: ptr.To("latest"),
},
},
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 2,
ScaleTarget: ptr.To(int32(80)),
TimeoutSeconds: ptr.To(int64(30)),
},
}
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: predictor,
Explainer: explainer,
},
}
return isvc
}
It("Should keep the explainer knative service/virtualService/service when the annotation is set to false", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-explainer-false-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultExplainerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Predictor knative service
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
// Explainer knative service
explainerServiceKey := types.NamespacedName{
Name: constants.ExplainerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectExplainerKsvcToBeReady(context.Background(), serviceKey, explainerServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
expectIsvcExplainerReadyStatus(ctx, serviceKey)
})
It("Should delete the explainer knative service/virtualService/service when the annotation is set to true", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-explainer-true-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultExplainerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Check that the KSVC does not exist
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
// Explainer knative service
explainerServiceKey := types.NamespacedName{
Name: constants.ExplainerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, explainerServiceKey)
// Check that the services were not created
expectResourceIsDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceIsDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
})
It("Should delete the explainer knative service/virtualService/service when the annotation is updated to true on an existing ISVC", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-explainer-edit-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultExplainerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Predictor knative service
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
// Explainer knative service
explainerServiceKey := types.NamespacedName{
Name: constants.ExplainerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectExplainerKsvcToBeReady(context.Background(), serviceKey, explainerServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
expectIsvcExplainerReadyStatus(ctx, serviceKey)
// Stop the inference service
actualIsvc := expectIsvcToExist(ctx, serviceKey)
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
// Check that the KSVCs were deleted
expectResourceToBeDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
expectResourceToBeDeleted(context.Background(), &knservingv1.Service{}, explainerServiceKey)
// Check that the services were deleted
expectResourceToBeDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToBeDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
})
It("Should create the explainer knative service/virtualService/service when the annotation is updated to false on an existing ISVC that is stopped", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
// Config map
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Serving runtime
serviceName := "stop-explainer-edit-false-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultExplainerIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations[constants.StopAnnotationKey] = "true"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Check that the KSVC does not exist
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, predictorServiceKey)
// Explainer knative service
explainerServiceKey := types.NamespacedName{
Name: constants.ExplainerServiceName(serviceName),
Namespace: serviceKey.Namespace,
}
expectResourceIsDeleted(context.Background(), &knservingv1.Service{}, explainerServiceKey)
// Check that the services were not created
expectResourceIsDeleted(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceIsDeleted(context.Background(), &corev1.Service{}, serviceKey)
// Check that the ISVC status reflects that it is stopped
actualIsvc := expectIsvcToExist(ctx, serviceKey)
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionTrue)
// Resume the inference service
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations[constants.StopAnnotationKey] = "false"
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
// Check the KSVCs
expectPredictorKsvcToBeReady(context.Background(), serviceKey, predictorServiceKey)
expectExplainerKsvcToBeReady(context.Background(), serviceKey, explainerServiceKey)
// Check that the ISVC was updated
expectIsvcToExist(ctx, serviceKey)
// Check the services
expectResourceToExist(context.Background(), &istioclientv1beta1.VirtualService{}, serviceKey)
expectResourceToExist(context.Background(), &corev1.Service{}, serviceKey)
// Check the ISVC statuses
expectIsvcConditionStatus(ctx, serviceKey, v1beta1.Stopped, corev1.ConditionFalse)
expectIsvcReadyStatus(ctx, serviceKey)
expectIsvcExplainerReadyStatus(ctx, serviceKey)
})
})
})
Context("Inference Service with transformer", func() {
It("Should create successfully", func() {
serviceName := "svc-with-transformer"
namespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: namespace}}
serviceKey := expectedRequest.NamespacedName
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: namespace,
}
transformerServiceKey := types.NamespacedName{
Name: constants.TransformerServiceName(serviceName),
Namespace: namespace,
}
transformer := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
Labels: map[string]string{
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
Labels: map[string]string{
"key2": "val2FromPredictor",
},
Annotations: map[string]string{
"key2": "val2FromPredictor",
},
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
RuntimeVersion: ptr.To("1.13.0"),
},
},
},
Transformer: &v1beta1.TransformerSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
Labels: map[string]string{
"key2": "val2FromTransformer",
},
Annotations: map[string]string{
"key2": "val2FromTransformer",
},
},
PodSpec: v1beta1.PodSpec{
Containers: []corev1.Container{
{
Image: "transformer:v1",
Resources: defaultResource,
},
},
},
},
},
Status: v1beta1.InferenceServiceStatus{
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
v1beta1.PredictorComponent: {
LatestReadyRevision: "revision-v1",
},
},
},
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create ServingRuntime
servingRuntime := getServingRuntime("tf-serving", "default")
_ = k8sClient.Create(context.TODO(), &servingRuntime)
defer k8sClient.Delete(context.TODO(), &servingRuntime)
// Create the InferenceService object and expect the Reconcile and knative service to be created
transformer.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
instance := transformer.DeepCopy()
Expect(k8sClient.Create(context.TODO(), instance)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), instance)
predictorService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, predictorService)
}, timeout, interval).Should(Succeed())
transformerService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), transformerServiceKey, transformerService)
}, timeout, interval).Should(Succeed())
expectedTransformerService := &knservingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: constants.TransformerServiceName(instance.Name),
Namespace: instance.Namespace,
},
Spec: knservingv1.ServiceSpec{
ConfigurationSpec: knservingv1.ConfigurationSpec{
Template: knservingv1.RevisionTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"serving.kserve.io/inferenceservice": serviceName,
constants.KServiceComponentLabel: constants.Transformer.String(),
"key1": "val1FromISVC",
"key2": "val2FromTransformer",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev",
"autoscaling.knative.dev/max-scale": "3",
"autoscaling.knative.dev/min-scale": "1",
"key1": "val1FromISVC",
"key2": "val2FromTransformer",
},
},
Spec: knservingv1.RevisionSpec{
ContainerConcurrency: nil,
TimeoutSeconds: nil,
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{
{
Image: "transformer:v1",
Args: []string{
"--model_name",
serviceName,
"--predictor_host",
constants.PredictorServiceName(instance.Name) + "." + instance.Namespace,
constants.ArgumentHttpPort,
constants.InferenceServiceDefaultHttpPort,
},
Name: constants.InferenceServiceContainerName,
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
},
},
AutomountServiceAccountToken: ptr.To(false),
},
},
},
},
RouteSpec: knservingv1.RouteSpec{
Traffic: []knservingv1.TrafficTarget{{LatestRevision: ptr.To(true), Percent: ptr.To(int64(100))}},
},
},
}
// Set ResourceVersion which is required for update operation.
expectedTransformerService.ResourceVersion = transformerService.ResourceVersion
// Do a dry-run update. This will populate our local knative service object with any default values
// that are present on the remote version.
err := k8sClient.Update(context.TODO(), expectedTransformerService, client.DryRunAll)
Expect(err).ShouldNot(HaveOccurred())
Expect(cmp.Diff(transformerService.Spec, expectedTransformerService.Spec)).To(Equal(""))
// mock update knative service status since knative serving controller is not running in test
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
transformerUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.TransformerServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor
updatedPredictorService := predictorService.DeepCopy()
updatedPredictorService.Status.LatestCreatedRevisionName = "revision-v1"
updatedPredictorService.Status.LatestReadyRevisionName = "revision-v1"
updatedPredictorService.Status.URL = predictorUrl
updatedPredictorService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(context.TODO(), updatedPredictorService)).NotTo(HaveOccurred())
// update transformer
updatedTransformerService := transformerService.DeepCopy()
updatedTransformerService.Status.LatestCreatedRevisionName = "t-revision-v1"
updatedTransformerService.Status.LatestReadyRevisionName = "t-revision-v1"
updatedTransformerService.Status.URL = transformerUrl
updatedTransformerService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(context.TODO(), updatedTransformerService)).NotTo(HaveOccurred())
// verify if InferenceService status is updated
expectedIsvcStatus := v1beta1.InferenceServiceStatus{
Status: duckv1.Status{
Conditions: duckv1.Conditions{
{
Type: v1beta1.IngressReady,
Status: "True",
},
{
Type: v1beta1.PredictorReady,
Status: "True",
},
{
Type: v1beta1.PredictorRouteReady,
Severity: "Info",
Status: "True",
},
{
Type: v1beta1.PredictorConfigurationReady,
Severity: "Info",
Status: "True",
},
{
Type: apis.ConditionReady,
Status: "True",
},
{
Type: v1beta1.RoutesReady,
Severity: "Info",
Status: "True",
},
{
Type: v1beta1.LatestDeploymentReady,
Severity: "Info",
Status: "True",
},
{
Type: v1beta1.TransformerReady,
Severity: "Info",
Status: "True",
},
{
Type: v1beta1.TransformerRouteReady,
Severity: "Info",
Status: "True",
},
{
Type: v1beta1.TransformerConfigurationReady,
Severity: "Info",
Status: "True",
},
},
},
URL: &apis.URL{
Scheme: "http",
Host: constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain),
},
Address: &duckv1.Addressable{
URL: &apis.URL{
Scheme: "http",
Host: network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace),
},
},
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
v1beta1.PredictorComponent: {
LatestReadyRevision: "revision-v1",
LatestCreatedRevision: "revision-v1",
URL: predictorUrl,
},
v1beta1.TransformerComponent: {
LatestReadyRevision: "t-revision-v1",
LatestCreatedRevision: "t-revision-v1",
URL: transformerUrl,
},
},
ModelStatus: v1beta1.ModelStatus{
TransitionStatus: "InProgress",
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
ModelCopies: &v1beta1.ModelCopies{},
},
ServingRuntimeName: "tf-serving",
}
Eventually(func() string {
isvc := &v1beta1.InferenceService{}
if err := k8sClient.Get(context.TODO(), serviceKey, isvc); err != nil {
return err.Error()
}
return cmp.Diff(&expectedIsvcStatus, &isvc.Status, cmpopts.IgnoreTypes(apis.Condition{}, "LastTransitionTime", "Severity"))
}, timeout, interval).Should(BeEmpty())
})
})
Context("Inference Service with transforemer and predictor collocation", func() {
Context("When predictor and transformer are collocated", func() {
It("Should create knative service and ingress successfully", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "isvc-with-collocated-transformer"
namespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: namespace}}
serviceKey := expectedRequest.NamespacedName
httpPort := int32(8060)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: namespace,
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Create ServingRuntime
servingRuntime := &v1alpha1.ServingRuntime{
ObjectMeta: metav1.ObjectMeta{
Name: "tf-serving",
Namespace: "default",
},
Spec: v1alpha1.ServingRuntimeSpec{
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
{
Name: "tensorflow",
Version: ptr.To("1"),
AutoSelect: ptr.To(true),
},
},
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Resources: defaultResource,
},
},
},
Disabled: ptr.To(false),
},
}
Expect(k8sClient.Create(ctx, servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, servingRuntime)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
Labels: map[string]string{
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "tensorflow",
},
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
RuntimeVersion: ptr.To("1.13.0"),
},
},
PodSpec: v1beta1.PodSpec{
Containers: []corev1.Container{
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
},
},
},
}
// Create the InferenceService object and expect the Reconcile and knative service to be created
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(ctx, predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
expectedService := &knservingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: predictorServiceKey.Name,
Namespace: predictorServiceKey.Namespace,
},
Spec: knservingv1.ServiceSpec{
ConfigurationSpec: knservingv1.ConfigurationSpec{
Template: knservingv1.RevisionTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.KServiceComponentLabel: constants.Predictor.String(),
constants.InferenceServicePodLabelKey: serviceName,
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
"autoscaling.knative.dev/max-scale": "3",
"autoscaling.knative.dev/min-scale": "1",
"autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: knservingv1.RevisionSpec{
ContainerConcurrency: isvc.Spec.Predictor.ContainerConcurrency,
TimeoutSeconds: isvc.Spec.Predictor.TimeoutSeconds,
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{
{
Image: "tensorflow/serving:" +
*isvc.Spec.Predictor.Model.RuntimeVersion,
Name: constants.InferenceServiceContainerName,
Command: []string{v1beta1.TensorflowEntrypointCommand},
Args: []string{
"--port=" + v1beta1.TensorflowServingGRPCPort,
"--rest_api_port=" + v1beta1.TensorflowServingRestPort,
"--model_base_path=" + constants.DefaultModelLocalMountPath,
"--rest_api_timeout_in_ms=60000",
},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
},
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
AutomountServiceAccountToken: ptr.To(false),
},
},
},
},
RouteSpec: knservingv1.RouteSpec{
Traffic: []knservingv1.TrafficTarget{{LatestRevision: ptr.To(true), Percent: ptr.To(int64(100))}},
},
},
}
// Set ResourceVersion which is required for update operation.
expectedService.ResourceVersion = actualService.ResourceVersion
// Do a dry-run update. This will populate our local knative service object with any default values
// that are present on the remote version.
err := k8sClient.Update(ctx, expectedService, client.DryRunAll)
Expect(err).ShouldNot(HaveOccurred())
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor
{
updatedService := actualService.DeepCopy()
updatedService.Status.LatestCreatedRevisionName = "revision-v1"
updatedService.Status.LatestReadyRevisionName = "revision-v1"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(ctx, updatedService)).NotTo(HaveOccurred())
}
// assert ingress
virtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() error {
return k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, virtualService)
}, timeout, interval).Should(Succeed())
expectedVirtualService := &istioclientv1beta1.VirtualService{
Spec: istiov1beta1.VirtualService{
Gateways: []string{
constants.KnativeLocalGateway,
constants.IstioMeshGateway,
constants.KnativeIngressGateway,
},
Hosts: []string{
network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace),
constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain),
},
Http: []*istiov1beta1.HTTPRoute{
{
Match: []*istiov1beta1.HTTPMatchRequest{
{
Gateways: []string{constants.KnativeLocalGateway, constants.IstioMeshGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace)),
},
},
},
{
Gateways: []string{constants.KnativeIngressGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain)),
},
},
},
},
Route: []*istiov1beta1.HTTPRouteDestination{
{
Destination: &istiov1beta1.Destination{
Host: network.GetServiceHostname("knative-local-gateway", "istio-system"),
Port: &istiov1beta1.PortSelector{Number: constants.CommonDefaultHttpPort},
},
Weight: 100,
},
},
Headers: &istiov1beta1.Headers{
Request: &istiov1beta1.Headers_HeaderOperations{
Set: map[string]string{
"Host": network.GetServiceHostname(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace),
"KServe-Isvc-Name": serviceName,
"KServe-Isvc-Namespace": serviceKey.Namespace,
},
},
},
},
},
},
}
Expect(virtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
// get inference service
actualIsvc := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, expectedRequest.NamespacedName, actualIsvc)
return err == nil
}, timeout, interval).Should(BeTrue())
// update inference service with annotations and labels
annotations := map[string]string{"testAnnotation": "test"}
labels := map[string]string{"testLabel": "test"}
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations = annotations
updatedIsvc.Labels = labels
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
updatedVirtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, updatedVirtualService)
if err != nil {
return false
}
return reflect.DeepEqual(updatedVirtualService.Labels, labels) &&
reflect.DeepEqual(updatedVirtualService.Annotations, annotations)
}, timeout, interval).Should(BeTrue())
Expect(updatedVirtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
})
})
Context("When predictor and transformer container is collocated in serving runtime", func() {
It("Should create knative service and ingress successfully", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "isvc-with-collocation-transformer-runtime"
namespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: namespace}}
serviceKey := expectedRequest.NamespacedName
httpPort := int32(8060)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: namespace,
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Create ServingRuntime
servingRuntime := &v1alpha1.ServingRuntime{
ObjectMeta: metav1.ObjectMeta{
Name: "tf-serving-collocation",
Namespace: "default",
},
Spec: v1alpha1.ServingRuntimeSpec{
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
{
Name: "tensorflow",
Version: ptr.To("1"),
AutoSelect: ptr.To(true),
},
},
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Resources: defaultResource,
},
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Args: []string{
"--model_name={{.Name}}",
},
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
},
Disabled: ptr.To(false),
},
}
Expect(k8sClient.Create(ctx, servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, servingRuntime)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
Labels: map[string]string{
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "tensorflow",
},
Runtime: ptr.To("tf-serving-collocation"),
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
RuntimeVersion: ptr.To("1.13.0"),
},
},
},
},
}
// Create the InferenceService object and expect the Reconcile and knative service to be created
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(ctx, predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
expectedService := &knservingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: predictorServiceKey.Name,
Namespace: predictorServiceKey.Namespace,
},
Spec: knservingv1.ServiceSpec{
ConfigurationSpec: knservingv1.ConfigurationSpec{
Template: knservingv1.RevisionTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.KServiceComponentLabel: constants.Predictor.String(),
constants.InferenceServicePodLabelKey: serviceName,
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
"autoscaling.knative.dev/max-scale": "3",
"autoscaling.knative.dev/min-scale": "1",
"autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: knservingv1.RevisionSpec{
ContainerConcurrency: isvc.Spec.Predictor.ContainerConcurrency,
TimeoutSeconds: isvc.Spec.Predictor.TimeoutSeconds,
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{
{
Image: "tensorflow/serving:" +
*isvc.Spec.Predictor.Model.RuntimeVersion,
Name: constants.InferenceServiceContainerName,
Command: []string{v1beta1.TensorflowEntrypointCommand},
Args: []string{
"--port=" + v1beta1.TensorflowServingGRPCPort,
"--rest_api_port=" + v1beta1.TensorflowServingRestPort,
"--model_base_path=" + constants.DefaultModelLocalMountPath,
"--rest_api_timeout_in_ms=60000",
},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
},
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Args: []string{"--model_name=" + serviceName},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
AutomountServiceAccountToken: ptr.To(false),
},
},
},
},
RouteSpec: knservingv1.RouteSpec{
Traffic: []knservingv1.TrafficTarget{{LatestRevision: ptr.To(true), Percent: ptr.To(int64(100))}},
},
},
}
// Set ResourceVersion which is required for update operation.
expectedService.ResourceVersion = actualService.ResourceVersion
// Do a dry-run update. This will populate our local knative service object with any default values
// that are present on the remote version.
err := k8sClient.Update(ctx, expectedService, client.DryRunAll)
Expect(err).ShouldNot(HaveOccurred())
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor
{
updatedService := actualService.DeepCopy()
updatedService.Status.LatestCreatedRevisionName = "revision-v1"
updatedService.Status.LatestReadyRevisionName = "revision-v1"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(ctx, updatedService)).NotTo(HaveOccurred())
}
// assert ingress
virtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() error {
return k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, virtualService)
}, timeout, interval).Should(Succeed())
expectedVirtualService := &istioclientv1beta1.VirtualService{
Spec: istiov1beta1.VirtualService{
Gateways: []string{
constants.KnativeLocalGateway,
constants.IstioMeshGateway,
constants.KnativeIngressGateway,
},
Hosts: []string{
network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace),
constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain),
},
Http: []*istiov1beta1.HTTPRoute{
{
Match: []*istiov1beta1.HTTPMatchRequest{
{
Gateways: []string{constants.KnativeLocalGateway, constants.IstioMeshGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace)),
},
},
},
{
Gateways: []string{constants.KnativeIngressGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain)),
},
},
},
},
Route: []*istiov1beta1.HTTPRouteDestination{
{
Destination: &istiov1beta1.Destination{
Host: network.GetServiceHostname("knative-local-gateway", "istio-system"),
Port: &istiov1beta1.PortSelector{Number: constants.CommonDefaultHttpPort},
},
Weight: 100,
},
},
Headers: &istiov1beta1.Headers{
Request: &istiov1beta1.Headers_HeaderOperations{
Set: map[string]string{
"Host": network.GetServiceHostname(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace),
"KServe-Isvc-Name": serviceName,
"KServe-Isvc-Namespace": serviceKey.Namespace,
},
},
},
},
},
},
}
Expect(virtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
// get inference service
actualIsvc := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, expectedRequest.NamespacedName, actualIsvc)
return err == nil
}, timeout, interval).Should(BeTrue())
// update inference service with annotations and labels
annotations := map[string]string{"testAnnotation": "test"}
labels := map[string]string{"testLabel": "test"}
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations = annotations
updatedIsvc.Labels = labels
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
updatedVirtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, updatedVirtualService)
if err != nil {
return false
}
return reflect.DeepEqual(updatedVirtualService.Labels, labels) &&
reflect.DeepEqual(updatedVirtualService.Annotations, annotations)
}, timeout, interval).Should(BeTrue())
Expect(updatedVirtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
})
})
Context("When transformer container is present in both serving runtime and inference service", func() {
It("Transformer container should be merged successfully", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "isvc-with-collocated-transformer-runtime"
namespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: namespace}}
serviceKey := expectedRequest.NamespacedName
httpPort := int32(8060)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: namespace,
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
// Create ServingRuntime
servingRuntime := &v1alpha1.ServingRuntime{
ObjectMeta: metav1.ObjectMeta{
Name: "tf-serving-collocation",
Namespace: "default",
},
Spec: v1alpha1.ServingRuntimeSpec{
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
{
Name: "tensorflow",
Version: ptr.To("1"),
AutoSelect: ptr.To(true),
},
},
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Resources: defaultResource,
},
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Args: []string{
"--model_name={{.Name}}",
},
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
},
Disabled: ptr.To(false),
},
}
Expect(k8sClient.Create(ctx, servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, servingRuntime)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
Labels: map[string]string{
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "tensorflow",
},
Runtime: ptr.To("tf-serving-collocation"),
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
RuntimeVersion: ptr.To("1.13.0"),
},
},
PodSpec: v1beta1.PodSpec{
Containers: []corev1.Container{
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Command: []string{
"transformer",
},
Args: []string{
"--http-port",
strconv.Itoa(int(httpPort)),
},
Resources: defaultResource,
},
},
},
},
},
}
// Create the InferenceService object and expect the Reconcile and knative service to be created
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(ctx, predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
expectedService := &knservingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: predictorServiceKey.Name,
Namespace: predictorServiceKey.Namespace,
},
Spec: knservingv1.ServiceSpec{
ConfigurationSpec: knservingv1.ConfigurationSpec{
Template: knservingv1.RevisionTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.KServiceComponentLabel: constants.Predictor.String(),
constants.InferenceServicePodLabelKey: serviceName,
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
"autoscaling.knative.dev/max-scale": "3",
"autoscaling.knative.dev/min-scale": "1",
"autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: knservingv1.RevisionSpec{
ContainerConcurrency: isvc.Spec.Predictor.ContainerConcurrency,
TimeoutSeconds: isvc.Spec.Predictor.TimeoutSeconds,
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{
{
Image: "tensorflow/serving:" +
*isvc.Spec.Predictor.Model.RuntimeVersion,
Name: constants.InferenceServiceContainerName,
Command: []string{v1beta1.TensorflowEntrypointCommand},
Args: []string{
"--port=" + v1beta1.TensorflowServingGRPCPort,
"--rest_api_port=" + v1beta1.TensorflowServingRestPort,
"--model_base_path=" + constants.DefaultModelLocalMountPath,
"--rest_api_timeout_in_ms=60000",
},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
},
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Command: []string{
"transformer",
},
Args: []string{"--model_name=" + serviceName, "--http-port", strconv.Itoa(int(httpPort))},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
AutomountServiceAccountToken: ptr.To(false),
},
},
},
},
RouteSpec: knservingv1.RouteSpec{
Traffic: []knservingv1.TrafficTarget{{LatestRevision: ptr.To(true), Percent: ptr.To(int64(100))}},
},
},
}
// Set ResourceVersion which is required for update operation.
expectedService.ResourceVersion = actualService.ResourceVersion
// Do a dry-run update. This will populate our local knative service object with any default values
// that are present on the remote version.
err := k8sClient.Update(ctx, expectedService, client.DryRunAll)
Expect(err).ShouldNot(HaveOccurred())
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor
{
updatedService := actualService.DeepCopy()
updatedService.Status.LatestCreatedRevisionName = "revision-v1"
updatedService.Status.LatestReadyRevisionName = "revision-v1"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(ctx, updatedService)).NotTo(HaveOccurred())
}
// assert ingress
virtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() error {
return k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, virtualService)
}, timeout, interval).Should(Succeed())
expectedVirtualService := &istioclientv1beta1.VirtualService{
Spec: istiov1beta1.VirtualService{
Gateways: []string{
constants.KnativeLocalGateway,
constants.IstioMeshGateway,
constants.KnativeIngressGateway,
},
Hosts: []string{
network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace),
constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain),
},
Http: []*istiov1beta1.HTTPRoute{
{
Match: []*istiov1beta1.HTTPMatchRequest{
{
Gateways: []string{constants.KnativeLocalGateway, constants.IstioMeshGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace)),
},
},
},
{
Gateways: []string{constants.KnativeIngressGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain)),
},
},
},
},
Route: []*istiov1beta1.HTTPRouteDestination{
{
Destination: &istiov1beta1.Destination{
Host: network.GetServiceHostname("knative-local-gateway", "istio-system"),
Port: &istiov1beta1.PortSelector{Number: constants.CommonDefaultHttpPort},
},
Weight: 100,
},
},
Headers: &istiov1beta1.Headers{
Request: &istiov1beta1.Headers_HeaderOperations{
Set: map[string]string{
"Host": network.GetServiceHostname(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace),
"KServe-Isvc-Name": serviceName,
"KServe-Isvc-Namespace": serviceKey.Namespace,
},
},
},
},
},
},
}
Expect(virtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
// get inference service
actualIsvc := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, expectedRequest.NamespacedName, actualIsvc)
return err == nil
}, timeout, interval).Should(BeTrue())
// update inference service with annotations and labels
annotations := map[string]string{"testAnnotation": "test"}
labels := map[string]string{"testLabel": "test"}
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations = annotations
updatedIsvc.Labels = labels
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
updatedVirtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, updatedVirtualService)
if err != nil {
return false
}
return reflect.DeepEqual(updatedVirtualService.Labels, labels) &&
reflect.DeepEqual(updatedVirtualService.Annotations, annotations)
}, timeout, interval).Should(BeTrue())
Expect(updatedVirtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
})
})
Context("When custom predictor and transformer are collocated", func() {
It("Should create knative service and ingress successfully", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "isvc-custom-collocated-transformer"
namespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: namespace}}
serviceKey := expectedRequest.NamespacedName
httpPort := int32(8060)
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: namespace,
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
Labels: map[string]string{
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
PodSpec: v1beta1.PodSpec{
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
},
Resources: defaultResource,
},
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
},
},
},
}
// Create the InferenceService object and expect the Reconcile and knative service to be created
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
actualService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(ctx, predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
expectedService := &knservingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: predictorServiceKey.Name,
Namespace: predictorServiceKey.Namespace,
},
Spec: knservingv1.ServiceSpec{
ConfigurationSpec: knservingv1.ConfigurationSpec{
Template: knservingv1.RevisionTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.KServiceComponentLabel: constants.Predictor.String(),
constants.InferenceServicePodLabelKey: serviceName,
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"serving.kserve.io/deploymentMode": "Knative",
"autoscaling.knative.dev/max-scale": "3",
"autoscaling.knative.dev/min-scale": "1",
"autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev",
"key1": "val1FromISVC",
"key2": "val2FromISVC",
},
},
Spec: knservingv1.RevisionSpec{
ContainerConcurrency: isvc.Spec.Predictor.ContainerConcurrency,
TimeoutSeconds: isvc.Spec.Predictor.TimeoutSeconds,
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{
{
Image: "tensorflow/serving:1.14.0",
Name: constants.InferenceServiceContainerName,
Command: []string{v1beta1.TensorflowEntrypointCommand},
Args: []string{
"--port=" + v1beta1.TensorflowServingGRPCPort,
"--rest_api_port=" + v1beta1.TensorflowServingRestPort,
"--model_base_path=" + constants.DefaultModelLocalMountPath,
},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
},
{
Name: constants.TransformerContainerName,
Image: "transformer:v1",
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
Ports: []corev1.ContainerPort{
{
ContainerPort: httpPort,
},
},
},
},
AutomountServiceAccountToken: ptr.To(false),
},
},
},
},
RouteSpec: knservingv1.RouteSpec{
Traffic: []knservingv1.TrafficTarget{{LatestRevision: ptr.To(true), Percent: ptr.To(int64(100))}},
},
},
}
// Set ResourceVersion which is required for update operation.
expectedService.ResourceVersion = actualService.ResourceVersion
// Do a dry-run update. This will populate our local knative service object with any default values
// that are present on the remote version.
err := k8sClient.Update(ctx, expectedService, client.DryRunAll)
Expect(err).ShouldNot(HaveOccurred())
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor
{
updatedService := actualService.DeepCopy()
updatedService.Status.LatestCreatedRevisionName = "revision-v1"
updatedService.Status.LatestReadyRevisionName = "revision-v1"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionRoutesReady,
Status: "True",
},
{
Type: knservingv1.ServiceConditionConfigurationsReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(ctx, updatedService)).NotTo(HaveOccurred())
}
// assert ingress
virtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() error {
return k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, virtualService)
}, timeout, interval).Should(Succeed())
expectedVirtualService := &istioclientv1beta1.VirtualService{
Spec: istiov1beta1.VirtualService{
Gateways: []string{
constants.KnativeLocalGateway,
constants.IstioMeshGateway,
constants.KnativeIngressGateway,
},
Hosts: []string{
network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace),
constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain),
},
Http: []*istiov1beta1.HTTPRoute{
{
Match: []*istiov1beta1.HTTPMatchRequest{
{
Gateways: []string{constants.KnativeLocalGateway, constants.IstioMeshGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(network.GetServiceHostname(serviceKey.Name, serviceKey.Namespace)),
},
},
},
{
Gateways: []string{constants.KnativeIngressGateway},
Authority: &istiov1beta1.StringMatch{
MatchType: &istiov1beta1.StringMatch_Regex{
Regex: constants.HostRegExp(constants.InferenceServiceHostName(serviceKey.Name, serviceKey.Namespace, domain)),
},
},
},
},
Route: []*istiov1beta1.HTTPRouteDestination{
{
Destination: &istiov1beta1.Destination{
Host: network.GetServiceHostname("knative-local-gateway", "istio-system"),
Port: &istiov1beta1.PortSelector{Number: constants.CommonDefaultHttpPort},
},
Weight: 100,
},
},
Headers: &istiov1beta1.Headers{
Request: &istiov1beta1.Headers_HeaderOperations{
Set: map[string]string{
"Host": network.GetServiceHostname(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace),
"KServe-Isvc-Name": serviceName,
"KServe-Isvc-Namespace": serviceKey.Namespace,
},
},
},
},
},
},
}
Expect(virtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
// get inference service
actualIsvc := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, expectedRequest.NamespacedName, actualIsvc)
return err == nil
}, timeout, interval).Should(BeTrue())
// update inference service with annotations and labels
annotations := map[string]string{"testAnnotation": "test"}
labels := map[string]string{"testLabel": "test"}
updatedIsvc := actualIsvc.DeepCopy()
updatedIsvc.Annotations = annotations
updatedIsvc.Labels = labels
Expect(k8sClient.Update(ctx, updatedIsvc)).NotTo(HaveOccurred())
updatedVirtualService := &istioclientv1beta1.VirtualService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
}, updatedVirtualService)
if err != nil {
return false
}
return reflect.DeepEqual(updatedVirtualService.Labels, labels) &&
reflect.DeepEqual(updatedVirtualService.Annotations, annotations)
}, timeout, interval).Should(BeTrue())
Expect(updatedVirtualService.Spec.DeepCopy()).To(Equal(expectedVirtualService.Spec.DeepCopy()))
Expect(updatedVirtualService.Annotations).To(Equal(annotations))
Expect(updatedVirtualService.Labels).To(Equal(labels))
})
})
})
Context("When doing canary out with inference service", func() {
It("Should have traffic split between two revisions", func() {
By("By moving canary traffic percent to the latest revision")
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create ServingRuntime
servingRuntime := getServingRuntime("tf-serving", "default")
_ = k8sClient.Create(context.TODO(), &servingRuntime)
defer k8sClient.Delete(context.TODO(), &servingRuntime)
// Create Canary InferenceService
serviceName := "foo-canary"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
storageUri2 := "s3://test/mnist/export/v2"
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
updatedService := &knservingv1.Service{}
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, updatedService)
}, timeout, interval).Should(Succeed())
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor status
updatedService.Status.LatestCreatedRevisionName = "revision-v1"
updatedService.Status.LatestReadyRevisionName = "revision-v1"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
}
updatedService.Status.Traffic = []knservingv1.TrafficTarget{
{
LatestRevision: ptr.To(true),
RevisionName: "revision-v1",
Percent: ptr.To(int64(100)),
},
}
Expect(retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return k8sClient.Status().Update(context.TODO(), updatedService)
})).NotTo(HaveOccurred())
// assert inference service predictor status
Eventually(func() string {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
if err != nil {
return ""
}
return inferenceService.Status.Components[v1beta1.PredictorComponent].LatestReadyRevision
}, timeout, interval).Should(Equal("revision-v1"))
// assert latest rolled out revision
Eventually(func() string {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
if err != nil {
return ""
}
return inferenceService.Status.Components[v1beta1.PredictorComponent].LatestRolledoutRevision
}, timeout, interval).Should(Equal("revision-v1"))
// update canary traffic percent to 20%
updatedIsvc := inferenceService.DeepCopy()
updatedIsvc.Spec.Predictor.Model.StorageURI = &storageUri2
updatedIsvc.Spec.Predictor.CanaryTrafficPercent = ptr.To(int64(20))
Expect(k8sClient.Update(context.TODO(), updatedIsvc)).NotTo(HaveOccurred())
// update predictor status
canaryService := &knservingv1.Service{}
Eventually(func() string {
_ = k8sClient.Get(context.TODO(), predictorServiceKey, canaryService)
return canaryService.Spec.Template.Annotations[constants.StorageInitializerSourceUriInternalAnnotationKey]
}, timeout, interval).Should(Equal(storageUri2))
canaryService.Status.LatestCreatedRevisionName = "revision-v2"
canaryService.Status.LatestReadyRevisionName = "revision-v2"
canaryService.Status.URL = predictorUrl
canaryService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(context.TODO(), canaryService)).NotTo(HaveOccurred())
expectedTrafficTarget := []knservingv1.TrafficTarget{
{
LatestRevision: ptr.To(true),
Percent: ptr.To(int64(20)),
},
{
Tag: "prev",
RevisionName: "revision-v1",
LatestRevision: ptr.To(false),
Percent: ptr.To(int64(80)),
},
}
Eventually(func() []knservingv1.TrafficTarget {
actualService := &knservingv1.Service{}
err := k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
if err != nil {
return []knservingv1.TrafficTarget{}
} else {
return actualService.Spec.Traffic
}
}, timeout, interval).Should(Equal(expectedTrafficTarget))
rolloutIsvc := &v1beta1.InferenceService{}
Eventually(func() string {
err := k8sClient.Get(ctx, serviceKey, rolloutIsvc)
if err != nil {
return ""
}
return rolloutIsvc.Status.Components[v1beta1.PredictorComponent].LatestReadyRevision
}, timeout, interval).Should(Equal("revision-v2"))
// rollout canary
rolloutIsvc.Spec.Predictor.CanaryTrafficPercent = nil
Expect(k8sClient.Update(context.TODO(), rolloutIsvc)).NotTo(HaveOccurred())
expectedTrafficTarget = []knservingv1.TrafficTarget{
{
LatestRevision: ptr.To(true),
Percent: ptr.To(int64(100)),
},
}
Eventually(func() []knservingv1.TrafficTarget {
actualService := &knservingv1.Service{}
err := k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
if err != nil {
return []knservingv1.TrafficTarget{}
} else {
return actualService.Spec.Traffic
}
}, timeout, interval).Should(Equal(expectedTrafficTarget))
// update predictor knative service status
serviceRevision2 := &knservingv1.Service{}
Eventually(func() string {
_ = k8sClient.Get(context.TODO(), predictorServiceKey, serviceRevision2)
return serviceRevision2.Spec.Template.Annotations[constants.StorageInitializerSourceUriInternalAnnotationKey]
}, timeout, interval).Should(Equal(storageUri2))
serviceRevision2.Status.Traffic = []knservingv1.TrafficTarget{
{
LatestRevision: ptr.To(true),
RevisionName: "revision-v2",
Percent: ptr.To(int64(100)),
},
}
Expect(k8sClient.Status().Update(context.TODO(), serviceRevision2)).NotTo(HaveOccurred())
// assert latest rolled out revision
expectedIsvc := &v1beta1.InferenceService{}
Eventually(func() string {
err := k8sClient.Get(ctx, serviceKey, expectedIsvc)
if err != nil {
return ""
}
return expectedIsvc.Status.Components[v1beta1.PredictorComponent].LatestRolledoutRevision
}, timeout, interval).Should(Equal("revision-v2"))
// assert previous rolled out revision
Eventually(func() string {
err := k8sClient.Get(ctx, serviceKey, expectedIsvc)
if err != nil {
return ""
}
return expectedIsvc.Status.Components[v1beta1.PredictorComponent].PreviousRolledoutRevision
}, timeout, interval).Should(Equal("revision-v1"))
})
})
Context("When creating and deleting inference service without storageUri (multi-model inferenceservice)", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
serviceName := "bar"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
modelConfigMapKey := types.NamespacedName{
Name: constants.ModelConfigName(serviceName, 0),
Namespace: serviceKey.Namespace,
}
ctx := context.Background()
instance := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
SKLearn: &v1beta1.SKLearnSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
RuntimeVersion: ptr.To("1.14.0"),
},
},
},
},
}
It("Should have model config created and mounted", func() {
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
By("By creating a new InferenceService")
Expect(k8sClient.Create(ctx, instance)).Should(Succeed())
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
// Check if InferenceService is created
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
modelConfigMap := &corev1.ConfigMap{}
Eventually(func() bool {
// Check if modelconfig is created
err := k8sClient.Get(ctx, modelConfigMapKey, modelConfigMap)
if err != nil {
return false
}
// Verify that this configmap's ownerreference is it's parent InferenceService
Expect(modelConfigMap.OwnerReferences[0].Name).To(Equal(serviceKey.Name))
return true
}, timeout, interval).Should(BeTrue())
})
})
Context("When creating an inference service using a ServingRuntime", func() {
It("Should create successfully", func() {
serviceName := "svc-with-servingruntime"
namespace := "default"
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: namespace,
}
servingRuntime := &v1alpha1.ServingRuntime{
ObjectMeta: metav1.ObjectMeta{
Name: "tf-serving",
Namespace: "default",
},
Spec: v1alpha1.ServingRuntimeSpec{
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
{
Name: "tensorflow",
Version: ptr.To("1"),
AutoSelect: ptr.To(true),
},
},
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
Labels: map[string]string{
"key1": "val1FromSR",
"key2": "val2FromSR",
},
Annotations: map[string]string{
"key1": "val1FromSR",
"key2": "val2FromSR",
},
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Resources: defaultResource,
},
},
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: "sr-image-pull-secret"},
},
},
Disabled: ptr.To(false),
},
}
Expect(k8sClient.Create(context.TODO(), servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), servingRuntime)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
Labels: map[string]string{
"key2": "val2FromISVC",
},
Annotations: map[string]string{
"key2": "val2FromISVC",
},
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "tensorflow",
},
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
},
},
PodSpec: v1beta1.PodSpec{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: "isvc-image-pull-secret"},
},
},
},
},
Status: v1beta1.InferenceServiceStatus{
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
v1beta1.PredictorComponent: {
LatestReadyRevision: "revision-v1",
},
},
},
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create the InferenceService object and expect the Reconcile and knative service to be created
instance := isvc.DeepCopy()
Expect(k8sClient.Create(context.TODO(), instance)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), instance)
predictorService := &knservingv1.Service{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, predictorService)
}, timeout, interval).Should(Succeed())
expectedPredictorService := &knservingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: constants.PredictorServiceName(serviceName),
Namespace: instance.Namespace,
},
Spec: knservingv1.ServiceSpec{
ConfigurationSpec: knservingv1.ConfigurationSpec{
Template: knservingv1.RevisionTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"serving.kserve.io/inferenceservice": serviceName,
constants.KServiceComponentLabel: constants.Predictor.String(),
"key1": "val1FromSR",
"key2": "val2FromISVC",
},
Annotations: map[string]string{
constants.StorageInitializerSourceUriInternalAnnotationKey: "s3://test/mnist/export",
"autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev",
"autoscaling.knative.dev/max-scale": "3",
"autoscaling.knative.dev/min-scale": "1",
"key1": "val1FromSR",
"key2": "val2FromISVC",
},
},
Spec: knservingv1.RevisionSpec{
ContainerConcurrency: nil,
TimeoutSeconds: nil,
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Env: []corev1.EnvVar{
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
},
Resources: defaultResource,
},
},
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: "isvc-image-pull-secret"},
{Name: "sr-image-pull-secret"},
},
},
},
},
},
RouteSpec: knservingv1.RouteSpec{
Traffic: []knservingv1.TrafficTarget{{LatestRevision: ptr.To(true), Percent: ptr.To(int64(100))}},
},
},
}
// Set ResourceVersion which is required for update operation.
expectedPredictorService.ResourceVersion = predictorService.ResourceVersion
// Do a dry-run update. This will populate our local knative service object with any default values
// that are present on the remote version.
err := k8sClient.Update(context.TODO(), predictorService, client.DryRunAll)
Expect(err).ShouldNot(HaveOccurred())
Expect(cmp.Diff(predictorService.Spec, expectedPredictorService.Spec)).To(Equal(""))
})
})
Context("When creating an inference service with a ServingRuntime which does not exists", func() {
It("Should fail with reason RuntimeNotRecognized", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "svc-with-unknown-servingruntime"
servingRuntimeName := "tf-serving-unknown"
namespace := "default"
predictorServiceKey := types.NamespacedName{Name: serviceName, Namespace: namespace}
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "tensorflow",
},
Runtime: &servingRuntimeName,
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
},
},
},
},
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create the InferenceService object and expect the Reconcile and knative service to be created
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, predictorServiceKey, inferenceService)
if err != nil {
return false
}
if inferenceService.Status.ModelStatus.LastFailureInfo == nil {
return false
}
return true
}, timeout, interval).Should(BeTrue())
failureInfo := v1beta1.FailureInfo{
Reason: v1beta1.RuntimeNotRecognized,
Message: "Waiting for runtime to become available",
}
Expect(inferenceService.Status.ModelStatus.TransitionStatus).To(Equal(v1beta1.InvalidSpec))
Expect(inferenceService.Status.ModelStatus.ModelRevisionStates.TargetModelState).To(Equal(v1beta1.FailedToLoad))
Expect(cmp.Diff(&failureInfo, inferenceService.Status.ModelStatus.LastFailureInfo)).To(Equal(""))
})
})
Context("When creating an inference service with a ServingRuntime which is disabled", func() {
It("Should fail with reason RuntimeDisabled", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "svc-with-disabled-servingruntime"
servingRuntimeName := "tf-serving-disabled"
namespace := "default"
predictorServiceKey := types.NamespacedName{Name: serviceName, Namespace: namespace}
servingRuntime := &v1alpha1.ServingRuntime{
ObjectMeta: metav1.ObjectMeta{
Name: servingRuntimeName,
Namespace: namespace,
},
Spec: v1alpha1.ServingRuntimeSpec{
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
{
Name: "tensorflow",
Version: ptr.To("1"),
AutoSelect: ptr.To(true),
},
},
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Resources: defaultResource,
},
},
},
Disabled: ptr.To(true),
},
}
Expect(k8sClient.Create(context.TODO(), servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), servingRuntime)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "tensorflow",
},
Runtime: &servingRuntimeName,
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
},
},
},
},
}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create the InferenceService object and expect the Reconcile and knative service to be created
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, predictorServiceKey, inferenceService)
if err != nil {
return false
}
if inferenceService.Status.ModelStatus.LastFailureInfo == nil {
return false
}
return true
}, timeout, interval).Should(BeTrue())
failureInfo := v1beta1.FailureInfo{
Reason: v1beta1.RuntimeDisabled,
Message: "Specified runtime is disabled",
}
Expect(inferenceService.Status.ModelStatus.TransitionStatus).To(Equal(v1beta1.InvalidSpec))
Expect(inferenceService.Status.ModelStatus.ModelRevisionStates.TargetModelState).To(Equal(v1beta1.FailedToLoad))
Expect(cmp.Diff(&failureInfo, inferenceService.Status.ModelStatus.LastFailureInfo)).To(Equal(""))
})
})
Context("When creating an inference service with a ServingRuntime which does not support specified model format", func() {
It("Should fail with reason NoSupportingRuntime", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "svc-with-unsupported-servingruntime"
servingRuntimeName := "tf-serving-unsupported"
namespace := "default"
predictorServiceKey := types.NamespacedName{Name: serviceName, Namespace: namespace}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
servingRuntime := &v1alpha1.ServingRuntime{
ObjectMeta: metav1.ObjectMeta{
Name: servingRuntimeName,
Namespace: namespace,
},
Spec: v1alpha1.ServingRuntimeSpec{
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
{
Name: "tensorflow",
Version: ptr.To("1"),
AutoSelect: ptr.To(true),
},
},
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Resources: defaultResource,
},
},
},
Disabled: ptr.To(false),
},
}
Expect(k8sClient.Create(context.TODO(), servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), servingRuntime)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "sklearn",
},
Runtime: &servingRuntimeName,
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/export"),
},
},
},
},
}
// Create the InferenceService object and expect the Reconcile and knative service to be created
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, predictorServiceKey, inferenceService)
if err != nil {
return false
}
if inferenceService.Status.ModelStatus.LastFailureInfo == nil {
return false
}
return true
}, timeout, interval).Should(BeTrue())
failureInfo := v1beta1.FailureInfo{
Reason: v1beta1.NoSupportingRuntime,
Message: "Specified runtime does not support specified framework/version",
}
Expect(inferenceService.Status.ModelStatus.TransitionStatus).To(Equal(v1beta1.InvalidSpec))
Expect(inferenceService.Status.ModelStatus.ModelRevisionStates.TargetModelState).To(Equal(v1beta1.FailedToLoad))
Expect(cmp.Diff(&failureInfo, inferenceService.Status.ModelStatus.LastFailureInfo)).To(Equal(""))
})
})
Context("When creating inference service with storage.kserve.io/readonly", func() {
defaultIsvc := func(namespace string, name string, storageUri string) *v1beta1.InferenceService {
predictor := v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
VolumeMounts: []corev1.VolumeMount{
{Name: "predictor-volume"},
},
},
},
},
}
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: predictor,
},
}
return isvc
}
It("should have the readonly annotation set to true in the knative serving pod spec", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
serviceName := "readonly-true-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations = map[string]string{}
isvc.Annotations[constants.StorageReadonlyAnnotationKey] = "true"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Knative service
actualService := &knservingv1.Service{}
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
// Check readonly value
Expect(actualService.Spec.Template.Annotations[constants.StorageReadonlyAnnotationKey]).
To(Equal("true"))
})
It("should have the readonly annotation set to false in the knative serving pod spec", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, configMap)
serviceName := "readonly-false-isvc"
serviceNamespace := "default"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
serviceKey := expectedRequest.NamespacedName
servingRuntime := getServingRuntime("tf-serving", serviceKey.Namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, &servingRuntime)
// Define InferenceService
isvc := defaultIsvc(serviceKey.Namespace, serviceKey.Name, storageUri)
isvc.Annotations = map[string]string{}
isvc.Annotations[constants.StorageReadonlyAnnotationKey] = "false"
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(ctx, isvc)
// Knative service
actualService := &knservingv1.Service{}
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
// Check readonly value
Expect(actualService.Spec.Template.Annotations[constants.StorageReadonlyAnnotationKey]).
To(Equal("false"))
})
})
Context("When creating an inference service with invalid Storage URI", func() {
It("Should fail with reason ModelLoadFailed", func() {
ctx, cancel := context.WithCancel(context.Background())
DeferCleanup(cancel)
serviceName := "servingruntime-test"
servingRuntimeName := "tf-serving"
namespace := "default"
inferenceServiceKey := types.NamespacedName{Name: serviceName, Namespace: namespace}
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
servingRuntime := getServingRuntime(servingRuntimeName, namespace)
Expect(k8sClient.Create(context.TODO(), &servingRuntime)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), &servingRuntime)
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Model: &v1beta1.ModelSpec{
ModelFormat: v1beta1.ModelFormat{
Name: "tensorflow",
},
Runtime: &servingRuntimeName,
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: ptr.To("s3://test/mnist/invalid"),
},
},
},
},
}
// Create the InferenceService object and expect the Reconcile and knative service to be created
Expect(k8sClient.Create(context.TODO(), isvc)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), isvc)
revisionName := serviceName + "-predictor-" + namespace + "-00001"
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: revisionName + "-deployment-76464ds2zpv",
Namespace: namespace,
Labels: map[string]string{
constants.InferenceServicePodLabelKey: serviceName,
constants.RevisionLabel: revisionName,
},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "storage-initializer",
Image: "kserve/storage-initializer:latest",
Args: []string{
"gs://kserve-invalid/models/sklearn/1.0/model",
"/mnt/models",
},
Resources: defaultResource,
},
},
Containers: []corev1.Container{
{
Name: constants.InferenceServiceContainerName,
Image: "tensorflow/serving:1.14.0",
Command: []string{"/usr/bin/tensorflow_model_server"},
Args: []string{
"--port=9000",
"--rest_api_port=8080",
"--model_base_path=/mnt/models",
"--rest_api_timeout_in_ms=60000",
},
Env: []corev1.EnvVar{
{
Name: "PORT",
Value: "8080",
},
{
Name: "K_REVISION",
Value: serviceName + "-predictor-" + namespace + "-00001",
},
{
Name: "K_CONFIGURATION",
Value: serviceName + "-predictor-" + namespace,
},
{
Name: "K_SERVICE",
Value: serviceName + "-predictor-" + namespace,
},
},
Resources: defaultResource,
},
},
},
}
Eventually(func() bool {
err := k8sClient.Create(context.TODO(), pod)
return err == nil
}, timeout, interval).Should(BeTrue())
defer k8sClient.Delete(context.TODO(), pod)
podStatusPatch := []byte(`{"status":{"containerStatuses":[{"image":"tensorflow/serving:1.14.0","name":"kserve-container","lastState":{},"state":{"waiting":{"reason":"PodInitializing"}}}],"initContainerStatuses":[{"image":"kserve/storage-initializer:latest","name":"storage-initializer","lastState":{"terminated":{"exitCode":1,"message":"Invalid Storage URI provided","reason":"Error"}},"state":{"waiting":{"reason":"CrashLoopBackOff"}}}]}}`)
Eventually(func() bool {
err := k8sClient.Status().Patch(context.TODO(), pod, client.RawPatch(types.StrategicMergePatchType, podStatusPatch))
return err == nil
}, timeout, interval).Should(BeTrue())
actualService := &knservingv1.Service{}
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceName),
Namespace: namespace,
}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceName), namespace, domain))
// update predictor status
updatedService := actualService.DeepCopy()
updatedService.Status.LatestCreatedRevisionName = serviceName + "-predictor-" + namespace + "-00001"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "False",
},
}
Expect(retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return k8sClient.Status().Update(context.TODO(), updatedService)
})).NotTo(HaveOccurred())
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, inferenceServiceKey, inferenceService)
if err != nil {
return false
}
if inferenceService.Status.ModelStatus.LastFailureInfo == nil {
return false
}
return true
}, timeout, interval).Should(BeTrue())
Expect(inferenceService.Status.ModelStatus.TransitionStatus).To(Equal(v1beta1.BlockedByFailedLoad))
Expect(inferenceService.Status.ModelStatus.ModelRevisionStates.TargetModelState).To(Equal(v1beta1.FailedToLoad))
Expect(inferenceService.Status.ModelStatus.LastFailureInfo.Reason).To(Equal(v1beta1.ModelLoadFailed))
Expect(inferenceService.Status.ModelStatus.LastFailureInfo.Message).To(Equal("Invalid Storage URI provided"))
})
})
Context("When creating inference service with predictor and without top level istio virtual service", func() {
It("Should have knative service created", func() {
By("By creating a new InferenceService")
// Create configmap
copiedConfigs := make(map[string]string)
for key, value := range configs {
if key == "ingress" {
copiedConfigs[key] = `{
"kserveIngressGateway": "kserve/kserve-ingress-gateway",
"disableIstioVirtualHost": true,
"ingressGateway": "knative-serving/knative-ingress-gateway",
"localGateway": "knative-serving/knative-local-gateway",
"localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local"
}`
} else {
copiedConfigs[key] = value
}
}
configMap := createInferenceServiceConfigMap(copiedConfigs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
serviceName := "foo-disable-istio"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
inferenceService := &v1beta1.InferenceService{}
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
return err == nil
}, timeout, interval).Should(BeTrue())
actualService := &knservingv1.Service{}
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, actualService)
}, timeout, interval).Should(Succeed())
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor
updatedService := actualService.DeepCopy()
updatedService.Status.LatestCreatedRevisionName = "revision-v1"
updatedService.Status.LatestReadyRevisionName = "revision-v1"
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: "True",
},
}
Expect(k8sClient.Status().Update(context.TODO(), updatedService)).NotTo(HaveOccurred())
// get inference service
actualIsvc := &v1beta1.InferenceService{}
expectedURL := &apis.URL{
Scheme: "http",
Host: constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain),
}
expectedAddressURL := &duckv1.Addressable{
URL: &apis.URL{
Scheme: "http",
Host: network.GetServiceHostname(fmt.Sprintf("%s-%s", serviceKey.Name, string(constants.Predictor)), serviceKey.Namespace),
},
}
Eventually(func() bool {
err := k8sClient.Get(ctx, expectedRequest.NamespacedName, actualIsvc)
if err != nil {
return false
}
return reflect.DeepEqual(actualIsvc.Status.URL, expectedURL) &&
reflect.DeepEqual(actualIsvc.Status.Address.URL, expectedAddressURL.URL)
}, timeout, interval).Should(BeTrue())
})
})
Context("Set CaBundle ConfigMap in inferenceservice-config confimap", func() {
It("Should not create a global cabundle configMap in a user namespace when CaBundleConfigMapName set ''", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
By("By creating a new InferenceService")
serviceName := "sample-isvc"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
caBundleConfigMap := &corev1.ConfigMap{}
Consistently(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{Name: constants.DefaultGlobalCaBundleConfigMapName, Namespace: "default"}, caBundleConfigMap)
if err != nil {
if apierr.IsNotFound(err) {
return true
}
}
return false
}, fastTimeout, interval).Should(BeTrue())
})
It("Should not create a global cabundle configmap in a user namespace when the target cabundle configmap in the 'inferenceservice-config' configmap does not exist", func() {
// Create configmap
copiedConfigs := make(map[string]string)
for key, value := range configs {
if key == "storageInitializer" {
copiedConfigs[key] = `{
"image" : "kserve/storage-initializer:latest",
"memoryRequest": "100Mi",
"memoryLimit": "1Gi",
"cpuRequest": "100m",
"cpuLimit": "1",
"cpuModelcar": "10m",
"memoryModelcar": "15Mi",
"CaBundleConfigMapName": "not-exist-configmap",
"caBundleVolumeMountPath": "/etc/ssl/custom-certs"
}`
} else {
copiedConfigs[key] = value
}
}
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
By("By creating a new InferenceService")
serviceName := "sample-isvc-2"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
caBundleConfigMap := &corev1.ConfigMap{}
Consistently(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{Name: constants.DefaultGlobalCaBundleConfigMapName, Namespace: "default"}, caBundleConfigMap)
if err != nil {
if apierr.IsNotFound(err) {
return true
}
}
return false
}, fastTimeout, interval).Should(BeTrue())
})
It("Should not create a global cabundle configmap in a user namespace when the cabundle.crt file data does not exist in the target cabundle configmap in the 'inferenceservice-config' configmap", func() {
// Create configmap
copiedConfigs := make(map[string]string)
for key, value := range configs {
if key == "storageInitializer" {
copiedConfigs[key] = `{
"image" : "kserve/storage-initializer:latest",
"memoryRequest": "100Mi",
"memoryLimit": "1Gi",
"cpuRequest": "100m",
"cpuLimit": "1",
"cpuModelcar": "10m",
"memoryModelcar": "15Mi",
"CaBundleConfigMapName": "test-cabundle-with-wrong-file-name",
"caBundleVolumeMountPath": "/etc/ssl/custom-certs"
}`
} else {
copiedConfigs[key] = value
}
}
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create original cabundle configmap with wrong file name
cabundleConfigMapData := make(map[string]string)
cabundleConfigMapData["wrong-cabundle-name.crt"] = "SAMPLE_CA_BUNDLE"
originalCabundleConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cabundle-with-wrong-file-name",
Namespace: constants.KServeNamespace,
},
Data: cabundleConfigMapData,
}
Expect(k8sClient.Create(context.TODO(), originalCabundleConfigMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), originalCabundleConfigMap)
By("By creating a new InferenceService")
serviceName := "sample-isvc-3"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
caBundleConfigMap := &corev1.ConfigMap{}
Consistently(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{Name: constants.DefaultGlobalCaBundleConfigMapName, Namespace: "default"}, caBundleConfigMap)
if err != nil {
if apierr.IsNotFound(err) {
return true
}
}
return false
}, fastTimeout, interval).Should(BeTrue())
})
It("Should create a global cabundle configmap in a user namespace when it meets all conditions and an inferenceservice is created", func() {
// Create configmap
copiedConfigs := make(map[string]string)
for key, value := range configs {
if key == "storageInitializer" {
copiedConfigs[key] = `{
"image" : "kserve/storage-initializer:latest",
"memoryRequest": "100Mi",
"memoryLimit": "1Gi",
"cpuRequest": "100m",
"cpuLimit": "1",
"cpuModelcar": "10m",
"memoryModelcar": "15Mi",
"CaBundleConfigMapName": "test-cabundle-with-right-file-name",
"caBundleVolumeMountPath": "/etc/ssl/custom-certs"
}`
} else {
copiedConfigs[key] = value
}
}
configMap := createInferenceServiceConfigMap(copiedConfigs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create original cabundle configmap with right file name
cabundleConfigMapData := make(map[string]string)
// cabundle data
cabundleConfigMapData["cabundle.crt"] = "SAMPLE_CA_BUNDLE"
originalCabundleConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cabundle-with-right-file-name",
Namespace: constants.KServeNamespace,
},
Data: cabundleConfigMapData,
}
Expect(k8sClient.Create(context.TODO(), originalCabundleConfigMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), originalCabundleConfigMap)
By("By creating a new InferenceService")
serviceName := "sample-isvc-4"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
caBundleConfigMap := &corev1.ConfigMap{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{Name: constants.DefaultGlobalCaBundleConfigMapName, Namespace: "default"}, caBundleConfigMap)
if err != nil {
if apierr.IsNotFound(err) {
return false
}
}
return true
}, timeout, interval).Should(BeTrue())
})
})
Context("If the InferenceService occurred any error", func() {
It("InferenceService should generate event message about non-ready conditions", func() {
// Create configmap
configMap := createInferenceServiceConfigMap(configs)
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
defer k8sClient.Delete(context.TODO(), configMap)
// Create ServingRuntime
servingRuntime := getServingRuntime("tf-serving", "default")
_ = k8sClient.Create(context.TODO(), &servingRuntime)
defer k8sClient.Delete(context.TODO(), &servingRuntime)
serviceName := "test-err-msg"
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
serviceKey := expectedRequest.NamespacedName
ctx := context.Background()
isvc := &v1beta1.InferenceService{
ObjectMeta: metav1.ObjectMeta{
Name: serviceKey.Name,
Namespace: serviceKey.Namespace,
},
Spec: v1beta1.InferenceServiceSpec{
Predictor: v1beta1.PredictorSpec{
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
},
Tensorflow: &v1beta1.TFServingSpec{
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
StorageURI: &storageUri,
RuntimeVersion: ptr.To("1.14.0"),
Container: corev1.Container{
Name: constants.InferenceServiceContainerName,
Resources: defaultResource,
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
defer k8sClient.Delete(ctx, isvc)
inferenceService := &v1beta1.InferenceService{}
updatedService := &knservingv1.Service{}
predictorServiceKey := types.NamespacedName{
Name: constants.PredictorServiceName(serviceKey.Name),
Namespace: serviceKey.Namespace,
}
Eventually(func() error {
return k8sClient.Get(context.TODO(), predictorServiceKey, updatedService)
}, timeout, interval).Should(Succeed())
predictorUrl, _ := apis.ParseURL("http://" + constants.InferenceServiceHostName(constants.PredictorServiceName(serviceKey.Name), serviceKey.Namespace, domain))
// update predictor status
updatedService.Status.URL = predictorUrl
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: corev1.ConditionTrue,
},
}
Expect(retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return k8sClient.Status().Update(context.TODO(), updatedService)
})).NotTo(HaveOccurred())
// assert inference service predictor status
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
if err != nil {
return false
}
for _, cond := range inferenceService.Status.Conditions {
if cond.Status == corev1.ConditionTrue {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())
// should turn to fail
updatedService.Status.Conditions = duckv1.Conditions{
{
Type: knservingv1.ServiceConditionReady,
Status: corev1.ConditionFalse,
},
}
Expect(retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return k8sClient.Status().Update(context.TODO(), updatedService)
})).NotTo(HaveOccurred())
// assert inference service predictor status
Eventually(func() bool {
err := k8sClient.Get(ctx, serviceKey, inferenceService)
if err != nil {
return false
}
for _, cond := range inferenceService.Status.Conditions {
if cond.Status == corev1.ConditionFalse {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())
r := &InferenceServiceReconciler{
Client: k8sClient,
}
Eventually(func() bool {
events := &corev1.EventList{}
err := k8sClient.List(ctx, events, client.InNamespace(serviceKey.Namespace))
if err != nil {
return false
}
notReadyEventFound := false
for _, event := range events.Items {
if event.InvolvedObject.Kind == "InferenceService" &&
event.InvolvedObject.Name == serviceKey.Name &&
event.Reason == string(InferenceServiceNotReadyState) {
notReadyEventFound = true
break
}
}
err = k8sClient.Get(ctx, serviceKey, inferenceService)
if err != nil {
return false
}
failConditions := r.GetFailConditions(inferenceService)
expectedConditions := strings.Split(failConditions, ", ")
for _, expectedCondition := range expectedConditions {
found := false
for _, cond := range inferenceService.Status.Conditions {
if string(cond.Type) == expectedCondition && cond.Status == corev1.ConditionFalse {
found = true
break
}
}
if !found {
return false
}
}
return notReadyEventFound
}, timeout, interval).Should(BeTrue())
})
})
})