Files
2026-05-21 21:44:35 +00:00

474 lines
18 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 v1beta1
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"text/template"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
"github.com/kserve/kserve/pkg/constants"
"github.com/kserve/kserve/pkg/types"
"github.com/kserve/kserve/pkg/utils"
)
// ConfigMap Keys
const (
ExplainerConfigKeyName = "explainers"
InferenceServiceConfigKeyName = "inferenceService"
IngressConfigKeyName = "ingress"
DeployConfigName = "deploy"
LocalModelConfigName = "localModel"
SecurityConfigName = "security"
ServiceConfigName = "service"
ResourceConfigName = "resource"
MultiNodeConfigKeyName = "multiNode"
OtelCollectorConfigName = "opentelemetryCollector"
StorageInitializerConfigMapKeyName = "storageInitializer"
AutoscalerConfigName = "autoscaler"
)
const (
DefaultDomainTemplate = "{{ .Name }}-{{ .Namespace }}.{{ .IngressDomain }}"
DefaultIngressDomain = "example.com"
DefaultUrlScheme = "http"
DefaultModelBasedRoutingHeaderName = "X-Gateway-Model-Name"
DefaultModelBasedRoutingMode = "enabled"
)
// Error messages
const (
ErrKserveIngressGatewayRequired = "invalid ingress config - kserveIngressGateway is required"
ErrInvalidKserveIngressGatewayFormat = "invalid ingress config - kserveIngressGateway should be in the format <namespace>/<name>"
ErrInvalidKserveIngressGatewayName = "invalid ingress config - kserveIngressGateway gateway name is invalid"
ErrInvalidKserveIngressGatewayNamespace = "invalid ingress config - kserveIngressGateway gateway namespace is invalid"
)
// +kubebuilder:object:generate=false
type ExplainerConfig struct {
// explainer docker image name
ContainerImage string `json:"image"`
// default explainer docker image version
DefaultImageVersion string `json:"defaultImageVersion"`
}
// +kubebuilder:object:generate=false
type ExplainersConfig struct {
ARTExplainer ExplainerConfig `json:"art,omitempty"`
}
type OtelCollectorConfig struct {
ScrapeInterval string `json:"scrapeInterval,omitempty"`
MetricReceiverEndpoint string `json:"metricReceiverEndpoint,omitempty"`
MetricScalerEndpoint string `json:"metricScalerEndpoint,omitempty"`
Resource ResourceConfig `json:"resource,omitempty"` // Resource configuration for otel collector
}
type AutoscalerConfig struct {
ScaleUpStabilizationWindowSeconds string `json:"scaleUpStabilizationWindowSeconds,omitempty"`
ScaleDownStabilizationWindowSeconds string `json:"scaleDownStabilizationWindowSeconds,omitempty"`
}
// +kubebuilder:object:generate=false
type InferenceServicesConfig struct {
// Explainer configurations
Explainers ExplainersConfig `json:"explainers"`
// ServiceAnnotationDisallowedList is a list of annotations that are not allowed to be propagated to Knative
// revisions
ServiceAnnotationDisallowedList []string `json:"serviceAnnotationDisallowedList,omitempty"`
// ServiceLabelDisallowedList is a list of labels that are not allowed to be propagated to Knative revisions
ServiceLabelDisallowedList []string `json:"serviceLabelDisallowedList,omitempty"`
// Resource configurations
Resource ResourceConfig `json:"resource,omitempty"`
}
// +kubebuilder:object:generate=false
type MultiNodeConfig struct {
// CustomGPUResourceTypeList is a list of custom GPU resource types that are allowed to be used in the ServingRuntime and inferenceService
CustomGPUResourceTypeList []string `json:"customGPUResourceTypeList,omitempty"`
}
// +kubebuilder:object:generate=false
type IngressConfig struct {
EnableGatewayAPI bool `json:"enableGatewayApi,omitempty"`
KserveIngressGateway string `json:"kserveIngressGateway,omitempty"`
IngressGateway string `json:"ingressGateway,omitempty"`
KnativeLocalGatewayService string `json:"knativeLocalGatewayService,omitempty"`
LocalGateway string `json:"localGateway,omitempty"`
LocalGatewayServiceName string `json:"localGatewayService,omitempty"`
IngressDomain string `json:"ingressDomain,omitempty"`
IngressClassName *string `json:"ingressClassName,omitempty"`
AdditionalIngressDomains *[]string `json:"additionalIngressDomains,omitempty"`
DomainTemplate string `json:"domainTemplate,omitempty"`
UrlScheme string `json:"urlScheme,omitempty"`
EnableLLMInferenceServiceTLS bool `json:"enableLLMInferenceServiceTLS,omitempty"`
DisableIstioVirtualHost bool `json:"disableIstioVirtualHost,omitempty"`
PathTemplate string `json:"pathTemplate,omitempty"`
DisableIngressCreation bool `json:"disableIngressCreation,omitempty"`
DisableHTTPRouteTimeout bool `json:"disableHTTPRouteTimeout,omitempty"`
ModelBasedRoutingHeaderName string `json:"modelBasedRoutingHeaderName,omitempty"`
ModelBasedRoutingMode string `json:"modelBasedRoutingMode,omitempty"`
}
// +kubebuilder:object:generate=false
type DeployConfig struct {
DefaultDeploymentMode string `json:"defaultDeploymentMode,omitempty"`
DeploymentRolloutStrategy *DeploymentRolloutStrategy `json:"deploymentRolloutStrategy,omitempty"`
}
// DeploymentRolloutStrategy defines the rollout strategy configuration for deployments
type DeploymentRolloutStrategy struct {
// DefaultRollout specifies the default rollout configuration
// +optional
DefaultRollout *RolloutSpec `json:"defaultRollout,omitempty"`
}
// RolloutSpec defines the rollout strategy configuration using Kubernetes deployment strategy
type RolloutSpec struct {
// MaxSurge specifies the maximum number of pods that can be created above the desired replica count.
// Can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%).
MaxSurge string `json:"maxSurge"`
// MaxUnavailable specifies the maximum number of pods that can be unavailable during the update.
// Can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%).
MaxUnavailable string `json:"maxUnavailable"`
}
// +kubebuilder:object:generate=false
type LocalModelConfig struct {
Enabled bool `json:"enabled"`
JobNamespace string `json:"jobNamespace"`
DefaultJobImage string `json:"defaultJobImage,omitempty"`
FSGroup *int64 `json:"fsGroup,omitempty"`
JobTTLSecondsAfterFinished *int32 `json:"jobTTLSecondsAfterFinished,omitempty"`
ReconcilationFrequencyInSecs *int64 `json:"reconcilationFrequencyInSecs,omitempty"`
DisableVolumeManagement bool `json:"disableVolumeManagement,omitempty"`
}
// +kubebuilder:object:generate=false
type ResourceConfig struct {
CPULimit string `json:"cpuLimit,omitempty"`
MemoryLimit string `json:"memoryLimit,omitempty"`
CPURequest string `json:"cpuRequest,omitempty"`
MemoryRequest string `json:"memoryRequest,omitempty"`
}
// +kubebuilder:object:generate=false
type SecurityConfig struct {
AutoMountServiceAccountToken bool `json:"autoMountServiceAccountToken"`
}
// +kubebuilder:object:generate=false
type ServiceConfig struct {
// ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None.
// If the DeploymentMode is Raw, the default value for ServiceClusterIPNone is false when the value is absent.
ServiceClusterIPNone bool `json:"serviceClusterIPNone,omitempty"`
}
func GetInferenceServiceConfigMap(ctx context.Context, clientset kubernetes.Interface) (*corev1.ConfigMap, error) {
if configMap, err := clientset.CoreV1().ConfigMaps(constants.KServeNamespace).Get(
ctx, constants.InferenceServiceConfigMapName, metav1.GetOptions{}); err != nil {
return nil, err
} else {
return configMap, nil
}
}
func NewOtelCollectorConfig(isvcConfigMap *corev1.ConfigMap) (*OtelCollectorConfig, error) {
otelConfig := &OtelCollectorConfig{}
if otel, ok := isvcConfigMap.Data[OtelCollectorConfigName]; ok {
err := json.Unmarshal([]byte(otel), otelConfig)
if err != nil {
return nil, fmt.Errorf("unable to parse otel config json: %w", err)
}
}
return otelConfig, nil
}
func NewAutoscalerConfig(isvcConfigMap *corev1.ConfigMap) (*AutoscalerConfig, error) {
autoscalerConfig := &AutoscalerConfig{}
if autoscaler, ok := isvcConfigMap.Data[AutoscalerConfigName]; ok {
err := json.Unmarshal([]byte(autoscaler), autoscalerConfig)
if err != nil {
return nil, fmt.Errorf("unable to parse autoscaler config json: %w", err)
}
}
return autoscalerConfig, nil
}
func NewInferenceServicesConfig(isvcConfigMap *corev1.ConfigMap) (*InferenceServicesConfig, error) {
icfg := &InferenceServicesConfig{}
for _, err := range []error{
getComponentConfig(ExplainerConfigKeyName, isvcConfigMap, &icfg.Explainers),
getComponentConfig(InferenceServiceConfigKeyName, isvcConfigMap, &icfg),
} {
if err != nil {
return nil, err
}
}
if isvc, ok := isvcConfigMap.Data[InferenceServiceConfigKeyName]; ok {
errisvc := json.Unmarshal([]byte(isvc), &icfg)
if errisvc != nil {
return nil, fmt.Errorf("unable to parse isvc config json: %w", errisvc)
}
if icfg.ServiceAnnotationDisallowedList == nil {
icfg.ServiceAnnotationDisallowedList = constants.ServiceAnnotationDisallowedList
} else {
icfg.ServiceAnnotationDisallowedList = append(
constants.ServiceAnnotationDisallowedList,
icfg.ServiceAnnotationDisallowedList...)
}
if icfg.ServiceLabelDisallowedList == nil {
icfg.ServiceLabelDisallowedList = constants.RevisionTemplateLabelDisallowedList
} else {
icfg.ServiceLabelDisallowedList = append(
constants.RevisionTemplateLabelDisallowedList,
icfg.ServiceLabelDisallowedList...)
}
}
return icfg, nil
}
func NewMultiNodeConfig(isvcConfigMap *corev1.ConfigMap) (*MultiNodeConfig, error) {
mncfg := &MultiNodeConfig{}
for _, err := range []error{
getComponentConfig(MultiNodeConfigKeyName, isvcConfigMap, &mncfg),
} {
if err != nil {
return nil, err
}
}
if mncfg.CustomGPUResourceTypeList == nil {
mncfg.CustomGPUResourceTypeList = []string{}
}
// update global GPU resource type list
_ = utils.UpdateGlobalGPUResourceTypeList(append(mncfg.CustomGPUResourceTypeList, constants.DefaultGPUResourceTypeList...))
return mncfg, nil
}
func validateIngressGateway(ingressConfig *IngressConfig) error {
if ingressConfig.KserveIngressGateway == "" {
return errors.New(ErrKserveIngressGatewayRequired)
}
splits := strings.Split(ingressConfig.KserveIngressGateway, "/")
if len(splits) != 2 {
return errors.New(ErrInvalidKserveIngressGatewayFormat)
}
errs := validation.IsDNS1123Label(splits[0])
if len(errs) != 0 {
return errors.New(ErrInvalidKserveIngressGatewayNamespace)
}
errs = validation.IsDNS1123Label(splits[1])
if len(errs) != 0 {
return errors.New(ErrInvalidKserveIngressGatewayName)
}
return nil
}
func NewIngressConfig(isvcConfigMap *corev1.ConfigMap) (*IngressConfig, error) {
ingressConfig := &IngressConfig{}
if ingress, ok := isvcConfigMap.Data[IngressConfigKeyName]; ok {
err := json.Unmarshal([]byte(ingress), &ingressConfig)
if err != nil {
return nil, fmt.Errorf("unable to parse ingress config json: %w", err)
}
if ingressConfig.EnableGatewayAPI {
if ingressConfig.KserveIngressGateway == "" {
return nil, errors.New("invalid ingress config - kserveIngressGateway is required")
}
if err := validateIngressGateway(ingressConfig); err != nil {
return nil, err
}
}
if ingressConfig.IngressGateway == "" {
return nil, errors.New("invalid ingress config - ingressGateway is required")
}
if ingressConfig.PathTemplate != "" {
// TODO: ensure that the generated path is valid, that is:
// * both Name and Namespace are used to avoid collisions
// * starts with a /
// For now simply check that this is a valid template.
_, err := template.New("path-template").Parse(ingressConfig.PathTemplate)
if err != nil {
return nil, fmt.Errorf("invalid ingress config, unable to parse pathTemplate: %w", err)
}
if ingressConfig.IngressDomain == "" {
return nil, errors.New("invalid ingress config - ingressDomain is required if pathTemplate is given")
}
}
if len(ingressConfig.KnativeLocalGatewayService) == 0 {
ingressConfig.KnativeLocalGatewayService = ingressConfig.LocalGatewayServiceName
}
}
if ingressConfig.DomainTemplate == "" {
ingressConfig.DomainTemplate = DefaultDomainTemplate
}
if ingressConfig.IngressDomain == "" {
ingressConfig.IngressDomain = DefaultIngressDomain
}
if ingressConfig.UrlScheme == "" {
ingressConfig.UrlScheme = DefaultUrlScheme
}
if ingressConfig.ModelBasedRoutingHeaderName == "" {
ingressConfig.ModelBasedRoutingHeaderName = DefaultModelBasedRoutingHeaderName
}
if ingressConfig.ModelBasedRoutingMode == "" {
ingressConfig.ModelBasedRoutingMode = DefaultModelBasedRoutingMode
}
return ingressConfig, nil
}
func getComponentConfig(key string, configMap *corev1.ConfigMap, componentConfig interface{}) error {
if data, ok := configMap.Data[key]; ok {
err := json.Unmarshal([]byte(data), componentConfig)
if err != nil {
return fmt.Errorf("unable to unmarshall %v json string due to %w ", key, err)
}
}
return nil
}
func NewDeployConfig(isvcConfigMap *corev1.ConfigMap) (*DeployConfig, error) {
deployConfig := &DeployConfig{}
if deploy, ok := isvcConfigMap.Data[DeployConfigName]; ok {
err := json.Unmarshal([]byte(deploy), &deployConfig)
if err != nil {
return nil, fmt.Errorf("unable to parse deploy config json: %w", err)
}
if deployConfig.DefaultDeploymentMode == "" {
return nil, errors.New("invalid deploy config, defaultDeploymentMode is required")
}
if deployConfig.DefaultDeploymentMode == string(constants.LegacyServerless) {
// LegacyServerless is deprecated, so we convert it to Knative
deployConfig.DefaultDeploymentMode = string(constants.Knative)
}
if deployConfig.DefaultDeploymentMode == string(constants.LegacyRawDeployment) {
// LegacyRawDeployment is deprecated, so we convert it to Standard
deployConfig.DefaultDeploymentMode = string(constants.Standard)
}
if deployConfig.DefaultDeploymentMode != string(constants.Knative) &&
deployConfig.DefaultDeploymentMode != string(constants.Standard) &&
deployConfig.DefaultDeploymentMode != string(constants.ModelMeshDeployment) {
return nil, errors.New("invalid deployment mode. Supported modes are Knative," +
" Standard and ModelMesh")
}
}
return deployConfig, nil
}
func NewLocalModelConfig(isvcConfigMap *corev1.ConfigMap) (*LocalModelConfig, error) {
localModelConfig := &LocalModelConfig{}
if localModel, ok := isvcConfigMap.Data[LocalModelConfigName]; ok {
err := json.Unmarshal([]byte(localModel), &localModelConfig)
if err != nil {
return nil, err
}
}
return localModelConfig, nil
}
func NewSecurityConfig(isvcConfigMap *corev1.ConfigMap) (*SecurityConfig, error) {
securityConfig := &SecurityConfig{}
if security, ok := isvcConfigMap.Data[SecurityConfigName]; ok {
err := json.Unmarshal([]byte(security), &securityConfig)
if err != nil {
return nil, err
}
}
return securityConfig, nil
}
func NewServiceConfig(isvcConfigMap *corev1.ConfigMap) (*ServiceConfig, error) {
serviceConfig := &ServiceConfig{}
if service, ok := isvcConfigMap.Data[ServiceConfigName]; ok {
err := json.Unmarshal([]byte(service), &serviceConfig)
if err != nil {
return nil, fmt.Errorf("unable to parse service config json: %w", err)
}
}
return serviceConfig, nil
}
// GetStorageInitializerConfigs parses the StorageInitializer configuration from the provided ConfigMap.
// It unmarshalls JSON configuration under the "storageInitializer" key into a StorageInitializerConfig struct.
// The fields related to CPU and memory resource values are validated to contain valid Kubernetes resource quantities.
//
// Parameters:
//
// configMap: The Kubernetes ConfigMap containing the storage initializer configuration.
//
// Returns:
//
// *types.StorageInitializerConfig: The parsed storage initializer configuration.
// error: An error if the configuration is missing, invalid, or resource values cannot be parsed.
func GetStorageInitializerConfigs(configMap *corev1.ConfigMap) (*types.StorageInitializerConfig, error) {
storageInitializerConfig := &types.StorageInitializerConfig{}
if initializerConfig, ok := configMap.Data[StorageInitializerConfigMapKeyName]; ok {
err := json.Unmarshal([]byte(initializerConfig), &storageInitializerConfig)
if err != nil {
panic(fmt.Errorf("Unable to unmarshall %v json string due to %w ", StorageInitializerConfigMapKeyName, err))
}
}
// Ensure that we set proper values for CPU/Memory Limit/Request
resourceDefaults := map[string]string{
"memoryRequest": storageInitializerConfig.MemoryRequest,
"memoryLimit": storageInitializerConfig.MemoryLimit,
"cpuRequest": storageInitializerConfig.CpuRequest,
"cpuLimit": storageInitializerConfig.CpuLimit,
}
// Only validate optional modelcar fields if they're set
if storageInitializerConfig.CpuModelcar != "" {
resourceDefaults["cpuModelcar"] = storageInitializerConfig.CpuModelcar
}
if storageInitializerConfig.MemoryModelcar != "" {
resourceDefaults["memoryModelcar"] = storageInitializerConfig.MemoryModelcar
}
for key, value := range resourceDefaults {
_, err := resource.ParseQuantity(value)
if err != nil {
return storageInitializerConfig, fmt.Errorf("failed to parse resource configuration for %q.%q: %w", StorageInitializerConfigMapKeyName, key, err)
}
}
return storageInitializerConfig, nil
}