Repository: kserve/kserve

Issue:
Kserve defaulting causing duplicates of environment variable 
/kind bug

**What steps did you take and what happened:**
Create example xgboost isvc and enable gRPC
```
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
  name: "xgboost-iris"
spec:
  predictor:
    xgboost:
      protocolVersion: "v2"
      storageUri: "gs://kfserving-examples/models/xgboost/iris"
      ports:
      - containerPort: 9000
        name: h2c
        protocol: TCP
```

The pod spec has duplicated environment variable
```
    Environment:
      MLSERVER_MODEL_NAME:            xgboost-iris
      MLSERVER_MODEL_URI:             /mnt/models
      MLSERVER_MODEL_NAME:            xgboost-iris
      MLSERVER_MODEL_URI:             /mnt/models
```

Additionally, attempt to override the defaults leads to duplicated environment variable with different values

```
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
  name: "xgboost-iris"
spec:
  predictor:
    xgboost:
      protocolVersion: "v2"
      storageUri: "gs://kfserving-examples/models/xgboost/iris"
      ports:
      - containerPort: 9000
        name: h2c
        protocol: TCP
      env:
      - name: MLSERVER_MODEL_NAME
        value: my-model
```

The pod spec:
```
    Environment:
      MLSERVER_MODEL_NAME:            my-model
      MLSERVER_MODEL_NAME:            xgboost-iris
      MLSERVER_MODEL_URI:                /mnt/models
```

**What did you expect to happen:**
- Defaulting should not duplicate environment variable and should prioritise user's defined environment variable

**Anything else you would like to add:**
I believe it's because the defaulter always append `.Env` without checking the presence of existing environment variable. (https://github.com/kserve/kserve/blob/a6ed8e4b006e27433de2336e0e8b7cec11137dc1/pkg/apis/serving/v1beta1/inference_service_defaults.go#L264)


**Environment:**

- Kserve: 0.8.0


Discussion:

Resolving patch:
diff --git a/python/custom_transformer/model_grpc.py b/python/custom_transformer/model_grpc.py
--- a/python/custom_transformer/model_grpc.py
+++ b/python/custom_transformer/model_grpc.py
@@ -18,7 +18,6 @@
 
 from kserve import Model, ModelServer, model_server
 from kserve.grpc.grpc_predict_v2_pb2 import ModelInferRequest
-from kserve.handlers.v2_datamodels import InferenceRequest
 
 
 class ImageTransformer(Model):
@@ -28,7 +27,7 @@ def __init__(self, name: str, predictor_host: str, protocol: str):
         self.protocol = protocol
         self.model_name = name
 
-    def preprocess(self, request: Union[Dict, ModelInferRequest, InferenceRequest], headers=None) -> ModelInferRequest:
+    def preprocess(self, request: Union[Dict, ModelInferRequest], headers=None) -> ModelInferRequest:
         if isinstance(request, ModelInferRequest):
             return request
         else:


Test patch:
diff --git a/pkg/apis/serving/v1beta1/predictor_custom_test.go b/pkg/apis/serving/v1beta1/predictor_custom_test.go
--- a/pkg/apis/serving/v1beta1/predictor_custom_test.go
+++ b/pkg/apis/serving/v1beta1/predictor_custom_test.go
@@ -286,94 +286,6 @@ func TestCreateCustomPredictorContainer(t *testing.T) {
 	}
 }
 
-func TestCustomPredictorIsMMS(t *testing.T) {
-	g := gomega.NewGomegaWithT(t)
-	config := InferenceServicesConfig{}
-
-	defaultResource = v1.ResourceList{
-		v1.ResourceCPU:    resource.MustParse("1"),
-		v1.ResourceMemory: resource.MustParse("2Gi"),
-	}
-
-	mmsCase := false
-	scenarios := map[string]struct {
-		spec     PredictorSpec
-		expected bool
-	}{
-		"DefaultResources": {
-			spec: PredictorSpec{
-				PodSpec: PodSpec{
-					Containers: []v1.Container{
-						{
-							Env: []v1.EnvVar{
-								{
-									Name:  "STORAGE_URI",
-									Value: "hdfs://modelzoo",
-								},
-							},
-						},
-					},
-				},
-			},
-			expected: mmsCase,
-		},
-	}
-
-	for name, scenario := range scenarios {
-		t.Run(name, func(t *testing.T) {
-			customPredictor := NewCustomPredictor(&scenario.spec.PodSpec)
-			res := customPredictor.IsMMS(&config)
-			if !g.Expect(res).To(gomega.Equal(scenario.expected)) {
-				t.Errorf("got %t, want %t", res, scenario.expected)
-			}
-		})
-	}
-}
-
-func TestCustomPredictorIsFrameworkSupported(t *testing.T) {
-	g := gomega.NewGomegaWithT(t)
-	framework := "framework"
-	config := InferenceServicesConfig{}
-
-	defaultResource = v1.ResourceList{
-		v1.ResourceCPU:    resource.MustParse("1"),
-		v1.ResourceMemory: resource.MustParse("2Gi"),
-	}
-
-	scenarios := map[string]struct {
-		spec     PredictorSpec
-		expected bool
-	}{
-		"DefaultResources": {
-			spec: PredictorSpec{
-				PodSpec: PodSpec{
-					Containers: []v1.Container{
-						{
-							Env: []v1.EnvVar{
-								{
-									Name:  "STORAGE_URI",
-									Value: "hdfs://modelzoo",
-								},
-							},
-						},
-					},
-				},
-			},
-			expected: true,
-		},
-	}
-
-	for name, scenario := range scenarios {
-		t.Run(name, func(t *testing.T) {
-			customPredictor := NewCustomPredictor(&scenario.spec.PodSpec)
-			res := customPredictor.IsFrameworkSupported(framework, &config)
-			if !g.Expect(res).To(gomega.Equal(scenario.expected)) {
-				t.Errorf("got %t, want %t", res, scenario.expected)
-			}
-		})
-	}
-}
-
 func TestCustomPredictorGetProtocol(t *testing.T) {
 	g := gomega.NewGomegaWithT(t)
 	scenarios := map[string]struct {
diff --git a/pkg/controller/v1beta1/inferenceservice/controller_test.go b/pkg/controller/v1beta1/inferenceservice/controller_test.go
--- a/pkg/controller/v1beta1/inferenceservice/controller_test.go
+++ b/pkg/controller/v1beta1/inferenceservice/controller_test.go
@@ -201,8 +201,8 @@ var _ = Describe("v1beta1 inference service controller", func() {
 								},
 								Annotations: map[string]string{
 									constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI,
-									"autoscaling.knative.dev/max-scale":                         "3",
-									"autoscaling.knative.dev/min-scale":                         "1",
+									"autoscaling.knative.dev/max-scale":                        "3",
+									"autoscaling.knative.dev/min-scale":                        "1",
 									"autoscaling.knative.dev/class":                            "kpa.autoscaling.knative.dev",
 									"key1":                                                     "val1FromSR",
 								},
@@ -467,7 +467,7 @@ var _ = Describe("v1beta1 inference service controller", func() {
 									constants.KServiceComponentLabel: constants.Transformer.String(),
 								},
 								Annotations: map[string]string{
-									"autoscaling.knative.dev/class":    "kpa.autoscaling.knative.dev",
+									"autoscaling.knative.dev/class":     "kpa.autoscaling.knative.dev",
 									"autoscaling.knative.dev/max-scale": "3",
 									"autoscaling.knative.dev/min-scale": "1",
 								},
@@ -688,8 +688,8 @@ var _ = Describe("v1beta1 inference service controller", func() {
 								},
 								Annotations: map[string]string{
 									"autoscaling.knative.dev/class":                            "kpa.autoscaling.knative.dev",
-									"autoscaling.knative.dev/max-scale":                         "3",
-									"autoscaling.knative.dev/min-scale":                         "1",
+									"autoscaling.knative.dev/max-scale":                        "3",
+									"autoscaling.knative.dev/min-scale":                        "1",
 									"internal.serving.kserve.io/storage-initializer-sourceuri": "s3://test/mnist/explainer",
 								},
 							},
@@ -1261,8 +1261,8 @@ var _ = Describe("v1beta1 inference service controller", func() {
 								Annotations: map[string]string{
 									constants.StorageInitializerSourceUriInternalAnnotationKey: "s3://test/mnist/export",
 									"autoscaling.knative.dev/class":                            "kpa.autoscaling.knative.dev",
-									"autoscaling.knative.dev/max-scale":                         "3",
-									"autoscaling.knative.dev/min-scale":                         "1",
+									"autoscaling.knative.dev/max-scale":                        "3",
+									"autoscaling.knative.dev/min-scale":                        "1",
 									"key1":                                                     "val1FromSR",
 									"key2":                                                     "val2FromISVC",
 								},
diff --git a/pkg/controller/v1beta1/inferenceservice/utils/utils_test.go b/pkg/controller/v1beta1/inferenceservice/utils/utils_test.go
--- a/pkg/controller/v1beta1/inferenceservice/utils/utils_test.go
+++ b/pkg/controller/v1beta1/inferenceservice/utils/utils_test.go
@@ -126,7 +126,28 @@ func TestIsMMSPredictor(t *testing.T) {
 							Containers: []v1.Container{
 								{Name: constants.InferenceServiceContainerName,
 									Image: "some-image",
-									Env:   []v1.EnvVar{{Name: constants.CustomSpecMultiModelServerEnvVarKey, Value: strconv.FormatBool(false)}, {Name: constants.CustomSpecStorageUriEnvVarKey, Value: "gs://some-uri"}},
+									Env: []v1.EnvVar{
+										{Name: constants.CustomSpecMultiModelServerEnvVarKey, Value: strconv.FormatBool(false)},
+										{Name: constants.CustomSpecStorageUriEnvVarKey, Value: "gs://some-uri"}},
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		"CustomSpecWithoutURI": {
+			isvc: InferenceService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "CustomSpecMMSWithURI",
+				},
+				Spec: InferenceServiceSpec{
+					Predictor: PredictorSpec{
+						PodSpec: PodSpec{
+							Containers: []v1.Container{
+								{Name: constants.InferenceServiceContainerName,
+									Image: "some-image",
 								},
 							},
 						},
@@ -1236,3 +1257,171 @@ func TestGetDeploymentMode(t *testing.T) {
 		})
 	}
 }
+
+func TestModelName(t *testing.T) {
+	g := gomega.NewGomegaWithT(t)
+	var requestedResource = v1.ResourceRequirements{
+		Limits: v1.ResourceList{
+			"cpu": resource.MustParse("100m"),
+		},
+		Requests: v1.ResourceList{
+			"cpu": resource.MustParse("90m"),
+		},
+	}
+
+	scenarios := map[string]struct {
+		isvc     InferenceService
+		expected string
+	}{
+		"ModelSpec": {
+			isvc: InferenceService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "sklearn",
+				},
+				Spec: InferenceServiceSpec{
+					Predictor: PredictorSpec{
+						Model: &ModelSpec{
+							ModelFormat: ModelFormat{
+								Name: "sklearn",
+							},
+							PredictorExtensionSpec: PredictorExtensionSpec{
+								Container: v1.Container{
+									Image:     "customImage:0.1.0",
+									Resources: requestedResource,
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: "sklearn",
+		},
+		"CustomModelName": {
+			isvc: InferenceService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "sklearn-iris",
+				},
+				Spec: InferenceServiceSpec{
+					Predictor: PredictorSpec{
+						Model: &ModelSpec{
+							ModelFormat: ModelFormat{
+								Name: "sklearn",
+							},
+							PredictorExtensionSpec: PredictorExtensionSpec{
+								StorageURI: proto.String("gs://someUri"),
+								Container: v1.Container{
+									Args:      []string{"--model_name=sklearn-custom"},
+									Resources: requestedResource,
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: "sklearn-custom",
+		},
+		"CustomSpec": {
+			isvc: InferenceService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "CustomSpecMMS",
+				},
+				Spec: InferenceServiceSpec{
+					Predictor: PredictorSpec{
+						PodSpec: PodSpec{
+							Containers: []v1.Container{
+								{Name: constants.InferenceServiceContainerName,
+									Image: "some-image",
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: "CustomSpecMMS",
+		},
+		"CustomSpecWithModelNameArg": {
+			isvc: InferenceService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "Custom",
+				},
+				Spec: InferenceServiceSpec{
+					Predictor: PredictorSpec{
+						PodSpec: PodSpec{
+							Containers: []v1.Container{
+								{Name: constants.InferenceServiceContainerName,
+									Image: "some-image",
+									Args:  []string{"--model_name=custom-model"},
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: "custom-model",
+		},
+		"ModelSpecWithModelNameEnv": {
+			isvc: InferenceService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "sklearn-iris-v2",
+				},
+				Spec: InferenceServiceSpec{
+					Predictor: PredictorSpec{
+						Model: &ModelSpec{
+							ModelFormat: ModelFormat{
+								Name: "sklearn",
+							},
+							PredictorExtensionSpec: PredictorExtensionSpec{
+								Container: v1.Container{
+									Env:       []v1.EnvVar{{Name: constants.MLServerModelNameEnv, Value: "sklearn-custom"}},
+									Resources: requestedResource,
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: "sklearn-custom",
+		},
+		"TransformerWithModelNameArg": {
+			isvc: InferenceService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "sklearn-iris-transformer",
+				},
+				Spec: InferenceServiceSpec{
+					Transformer: &TransformerSpec{
+						PodSpec: PodSpec{
+							Containers: []v1.Container{
+								{Name: constants.InferenceServiceContainerName,
+									Image: "some-image",
+									Args:  []string{"--model_name=custom-model"},
+								},
+							},
+						},
+					},
+					Predictor: PredictorSpec{
+						Model: &ModelSpec{
+							ModelFormat: ModelFormat{
+								Name: "sklearn",
+							},
+							PredictorExtensionSpec: PredictorExtensionSpec{
+								Container: v1.Container{
+									Resources: requestedResource,
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: "custom-model",
+		},
+	}
+
+	for name, scenario := range scenarios {
+		t.Run(name, func(t *testing.T) {
+			res := GetModelName(&scenario.isvc)
+			if !g.Expect(res).To(gomega.Equal(scenario.expected)) {
+				t.Errorf("got %s, want %s", res, scenario.expected)
+			}
+		})
+	}
+}