mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
Signed-off-by: Vedant Mahabaleshwarkar <[email protected]>
10128 lines
369 KiB
Go
10128 lines
369 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"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
|
|
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
|
corev1 "k8s.io/api/core/v1"
|
|
netv1 "k8s.io/api/networking/v1"
|
|
apierr "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"k8s.io/utils/ptr"
|
|
"knative.dev/pkg/apis"
|
|
duckv1 "knative.dev/pkg/apis/duck/v1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
|
|
|
|
"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/controller/v1beta1/inferenceservice/components"
|
|
"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.
|
|
|
|
kserveGateway := types.NamespacedName{Name: "kserve-ingress-gateway", Namespace: "kserve"}
|
|
|
|
configs := getRawKubeTestConfigs()
|
|
|
|
// Checks that any Kubernetes object does not exist
|
|
expectResourceDoesNotExist := 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)
|
|
}
|
|
|
|
Context("When creating inference service with raw kube predictor", func() {
|
|
It("Should have httproute/service/deployment/httproute 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 := getServingRuntime("tf-serving-raw", "default")
|
|
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
serviceName := "raw-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,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.AverageValueMetricType,
|
|
AverageValue: v1beta1.NewMetricQuantity("10Gi"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedDeployment := getDeploymentWithKServiceLabel(predictorDeploymentKey, serviceName, isvc)
|
|
Expect(actualDeployment.Spec).To(BeComparableTo(expectedDeployment.Spec))
|
|
|
|
// Verify INFERENCE_SERVICE_NAME environment variable is set
|
|
verifyEnvKeyValueDeployments(actualDeployment, constants.InferenceServiceNameEnvVarKey, serviceName)
|
|
|
|
// check service
|
|
actualService := &corev1.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 := getExectedService(predictorServiceKey, serviceName)
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
|
|
|
|
// check isvc status
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http route
|
|
actualTopLevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualTopLevelHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedTopLevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace))},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualTopLevelHttpRoute.Spec).To(BeComparableTo(expectedTopLevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualTopLevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualTopLevelHttpRoute)).NotTo(HaveOccurred())
|
|
|
|
// verify if InferenceService status is updated
|
|
expectedIsvcStatus := getExpectedIsvcStatus(serviceKey)
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check HPA
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
expectedHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceMemory,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.AverageValueMetricType,
|
|
AverageValue: &v1beta1.NewMetricQuantity("10Gi").Quantity,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualHPA.Spec).To(BeComparableTo(expectedHPA.Spec))
|
|
})
|
|
|
|
It("Should have httproute/service/deployment/hpa created with DeploymentStrategy", func() {
|
|
By("By creating a new InferenceService with DeploymentStrategy in PredictorSpec")
|
|
// 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-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-customized"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
var cpuUtilization int32 = 75
|
|
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
DeploymentStrategy: &appsv1.DeploymentStrategy{
|
|
Type: appsv1.RecreateDeploymentStrategyType,
|
|
},
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment) }, timeout).
|
|
Should(Succeed())
|
|
|
|
expectedDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: predictorDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Predictor.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: map[string]string{
|
|
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassHPA),
|
|
},
|
|
},
|
|
Spec: 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,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
// This is now customized and different from defaults set via `setDefaultDeploymentSpec`.
|
|
Strategy: appsv1.DeploymentStrategy{
|
|
Type: appsv1.RecreateDeploymentStrategyType,
|
|
},
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualDeployment.Spec).To(BeComparableTo(expectedDeployment.Spec))
|
|
|
|
// check isvc status
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check service
|
|
actualService := &corev1.Service{}
|
|
predictorServiceKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorServiceKey, actualService) }, timeout).
|
|
Should(Succeed())
|
|
|
|
expectedService := getExectedService(predictorServiceKey, serviceName)
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
|
|
|
|
// check http route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout).
|
|
Should(Succeed())
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace))},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout).
|
|
Should(Succeed())
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).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: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: "raw-foo-customized-default.example.com",
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: "raw-foo-customized-predictor-default.example.com",
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
// Check that the ISVC was updated
|
|
actualIsvc := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, actualIsvc)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Check that the inference service is ready
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, actualIsvc)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return actualIsvc.Status.IsConditionReady(v1beta1.PredictorReady)
|
|
}, timeout, interval).Should(BeTrue(), "The predictor should be ready")
|
|
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, actualIsvc)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return actualIsvc.Status.IsConditionReady(v1beta1.IngressReady)
|
|
}, timeout, interval).Should(BeTrue(), "The ingress should be ready")
|
|
diff := cmp.Diff(&expectedIsvcStatus, &actualIsvc.Status, cmpopts.IgnoreTypes(apis.VolatileTime{}))
|
|
Expect(diff).To(BeEmpty())
|
|
|
|
// check HPA
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA) }, timeout).
|
|
Should(Succeed())
|
|
expectedHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualHPA.Spec).To(BeComparableTo(expectedHPA.Spec))
|
|
})
|
|
It("Should have httproute/service/deployment created", func() {
|
|
By("By creating a new InferenceService with AutoscalerClass None")
|
|
// 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-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-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,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment) }, timeout).
|
|
Should(Succeed())
|
|
|
|
expectedDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: predictorDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Predictor.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: map[string]string{
|
|
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassNone),
|
|
},
|
|
},
|
|
Spec: 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,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
Strategy: getDefaultRollingStrategy(),
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualDeployment.Spec).To(BeComparableTo(expectedDeployment.Spec))
|
|
|
|
// check service
|
|
actualService := &corev1.Service{}
|
|
predictorServiceKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorServiceKey, actualService) }, timeout).
|
|
Should(Succeed())
|
|
|
|
expectedService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: predictorServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: "raw-foo-2-predictor",
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc.raw-foo-2-predictor",
|
|
},
|
|
},
|
|
}
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
|
|
|
|
// check isvc status
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http Route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout).
|
|
Should(Succeed())
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace))},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout).
|
|
Should(Succeed())
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).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: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: "raw-foo-2-default.example.com",
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: "raw-foo-2-predictor-default.example.com",
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}), cmpopts.IgnoreFields(v1beta1.InferenceServiceStatus{}, "DeploymentMode"))
|
|
}, timeout).Should(BeEmpty())
|
|
|
|
// check HPA is not created
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA) }, timeout).
|
|
Should(HaveOccurred())
|
|
|
|
// Replica should not be nil and it should be set to minReplicas if it was set.
|
|
updated_isvc := &v1beta1.InferenceService{}
|
|
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, serviceKey, updated_isvc)
|
|
}, timeout, interval).Should(Succeed())
|
|
if updated_isvc.Labels == nil {
|
|
updated_isvc.Labels = make(map[string]string)
|
|
}
|
|
updated_isvc.Spec.Predictor.ComponentExtensionSpec = v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(2)),
|
|
}
|
|
Expect(k8sClient.Update(context.TODO(), updated_isvc)).NotTo(HaveOccurred())
|
|
|
|
updatedDeployment_isvc_updated := &appsv1.Deployment{}
|
|
Eventually(func() bool {
|
|
if err := k8sClient.Get(context.TODO(), predictorDeploymentKey, updatedDeployment_isvc_updated); err == nil {
|
|
return updatedDeployment_isvc_updated.Spec.Replicas != nil && *updatedDeployment_isvc_updated.Spec.Replicas == 2
|
|
} else {
|
|
return false
|
|
}
|
|
}, timeout, interval).Should(BeTrue())
|
|
})
|
|
})
|
|
Context("When updating ISVC envs", func() {
|
|
It("Should reconcile the deployment if isvc envs are updated", func() {
|
|
defaultEnvs := []corev1.EnvVar{
|
|
{
|
|
Name: "env1",
|
|
Value: "val1",
|
|
},
|
|
{
|
|
Name: "env2",
|
|
Value: "val2",
|
|
},
|
|
{
|
|
Name: "env3",
|
|
Value: "val3",
|
|
},
|
|
}
|
|
|
|
// 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-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-test-env"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
// create isvc
|
|
|
|
isvcOriginal := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: map[string]string{
|
|
"serving.kserve.io/deploymentMode": "Standard",
|
|
"serving.kserve.io/autoscalerClass": "hpa",
|
|
"serving.kserve.io/metrics": "cpu",
|
|
"serving.kserve.io/targetUtilizationPercentage": "75",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvcOriginal.Spec.Predictor.Tensorflow.Env = defaultEnvs
|
|
|
|
isvcOriginal.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
|
|
Expect(k8sClient.Create(context.TODO(), isvcOriginal)).Should(Succeed())
|
|
|
|
inferenceService := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(context.TODO(), serviceKey, inferenceService)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
deployed1 := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, deployed1)
|
|
}, timeout, interval).Should(Succeed())
|
|
Expect(deployed1.Spec.Template.Spec.Containers[0].Env).To(ContainElements(defaultEnvs))
|
|
|
|
// Now, update the isvc with new env
|
|
newEnvs := []corev1.EnvVar{
|
|
{
|
|
Name: "newEnv1",
|
|
Value: "newValue1",
|
|
},
|
|
{
|
|
Name: "newEnv2",
|
|
Value: "delete",
|
|
},
|
|
}
|
|
|
|
// Update the isvc to add new envs
|
|
fmt.Fprintln(GinkgoWriter, "### Adding new envs")
|
|
isvcUpdated1 := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
// get the latest deployed version
|
|
if err := k8sClient.Get(context.TODO(), serviceKey, inferenceService); err != nil {
|
|
return false
|
|
}
|
|
|
|
isvcUpdated1 = inferenceService.DeepCopy()
|
|
isvcUpdated1.Spec.Predictor.Model.Env = append(isvcUpdated1.Spec.Predictor.Model.Env, newEnvs...)
|
|
if err1 := k8sClient.Update(context.TODO(), isvcUpdated1); err1 != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// The deployment should be reconciled
|
|
deployed2 := &appsv1.Deployment{}
|
|
appendedEnvs := append(defaultEnvs, newEnvs...)
|
|
Eventually(func() []corev1.EnvVar {
|
|
_ = k8sClient.Get(context.TODO(), predictorDeploymentKey, deployed2)
|
|
return deployed2.Spec.Template.Spec.Containers[0].Env
|
|
}, timeout, interval).Should(ContainElements(appendedEnvs))
|
|
|
|
// Now remove the default envs and update the isvc
|
|
fmt.Fprintln(GinkgoWriter, "### Removing default envs")
|
|
isvcUpdated2 := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
// get the latest deployed version
|
|
if err := k8sClient.Get(context.TODO(), serviceKey, isvcUpdated1); err != nil {
|
|
return false
|
|
}
|
|
|
|
isvcUpdated2 = isvcUpdated1.DeepCopy()
|
|
isvcUpdated2.Spec.Predictor.Model.Env = newEnvs
|
|
// Make sure the default envs were removed before updating the isvc
|
|
Expect(isvcUpdated2.Spec.Predictor.Model.Env).ToNot(ContainElements(defaultEnvs))
|
|
|
|
if err1 := k8sClient.Update(context.TODO(), isvcUpdated2); err1 != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
deployed3 := &appsv1.Deployment{}
|
|
Eventually(func() []corev1.EnvVar {
|
|
_ = k8sClient.Get(context.TODO(), predictorDeploymentKey, deployed3)
|
|
return deployed3.Spec.Template.Spec.Containers[0].Env
|
|
}, timeout, interval).Should(Not(ContainElements(defaultEnvs)))
|
|
|
|
Expect(deployed3.Spec.Template.Spec.Containers[0].Env).ToNot(ContainElement(HaveField("Value", "env_marked_for_deletion")))
|
|
Expect(deployed3.Spec.Template.Spec.Containers[0].Env).To(ContainElements(newEnvs))
|
|
})
|
|
})
|
|
|
|
Context("When creating inference service with raw kube predictor and serving.kserve.io/stop", func() {
|
|
// --- Default values ---
|
|
configs := getRawKubeTestConfigs()
|
|
|
|
defaultIsvc := func(serviceKey types.NamespacedName, autoscaler string, qty *v1beta1.MetricQuantity) *v1beta1.InferenceService {
|
|
predictor := v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.AverageValueMetricType,
|
|
AverageValue: qty,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
}
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: map[string]string{
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: autoscaler,
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: predictor,
|
|
},
|
|
}
|
|
|
|
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
|
|
return isvc
|
|
}
|
|
|
|
// --- Reusable Check Functions ---
|
|
// Wait for the ISVC to exist.
|
|
expectIsvcToExist := func(ctx context.Context, serviceKey types.NamespacedName) v1beta1.InferenceService {
|
|
// Check that the ISVC was updated
|
|
updatedIsvc := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
return *updatedIsvc
|
|
}
|
|
|
|
// Wait for the Deployment to exist and then set the DeploymentAvailable condition to true
|
|
expectDeploymentToBeReady := func(ctx context.Context, predictorKey types.NamespacedName) {
|
|
actualDeployment := &appsv1.Deployment{}
|
|
Eventually(func() error { return k8sClient.Get(ctx, predictorKey, actualDeployment) }, timeout).
|
|
Should(Succeed())
|
|
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(ctx, updatedDeployment)).NotTo(HaveOccurred())
|
|
}
|
|
|
|
// Waits for the http route to be ready
|
|
// Note: top level route uses serviceKey, predictor route uses predictorKey
|
|
expectHttpRouteToBeReady := func(ctx context.Context, objKey types.NamespacedName) {
|
|
actualHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, objKey, actualHttpRoute)
|
|
}, timeout).Should(Succeed())
|
|
|
|
// Mark the route as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(ctx, actualHttpRoute)).NotTo(HaveOccurred())
|
|
}
|
|
|
|
// Wait for the InferenceService's Stopped condition to be false.
|
|
expectIsvcFalseStoppedStatus := func(ctx context.Context, serviceKey types.NamespacedName) {
|
|
// Check that the stopped condition is false
|
|
updatedIsvc := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
|
|
if err == nil {
|
|
stopped_cond := updatedIsvc.Status.GetCondition(v1beta1.Stopped)
|
|
if stopped_cond != nil && stopped_cond.Status == corev1.ConditionFalse {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, timeout, interval).Should(BeTrue(), "The stopped condition should be set to false")
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// Wait for the InferenceService's Stopped condition to be true.
|
|
expectIsvcTrueStoppedStatus := func(ctx context.Context, serviceKey types.NamespacedName) {
|
|
// Check that the ISVC status reflects that it is stopped
|
|
updatedIsvc := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
|
|
if err == nil {
|
|
stopped_cond := updatedIsvc.Status.GetCondition(v1beta1.Stopped)
|
|
if stopped_cond != nil && stopped_cond.Status == corev1.ConditionTrue {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, timeout, interval).Should(BeTrue(), "The stopped condition should be set to true")
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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 httproute/service/deployment/hpa created when the annotation is set to false", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
// Config map
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-false-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassHPA), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
|
|
// check the service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(ctx, serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(ctx, predictorKey)
|
|
|
|
// check the HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should keep the ingress/service/deployment/keda/otel created when gateway api is disabled and the annotation is set to false", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
// Config map
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
configMap.Data["ingress"] = `{
|
|
"enableGatewayAPI": false,
|
|
"ingressGateway": "knative-serving/knative-ingress-gateway",
|
|
"localGateway": "knative-serving/knative-local-gateway",
|
|
"localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local"
|
|
}`
|
|
configMap.Data["opentelemetryCollector"] = `{
|
|
"scrapeInterval": "5s",
|
|
"metricReceiverEndpoint": "keda-otel-scaler.keda.svc:4317",
|
|
"metricScalerEndpoint": "keda-otel-scaler.keda.svc:4318"
|
|
}`
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-false-ingress-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassKeda), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
isvc.Annotations["sidecar.opentelemetry.io/inject"] = "true"
|
|
isvc.Spec.Predictor.AutoScaling = &v1beta1.AutoScalingSpec{
|
|
Metrics: getDefaultMetrics(),
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the OpenTelemetry Collector
|
|
expectResourceToExist(ctx, &otelv1beta1.OpenTelemetryCollector{}, predictorKey)
|
|
|
|
// Check the deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
|
|
// Check the service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// Check ingress
|
|
expectResourceToExist(ctx, &netv1.Ingress{}, serviceKey)
|
|
|
|
// Check KEDA
|
|
expectResourceToExist(ctx, &kedav1alpha1.ScaledObject{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should not create the httproute/service/deployment/hpa when the annotation is set to true", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
// Config map
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-true-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassHPA), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, predictorKey)
|
|
|
|
// check that the service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// check that the http routes were not created
|
|
// top level http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, predictorKey)
|
|
|
|
// check that the HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should not create the ingress/service/deployment/keda/otel when gateway api is disabled and the annotation is set to true", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
// Config map
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
configMap.Data["ingress"] = `{
|
|
"enableGatewayAPI": false,
|
|
"ingressGateway": "knative-serving/knative-ingress-gateway",
|
|
"localGateway": "knative-serving/knative-local-gateway",
|
|
"localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local"
|
|
}`
|
|
configMap.Data["opentelemetryCollector"] = `{
|
|
"scrapeInterval": "5s",
|
|
"metricReceiverEndpoint": "keda-otel-scaler.keda.svc:4317",
|
|
"metricScalerEndpoint": "keda-otel-scaler.keda.svc:4318"
|
|
}`
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-true-ingress-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(context.Background(), &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassKeda), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
isvc.Annotations["sidecar.opentelemetry.io/inject"] = "true"
|
|
isvc.Spec.Predictor.AutoScaling = &v1beta1.AutoScalingSpec{
|
|
Metrics: getDefaultMetrics(),
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the OpenTelemetry Collector was not created
|
|
expectResourceDoesNotExist(ctx, &otelv1beta1.OpenTelemetryCollector{}, predictorKey)
|
|
|
|
// Check that the deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, predictorKey)
|
|
|
|
// check that the service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// Check that the ingress was not created
|
|
expectResourceDoesNotExist(ctx, &netv1.Ingress{}, serviceKey)
|
|
|
|
// Check that the KEDA autoscaler was not created
|
|
expectResourceDoesNotExist(ctx, &kedav1alpha1.ScaledObject{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should delete the httproute/service/deployment/hpa 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(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-edit-true-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassHPA), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
|
|
// check the service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// check the http routes
|
|
// top level http route
|
|
expectHttpRouteToBeReady(ctx, serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(ctx, predictorKey)
|
|
|
|
// check the HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
|
|
// Stop the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check that the deployment was deleted
|
|
expectResourceToBeDeleted(ctx, &appsv1.Deployment{}, predictorKey)
|
|
|
|
// check that the service was deleted
|
|
expectResourceToBeDeleted(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// check that the http routes were deleted
|
|
// top level http route
|
|
expectResourceToBeDeleted(ctx, &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceToBeDeleted(ctx, &gwapiv1.HTTPRoute{}, predictorKey)
|
|
|
|
// check that the HPA was deleted
|
|
expectResourceToBeDeleted(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should delete the ingress/service/deployment/keda/otel when gateway api is disabled and the annotation is updated to true on an existing ISVC", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
// Config map
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
configMap.Data["ingress"] = `{
|
|
"enableGatewayAPI": false,
|
|
"ingressGateway": "knative-serving/knative-ingress-gateway",
|
|
"localGateway": "knative-serving/knative-local-gateway",
|
|
"localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local"
|
|
}`
|
|
configMap.Data["opentelemetryCollector"] = `{
|
|
"scrapeInterval": "5s",
|
|
"metricReceiverEndpoint": "keda-otel-scaler.keda.svc:4317",
|
|
"metricScalerEndpoint": "keda-otel-scaler.keda.svc:4318"
|
|
}`
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-edit-ingress-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassKeda), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
isvc.Annotations["sidecar.opentelemetry.io/inject"] = "true"
|
|
isvc.Spec.Predictor.AutoScaling = &v1beta1.AutoScalingSpec{
|
|
Metrics: getDefaultMetrics(),
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the OpenTelemetry Collector
|
|
expectResourceToExist(ctx, &otelv1beta1.OpenTelemetryCollector{}, predictorKey)
|
|
|
|
// Check the deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
|
|
// Check the service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// Check ingress
|
|
expectResourceToExist(ctx, &netv1.Ingress{}, serviceKey)
|
|
|
|
// Check KEDA
|
|
expectResourceToExist(ctx, &kedav1alpha1.ScaledObject{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
|
|
// Stop the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check that the OpenTelemetry Collector was deleted
|
|
expectResourceToBeDeleted(ctx, &otelv1beta1.OpenTelemetryCollector{}, predictorKey)
|
|
|
|
// Check that the deployment was deleted
|
|
expectResourceToBeDeleted(ctx, &appsv1.Deployment{}, predictorKey)
|
|
|
|
// check that the service was deleted
|
|
expectResourceToBeDeleted(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// Check that the ingress was deleted
|
|
expectResourceToBeDeleted(ctx, &netv1.Ingress{}, serviceKey)
|
|
|
|
// Check that the KEDA autoscaler was deleted
|
|
expectResourceToBeDeleted(ctx, &kedav1alpha1.ScaledObject{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should create the httproute/service/deployment/hpa 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(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-edit-false-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassHPA), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, predictorKey)
|
|
|
|
// check that the service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// check that the http routes were not created
|
|
// top level http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, predictorKey)
|
|
|
|
// check that the predictor HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
|
|
// Resume the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check the deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
|
|
// check the service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(ctx, serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(ctx, predictorKey)
|
|
|
|
// check the HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should create the ingress/service/deployment/keda/otel when gateway api is disabled 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)
|
|
configMap.Data["ingress"] = `{
|
|
"enableGatewayAPI": false,
|
|
"ingressGateway": "knative-serving/knative-ingress-gateway",
|
|
"localGateway": "knative-serving/knative-local-gateway",
|
|
"localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local"
|
|
}`
|
|
configMap.Data["opentelemetryCollector"] = `{
|
|
"scrapeInterval": "5s",
|
|
"metricReceiverEndpoint": "keda-otel-scaler.keda.svc:4317",
|
|
"metricScalerEndpoint": "keda-otel-scaler.keda.svc:4318"
|
|
}`
|
|
Expect(k8sClient.Create(context.Background(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-edit-false-ingress-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(context.Background(), &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultIsvc(serviceKey, string(constants.AutoscalerClassKeda), v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
isvc.Annotations["sidecar.opentelemetry.io/inject"] = "true"
|
|
isvc.Spec.Predictor.AutoScaling = &v1beta1.AutoScalingSpec{
|
|
Metrics: getDefaultMetrics(),
|
|
}
|
|
Expect(k8sClient.Create(context.Background(), isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the OpenTelemetry Collector was not created
|
|
expectResourceDoesNotExist(ctx, &otelv1beta1.OpenTelemetryCollector{}, predictorKey)
|
|
|
|
// Check that the deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, predictorKey)
|
|
|
|
// check that the service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// Check that the ingress was not created
|
|
expectResourceDoesNotExist(ctx, &netv1.Ingress{}, serviceKey)
|
|
|
|
// Check that the KEDA autoscaler was not created
|
|
expectResourceDoesNotExist(ctx, &kedav1alpha1.ScaledObject{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
|
|
// Resume the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check the OpenTelemetry Collector
|
|
expectResourceToExist(ctx, &otelv1beta1.OpenTelemetryCollector{}, predictorKey)
|
|
|
|
// Check the deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
|
|
// Check the service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
|
|
// Check ingress
|
|
expectResourceToExist(ctx, &netv1.Ingress{}, serviceKey)
|
|
|
|
// Check KEDA
|
|
expectResourceToExist(ctx, &kedav1alpha1.ScaledObject{}, predictorKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
})
|
|
})
|
|
|
|
Describe("inference service with a transformer", func() {
|
|
// --- Default values ---
|
|
defaultTransformerIsvc := func(serviceKey types.NamespacedName, qty *v1beta1.MetricQuantity) *v1beta1.InferenceService {
|
|
predictor := v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.AverageValueMetricType,
|
|
AverageValue: qty,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
}
|
|
transformer := &v1beta1.TransformerSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 1,
|
|
ScaleTarget: ptr.To(int32(80)),
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
},
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Image: "transformer:v1",
|
|
Resources: defaultResource,
|
|
Args: []string{
|
|
"--port=8080",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: predictor,
|
|
Transformer: transformer,
|
|
},
|
|
}
|
|
|
|
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
|
|
return isvc
|
|
}
|
|
|
|
It("Should keep the transformer httproute/service/deployment/hpa created when the annotation is set to false", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
// Config map
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-transformer-false-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
transformerKey := types.NamespacedName{
|
|
Name: constants.TransformerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultTransformerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the predictor deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
// Check the transformer deployment
|
|
expectDeploymentToBeReady(ctx, transformerKey)
|
|
|
|
// check the predictor service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
// check the transformer service
|
|
expectResourceToExist(ctx, &corev1.Service{}, transformerKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(ctx, serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(ctx, predictorKey)
|
|
// transformer http route
|
|
expectHttpRouteToBeReady(ctx, transformerKey)
|
|
|
|
// check the predictor HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check the transformer HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, transformerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
expectIsvcTransformerReadyStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should not create the transformer httproute/service/deployment/hpa when the annotation is set to true", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
// Config map
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-transformer-true-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
transformerKey := types.NamespacedName{
|
|
Name: constants.TransformerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultTransformerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the predictor deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, predictorKey)
|
|
// Check that the transformer deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, transformerKey)
|
|
|
|
// check that the predictor service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, predictorKey)
|
|
// check that the transformer service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, transformerKey)
|
|
|
|
// check that the http routes were not created
|
|
// top level http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, predictorKey)
|
|
// transformer http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, transformerKey)
|
|
|
|
// check that the predictor HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check that the transformer HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, transformerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should delete the transformer httproute/service/deployment/hpa 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(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-transformer-edit-true-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
transformerKey := types.NamespacedName{
|
|
Name: constants.TransformerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(context.Background(), &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultTransformerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the predictor deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
// Check the transformer deployment
|
|
expectDeploymentToBeReady(ctx, transformerKey)
|
|
|
|
// check the predictor service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
// check the transformer service
|
|
expectResourceToExist(ctx, &corev1.Service{}, transformerKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(ctx, serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(ctx, predictorKey)
|
|
// transformer http route
|
|
expectHttpRouteToBeReady(ctx, transformerKey)
|
|
|
|
// check the predictor HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check the transformer HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, transformerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
expectIsvcTransformerReadyStatus(ctx, serviceKey)
|
|
|
|
// Stop the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check that the predictor deployment was deleted
|
|
expectResourceToBeDeleted(ctx, &appsv1.Deployment{}, predictorKey)
|
|
// Check that the transformer deployment was deleted
|
|
expectResourceToBeDeleted(ctx, &appsv1.Deployment{}, transformerKey)
|
|
|
|
// check that the predictor service was deleted
|
|
expectResourceToBeDeleted(ctx, &corev1.Service{}, predictorKey)
|
|
// check that the transformer service was deleted
|
|
expectResourceToBeDeleted(ctx, &corev1.Service{}, transformerKey)
|
|
|
|
// check that the http routes were deleted
|
|
// top level http route
|
|
expectResourceToBeDeleted(ctx, &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceToBeDeleted(ctx, &gwapiv1.HTTPRoute{}, predictorKey)
|
|
// transformer http route
|
|
expectResourceToBeDeleted(ctx, &gwapiv1.HTTPRoute{}, transformerKey)
|
|
|
|
// check that the predictor HPA was deleted
|
|
expectResourceToBeDeleted(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check that the transformer HPA was deleted
|
|
expectResourceToBeDeleted(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, transformerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should create the transformer httproute/service/deployment/hpa 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(ctx, configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-transformer-edit-false-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
transformerKey := types.NamespacedName{
|
|
Name: constants.TransformerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultTransformerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Create(ctx, isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the predictor deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, predictorKey)
|
|
// Check that the transformer deployment was not created
|
|
expectResourceDoesNotExist(ctx, &appsv1.Deployment{}, transformerKey)
|
|
|
|
// check that the predictor service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, predictorKey)
|
|
// check that the transformer service was not created
|
|
expectResourceDoesNotExist(ctx, &corev1.Service{}, transformerKey)
|
|
|
|
// check that the http routes were not created
|
|
// top level http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, predictorKey)
|
|
// transformer http route
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, transformerKey)
|
|
|
|
// check that the predictor HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check that the transformer HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, transformerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
|
|
// Resume the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check the predictor deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
// Check the transformer deployment
|
|
expectDeploymentToBeReady(ctx, transformerKey)
|
|
|
|
// check the predictor service
|
|
expectResourceToExist(ctx, &corev1.Service{}, predictorKey)
|
|
// check the transformer service
|
|
expectResourceToExist(ctx, &corev1.Service{}, transformerKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(ctx, serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(ctx, predictorKey)
|
|
// transformer http route
|
|
expectHttpRouteToBeReady(ctx, transformerKey)
|
|
|
|
// check the predictor HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check the transformer HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, transformerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
expectIsvcTransformerReadyStatus(ctx, serviceKey)
|
|
})
|
|
})
|
|
|
|
Describe("inference service with an explainer", func() {
|
|
// --- Default values ---
|
|
defaultExplainerIsvc := func(serviceKey types.NamespacedName, qty *v1beta1.MetricQuantity) *v1beta1.InferenceService {
|
|
predictor := v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.AverageValueMetricType,
|
|
AverageValue: qty,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
}
|
|
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: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: map[string]string{
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassHPA),
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: predictor,
|
|
Explainer: explainer,
|
|
},
|
|
}
|
|
|
|
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
|
|
return isvc
|
|
}
|
|
|
|
It("Should keep the explainer httproute/service/deployment/hpa created 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.Background(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-explainer-false-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
explainerKey := types.NamespacedName{
|
|
Name: constants.ExplainerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(context.Background(), &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultExplainerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Create(context.Background(), isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the predictor deployment
|
|
expectDeploymentToBeReady(context.Background(), predictorKey)
|
|
// Check the explainer deployment
|
|
expectDeploymentToBeReady(context.Background(), explainerKey)
|
|
|
|
// check the predictor service
|
|
expectResourceToExist(context.Background(), &corev1.Service{}, predictorKey)
|
|
// check the explainer service
|
|
expectResourceToExist(context.Background(), &corev1.Service{}, explainerKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(context.Background(), serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(context.Background(), predictorKey)
|
|
// explainer http route
|
|
expectHttpRouteToBeReady(context.Background(), explainerKey)
|
|
|
|
// check the predictor HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check the explainer HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, explainerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
expectIsvcExplainerReadyStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should not create the explainer httproute/service/deployment/hpa 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.Background(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-explainer-true-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
explainerKey := types.NamespacedName{
|
|
Name: constants.ExplainerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(context.Background(), &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultExplainerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Create(context.Background(), isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the predictor deployment was not created
|
|
expectResourceDoesNotExist(context.Background(), &appsv1.Deployment{}, predictorKey)
|
|
// Check that the explainer deployment was not created
|
|
expectResourceDoesNotExist(context.Background(), &appsv1.Deployment{}, explainerKey)
|
|
|
|
// check that the predictor service was not created
|
|
expectResourceDoesNotExist(context.Background(), &corev1.Service{}, predictorKey)
|
|
// check that the explainer service was not created
|
|
expectResourceDoesNotExist(context.Background(), &corev1.Service{}, explainerKey)
|
|
|
|
// check that the http routes were not created
|
|
// top level http route
|
|
expectResourceDoesNotExist(context.Background(), &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceDoesNotExist(context.Background(), &gwapiv1.HTTPRoute{}, predictorKey)
|
|
// explainer http route
|
|
expectResourceDoesNotExist(context.Background(), &gwapiv1.HTTPRoute{}, explainerKey)
|
|
|
|
// check that the predictor HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check that the explainer HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, explainerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should delete the explainer httproute/service/deployment/hpa 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.Background(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-explainer-edit-true-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
explainerKey := types.NamespacedName{
|
|
Name: constants.ExplainerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(context.Background(), &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultExplainerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Create(context.Background(), isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check the predictor deployment
|
|
expectDeploymentToBeReady(ctx, predictorKey)
|
|
// Check the explainer deployment
|
|
expectDeploymentToBeReady(ctx, explainerKey)
|
|
|
|
// check the predictor service
|
|
expectResourceToExist(context.Background(), &corev1.Service{}, predictorKey)
|
|
// check the explainer service
|
|
expectResourceToExist(context.Background(), &corev1.Service{}, explainerKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(context.Background(), serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(context.Background(), predictorKey)
|
|
// explainer http route
|
|
expectHttpRouteToBeReady(context.Background(), explainerKey)
|
|
|
|
// check the predictor HPA
|
|
expectResourceToExist(context.Background(), &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check the explainer HPA
|
|
expectResourceToExist(context.Background(), &autoscalingv2.HorizontalPodAutoscaler{}, explainerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
expectIsvcExplainerReadyStatus(ctx, serviceKey)
|
|
|
|
// Stop the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check that the predictor deployment was deleted
|
|
expectResourceToBeDeleted(context.Background(), &appsv1.Deployment{}, predictorKey)
|
|
// Check that the explainer deployment was deleted
|
|
expectResourceToBeDeleted(context.Background(), &appsv1.Deployment{}, explainerKey)
|
|
|
|
// check that the predictor service was deleted
|
|
expectResourceToBeDeleted(context.Background(), &corev1.Service{}, predictorKey)
|
|
// check that the explainer service was deleted
|
|
expectResourceToBeDeleted(context.Background(), &corev1.Service{}, explainerKey)
|
|
|
|
// check that the http routes were deleted
|
|
// top level http route
|
|
expectResourceToBeDeleted(context.Background(), &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceToBeDeleted(context.Background(), &gwapiv1.HTTPRoute{}, predictorKey)
|
|
// explainer http route
|
|
expectResourceToBeDeleted(context.Background(), &gwapiv1.HTTPRoute{}, explainerKey)
|
|
|
|
// check that the predictor HPA was deleted
|
|
expectResourceToBeDeleted(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check that the explainer HPA was deleted
|
|
expectResourceToBeDeleted(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, explainerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
})
|
|
|
|
It("Should create the explainer httproute/service/deployment/hpa 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.Background(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, configMap)
|
|
|
|
// Setup values
|
|
serviceName := "stop-explainer-edit-false-isvc"
|
|
serviceNamespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
|
|
predictorKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
explainerKey := types.NamespacedName{
|
|
Name: constants.ExplainerServiceName(serviceName),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
|
|
// Serving runtime
|
|
servingRuntime := getServingRuntime("tf-serving-raw", serviceKey.Namespace)
|
|
Expect(k8sClient.Create(context.Background(), &servingRuntime)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, &servingRuntime)
|
|
|
|
// Define InferenceService
|
|
isvc := defaultExplainerIsvc(serviceKey, v1beta1.NewMetricQuantity("10Gi"))
|
|
isvc.Annotations[constants.StopAnnotationKey] = "true"
|
|
Expect(k8sClient.Create(context.Background(), isvc)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the predictor deployment was not created
|
|
expectResourceDoesNotExist(context.Background(), &appsv1.Deployment{}, predictorKey)
|
|
// Check that the explainer deployment was not created
|
|
expectResourceDoesNotExist(context.Background(), &appsv1.Deployment{}, explainerKey)
|
|
|
|
// check that the predictor service was not created
|
|
expectResourceDoesNotExist(context.Background(), &corev1.Service{}, predictorKey)
|
|
// check that the explainer service was not created
|
|
expectResourceDoesNotExist(context.Background(), &corev1.Service{}, explainerKey)
|
|
|
|
// check that the http routes were not created
|
|
// top level http route
|
|
expectResourceDoesNotExist(context.Background(), &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// predictor http route
|
|
expectResourceDoesNotExist(context.Background(), &gwapiv1.HTTPRoute{}, predictorKey)
|
|
// explainer http route
|
|
expectResourceDoesNotExist(context.Background(), &gwapiv1.HTTPRoute{}, explainerKey)
|
|
|
|
// check that the predictor HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check that the explainer HPA was not created
|
|
expectResourceDoesNotExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, explainerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the ISVC status reflects that it is stopped
|
|
expectIsvcTrueStoppedStatus(ctx, serviceKey)
|
|
|
|
// Resume the inference service
|
|
updatedIsvc := expectIsvcToExist(ctx, serviceKey)
|
|
stoppedIsvc := updatedIsvc.DeepCopy()
|
|
stoppedIsvc.Annotations[constants.StopAnnotationKey] = "false"
|
|
Expect(k8sClient.Update(ctx, stoppedIsvc)).NotTo(HaveOccurred())
|
|
|
|
// Check the predictor deployment
|
|
expectDeploymentToBeReady(context.Background(), predictorKey)
|
|
// Check the explainer deployment
|
|
expectDeploymentToBeReady(context.Background(), explainerKey)
|
|
|
|
// check the predictor service
|
|
expectResourceToExist(context.Background(), &corev1.Service{}, predictorKey)
|
|
// check the explainer service
|
|
expectResourceToExist(context.Background(), &corev1.Service{}, explainerKey)
|
|
|
|
// top level http route
|
|
expectHttpRouteToBeReady(context.Background(), serviceKey)
|
|
// predictor http route
|
|
expectHttpRouteToBeReady(context.Background(), predictorKey)
|
|
// explainer http route
|
|
expectHttpRouteToBeReady(context.Background(), explainerKey)
|
|
|
|
// check the predictor HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, predictorKey)
|
|
// check the explainer HPA
|
|
expectResourceToExist(ctx, &autoscalingv2.HorizontalPodAutoscaler{}, explainerKey)
|
|
|
|
// Check that the ISVC was updated
|
|
expectIsvcToExist(ctx, serviceKey)
|
|
|
|
// Check that the stopped condition is false
|
|
expectIsvcFalseStoppedStatus(ctx, serviceKey)
|
|
|
|
// Check that the inference service is ready
|
|
expectIsvcReadyStatus(ctx, serviceKey)
|
|
expectIsvcExplainerReadyStatus(ctx, serviceKey)
|
|
})
|
|
})
|
|
})
|
|
Context("When Updating a Serving Runtime", func() {
|
|
configs := getRawKubeTestConfigs()
|
|
|
|
ctx := context.Background()
|
|
It("InferenceService should reconcile the deployment if auto-update annotation is not present", func() {
|
|
// Create configmap
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(context.TODO(), configMap)
|
|
|
|
isvcNamespace := constants.KServeNamespace
|
|
Eventually(func() error {
|
|
cm := &corev1.ConfigMap{}
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: constants.InferenceServiceConfigMapName, Namespace: isvcNamespace}, cm)
|
|
}, timeout, interval).Should(Succeed())
|
|
isvcName := "isvc-enable-auto-update-missing"
|
|
serviceKey := types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
servingRuntimeName := "pytorch-serving-auto-update-missing"
|
|
servingRuntime := &v1alpha1.ServingRuntime{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: servingRuntimeName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Spec: v1alpha1.ServingRuntimeSpec{
|
|
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
|
|
{
|
|
Name: "pytorch",
|
|
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: "pytorch/serving:1.14.0",
|
|
Command: []string{"/usr/bin/pytorch_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(ctx, servingRuntime)).Should(Succeed())
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, &v1alpha1.ServingRuntime{})
|
|
}, timeout, interval).Should(Succeed())
|
|
defer k8sClient.Delete(ctx, servingRuntime)
|
|
|
|
// Define InferenceService with auto-update disabled.
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: isvcNamespace,
|
|
Annotations: map[string]string{
|
|
"serving.kserve.io/deploymentMode": "Standard",
|
|
"serving.kserve.io/autoscalerClass": "hpa",
|
|
"serving.kserve.io/metrics": "cpu",
|
|
"serving.kserve.io/targetUtilizationPercentage": "75",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
PyTorch: &v1beta1.TorchServeSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
createdConfigMap := &corev1.ConfigMap{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: constants.InferenceServiceConfigMapName, Namespace: isvcNamespace}, createdConfigMap)
|
|
}, timeout, interval).Should(Succeed())
|
|
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())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
originalDeployment := &appsv1.Deployment{}
|
|
deploymentName := constants.PredictorServiceName(serviceKey.Name)
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: serviceKey.Namespace}, originalDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
// Update the ServingRuntime spec
|
|
servingRuntimeToUpdate := &v1alpha1.ServingRuntime{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, servingRuntimeToUpdate)).Should(Succeed())
|
|
servingRuntimeToUpdate.Spec.Labels["key1"] = "updatedServingRuntime"
|
|
Eventually(func() error {
|
|
return k8sClient.Update(ctx, servingRuntimeToUpdate)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
// Wait until the ServingRuntime reflects the updated spec.
|
|
servingRuntimeAfterUpdate := &v1alpha1.ServingRuntime{}
|
|
Eventually(func() (string, error) {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, servingRuntimeAfterUpdate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return servingRuntimeAfterUpdate.Spec.ServingRuntimePodSpec.Labels["key1"], nil
|
|
}, timeout, interval).Should(Equal("updatedServingRuntime"))
|
|
deploymentAfterUpdate := &appsv1.Deployment{}
|
|
Eventually(func() (string, error) {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: serviceKey.Namespace}, deploymentAfterUpdate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return deploymentAfterUpdate.Spec.Template.ObjectMeta.Labels["key1"], nil
|
|
}, timeout, interval).Should(Equal("updatedServingRuntime"))
|
|
})
|
|
|
|
It("InferenceService should reconcile the deployment if auto-update is enabled ", func() {
|
|
// Create configmap
|
|
isvcNamespace := constants.KServeNamespace
|
|
configMap := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: constants.InferenceServiceConfigMapName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Data: configs,
|
|
}
|
|
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(context.TODO(), configMap)
|
|
Eventually(func() error {
|
|
cm := &corev1.ConfigMap{}
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: constants.InferenceServiceConfigMapName, Namespace: isvcNamespace}, cm)
|
|
}, timeout, interval).Should(Succeed())
|
|
isvcName := "isvc-enable-auto-update-true"
|
|
serviceKey := types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
servingRuntimeName := "pytorch-serving-auto-update-true"
|
|
servingRuntime := &v1alpha1.ServingRuntime{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: servingRuntimeName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Spec: v1alpha1.ServingRuntimeSpec{
|
|
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
|
|
{
|
|
Name: "pytorch",
|
|
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: "pytorch/serving:1.14.0",
|
|
Command: []string{"/usr/bin/pytorch_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(ctx, servingRuntime)).Should(Succeed())
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, &v1alpha1.ServingRuntime{})
|
|
}, timeout, interval).Should(Succeed())
|
|
defer k8sClient.Delete(ctx, servingRuntime)
|
|
// Define InferenceService with auto-update disabled.
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: isvcNamespace,
|
|
Annotations: map[string]string{
|
|
"serving.kserve.io/deploymentMode": "Standard",
|
|
"serving.kserve.io/autoscalerClass": "external",
|
|
constants.DisableAutoUpdateAnnotationKey: "false",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
PyTorch: &v1beta1.TorchServeSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
originalDeployment := &appsv1.Deployment{}
|
|
deploymentName := constants.PredictorServiceName(serviceKey.Name)
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: serviceKey.Namespace}, originalDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
// Update the ServingRuntime spec
|
|
servingRuntimeToUpdate := &v1alpha1.ServingRuntime{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, servingRuntimeToUpdate)).Should(Succeed())
|
|
servingRuntimeToUpdate.Spec.Labels["key1"] = "updatedServingRuntime"
|
|
Eventually(func() error {
|
|
return k8sClient.Update(ctx, servingRuntimeToUpdate)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
// Wait until the ServingRuntime reflects the updated spec.
|
|
servingRuntimeAfterUpdate := &v1alpha1.ServingRuntime{}
|
|
Eventually(func() (string, error) {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, servingRuntimeAfterUpdate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return servingRuntimeAfterUpdate.Spec.ServingRuntimePodSpec.Labels["key1"], nil
|
|
}, timeout, interval).Should(Equal("updatedServingRuntime"))
|
|
// Wait until the Deployment reflects the update
|
|
deploymentAfterUpdate := &appsv1.Deployment{}
|
|
Eventually(func() (string, error) {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: serviceKey.Namespace}, deploymentAfterUpdate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return deploymentAfterUpdate.Spec.Template.ObjectMeta.Labels["key1"], nil
|
|
}, timeout, interval).Should(Equal("updatedServingRuntime"))
|
|
})
|
|
|
|
It("InferenceService should not reconcile the deployment if auto-update is disabled", func() {
|
|
// Create configmap
|
|
isvcNamespace := constants.KServeNamespace
|
|
configMap := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: constants.InferenceServiceConfigMapName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Data: configs,
|
|
}
|
|
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(context.TODO(), configMap)
|
|
Eventually(func() error {
|
|
cm := &corev1.ConfigMap{}
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: constants.InferenceServiceConfigMapName, Namespace: isvcNamespace}, cm)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
isvcName := "isvc-enable-auto-update-false"
|
|
serviceKey := types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
servingRuntimeName := "pytorch-serving-auto-update-false"
|
|
servingRuntime := &v1alpha1.ServingRuntime{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: servingRuntimeName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Spec: v1alpha1.ServingRuntimeSpec{
|
|
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
|
|
{
|
|
Name: "pytorch",
|
|
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: "pytorch/serving:1.14.0",
|
|
Command: []string{"/usr/bin/pytorch_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(ctx, servingRuntime)).Should(Succeed())
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, &v1alpha1.ServingRuntime{})
|
|
}, timeout, interval).Should(Succeed())
|
|
defer k8sClient.Delete(ctx, servingRuntime)
|
|
|
|
// Define InferenceService with auto-update disabled.
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: isvcNamespace,
|
|
Annotations: map[string]string{
|
|
"serving.kserve.io/deploymentMode": "Standard",
|
|
"serving.kserve.io/autoscalerClass": "external",
|
|
constants.DisableAutoUpdateAnnotationKey: "true",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
PyTorch: &v1beta1.TorchServeSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
originalDeployment := &appsv1.Deployment{}
|
|
deploymentName := constants.PredictorServiceName(serviceKey.Name)
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: serviceKey.Namespace}, originalDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorReadyCondition := &apis.Condition{
|
|
Type: v1beta1.PredictorReady,
|
|
Status: corev1.ConditionTrue,
|
|
}
|
|
ingressReadyCondition := &apis.Condition{
|
|
Type: v1beta1.IngressReady,
|
|
Status: corev1.ConditionTrue,
|
|
}
|
|
Expect(k8sClient.Get(ctx, serviceKey, inferenceService)).Should(Succeed())
|
|
inferenceService.Status.SetCondition(v1beta1.PredictorReady, predictorReadyCondition)
|
|
inferenceService.Status.SetCondition(v1beta1.IngressReady, ingressReadyCondition)
|
|
Expect(k8sClient.Status().Update(ctx, inferenceService)).Should(Succeed())
|
|
|
|
updatedIsvc := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, updatedIsvc)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return updatedIsvc.Status.IsReady()
|
|
}, timeout, interval).Should(BeTrue(), "The InferenceService should be ready")
|
|
|
|
// Update the ServingRuntime spec
|
|
servingRuntimeToUpdate := &v1alpha1.ServingRuntime{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, servingRuntimeToUpdate)).Should(Succeed())
|
|
servingRuntimeToUpdate.Spec.Labels["key1"] = "updatedServingRuntime"
|
|
Expect(k8sClient.Update(ctx, servingRuntimeToUpdate)).Should(Succeed())
|
|
|
|
// Wait until the ServingRuntime reflects the updated spec.
|
|
servingRuntimeAfterUpdate := &v1alpha1.ServingRuntime{}
|
|
Eventually(func() (string, error) {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeName, Namespace: isvcNamespace}, servingRuntimeAfterUpdate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return servingRuntimeAfterUpdate.Spec.ServingRuntimePodSpec.Labels["key1"], nil
|
|
}, timeout, interval).Should(Equal("updatedServingRuntime"))
|
|
|
|
// Check to make sure deployment didn't update
|
|
deploymentAfterUpdate := &appsv1.Deployment{}
|
|
Consistently(func() (string, error) {
|
|
if err := k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: serviceKey.Namespace}, deploymentAfterUpdate); err != nil {
|
|
return "", err
|
|
}
|
|
return deploymentAfterUpdate.Spec.Template.ObjectMeta.Labels["key1"], nil
|
|
}, fastTimeout, interval).Should(Equal("val1FromSR"))
|
|
})
|
|
It("InferenceService should reconcile only if the matching serving runtime was updated even if multiple exist", func() {
|
|
// Create configmap
|
|
isvcNamespace := constants.KServeNamespace
|
|
configMap := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: constants.InferenceServiceConfigMapName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Data: configs,
|
|
}
|
|
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(context.TODO(), configMap)
|
|
Eventually(func() error {
|
|
cm := &corev1.ConfigMap{}
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: constants.InferenceServiceConfigMapName, Namespace: isvcNamespace}, cm)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
isvcNamePytorch := "isvc-enable-auto-update-multiple-pytorch"
|
|
serviceKeyPytorch := types.NamespacedName{Name: isvcNamePytorch, Namespace: isvcNamespace}
|
|
isvcNameTensorflow := "isvc-enable-auto-update-multiple-tensorflow"
|
|
serviceKeyTensorflow := types.NamespacedName{Name: isvcNameTensorflow, Namespace: isvcNamespace}
|
|
|
|
servingRuntimePytorchName := "pytorch-serving-auto-update-true-multiple"
|
|
servingRuntimeTensorflowName := "tensorflow-serving-auto-update-true-multiple"
|
|
pytorchServingRuntime := &v1alpha1.ServingRuntime{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: servingRuntimePytorchName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Spec: v1alpha1.ServingRuntimeSpec{
|
|
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
|
|
{
|
|
Name: "pytorch",
|
|
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: "pytorch/serving:1.14.0",
|
|
Command: []string{"/usr/bin/pytorch_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(ctx, pytorchServingRuntime)).Should(Succeed())
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimePytorchName, Namespace: isvcNamespace}, &v1alpha1.ServingRuntime{})
|
|
}, timeout, interval).Should(Succeed())
|
|
defer k8sClient.Delete(ctx, pytorchServingRuntime)
|
|
|
|
tensorflowServingRuntime := &v1alpha1.ServingRuntime{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: servingRuntimeTensorflowName,
|
|
Namespace: isvcNamespace,
|
|
},
|
|
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_server_model"},
|
|
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(ctx, tensorflowServingRuntime)).Should(Succeed())
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimeTensorflowName, Namespace: isvcNamespace}, &v1alpha1.ServingRuntime{})
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
defer k8sClient.Delete(ctx, tensorflowServingRuntime)
|
|
// Define InferenceService with auto-update disabled.
|
|
pytorchIsvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKeyPytorch.Name,
|
|
Namespace: isvcNamespace,
|
|
Annotations: map[string]string{
|
|
"serving.kserve.io/deploymentMode": "Standard",
|
|
"serving.kserve.io/autoscalerClass": "external",
|
|
constants.DisableAutoUpdateAnnotationKey: "false",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
PyTorch: &v1beta1.TorchServeSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
pytorchIsvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
|
|
Expect(k8sClient.Create(ctx, pytorchIsvc)).Should(Succeed())
|
|
|
|
inferenceService := &v1beta1.InferenceService{}
|
|
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKeyPytorch, inferenceService)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
defer k8sClient.Delete(ctx, pytorchIsvc)
|
|
|
|
tensorflowIsvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKeyTensorflow.Name,
|
|
Namespace: isvcNamespace,
|
|
Annotations: map[string]string{
|
|
"serving.kserve.io/deploymentMode": "Standard",
|
|
"serving.kserve.io/autoscalerClass": "external",
|
|
constants.DisableAutoUpdateAnnotationKey: "false",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
tensorflowIsvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
|
|
Expect(k8sClient.Create(ctx, tensorflowIsvc)).Should(Succeed())
|
|
|
|
inferenceServiceTensorflow := &v1beta1.InferenceService{}
|
|
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKeyTensorflow, inferenceServiceTensorflow)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
defer k8sClient.Delete(ctx, tensorflowIsvc)
|
|
|
|
// Update the ServingRuntime spec
|
|
servingRuntimeToUpdate := &v1alpha1.ServingRuntime{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimePytorchName, Namespace: isvcNamespace}, servingRuntimeToUpdate)).Should(Succeed())
|
|
servingRuntimeToUpdate.Spec.Labels["key1"] = "updatedServingRuntime"
|
|
Eventually(func() error {
|
|
return k8sClient.Update(ctx, servingRuntimeToUpdate)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
// Wait until the ServingRuntime reflects the updated spec.
|
|
pytorchServingRuntimeAfterUpdate := &v1alpha1.ServingRuntime{}
|
|
Eventually(func() (string, error) {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: servingRuntimePytorchName, Namespace: isvcNamespace}, pytorchServingRuntimeAfterUpdate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return pytorchServingRuntimeAfterUpdate.Spec.ServingRuntimePodSpec.Labels["key1"], nil
|
|
}, timeout, interval).Should(Equal("updatedServingRuntime"))
|
|
|
|
// Wait until the Deployment reflects the update
|
|
pytorchDeploymentAfterUpdate := &appsv1.Deployment{}
|
|
deploymentName := constants.PredictorServiceName(serviceKeyPytorch.Name)
|
|
Eventually(func() (string, error) {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: serviceKeyPytorch.Namespace}, pytorchDeploymentAfterUpdate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return pytorchDeploymentAfterUpdate.Spec.Template.Labels["key1"], nil
|
|
}, timeout, interval).Should(Equal("updatedServingRuntime"))
|
|
|
|
tensorFlowDeploymentAfterUpdate := &appsv1.Deployment{}
|
|
tensorflowDeploymentName := constants.PredictorServiceName(serviceKeyTensorflow.Name)
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: tensorflowDeploymentName, Namespace: serviceKeyTensorflow.Namespace}, tensorFlowDeploymentAfterUpdate)).Should(Succeed())
|
|
Expect(tensorFlowDeploymentAfterUpdate.Spec.Template.ObjectMeta.Labels["key1"]).Should(Equal("val1FromSR"))
|
|
})
|
|
})
|
|
|
|
Context("When creating inference service with raw kube predictor and ingress creation disabled", func() {
|
|
configs := mergeJSONField(getRawKubeTestConfigs(), "ingress", map[string]interface{}{
|
|
"disableIngressCreation": true,
|
|
"ingressDomain": "example.com",
|
|
})
|
|
|
|
It("Should have service/deployment/hpa created and http route should not be 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
// Create InferenceService
|
|
serviceName := "raw-foo-no-ingress-class"
|
|
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,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedDeployment := getDeploymentWithKServiceLabel(predictorDeploymentKey, serviceName, isvc)
|
|
Expect(actualDeployment.Spec).To(BeComparableTo(expectedDeployment.Spec))
|
|
|
|
// check service
|
|
actualService := &corev1.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 := getExectedService(predictorServiceKey, serviceName)
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
|
|
|
|
// check isvc status
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check ingress not created
|
|
// expectResourceDoesNotExist
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, serviceKey)
|
|
// actualPredictorHttpRoute
|
|
expectResourceDoesNotExist(ctx, &gwapiv1.HTTPRoute{}, predictorServiceKey)
|
|
|
|
// 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: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: serviceName + "-default.example.com",
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: serviceName + "-predictor-default.example.com",
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check HPA
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var cpuUtilization int32 = 75
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualHPA.Spec).To(BeComparableTo(expectedHPA.Spec))
|
|
})
|
|
})
|
|
Context("When creating inference service with raw kube predictor with domain template", func() {
|
|
configs := mergeJSONField(getRawKubeTestConfigs(), "ingress", map[string]interface{}{
|
|
"ingressDomain": "example.com",
|
|
"domainTemplate": "{{ .Name }}.{{ .Namespace }}.{{ .IngressDomain }}",
|
|
})
|
|
|
|
It("Should have httproute/service/deployment/hpa 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
// Create InferenceService
|
|
serviceName := "model"
|
|
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,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedDeployment := getDeploymentWithKServiceLabel(predictorDeploymentKey, serviceName, isvc)
|
|
Expect(actualDeployment.Spec).To(BeComparableTo(expectedDeployment.Spec))
|
|
|
|
// check service
|
|
actualService := &corev1.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 := getExectedService(predictorServiceKey, serviceName)
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
|
|
|
|
// check isvc status
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
topLevelHost := fmt.Sprintf("%s.%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s.%s.additional.example.com", serviceKey.Name, serviceKey.Namespace))},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorHost := fmt.Sprintf("%s.%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).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: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s.%s.%s", serviceName, serviceKey.Namespace, domain),
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-predictor.%s.%s", serviceName, serviceKey.Namespace, domain),
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check HPA
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var cpuUtilization int32 = 75
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualHPA.Spec).To(BeComparableTo(expectedHPA.Spec))
|
|
})
|
|
})
|
|
Context("When creating inference service with raw kube predictor and transformer", func() {
|
|
configs := getRawKubeTestConfigs()
|
|
|
|
It("Should have httproute/service/deployment/hpa created for transformer and predictor", 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-trans"
|
|
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,
|
|
}
|
|
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var transformerMinReplicas int32 = 1
|
|
var transformerMaxReplicas int32 = 2
|
|
var transformerCpuUtilization int32 = 80
|
|
var transformerStabilizationWindowSeconds int32 = 0
|
|
transformerSelectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
ctx := context.Background()
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(minReplicas),
|
|
MaxReplicas: maxReplicas,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
Transformer: &v1beta1.TransformerSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(transformerMinReplicas),
|
|
MaxReplicas: transformerMaxReplicas,
|
|
ScaleTarget: ptr.To(transformerCpuUtilization),
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
},
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Image: "transformer:v1",
|
|
Resources: defaultResource,
|
|
Args: []string{
|
|
"--port=8080",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
// check predictor deployment
|
|
actualPredictorDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualPredictorDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: predictorDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Predictor.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: map[string]string{
|
|
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassHPA),
|
|
},
|
|
},
|
|
Spec: 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,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
Strategy: getDefaultRollingStrategy(),
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualPredictorDeployment.Spec).To(BeComparableTo(expectedPredictorDeployment.Spec))
|
|
|
|
// check transformer deployment
|
|
actualTransformerDeployment := &appsv1.Deployment{}
|
|
transformerDeploymentKey := types.NamespacedName{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), transformerDeploymentKey, actualTransformerDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedTransformerDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: transformerDeploymentKey.Name,
|
|
Namespace: transformerDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + transformerDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: transformerDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + transformerDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Transformer.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Image: "transformer:v1",
|
|
Name: constants.InferenceServiceContainerName,
|
|
Args: []string{
|
|
"--port=8080",
|
|
"--model_name",
|
|
serviceKey.Name,
|
|
"--predictor_host",
|
|
fmt.Sprintf("%s.%s", predictorServiceKey.Name, predictorServiceKey.Namespace),
|
|
"--http_port",
|
|
"8080",
|
|
},
|
|
Env: []corev1.EnvVar{
|
|
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
|
|
},
|
|
Resources: defaultResource,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
Strategy: getDefaultRollingStrategy(),
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualTransformerDeployment.Spec).To(BeComparableTo(expectedTransformerDeployment.Spec))
|
|
|
|
// check predictor service
|
|
actualPredictorService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorServiceKey, actualPredictorService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: predictorServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: predictorServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + predictorServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualPredictorService.Spec.ClusterIP = ""
|
|
actualPredictorService.Spec.ClusterIPs = nil
|
|
actualPredictorService.Spec.IPFamilies = nil
|
|
actualPredictorService.Spec.IPFamilyPolicy = nil
|
|
actualPredictorService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualPredictorService.Spec).To(BeComparableTo(expectedPredictorService.Spec))
|
|
|
|
// check transformer service
|
|
actualTransformerService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), transformerServiceKey, actualTransformerService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedTransformerService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: transformerServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: transformerServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + transformerServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualTransformerService.Spec.ClusterIP = ""
|
|
actualTransformerService.Spec.ClusterIPs = nil
|
|
actualTransformerService.Spec.IPFamilies = nil
|
|
actualTransformerService.Spec.IPFamilyPolicy = nil
|
|
actualTransformerService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualTransformerService.Spec).To(BeComparableTo(expectedTransformerService.Spec))
|
|
|
|
// update deployment status to make isvc ready
|
|
updatedPredictorDeployment := actualPredictorDeployment.DeepCopy()
|
|
updatedPredictorDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedPredictorDeployment)).NotTo(HaveOccurred())
|
|
updatedTransformerDeployment := actualTransformerDeployment.DeepCopy()
|
|
updatedTransformerDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedTransformerDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace))},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(transformerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
actualTransformerHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualTransformerHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
transformerHost := fmt.Sprintf("%s-%s.%s", transformerServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedTransformerHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(transformerHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(transformerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualTransformerHttpRoute.Spec).To(BeComparableTo(expectedTransformerHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualTransformerHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualTransformerHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).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: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
{
|
|
Type: v1beta1.TransformerReady,
|
|
Status: "True",
|
|
Severity: "Info",
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s.%s.svc.cluster.local", transformerServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", predictorServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
v1beta1.TransformerComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", transformerServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check predictor HPA
|
|
var cpuUtilization int32 = 75
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualPredictorHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHPA.Spec).To(BeComparableTo(expectedPredictorHPA.Spec))
|
|
|
|
// check transformer HPA
|
|
actualTransformerHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
transformerHPAKey := types.NamespacedName{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), transformerHPAKey, actualTransformerHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedTransformerHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: transformerServiceKey.Name,
|
|
},
|
|
MinReplicas: &transformerMinReplicas,
|
|
MaxReplicas: transformerMaxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &transformerCpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &transformerStabilizationWindowSeconds,
|
|
SelectPolicy: &transformerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &transformerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualTransformerHPA.Spec).To(BeComparableTo(expectedTransformerHPA.Spec))
|
|
})
|
|
})
|
|
Context("When creating inference service with raw kube predictor and explainer", func() {
|
|
configs := getRawKubeTestConfigs()
|
|
|
|
It("Should have httproute/service/deployment/hpa created for explainer and predictor", 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-exp"
|
|
namespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: namespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
predictorServiceKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceName),
|
|
Namespace: namespace,
|
|
}
|
|
explainerServiceKey := types.NamespacedName{
|
|
Name: constants.ExplainerServiceName(serviceName),
|
|
Namespace: namespace,
|
|
}
|
|
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var explainerMinReplicas int32 = 1
|
|
var explainerMaxReplicas int32 = 2
|
|
var explainerCpuUtilization int32 = 80
|
|
var explainerStabilizationWindowSeconds int32 = 0
|
|
ExplainerSelectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
ctx := context.Background()
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(minReplicas),
|
|
MaxReplicas: maxReplicas,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(explainerMinReplicas),
|
|
MaxReplicas: explainerMaxReplicas,
|
|
ScaleTarget: ptr.To(explainerCpuUtilization),
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvcConfigMap, err := v1beta1.GetInferenceServiceConfigMap(context.Background(), clientset)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
isvcConfig, err := v1beta1.NewInferenceServicesConfig(isvcConfigMap)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
isvc.DefaultInferenceService(isvcConfig, 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())
|
|
|
|
// check predictor deployment
|
|
actualPredictorDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualPredictorDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: predictorDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Predictor.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: map[string]string{
|
|
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassHPA),
|
|
},
|
|
},
|
|
Spec: 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,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
Strategy: getDefaultRollingStrategy(),
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualPredictorDeployment.Spec).To(BeComparableTo(expectedPredictorDeployment.Spec))
|
|
|
|
// check Explainer deployment
|
|
actualExplainerDeployment := &appsv1.Deployment{}
|
|
explainerDeploymentKey := types.NamespacedName{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), explainerDeploymentKey, actualExplainerDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedExplainerDeployment := getExpectedDeployment(explainerDeploymentKey, serviceName, serviceKey, predictorServiceKey)
|
|
Expect(actualExplainerDeployment.Spec).To(BeComparableTo(expectedExplainerDeployment.Spec))
|
|
|
|
// check predictor service
|
|
actualPredictorService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorServiceKey, actualPredictorService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: predictorServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: predictorServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + predictorServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualPredictorService.Spec.ClusterIP = ""
|
|
actualPredictorService.Spec.ClusterIPs = nil
|
|
actualPredictorService.Spec.IPFamilies = nil
|
|
actualPredictorService.Spec.IPFamilyPolicy = nil
|
|
actualPredictorService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualPredictorService.Spec).To(BeComparableTo(expectedPredictorService.Spec))
|
|
|
|
// check Explainer service
|
|
actualExplainerService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), explainerServiceKey, actualExplainerService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedExplainerService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: explainerServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: explainerServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + explainerServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualExplainerService.Spec.ClusterIP = ""
|
|
actualExplainerService.Spec.ClusterIPs = nil
|
|
actualExplainerService.Spec.IPFamilies = nil
|
|
actualExplainerService.Spec.IPFamilyPolicy = nil
|
|
actualExplainerService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualExplainerService.Spec).To(BeComparableTo(expectedExplainerService.Spec))
|
|
|
|
// update deployment status to make isvc ready
|
|
updatedPredictorDeployment := actualPredictorDeployment.DeepCopy()
|
|
updatedPredictorDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedPredictorDeployment)).NotTo(HaveOccurred())
|
|
updatedExplainerDeployment := actualExplainerDeployment.DeepCopy()
|
|
updatedExplainerDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedExplainerDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace))},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.ExplainPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(explainerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
actualExplainerHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualExplainerHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
explainerHost := fmt.Sprintf("%s-%s.%s", explainerServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedExplainerHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(explainerHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(explainerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualExplainerHttpRoute.Spec).To(BeComparableTo(expectedExplainerHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualExplainerHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualExplainerHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).NotTo(HaveOccurred())
|
|
|
|
// verify if InferenceService status is updated
|
|
expectedIsvcStatus := v1beta1.InferenceServiceStatus{
|
|
Status: duckv1.Status{
|
|
Conditions: duckv1.Conditions{
|
|
{
|
|
Type: v1beta1.ExplainerReady,
|
|
Status: "True",
|
|
Severity: "Info",
|
|
},
|
|
{
|
|
Type: v1beta1.IngressReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.PredictorReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s.%s.svc.cluster.local", predictorServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", predictorServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
v1beta1.ExplainerComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", explainerServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check predictor HPA
|
|
var cpuUtilization int32 = 75
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualPredictorHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHPA.Spec).To(BeComparableTo(expectedPredictorHPA.Spec))
|
|
|
|
// check Explainer HPA
|
|
actualExplainerHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
explainerHPAKey := types.NamespacedName{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), explainerHPAKey, actualExplainerHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedExplainerHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: explainerServiceKey.Name,
|
|
},
|
|
MinReplicas: &explainerMinReplicas,
|
|
MaxReplicas: explainerMaxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &explainerCpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &explainerStabilizationWindowSeconds,
|
|
SelectPolicy: &ExplainerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &ExplainerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualExplainerHPA.Spec).To(BeComparableTo(expectedExplainerHPA.Spec))
|
|
})
|
|
})
|
|
Context("When creating inference service with raw kube path based routing predictor", func() {
|
|
configs := mergeJSONField(getRawKubeTestConfigs(), "ingress", map[string]interface{}{
|
|
"ingressDomain": "example.com",
|
|
"pathTemplate": "/serving/{{ .Namespace }}/{{ .Name }}",
|
|
})
|
|
|
|
It("Should have httproute/service/deployment/hpa 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-path"
|
|
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,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedDeployment := getDeploymentWithKServiceLabel(predictorDeploymentKey, serviceName, isvc)
|
|
Expect(actualDeployment.Spec).To(BeComparableTo(expectedDeployment.Spec))
|
|
|
|
// check service
|
|
actualService := &corev1.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 := getExectedService(predictorServiceKey, serviceName)
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(BeComparableTo(expectedService.Spec))
|
|
|
|
// check isvc status
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
prefixUrlPath := fmt.Sprintf("/serving/%s/%s", serviceKey.Namespace, serviceKey.Name)
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace)), "example.com"},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(prefixUrlPath + "/"),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).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: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", predictorServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check HPA
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var cpuUtilization int32 = 75
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualHPA.Spec).To(BeComparableTo(expectedHPA.Spec))
|
|
})
|
|
})
|
|
Context("When creating inference service with raw kube path based routing predictor and transformer", func() {
|
|
configs := mergeJSONField(getRawKubeTestConfigs(), "ingress", map[string]interface{}{
|
|
"ingressDomain": "example.com",
|
|
"pathTemplate": "/serving/{{ .Namespace }}/{{ .Name }}",
|
|
})
|
|
|
|
It("Should have ingress/service/deployment/hpa created for transformer and predictor", 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-trans-path"
|
|
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,
|
|
}
|
|
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var transformerMinReplicas int32 = 1
|
|
var transformerMaxReplicas int32 = 2
|
|
var transformerCpuUtilization int32 = 80
|
|
var transformerStabilizationWindowSeconds int32 = 0
|
|
transformerSelectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
ctx := context.Background()
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(minReplicas),
|
|
MaxReplicas: maxReplicas,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
Transformer: &v1beta1.TransformerSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(transformerMinReplicas),
|
|
MaxReplicas: transformerMaxReplicas,
|
|
ScaleTarget: ptr.To(transformerCpuUtilization),
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
},
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Image: "transformer:v1",
|
|
Resources: defaultResource,
|
|
Args: []string{
|
|
"--port=8080",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvcConfigMap, err := v1beta1.GetInferenceServiceConfigMap(context.Background(), clientset)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
isvcConfig, err := v1beta1.NewInferenceServicesConfig(isvcConfigMap)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
isvc.DefaultInferenceService(isvcConfig, 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())
|
|
|
|
// check predictor deployment
|
|
actualPredictorDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualPredictorDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: predictorDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Predictor.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: map[string]string{
|
|
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassHPA),
|
|
},
|
|
},
|
|
Spec: 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,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
Strategy: getDefaultRollingStrategy(),
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualPredictorDeployment.Spec).To(BeComparableTo(expectedPredictorDeployment.Spec))
|
|
|
|
// check transformer deployment
|
|
actualTransformerDeployment := &appsv1.Deployment{}
|
|
transformerDeploymentKey := types.NamespacedName{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), transformerDeploymentKey, actualTransformerDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedTransformerDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: transformerDeploymentKey.Name,
|
|
Namespace: transformerDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + transformerDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: transformerDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + transformerDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Transformer.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Image: "transformer:v1",
|
|
Name: constants.InferenceServiceContainerName,
|
|
Args: []string{
|
|
"--port=8080",
|
|
"--model_name",
|
|
serviceKey.Name,
|
|
"--predictor_host",
|
|
fmt.Sprintf("%s.%s", predictorServiceKey.Name, predictorServiceKey.Namespace),
|
|
"--http_port",
|
|
"8080",
|
|
},
|
|
Env: []corev1.EnvVar{
|
|
{Name: constants.InferenceServiceNameEnvVarKey, Value: serviceName},
|
|
},
|
|
Resources: defaultResource,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
Strategy: getDefaultRollingStrategy(),
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualTransformerDeployment.Spec).To(BeComparableTo(expectedTransformerDeployment.Spec))
|
|
|
|
// check predictor service
|
|
actualPredictorService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorServiceKey, actualPredictorService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: predictorServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: predictorServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + predictorServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualPredictorService.Spec.ClusterIP = ""
|
|
actualPredictorService.Spec.ClusterIPs = nil
|
|
actualPredictorService.Spec.IPFamilies = nil
|
|
actualPredictorService.Spec.IPFamilyPolicy = nil
|
|
actualPredictorService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualPredictorService.Spec).To(BeComparableTo(expectedPredictorService.Spec))
|
|
|
|
// check transformer service
|
|
actualTransformerService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), transformerServiceKey, actualTransformerService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedTransformerService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: transformerServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: transformerServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + transformerServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualTransformerService.Spec.ClusterIP = ""
|
|
actualTransformerService.Spec.ClusterIPs = nil
|
|
actualTransformerService.Spec.IPFamilies = nil
|
|
actualTransformerService.Spec.IPFamilyPolicy = nil
|
|
actualTransformerService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualTransformerService.Spec).To(BeComparableTo(expectedTransformerService.Spec))
|
|
|
|
// update deployment status to make isvc ready
|
|
updatedPredictorDeployment := actualPredictorDeployment.DeepCopy()
|
|
updatedPredictorDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedPredictorDeployment)).NotTo(HaveOccurred())
|
|
updatedTransformerDeployment := actualTransformerDeployment.DeepCopy()
|
|
updatedTransformerDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedTransformerDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
prefixUrlPath := fmt.Sprintf("/serving/%s/%s", serviceKey.Namespace, serviceKey.Name)
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace)), "example.com"},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(transformerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(prefixUrlPath + "/"),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(transformerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
actualTransformerHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualTransformerHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
transformerHost := fmt.Sprintf("%s-%s.%s", transformerServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedTransformerHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(transformerHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(transformerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualTransformerHttpRoute.Spec).To(BeComparableTo(expectedTransformerHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualTransformerHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualTransformerHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).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: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
{
|
|
Type: v1beta1.TransformerReady,
|
|
Status: "True",
|
|
Severity: "Info",
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s.%s.svc.cluster.local", transformerServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", predictorServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
v1beta1.TransformerComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", transformerServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check predictor HPA
|
|
var defaultCpuUtilization int32 = 80
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualPredictorHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: &defaultCpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHPA.Spec).To(BeComparableTo(expectedPredictorHPA.Spec))
|
|
|
|
// check transformer HPA
|
|
actualTransformerHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
transformerHPAKey := types.NamespacedName{
|
|
Name: transformerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), transformerHPAKey, actualTransformerHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedTransformerHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: transformerServiceKey.Name,
|
|
},
|
|
MinReplicas: &transformerMinReplicas,
|
|
MaxReplicas: transformerMaxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &transformerCpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &transformerStabilizationWindowSeconds,
|
|
SelectPolicy: &transformerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &transformerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualTransformerHPA.Spec).To(BeComparableTo(expectedTransformerHPA.Spec))
|
|
})
|
|
})
|
|
Context("When creating inference service with raw kube path based routing predictor and explainer", func() {
|
|
configs := mergeJSONField(getRawKubeTestConfigs(), "ingress", map[string]interface{}{
|
|
"ingressDomain": "example.com",
|
|
"pathTemplate": "/serving/{{ .Namespace }}/{{ .Name }}",
|
|
})
|
|
|
|
It("Should have httproute/service/deployment/hpa created for explainer and predictor", 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-exp-path"
|
|
namespace := "default"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: namespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
predictorServiceKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceName),
|
|
Namespace: namespace,
|
|
}
|
|
explainerServiceKey := types.NamespacedName{
|
|
Name: constants.ExplainerServiceName(serviceName),
|
|
Namespace: namespace,
|
|
}
|
|
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var explainerMinReplicas int32 = 1
|
|
var explainerMaxReplicas int32 = 2
|
|
var explainerCpuUtilization int32 = 80
|
|
var explainerStabilizationWindowSeconds int32 = 0
|
|
ExplainerSelectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
ctx := context.Background()
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(minReplicas),
|
|
MaxReplicas: maxReplicas,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(explainerMinReplicas),
|
|
MaxReplicas: explainerMaxReplicas,
|
|
ScaleTarget: ptr.To(explainerCpuUtilization),
|
|
TimeoutSeconds: ptr.To(int64(30)),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvcConfigMap, err := v1beta1.GetInferenceServiceConfigMap(context.Background(), clientset)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
isvcConfig, err := v1beta1.NewInferenceServicesConfig(isvcConfigMap)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
isvc.DefaultInferenceService(isvcConfig, 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())
|
|
|
|
// check predictor deployment
|
|
actualPredictorDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualPredictorDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorDeployment := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: predictorDeploymentKey.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: ptr.To(REPLICAS),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorDeploymentKey.Name,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
"app": "isvc." + predictorDeploymentKey.Name,
|
|
constants.KServiceComponentLabel: constants.Predictor.String(),
|
|
constants.InferenceServicePodLabelKey: serviceName,
|
|
},
|
|
Annotations: map[string]string{
|
|
constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassHPA),
|
|
},
|
|
},
|
|
Spec: 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,
|
|
ReadinessProbe: &corev1.Probe{
|
|
ProbeHandler: corev1.ProbeHandler{
|
|
TCPSocket: &corev1.TCPSocketAction{
|
|
Port: intstr.IntOrString{
|
|
IntVal: 8080,
|
|
},
|
|
},
|
|
},
|
|
InitialDelaySeconds: 0,
|
|
TimeoutSeconds: 1,
|
|
PeriodSeconds: 10,
|
|
SuccessThreshold: 1,
|
|
FailureThreshold: 3,
|
|
},
|
|
TerminationMessagePath: "/dev/termination-log",
|
|
TerminationMessagePolicy: "File",
|
|
ImagePullPolicy: "IfNotPresent",
|
|
},
|
|
},
|
|
SchedulerName: "default-scheduler",
|
|
RestartPolicy: "Always",
|
|
TerminationGracePeriodSeconds: ptr.To(GRACE_PERIOD),
|
|
DNSPolicy: "ClusterFirst",
|
|
SecurityContext: defaultSecurityContext,
|
|
AutomountServiceAccountToken: ptr.To(false),
|
|
},
|
|
},
|
|
Strategy: getDefaultRollingStrategy(),
|
|
RevisionHistoryLimit: ptr.To(REVISION_HISTORY),
|
|
ProgressDeadlineSeconds: ptr.To(PROGRESSION_DEADLINE_SECODS),
|
|
},
|
|
}
|
|
Expect(actualPredictorDeployment.Spec).To(BeComparableTo(expectedPredictorDeployment.Spec))
|
|
|
|
// check Explainer deployment
|
|
actualExplainerDeployment := &appsv1.Deployment{}
|
|
explainerDeploymentKey := types.NamespacedName{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), explainerDeploymentKey, actualExplainerDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedExplainerDeployment := getExpectedDeployment(explainerDeploymentKey, serviceName, serviceKey, predictorServiceKey)
|
|
Expect(actualExplainerDeployment.Spec).To(BeComparableTo(expectedExplainerDeployment.Spec))
|
|
|
|
// check predictor service
|
|
actualPredictorService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorServiceKey, actualPredictorService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: predictorServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: predictorServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + predictorServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualPredictorService.Spec.ClusterIP = ""
|
|
actualPredictorService.Spec.ClusterIPs = nil
|
|
actualPredictorService.Spec.IPFamilies = nil
|
|
actualPredictorService.Spec.IPFamilyPolicy = nil
|
|
actualPredictorService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualPredictorService.Spec).To(BeComparableTo(expectedPredictorService.Spec))
|
|
|
|
// check Explainer service
|
|
actualExplainerService := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), explainerServiceKey, actualExplainerService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedExplainerService := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: explainerServiceKey.Namespace,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{
|
|
{
|
|
Name: explainerServiceKey.Name,
|
|
Protocol: "TCP",
|
|
Port: 80,
|
|
TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""},
|
|
},
|
|
},
|
|
Type: "ClusterIP",
|
|
SessionAffinity: "None",
|
|
Selector: map[string]string{
|
|
"app": "isvc." + explainerServiceKey.Name,
|
|
},
|
|
},
|
|
}
|
|
actualExplainerService.Spec.ClusterIP = ""
|
|
actualExplainerService.Spec.ClusterIPs = nil
|
|
actualExplainerService.Spec.IPFamilies = nil
|
|
actualExplainerService.Spec.IPFamilyPolicy = nil
|
|
actualExplainerService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualExplainerService.Spec).To(BeComparableTo(expectedExplainerService.Spec))
|
|
|
|
// update deployment status to make isvc ready
|
|
updatedPredictorDeployment := actualPredictorDeployment.DeepCopy()
|
|
updatedPredictorDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedPredictorDeployment)).NotTo(HaveOccurred())
|
|
updatedExplainerDeployment := actualExplainerDeployment.DeepCopy()
|
|
updatedExplainerDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedExplainerDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check http route
|
|
actualToplevelHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualToplevelHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
topLevelHost := fmt.Sprintf("%s-%s.%s", serviceKey.Name, serviceKey.Namespace, "example.com")
|
|
prefixUrlPath := fmt.Sprintf("/serving/%s/%s", serviceKey.Namespace, serviceKey.Name)
|
|
expectedToplevelHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(topLevelHost), gwapiv1.Hostname(fmt.Sprintf("%s-%s.additional.example.com", serviceKey.Name, serviceKey.Namespace)), "example.com"},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.ExplainPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(explainerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(prefixUrlPath + constants.PathBasedExplainPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(explainerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(prefixUrlPath + "/"),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualToplevelHttpRoute.Spec).To(BeComparableTo(expectedToplevelHttpRoute.Spec))
|
|
|
|
actualPredictorHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualPredictorHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
predictorHost := fmt.Sprintf("%s-%s.%s", predictorServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedPredictorHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(predictorHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(predictorServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("60s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHttpRoute.Spec).To(BeComparableTo(expectedPredictorHttpRoute.Spec))
|
|
|
|
actualExplainerHttpRoute := &gwapiv1.HTTPRoute{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), types.NamespacedName{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}, actualExplainerHttpRoute)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
explainerHost := fmt.Sprintf("%s-%s.%s", explainerServiceKey.Name, serviceKey.Namespace, "example.com")
|
|
expectedExplainerHttpRoute := gwapiv1.HTTPRoute{
|
|
Spec: gwapiv1.HTTPRouteSpec{
|
|
Hostnames: []gwapiv1.Hostname{gwapiv1.Hostname(explainerHost)},
|
|
Rules: []gwapiv1.HTTPRouteRule{
|
|
{
|
|
Matches: []gwapiv1.HTTPRouteMatch{
|
|
{
|
|
Path: &gwapiv1.HTTPPathMatch{
|
|
Type: ptr.To(gwapiv1.PathMatchRegularExpression),
|
|
Value: ptr.To(constants.FallbackPrefix()),
|
|
},
|
|
},
|
|
},
|
|
Filters: []gwapiv1.HTTPRouteFilter{
|
|
{
|
|
Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
|
|
RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
|
|
Set: []gwapiv1.HTTPHeader{
|
|
{
|
|
Name: constants.IsvcNameHeader,
|
|
Value: serviceKey.Name,
|
|
},
|
|
{
|
|
Name: constants.IsvcNamespaceHeader,
|
|
Value: serviceKey.Namespace,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
BackendRefs: []gwapiv1.HTTPBackendRef{
|
|
{
|
|
BackendRef: gwapiv1.BackendRef{
|
|
BackendObjectReference: gwapiv1.BackendObjectReference{
|
|
Group: (*gwapiv1.Group)(ptr.To("")),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.ServiceKind)),
|
|
Name: gwapiv1.ObjectName(explainerServiceKey.Name),
|
|
Namespace: (*gwapiv1.Namespace)(ptr.To(serviceKey.Namespace)),
|
|
Port: ptr.To(int32(constants.CommonDefaultHttpPort)),
|
|
},
|
|
Weight: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
Timeouts: &gwapiv1.HTTPRouteTimeouts{
|
|
Request: ptr.To(gwapiv1.Duration("30s")),
|
|
},
|
|
},
|
|
},
|
|
CommonRouteSpec: gwapiv1.CommonRouteSpec{
|
|
ParentRefs: []gwapiv1.ParentReference{
|
|
{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualExplainerHttpRoute.Spec).To(BeComparableTo(expectedExplainerHttpRoute.Spec))
|
|
|
|
// Mark the Ingress as accepted to make isvc ready
|
|
httpRouteStatus := gwapiv1.HTTPRouteStatus{
|
|
RouteStatus: gwapiv1.RouteStatus{
|
|
Parents: []gwapiv1.RouteParentStatus{
|
|
{
|
|
ParentRef: gwapiv1.ParentReference{
|
|
Name: gwapiv1.ObjectName(kserveGateway.Name),
|
|
Kind: ptr.To(gwapiv1.Kind(constants.GatewayKind)),
|
|
Group: (*gwapiv1.Group)(&gwapiv1.GroupVersion.Group),
|
|
Namespace: ptr.To(gwapiv1.Namespace(kserveGateway.Namespace)),
|
|
},
|
|
ControllerName: "istio.io/gateway-controller",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: string(gwapiv1.ListenerConditionAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Accepted",
|
|
Message: "Route was valid",
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
actualPredictorHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualPredictorHttpRoute)).NotTo(HaveOccurred())
|
|
actualExplainerHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualExplainerHttpRoute)).NotTo(HaveOccurred())
|
|
actualToplevelHttpRoute.Status = httpRouteStatus
|
|
Expect(k8sClient.Status().Update(context.Background(), actualToplevelHttpRoute)).NotTo(HaveOccurred())
|
|
|
|
// verify if InferenceService status is updated
|
|
expectedIsvcStatus := v1beta1.InferenceServiceStatus{
|
|
Status: duckv1.Status{
|
|
Conditions: duckv1.Conditions{
|
|
{
|
|
Type: v1beta1.ExplainerReady,
|
|
Status: "True",
|
|
Severity: "Info",
|
|
},
|
|
{
|
|
Type: v1beta1.IngressReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.PredictorReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: apis.ConditionReady,
|
|
Status: "True",
|
|
},
|
|
{
|
|
Type: v1beta1.Stopped,
|
|
Status: "False",
|
|
Severity: apis.ConditionSeverityInfo,
|
|
},
|
|
},
|
|
},
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", serviceKey.Name, serviceKey.Namespace),
|
|
},
|
|
Address: &duckv1.Addressable{
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s.%s.svc.cluster.local", predictorServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{
|
|
v1beta1.PredictorComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", predictorServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
v1beta1.ExplainerComponent: {
|
|
LatestCreatedRevision: "",
|
|
URL: &apis.URL{
|
|
Scheme: "http",
|
|
Host: fmt.Sprintf("%s-%s.example.com", explainerServiceKey.Name, serviceKey.Namespace),
|
|
},
|
|
},
|
|
},
|
|
ModelStatus: v1beta1.ModelStatus{
|
|
TransitionStatus: "InProgress",
|
|
ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"},
|
|
ModelCopies: &v1beta1.ModelCopies{},
|
|
},
|
|
DeploymentMode: string(constants.Standard),
|
|
ServingRuntimeName: "tf-serving-raw",
|
|
}
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check predictor HPA
|
|
var cpuUtilization int32 = 75
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: predictorServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualPredictorHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedPredictorHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: predictorServiceKey.Name,
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualPredictorHPA.Spec).To(BeComparableTo(expectedPredictorHPA.Spec))
|
|
|
|
// check Explainer HPA
|
|
actualExplainerHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
explainerHPAKey := types.NamespacedName{
|
|
Name: explainerServiceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), explainerHPAKey, actualExplainerHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedExplainerHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: explainerServiceKey.Name,
|
|
},
|
|
MinReplicas: &explainerMinReplicas,
|
|
MaxReplicas: explainerMaxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: "Utilization",
|
|
AverageUtilization: &explainerCpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &explainerStabilizationWindowSeconds,
|
|
SelectPolicy: &ExplainerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &ExplainerSelectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualExplainerHPA.Spec).To(BeComparableTo(expectedExplainerHPA.Spec))
|
|
})
|
|
})
|
|
Context("When creating inference service with raw kube predictor with gateway api disabled", func() {
|
|
configs := mergeJSONField(getRawKubeTestConfigs(), "ingress", map[string]interface{}{
|
|
"enableGatewayApi": false,
|
|
})
|
|
configs["opentelemetryCollector"] = `{
|
|
"scrapeInterval": "5s",
|
|
"metricReceiverEndpoint": "keda-otel-scaler.keda.svc:4317",
|
|
"metricScalerEndpoint": "keda-otel-scaler.keda.svc:4318"
|
|
}`
|
|
|
|
It("Should have KEDA ScaledObject 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-1"
|
|
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,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassKeda),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricMemory,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.AverageValueMetricType,
|
|
AverageValue: v1beta1.NewMetricQuantity("10Gi"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvc.DefaultInferenceService(nil, nil, nil, 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())
|
|
|
|
actualScaledObject := &kedav1alpha1.ScaledObject{}
|
|
|
|
predictorSObjectKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorSObjectKey, actualScaledObject)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedScaledobject := &kedav1alpha1.ScaledObject{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorSObjectKey.Name,
|
|
Namespace: predictorSObjectKey.Namespace,
|
|
},
|
|
Spec: kedav1alpha1.ScaledObjectSpec{
|
|
ScaleTargetRef: &kedav1alpha1.ScaleTarget{
|
|
Name: predictorSObjectKey.Name,
|
|
},
|
|
Triggers: []kedav1alpha1.ScaleTriggers{
|
|
{
|
|
Type: "memory",
|
|
Metadata: map[string]string{
|
|
"value": "10Gi",
|
|
},
|
|
MetricType: autoscalingv2.AverageValueMetricType,
|
|
},
|
|
},
|
|
MinReplicaCount: ptr.To(int32(1)),
|
|
MaxReplicaCount: ptr.To(int32(3)),
|
|
},
|
|
}
|
|
Expect(actualScaledObject.Spec).To(Equal(expectedScaledobject.Spec))
|
|
|
|
// check service
|
|
actualService := &corev1.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 := getExectedService(predictorServiceKey, serviceName)
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(Equal(expectedService.Spec))
|
|
|
|
// check isvc status
|
|
updatedScaledObject := actualScaledObject.DeepCopy()
|
|
updatedScaledObject.Status.Conditions = []kedav1alpha1.Condition{
|
|
{
|
|
Type: kedav1alpha1.ConditionReady,
|
|
Status: kedav1alpha1.GetInitializedConditions().GetActiveCondition().Status,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedScaledObject)).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("Should have OpenTelemetry Collector 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-foo-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,
|
|
Annotations: map[string]string{
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
constants.AutoscalerClass: string(constants.AutoscalerClassKeda),
|
|
"sidecar.opentelemetry.io/inject": "true",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.PodMetricSourceType,
|
|
PodMetric: &v1beta1.PodMetricSource{
|
|
Metric: v1beta1.PodMetrics{
|
|
Backend: v1beta1.OpenTelemetryBackend,
|
|
MetricNames: []string{"process_cpu_seconds_total"},
|
|
Query: "avg(process_cpu_seconds_total)",
|
|
},
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.ValueMetricType,
|
|
Value: v1beta1.NewMetricQuantity(""),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvc.DefaultInferenceService(nil, nil, nil, 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())
|
|
|
|
actualOTelCollector := &otelv1beta1.OpenTelemetryCollector{}
|
|
|
|
predictorSObjectKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorSObjectKey, actualOTelCollector)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedOTelCollector := &otelv1beta1.OpenTelemetryCollector{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: predictorSObjectKey.Name,
|
|
Namespace: predictorSObjectKey.Namespace,
|
|
},
|
|
Spec: otelv1beta1.OpenTelemetryCollectorSpec{
|
|
Mode: "sidecar",
|
|
Config: otelv1beta1.Config{
|
|
Receivers: otelv1beta1.AnyConfig{Object: map[string]interface{}{
|
|
"prometheus": map[string]interface{}{
|
|
"config": map[string]interface{}{
|
|
"scrape_configs": []interface{}{
|
|
map[string]interface{}{
|
|
"job_name": "otel-collector",
|
|
"scrape_interval": "5s",
|
|
"static_configs": []interface{}{
|
|
map[string]interface{}{
|
|
"targets": []interface{}{"localhost:8080"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Processors: &otelv1beta1.AnyConfig{Object: map[string]interface{}{
|
|
"filter/metrics": map[string]interface{}{
|
|
"metrics": map[string]interface{}{
|
|
"include": map[string]interface{}{
|
|
"match_type": "strict",
|
|
"metric_names": []interface{}{string("process_cpu_seconds_total")},
|
|
},
|
|
},
|
|
},
|
|
"resourcedetection/env": map[string]interface{}{
|
|
"detectors": []interface{}{string("env")},
|
|
"override": bool(false),
|
|
"timeout": string("2s"),
|
|
},
|
|
"transform": map[string]interface{}{
|
|
"metric_statements": []interface{}{map[string]interface{}{
|
|
"context": string("datapoint"),
|
|
"statements": []interface{}{
|
|
string("set(attributes[\"namespace\"], resource.attributes[\"k8s.namespace.name\"])"),
|
|
string("set(attributes[\"deployment\"], resource.attributes[\"k8s.deployment.name\"])"),
|
|
string("set(attributes[\"pod\"], resource.attributes[\"k8s.pod.name\"])"),
|
|
},
|
|
}},
|
|
},
|
|
}},
|
|
Exporters: otelv1beta1.AnyConfig{Object: map[string]interface{}{
|
|
"otlp": map[string]interface{}{
|
|
"endpoint": "keda-otel-scaler.keda.svc:4317",
|
|
"compression": "none",
|
|
"tls": map[string]interface{}{
|
|
"insecure": true,
|
|
},
|
|
},
|
|
}},
|
|
Service: otelv1beta1.Service{
|
|
Pipelines: map[string]*otelv1beta1.Pipeline{
|
|
"metrics": {
|
|
Receivers: []string{"prometheus"},
|
|
Processors: []string{"resourcedetection/env", "transform", "filter/metrics"},
|
|
Exporters: []string{"otlp"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualOTelCollector.Spec.Config).To(BeComparableTo(expectedOTelCollector.Spec.Config))
|
|
})
|
|
|
|
It("Should have ingress/service/deployment/hpa 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 := getServingRuntime("tf-serving-raw", "default")
|
|
_ = k8sClient.Create(context.TODO(), &servingRuntime)
|
|
defer k8sClient.Delete(context.TODO(), &servingRuntime)
|
|
|
|
serviceName := "raw-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,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassHPA),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 3,
|
|
AutoScaling: &v1beta1.AutoScalingSpec{
|
|
Metrics: []v1beta1.MetricsSpec{
|
|
{
|
|
Type: v1beta1.ResourceMetricSourceType,
|
|
Resource: &v1beta1.ResourceMetricSource{
|
|
Name: v1beta1.ResourceMetricCPU,
|
|
Target: v1beta1.MetricTarget{
|
|
Type: v1beta1.UtilizationMetricType,
|
|
AverageUtilization: ptr.To(int32(75)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedDeployment := getDeploymentWithKServiceLabel(predictorDeploymentKey, serviceName, isvc)
|
|
Expect(actualDeployment.Spec).To(Equal(expectedDeployment.Spec))
|
|
|
|
// check service
|
|
actualService := &corev1.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 := getExectedService(predictorServiceKey, serviceName)
|
|
actualService.Spec.ClusterIP = ""
|
|
actualService.Spec.ClusterIPs = nil
|
|
actualService.Spec.IPFamilies = nil
|
|
actualService.Spec.IPFamilyPolicy = nil
|
|
actualService.Spec.InternalTrafficPolicy = nil
|
|
Expect(actualService.Spec).To(Equal(expectedService.Spec))
|
|
|
|
// check isvc status
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// check ingress
|
|
pathType := netv1.PathTypePrefix
|
|
actualIngress := &netv1.Ingress{}
|
|
predictorIngressKey := types.NamespacedName{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorIngressKey, actualIngress)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedIngress := netv1.Ingress{
|
|
Spec: netv1.IngressSpec{
|
|
Rules: []netv1.IngressRule{
|
|
{
|
|
Host: "raw-foo-default.example.com",
|
|
IngressRuleValue: netv1.IngressRuleValue{
|
|
HTTP: &netv1.HTTPIngressRuleValue{
|
|
Paths: []netv1.HTTPIngressPath{
|
|
{
|
|
Path: "/",
|
|
PathType: &pathType,
|
|
Backend: netv1.IngressBackend{
|
|
Service: &netv1.IngressServiceBackend{
|
|
Name: serviceName + "-predictor",
|
|
Port: netv1.ServiceBackendPort{
|
|
Number: 80,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Host: "raw-foo-predictor-default.example.com",
|
|
IngressRuleValue: netv1.IngressRuleValue{
|
|
HTTP: &netv1.HTTPIngressRuleValue{
|
|
Paths: []netv1.HTTPIngressPath{
|
|
{
|
|
Path: "/",
|
|
PathType: &pathType,
|
|
Backend: netv1.IngressBackend{
|
|
Service: &netv1.IngressServiceBackend{
|
|
Name: serviceName + "-predictor",
|
|
Port: netv1.ServiceBackendPort{
|
|
Number: 80,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualIngress.Spec).To(Equal(expectedIngress.Spec))
|
|
// verify if InferenceService status is updated
|
|
expectedIsvcStatus := getExpectedIsvcStatus(serviceKey)
|
|
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.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// check HPA
|
|
var minReplicas int32 = 1
|
|
var maxReplicas int32 = 3
|
|
var cpuUtilization int32 = 75
|
|
var stabilizationWindowSeconds int32 = 0
|
|
selectPolicy := autoscalingv2.MaxChangePolicySelect
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
expectedHPA := &autoscalingv2.HorizontalPodAutoscaler{
|
|
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
|
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
},
|
|
MinReplicas: &minReplicas,
|
|
MaxReplicas: maxReplicas,
|
|
Metrics: []autoscalingv2.MetricSpec{
|
|
{
|
|
Type: autoscalingv2.ResourceMetricSourceType,
|
|
Resource: &autoscalingv2.ResourceMetricSource{
|
|
Name: corev1.ResourceCPU,
|
|
Target: autoscalingv2.MetricTarget{
|
|
Type: autoscalingv2.UtilizationMetricType,
|
|
AverageUtilization: &cpuUtilization,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
|
|
ScaleUp: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: &stabilizationWindowSeconds,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Pods",
|
|
Value: 4,
|
|
PeriodSeconds: 15,
|
|
},
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
ScaleDown: &autoscalingv2.HPAScalingRules{
|
|
StabilizationWindowSeconds: nil,
|
|
SelectPolicy: &selectPolicy,
|
|
Policies: []autoscalingv2.HPAScalingPolicy{
|
|
{
|
|
Type: "Percent",
|
|
Value: 100,
|
|
PeriodSeconds: 15,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(actualHPA.Spec).To(Equal(expectedHPA.Spec))
|
|
})
|
|
})
|
|
|
|
Context("When creating inference service with raw kube predictor with workerSpec", func() {
|
|
var (
|
|
serviceKey types.NamespacedName
|
|
storageUri string
|
|
isvc *v1beta1.InferenceService
|
|
)
|
|
|
|
isvcNamespace := constants.KServeNamespace
|
|
actualDefaultDeployment := &appsv1.Deployment{}
|
|
actualWorkerDeployment := &appsv1.Deployment{}
|
|
|
|
BeforeEach(func() {
|
|
ctx := context.Background()
|
|
storageUri = "pvc://llama-3-8b-pvc/hf/8b_instruction_tuned"
|
|
|
|
// Create a ConfigMap
|
|
configs := getRawKubeTestConfigs()
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
DeferCleanup(func() { _ = k8sClient.Delete(ctx, configMap) })
|
|
|
|
// Create a ServingRuntime
|
|
servingRuntime := &v1alpha1.ServingRuntime{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "huggingface-server-multinode",
|
|
Namespace: isvcNamespace,
|
|
},
|
|
Spec: v1alpha1.ServingRuntimeSpec{
|
|
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
|
|
{
|
|
Name: "huggingface",
|
|
Version: ptr.To("2"),
|
|
AutoSelect: ptr.To(true),
|
|
},
|
|
},
|
|
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Command: []string{
|
|
"bash",
|
|
"-c",
|
|
"python3 -m huggingfaceserver --model_name=${MODEL_NAME} --model_dir=${MODEL} --tensor-parallel-size=${TENSOR_PARALLEL_SIZE} --pipeline-parallel-size=${PIPELINE_PARALLEL_SIZE}",
|
|
},
|
|
Args: []string{
|
|
"--model_name={{.Name}}",
|
|
},
|
|
Resources: defaultResource,
|
|
},
|
|
},
|
|
},
|
|
WorkerSpec: &v1alpha1.WorkerSpec{
|
|
PipelineParallelSize: ptr.To(2),
|
|
TensorParallelSize: ptr.To(1),
|
|
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.WorkerContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Command: []string{
|
|
"bash",
|
|
"-c",
|
|
"ray start --address=$RAY_HEAD_ADDRESS --block",
|
|
},
|
|
Resources: defaultResource,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Disabled: ptr.To(false),
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, servingRuntime)).NotTo(HaveOccurred())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, servingRuntime)
|
|
})
|
|
})
|
|
It("Should have services/deployments for head/worker without an autoscaler when workerSpec is set in isvc", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-1"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, isvc)
|
|
})
|
|
|
|
// Verify inferenceService is created
|
|
inferenceService := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, serviceKey, inferenceService) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "2")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "1")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "2")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "2")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "1", "1", constants.NvidiaGPUResourceType, constants.NvidiaGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(1))))
|
|
|
|
// Check Services
|
|
actualService := &corev1.Service{}
|
|
headServiceName := constants.GetHeadServiceName(isvcName+"-predictor", "1")
|
|
defaultServiceName := isvcName + "-predictor"
|
|
expectedHeadServiceName := types.NamespacedName{Name: headServiceName, Namespace: isvcNamespace}
|
|
expectedDefaultServiceName := types.NamespacedName{Name: defaultServiceName, Namespace: isvcNamespace}
|
|
|
|
// Verify if head service is created
|
|
Eventually(func() bool {
|
|
if err := k8sClient.Get(ctx, expectedHeadServiceName, actualService); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
Expect(actualService.Spec.ClusterIP).Should(Equal("None"))
|
|
Expect(actualService.Spec.PublishNotReadyAddresses).Should(BeTrue())
|
|
|
|
// Verify if predictor service (default service) is created
|
|
Eventually(func() bool {
|
|
if err := k8sClient.Get(ctx, expectedDefaultServiceName, actualService); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify there if the default autoscaler(HPA) is not created.
|
|
actualHPA := &autoscalingv2.HorizontalPodAutoscaler{}
|
|
predictorHPAKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(isvcName),
|
|
Namespace: isvcNamespace,
|
|
}
|
|
|
|
Eventually(func() error {
|
|
err := k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA)
|
|
if err != nil && apierr.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected IsNotFound error, but got %w", err)
|
|
}, timeout, interval).Should(Succeed())
|
|
})
|
|
It("Should use WorkerSpec.PipelineParallelSize value in isvc when it is set", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("By creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-4"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
PipelineParallelSize: ptr.To(5),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, isvc)
|
|
})
|
|
|
|
// Verify inferenceService is created
|
|
inferenceService := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, serviceKey, inferenceService) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments details
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "5")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "1")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "5")
|
|
|
|
// Verify worker deployments details
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "5")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "1", "1", constants.NvidiaGPUResourceType, constants.NvidiaGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(4))))
|
|
})
|
|
It("Should use WorkerSpec.TensorParallelSize value in isvc when it is set", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-5"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
TensorParallelSize: ptr.To(16),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, isvc)
|
|
})
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "2")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "16")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "32")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "32")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "1", "1", constants.NvidiaGPUResourceType, constants.NvidiaGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(31))))
|
|
})
|
|
It("Should use head container GPU resource value in isvc when it is set", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-5-1"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
Container: corev1.Container{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
TensorParallelSize: ptr.To(4),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, isvc)
|
|
})
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "2")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "4")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "7")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "7")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "2", "1", constants.IntelGPUResourceType, constants.NvidiaGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(6))))
|
|
})
|
|
It("Should use worker container GPU resource value in isvc when it is set", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-5-2"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
TensorParallelSize: ptr.To(4),
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.WorkerContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, isvc)
|
|
})
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "2")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "4")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "4")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "4")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "2", "2", constants.NvidiaGPUResourceType, constants.IntelGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(3))))
|
|
})
|
|
It("Should run head node only, worker node replicas should be set 0 when head node gpu count is equal to total required gpu", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-5-3"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
Container: corev1.Container{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("8"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("8"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
TensorParallelSize: ptr.To(4),
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.WorkerContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, isvc)
|
|
})
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "2")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "4")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "1")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "1")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "8", "0", constants.IntelGPUResourceType, constants.IntelGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(0))))
|
|
})
|
|
It("Should run head node only, worker node replicas should be set 0 when worker node gpu count is equal to total required gpu", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-5-4"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
Container: corev1.Container{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
TensorParallelSize: ptr.To(4),
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.WorkerContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("8"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("8"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() {
|
|
_ = k8sClient.Delete(ctx, isvc)
|
|
})
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "2")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "4")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "1")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "1")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "8", "0", constants.IntelGPUResourceType, constants.IntelGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(0))))
|
|
})
|
|
It("Should return error if total required gpu count is less than what it should be assigned with head/worker", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-5-5"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
Container: corev1.Container{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("2"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
PipelineParallelSize: ptr.To(1),
|
|
TensorParallelSize: ptr.To(7),
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.WorkerContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("4"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("4"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() { _ = k8sClient.Delete(ctx, isvc) })
|
|
|
|
updatedIsvc := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
if err := k8sClient.Get(ctx, serviceKey, updatedIsvc); err == nil {
|
|
for _, condition := range updatedIsvc.Status.Status.Conditions {
|
|
if condition.Type == v1beta1.PredictorReady &&
|
|
condition.Reason == v1beta1.InvalidGPUAllocation &&
|
|
condition.Message == fmt.Sprintf(components.ErrRayClusterInsufficientGPUs, 7, 2, 4) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if predictor deployment (default deployment) is not created
|
|
Consistently(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment)
|
|
}, fastTimeout, interval).ShouldNot(Succeed())
|
|
|
|
// Verify if worker node deployment is not created.
|
|
Consistently(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment)
|
|
}, fastTimeout, interval).ShouldNot(Succeed())
|
|
})
|
|
|
|
It("Should deploy even if it assigns more GPUs than the total required GPU resource count.", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-5-6"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
Container: corev1.Container{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("4"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("4"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
TensorParallelSize: ptr.To(16),
|
|
PodSpec: v1beta1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.WorkerContainerName,
|
|
Image: "kserve/huggingfaceserver:latest-gpu",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("3"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.IntelGPUResourceType: resource.MustParse("3"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() { _ = k8sClient.Delete(ctx, isvc) })
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "2")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "16")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "11")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "11")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "4", "3", constants.IntelGPUResourceType, constants.IntelGPUResourceType)
|
|
|
|
// Verify worker node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(10))))
|
|
})
|
|
It("Should not set nil to replicas when multinode isvc(none autoscaler) is updated", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
By("creating a new InferenceService")
|
|
isvcName := "raw-huggingface-multinode-6"
|
|
predictorDeploymentName := constants.PredictorServiceName(isvcName)
|
|
workerDeploymentName := constants.PredictorWorkerServiceName(isvcName)
|
|
serviceKey = types.NamespacedName{Name: isvcName, Namespace: isvcNamespace}
|
|
|
|
// Create a infereceService
|
|
isvc = &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: isvcName,
|
|
Namespace: isvcNamespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "huggingface",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
},
|
|
},
|
|
WorkerSpec: &v1beta1.WorkerSpec{
|
|
PipelineParallelSize: ptr.To(3),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() { _ = k8sClient.Delete(ctx, isvc) })
|
|
|
|
// Verify if predictor deployment (default deployment) is created
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{
|
|
Name: predictorDeploymentName,
|
|
Namespace: isvcNamespace,
|
|
}, actualDefaultDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment is created.
|
|
Eventually(func() bool {
|
|
return k8sClient.Get(ctx, types.NamespacedName{
|
|
Name: workerDeploymentName,
|
|
Namespace: isvcNamespace,
|
|
}, actualWorkerDeployment) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify head deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.PipelineParallelSizeEnvName, "3")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.TensorParallelSizeEnvName, "1")
|
|
verifyEnvKeyValueDeployments(actualDefaultDeployment, constants.RayNodeCountEnvName, "3")
|
|
|
|
// Verify worker deployments environment variables
|
|
verifyEnvKeyValueDeployments(actualWorkerDeployment, constants.RayNodeCountEnvName, "3")
|
|
|
|
// Verify gpu resources for head/worker nodes
|
|
verifyGPUResourceSizeDeployment(actualDefaultDeployment, actualWorkerDeployment, "1", "1", constants.NvidiaGPUResourceType, constants.NvidiaGPUResourceType)
|
|
|
|
// Verify worker Node replicas
|
|
Expect(actualWorkerDeployment.Spec.Replicas).Should(Equal(ptr.To(int32(2))))
|
|
|
|
// Update a infereceService
|
|
By("updating the InferenceService")
|
|
updatedIsvc := &v1beta1.InferenceService{}
|
|
_ = k8sClient.Get(ctx, types.NamespacedName{Name: isvc.Name, Namespace: isvcNamespace}, updatedIsvc)
|
|
// Add label to isvc to create a new rs
|
|
if updatedIsvc.Labels == nil {
|
|
updatedIsvc.Labels = make(map[string]string)
|
|
}
|
|
updatedIsvc.Labels["newLabel"] = "test"
|
|
|
|
err := k8sClient.Update(ctx, updatedIsvc)
|
|
Expect(err).ShouldNot(HaveOccurred(), "Failed to update InferenceService with new label")
|
|
|
|
// Verify if predictor deployment (default deployment) has replicas
|
|
Eventually(func() bool {
|
|
if err := k8sClient.Get(ctx, types.NamespacedName{Name: predictorDeploymentName, Namespace: isvcNamespace}, actualDefaultDeployment); err == nil {
|
|
return actualDefaultDeployment.Spec.Replicas != nil && *actualDefaultDeployment.Spec.Replicas == 1
|
|
}
|
|
return false
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify if worker node deployment has replicas
|
|
Eventually(func() bool {
|
|
if err := k8sClient.Get(ctx, types.NamespacedName{Name: workerDeploymentName, Namespace: isvcNamespace}, actualWorkerDeployment); err == nil {
|
|
return actualWorkerDeployment.Spec.Replicas != nil && *actualWorkerDeployment.Spec.Replicas == 2
|
|
}
|
|
return false
|
|
}, timeout, interval).Should(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("When creating inference service with headless service (ServiceClusterIPNone)", func() {
|
|
It("Should include port 8080 in status.address.url", func() {
|
|
By("By creating a new InferenceService with headless service enabled")
|
|
ctx := context.Background()
|
|
|
|
// Create configmap with serviceClusterIPNone enabled
|
|
configs := map[string]string{
|
|
"ingress": `{
|
|
"ingressGateway": "knative-serving/knative-ingress-gateway",
|
|
"localGateway": "knative-serving/knative-local-gateway",
|
|
"localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local"
|
|
}`,
|
|
"storageInitializer": `{
|
|
"image": "kserve/storage-initializer:latest",
|
|
"memoryRequest": "100Mi",
|
|
"memoryLimit": "1Gi",
|
|
"cpuRequest": "100m",
|
|
"cpuLimit": "1",
|
|
"caBundleConfigMapName": "",
|
|
"caBundleVolumeMountPath": "/etc/ssl/custom-certs",
|
|
"cpuModelcar": "10m",
|
|
"memoryModelcar": "15Mi"
|
|
}`,
|
|
"service": `{"serviceClusterIPNone": true}`,
|
|
}
|
|
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-raw",
|
|
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: "kserve-container",
|
|
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)).Should(Succeed())
|
|
defer k8sClient.Delete(ctx, servingRuntime)
|
|
|
|
serviceName := "raw-headless-port"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
storageUri := "s3://test/mnist/export"
|
|
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: map[string]string{
|
|
constants.DeploymentMode: string(constants.Standard),
|
|
},
|
|
},
|
|
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())
|
|
defer k8sClient.Delete(ctx, isvc)
|
|
|
|
inferenceService := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, serviceKey, inferenceService)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Wait for deployment to be created
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, predictorDeploymentKey, actualDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
// Wait for service to be created and verify it's headless
|
|
actualService := &corev1.Service{}
|
|
predictorServiceKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, predictorServiceKey, actualService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
// Verify service is headless (ClusterIP: None)
|
|
Expect(actualService.Spec.ClusterIP).To(Equal(corev1.ClusterIPNone),
|
|
"Service should be headless (ClusterIP: None) when serviceClusterIPNone is enabled")
|
|
|
|
// Update deployment status to trigger status reconciliation
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionTrue,
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(ctx, updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
// Verify status.address.url includes port 8080
|
|
expectedIsvcStatus := getExpectedIsvcStatusWithPort(serviceKey, constants.InferenceServiceDefaultHttpPort)
|
|
Eventually(func() string {
|
|
isvc := &v1beta1.InferenceService{}
|
|
if err := k8sClient.Get(ctx, serviceKey, isvc); err != nil {
|
|
return err.Error()
|
|
}
|
|
return cmp.Diff(expectedIsvcStatus, isvc.Status, cmpopts.IgnoreTypes(apis.VolatileTime{}))
|
|
}, timeout, interval).Should(BeEmpty())
|
|
|
|
// Double-check the address URL contains :8080
|
|
Eventually(func() bool {
|
|
isvc := &v1beta1.InferenceService{}
|
|
if err := k8sClient.Get(ctx, serviceKey, isvc); err != nil {
|
|
return false
|
|
}
|
|
if isvc.Status.Address == nil || isvc.Status.Address.URL == nil {
|
|
return false
|
|
}
|
|
expectedHost := serviceKey.Name + "-predictor." + serviceKey.Namespace + ".svc.cluster.local:8080"
|
|
return isvc.Status.Address.URL.Host == expectedHost
|
|
}, timeout, interval).Should(BeTrue(),
|
|
"status.address.url should include :8080 for headless service")
|
|
})
|
|
})
|
|
|
|
Context("When creating an inference service with modelcar and raw deployment", func() {
|
|
configs := map[string]string{
|
|
"ingress": `{
|
|
"enableGatewayApi": true,
|
|
"kserveIngressGateway": "kserve/kserve-ingress-gateway",
|
|
"ingressGateway": "knative-serving/knative-ingress-gateway",
|
|
"localGateway": "knative-serving/knative-local-gateway",
|
|
"localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local",
|
|
"additionalIngressDomains": ["additional.example.com"]
|
|
}`,
|
|
"storageInitializer": `{
|
|
"image" : "kserve/storage-initializer:latest",
|
|
"memoryRequest": "100Mi",
|
|
"memoryLimit": "1Gi",
|
|
"cpuRequest": "100m",
|
|
"cpuLimit": "1",
|
|
"CaBundleConfigMapName": "",
|
|
"caBundleVolumeMountPath": "/etc/ssl/custom-certs",
|
|
"enableDirectPvcVolumeMount": false,
|
|
"cpuModelcar": "10m",
|
|
"memoryModelcar": "15Mi"
|
|
}`,
|
|
}
|
|
|
|
It("Should only have the ImagePullSecrets that are specified in the InferenceService", func() {
|
|
By("Updating an InferenceService with a new ImagePullSecret and checking the deployment")
|
|
configMap := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: constants.InferenceServiceConfigMapName,
|
|
Namespace: constants.KServeNamespace,
|
|
},
|
|
Data: configs,
|
|
}
|
|
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
|
|
defer k8sClient.Delete(context.TODO(), configMap)
|
|
|
|
servingRuntime := &v1alpha1.ServingRuntime{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "vllm-runtime",
|
|
Namespace: constants.KServeNamespace,
|
|
},
|
|
Spec: v1alpha1.ServingRuntimeSpec{
|
|
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
|
|
{
|
|
AutoSelect: ptr.To(true),
|
|
Name: "vLLM",
|
|
},
|
|
},
|
|
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Image: "kserve/vllm:latest",
|
|
Command: []string{"bash", "-c"},
|
|
Args: []string{
|
|
"python2 -m vllm --model_name=${MODEL_NAME} --model_dir=${MODEL} --tensor-parallel-size=${TENSOR_PARALLEL_SIZE} --pipeline-parallel-size=${PIPELINE_PARALLEL_SIZE}",
|
|
},
|
|
Resources: defaultResource,
|
|
},
|
|
},
|
|
},
|
|
Disabled: ptr.To(false),
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(context.TODO(), servingRuntime)).To(Succeed())
|
|
defer func() { _ = k8sClient.Delete(context.TODO(), servingRuntime) }()
|
|
serviceName := "modelcar-raw-deployment"
|
|
expectedRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: constants.KServeNamespace}}
|
|
serviceKey := expectedRequest.NamespacedName
|
|
storageUri := "oci://test/mnist/export"
|
|
ctx := context.Background()
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: map[string]string{
|
|
"serving.kserve.io/deploymentMode": "Standard",
|
|
"serving.kserve.io/autoscalerClass": "hpa",
|
|
"serving.kserve.io/metrics": "cpu",
|
|
"serving.kserve.io/targetUtilizationPercentage": "75",
|
|
},
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
|
|
MinReplicas: ptr.To(int32(1)),
|
|
MaxReplicas: 2,
|
|
},
|
|
PodSpec: v1beta1.PodSpec{
|
|
ImagePullSecrets: []corev1.LocalObjectReference{
|
|
{Name: "isvc-image-pull-secret"},
|
|
},
|
|
},
|
|
Model: &v1beta1.ModelSpec{
|
|
ModelFormat: v1beta1.ModelFormat{
|
|
Name: "vLLM",
|
|
},
|
|
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
|
|
StorageURI: &storageUri,
|
|
RuntimeVersion: ptr.To("0.14.0"),
|
|
Container: corev1.Container{
|
|
Name: constants.InferenceServiceContainerName,
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
constants.NvidiaGPUResourceType: resource.MustParse("1"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
constants.NvidiaGPUResourceType: resource.MustParse("1"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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 {
|
|
return k8sClient.Get(ctx, serviceKey, inferenceService) == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
actualDeployment := &appsv1.Deployment{}
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment) }, timeout, interval).
|
|
Should(Succeed())
|
|
|
|
Expect(actualDeployment.Spec.Template.Spec.ImagePullSecrets).To(HaveLen(1))
|
|
Expect(actualDeployment.Spec.Template.Spec.ImagePullSecrets[0].Name).To(Equal("isvc-image-pull-secret"))
|
|
|
|
Expect(k8sClient.Get(ctx, serviceKey, inferenceService)).Should(Succeed())
|
|
updateForInferenceService := inferenceService.DeepCopy()
|
|
updateForInferenceService.Spec.Predictor.ImagePullSecrets = []corev1.LocalObjectReference{
|
|
{Name: "new-image-pull-secret"},
|
|
}
|
|
expectedImagePullSecrets := updateForInferenceService.Spec.Predictor.ImagePullSecrets
|
|
Eventually(func() error {
|
|
return k8sClient.Update(ctx, updateForInferenceService)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
updatedDeployment := &appsv1.Deployment{}
|
|
Eventually(func() (bool, error) {
|
|
if err := k8sClient.Get(ctx, predictorDeploymentKey, updatedDeployment); err != nil {
|
|
return false, err
|
|
}
|
|
if len(updatedDeployment.Spec.Template.Spec.ImagePullSecrets) != 1 {
|
|
return false, nil
|
|
}
|
|
return reflect.DeepEqual(updatedDeployment.Spec.Template.Spec.ImagePullSecrets, expectedImagePullSecrets), nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
|
|
// TestDeploymentReplicaFailurePropagatedToIsvcStatus is an integration test that covers
|
|
// the combined fix for Bug 1 and Bug 2 from issue #5113.
|
|
//
|
|
// Bug 1: Without InitializeConditions on early paths the ISVC status could be completely
|
|
// empty. After the fix the status section must always contain at least the "Ready"
|
|
// condition once the first reconciliation cycle completes.
|
|
//
|
|
// Bug 2: Previously, DeploymentReconciler.Reconcile returned the in-memory desired
|
|
// deployment (no .Status.Conditions) instead of the server-side existing deployment.
|
|
// As a result, a ReplicaFailure condition (e.g. from an admission webhook denying pod
|
|
// creation) was silently lost. After the fix the ISVC PredictorReady condition must
|
|
// carry the reason and message from the deployment's ReplicaFailure condition.
|
|
var _ = Context("When a Standard-mode predictor deployment develops a ReplicaFailure", func() {
|
|
It("should surface the failure reason in the InferenceService PredictorReady condition", func() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
|
|
By("setting up configmap and serving runtime")
|
|
configs := getRawKubeTestConfigs()
|
|
configMap := createInferenceServiceConfigMap(configs)
|
|
Expect(k8sClient.Create(ctx, configMap)).NotTo(HaveOccurred())
|
|
DeferCleanup(func() { _ = k8sClient.Delete(ctx, configMap) })
|
|
|
|
servingRuntime := getServingRuntime("tf-serving-raw", "default")
|
|
Expect(k8sClient.Create(ctx, &servingRuntime)).To(Or(Succeed(), MatchError(ContainSubstring("already exists"))))
|
|
DeferCleanup(func() { _ = k8sClient.Delete(ctx, &servingRuntime) })
|
|
|
|
By("creating the InferenceService")
|
|
serviceName := "raw-replica-failure-test"
|
|
serviceKey := types.NamespacedName{Name: serviceName, Namespace: "default"}
|
|
|
|
isvc := &v1beta1.InferenceService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceKey.Name,
|
|
Namespace: serviceKey.Namespace,
|
|
Annotations: getDefaultAnnotations(constants.AutoscalerClassNone),
|
|
},
|
|
Spec: v1beta1.InferenceServiceSpec{
|
|
Predictor: v1beta1.PredictorSpec{
|
|
Tensorflow: &v1beta1.TFServingSpec{
|
|
PredictorExtensionSpec: getCommonPredictorExtensionSpec(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil, nil)
|
|
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
|
|
DeferCleanup(func() { _ = k8sClient.Delete(ctx, isvc) })
|
|
|
|
By("waiting for the controller to create the predictor deployment")
|
|
predictorDeploymentKey := types.NamespacedName{
|
|
Name: constants.PredictorServiceName(serviceKey.Name),
|
|
Namespace: serviceKey.Namespace,
|
|
}
|
|
actualDeployment := &appsv1.Deployment{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, predictorDeploymentKey, actualDeployment)
|
|
}, timeout, interval).Should(Succeed())
|
|
|
|
By("patching the deployment status with a ReplicaFailure condition (simulating webhook denial)")
|
|
replicaFailureMsg := `admission webhook "pod-policy.example.com" denied the request: identity "my-identity" not found`
|
|
updatedDeployment := actualDeployment.DeepCopy()
|
|
updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{
|
|
{
|
|
Type: appsv1.DeploymentReplicaFailure,
|
|
Status: corev1.ConditionTrue,
|
|
Reason: "FailedCreate",
|
|
Message: replicaFailureMsg,
|
|
},
|
|
{
|
|
Type: appsv1.DeploymentAvailable,
|
|
Status: corev1.ConditionFalse,
|
|
Reason: "MinimumReplicasUnavailable",
|
|
Message: "Deployment does not have minimum availability.",
|
|
},
|
|
}
|
|
Expect(k8sClient.Status().Update(ctx, updatedDeployment)).NotTo(HaveOccurred())
|
|
|
|
By("verifying the ISVC PredictorReady condition reflects ReplicaFailure (Bug 2 fix)")
|
|
updatedIsvc := &v1beta1.InferenceService{}
|
|
Eventually(func() bool {
|
|
if err := k8sClient.Get(ctx, serviceKey, updatedIsvc); err != nil {
|
|
return false
|
|
}
|
|
cond := updatedIsvc.Status.GetCondition(v1beta1.PredictorReady)
|
|
return cond != nil &&
|
|
cond.IsFalse() &&
|
|
cond.Reason == "FailedCreate" &&
|
|
cond.Message == replicaFailureMsg
|
|
}, timeout, interval).Should(BeTrue(),
|
|
"PredictorReady condition should be False with FailedCreate reason from ReplicaFailure")
|
|
|
|
By("verifying the ISVC status section is never completely empty (Bug 1 fix)")
|
|
// The overall Ready condition must exist regardless of deployment state.
|
|
Expect(updatedIsvc.Status.GetCondition(apis.ConditionReady)).NotTo(BeNil(),
|
|
"ConditionReady must be present: status section should not be empty after reconciliation")
|
|
})
|
|
})
|
|
|
|
func verifyEnvKeyValueDeployments(actualDefaultDeployment *appsv1.Deployment, envKey string, expectedEnvValue string) {
|
|
// default deployment
|
|
if envValue, exists := utils.GetEnvVarValue(actualDefaultDeployment.Spec.Template.Spec.Containers[0].Env, envKey); exists {
|
|
Expect(envValue).Should(Equal(expectedEnvValue))
|
|
} else {
|
|
Fail(envKey + " environment variable is not set")
|
|
}
|
|
}
|
|
|
|
func verifyGPUResourceSizeDeployment(actualDefaultDeployment *appsv1.Deployment, actualWorkerDeployment *appsv1.Deployment, targetHeadGPUResourceSize, targetWorkerGPUResourceSize string, headGpuResourceType corev1.ResourceName, workerGpuResourceType corev1.ResourceName) {
|
|
headGpuResourceQuantity := resource.MustParse(targetHeadGPUResourceSize)
|
|
workerGpuResourceQuantity := resource.MustParse(targetWorkerGPUResourceSize)
|
|
|
|
// head node deployment
|
|
Expect(actualDefaultDeployment.Spec.Template.Spec.Containers[0].Resources.Limits[headGpuResourceType]).Should(Equal(headGpuResourceQuantity))
|
|
Expect(actualDefaultDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[headGpuResourceType]).Should(Equal(headGpuResourceQuantity))
|
|
|
|
// worker node deployment
|
|
Expect(actualWorkerDeployment.Spec.Template.Spec.Containers[0].Resources.Limits[workerGpuResourceType]).Should(Equal(workerGpuResourceQuantity))
|
|
Expect(actualWorkerDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[workerGpuResourceType]).Should(Equal(workerGpuResourceQuantity))
|
|
}
|