Repository: kserve/kserve

Issue:
option to load credentials directly from a secret for s3
Currently to download from a private S3 bucket [you create both a secret and a service account that you link it to](https://github.com/kubeflow/kfserving/blob/master/docs/samples/s3/s3_secret.yaml). You then set the [serviceAccountName on the KFService ](https://github.com/kubeflow/kfserving/blob/master/docs/samples/s3/tensorflow_s3.yaml#L7) and it is [used to add env vars based on the secret to the initContainer that downloads the model](https://github.com/kubeflow/kfserving/blob/master/pkg/controller/kfservice/resources/credentials/service_account_credentials.go#L94).

It might be easier for s3 users to just create a secret containing entries intended as environment variables and link that directly by having a 'envSecretRefName' entry in the CRD. This could be used in the implementation to use 'envFrom' to apply the secret's values.

It seems the[ original idea for credentials was to use a Secret ](https://github.com/kubeflow/kfserving/issues/36)and this morphed into a ServiceAccount as a 'first pass'. Presumably there's no in principle objection to also supporting a direct secret for s3?


Discussion:
I would second this, and even making it easier by using a secret with the name `s3-auth` or similar by default if present, as this would make AWS support easier (and could be a similar approach to other cloud providers)
/kind feature
/area engprod
/priority p2
Did this ever get actioned?  I see still in the docs that a `ServiceAccount` is used as the way to pass credentials.  Can I also pass them directly via a `Secret`?
> Did this ever get actioned? I see still in the docs that a `ServiceAccount` is used as the way to pass credentials. Can I also pass them directly via a `Secret`?

Hi, I believe you can create a secret named `storage-config` and pass the credential key in the InferenceService spec. Here's an example. https://github.com/kserve/kserve/blob/master/docs/samples/storage/storageSpec/README.md

Here's the code pointer. https://github.com/kserve/kserve/blob/master/pkg/webhook/admission/pod/storage_initializer_injector.go#L301-L320

Resolving patch:
diff --git a/python/kserve/kserve/api_client.py b/python/kserve/kserve/api_client.py
--- a/python/kserve/kserve/api_client.py
+++ b/python/kserve/kserve/api_client.py
@@ -304,7 +304,7 @@ def __deserialize(self, data, klass):
         if data is None:
             return None
 
-        if type(klass) == str:
+        if type(klass) is str:
             if klass.startswith('list['):
                 sub_kls = re.match(r'list\[(.*)\]', klass).group(1)
                 return [self.__deserialize(sub_data, sub_kls)


Test patch:
diff --git a/pkg/credentials/service_account_credentials_test.go b/pkg/credentials/service_account_credentials_test.go
--- a/pkg/credentials/service_account_credentials_test.go
+++ b/pkg/credentials/service_account_credentials_test.go
@@ -36,6 +36,8 @@ import (
 var configMap = &v1.ConfigMap{
 	Data: map[string]string{
 		"credentials": `{
+            "storageSecretNameAnnotation": "serving.kserve.io/storageSecretName",
+            "storageSpecSecretName": "storage-secret",
 			"gcs" : {"gcsCredentialFileName": "gcloud-application-credentials.json"},
 			"s3" : {
 				"s3AccessKeyIDName": "awsAccessKeyID",
@@ -162,12 +164,145 @@ func TestS3CredentialBuilder(t *testing.T) {
 		},
 	}
 
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 		g.Expect(c.Create(context.TODO(), existingServiceAccount)).NotTo(gomega.HaveOccurred())
 		g.Expect(c.Create(context.TODO(), existingS3Secret)).NotTo(gomega.HaveOccurred())
 
-		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, nil,
+			scenario.serviceAccount.Name,
+			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
+			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
+		)
+		if scenario.shouldFail && err == nil {
+			t.Errorf("Test %q failed: returned success but expected error", name)
+		}
+		// Validate
+		if !scenario.shouldFail {
+			if err != nil {
+				t.Errorf("Test %q failed: returned error: %v", name, err)
+			}
+			if diff := cmp.Diff(scenario.expectedConfiguration, scenario.inputConfiguration); diff != "" {
+				t.Errorf("Test %q unexpected configuration spec (-want +got): %v", name, diff)
+			}
+		}
+		g.Expect(c.Delete(context.TODO(), existingServiceAccount)).NotTo(gomega.HaveOccurred())
+		g.Expect(c.Delete(context.TODO(), existingS3Secret)).NotTo(gomega.HaveOccurred())
+
+	}
+}
+
+func TestS3CredentialBuilderWithStorageSecret(t *testing.T) {
+	g := gomega.NewGomegaWithT(t)
+	existingServiceAccount := &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "default",
+			Namespace: "default",
+		},
+	}
+	existingS3Secret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "s3-secret",
+			Namespace: "default",
+			Annotations: map[string]string{
+				s3.InferenceServiceS3SecretEndpointAnnotation: "s3.aws.com",
+			},
+		},
+		Data: map[string][]byte{
+			"awsAccessKeyID":     {},
+			"awsSecretAccessKey": {},
+		},
+	}
+	scenarios := map[string]struct {
+		inputConfiguration    *knservingv1.Configuration
+		expectedConfiguration *knservingv1.Configuration
+		shouldFail            bool
+	}{
+		"Build s3 secrets envs": {
+			inputConfiguration: &knservingv1.Configuration{
+				Spec: knservingv1.ConfigurationSpec{
+					Template: knservingv1.RevisionTemplateSpec{
+						Spec: knservingv1.RevisionSpec{
+							PodSpec: v1.PodSpec{
+								Containers: []v1.Container{
+									{},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedConfiguration: &knservingv1.Configuration{
+				Spec: knservingv1.ConfigurationSpec{
+					Template: knservingv1.RevisionTemplateSpec{
+						Spec: knservingv1.RevisionSpec{
+							PodSpec: v1.PodSpec{
+								Containers: []v1.Container{
+									{
+										Env: []v1.EnvVar{
+											{
+												Name: s3.AWSAccessKeyId,
+												ValueFrom: &v1.EnvVarSource{
+													SecretKeyRef: &v1.SecretKeySelector{
+														LocalObjectReference: v1.LocalObjectReference{
+															Name: "s3-secret",
+														},
+														Key: "awsAccessKeyID",
+													},
+												},
+											},
+											{
+												Name: s3.AWSSecretAccessKey,
+												ValueFrom: &v1.EnvVarSource{
+													SecretKeyRef: &v1.SecretKeySelector{
+														LocalObjectReference: v1.LocalObjectReference{
+															Name: "s3-secret",
+														},
+														Key: "awsSecretAccessKey",
+													},
+												},
+											},
+											{
+												Name:  s3.S3Endpoint,
+												Value: "s3.aws.com",
+											},
+											{
+												Name:  s3.AWSEndpointUrl,
+												Value: "https://s3.aws.com",
+											},
+											{
+												Name:  s3.S3VerifySSL,
+												Value: "1",
+											},
+											{
+												Name:  s3.AWSAnonymousCredential,
+												Value: "false",
+											},
+											{
+												Name:  s3.AWSRegion,
+												Value: "us-east-2",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			shouldFail: false,
+		},
+	}
+
+	builder := NewCredentialBuilder(c, configMap)
+	for name, scenario := range scenarios {
+		g.Expect(c.Create(context.TODO(), existingServiceAccount)).NotTo(gomega.HaveOccurred())
+		g.Expect(c.Create(context.TODO(), existingS3Secret)).NotTo(gomega.HaveOccurred())
+		annotations := map[string]string{
+			"serving.kserve.io/storageSecretName": "s3-secret",
+		}
+		err := builder.CreateSecretVolumeAndEnv(existingServiceAccount.Namespace, annotations,
+			existingServiceAccount.Name,
 			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
 			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
 		)
@@ -263,11 +398,11 @@ func TestS3ServiceAccountCredentialBuilder(t *testing.T) {
 		},
 	}
 
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 		g.Expect(c.Create(context.TODO(), existingServiceAccount)).NotTo(gomega.HaveOccurred())
 
-		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, nil, scenario.serviceAccount.Name,
 			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
 			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
 		)
@@ -373,12 +508,12 @@ func TestGCSCredentialBuilder(t *testing.T) {
 		},
 	}
 
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 		g.Expect(c.Create(context.TODO(), existingServiceAccount)).NotTo(gomega.HaveOccurred())
 		g.Expect(c.Create(context.TODO(), existingGCSSecret)).NotTo(gomega.HaveOccurred())
 
-		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, nil, scenario.serviceAccount.Name,
 			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
 			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
 		)
@@ -515,10 +650,10 @@ func TestLegacyAzureCredentialBuilder(t *testing.T) {
 	g.Expect(c.Create(context.TODO(), customAzureSecret)).NotTo(gomega.HaveOccurred())
 	g.Expect(c.Create(context.TODO(), customOnlyServiceAccount)).NotTo(gomega.HaveOccurred())
 
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 
-		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, nil, scenario.serviceAccount.Name,
 			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
 			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
 		)
@@ -626,10 +761,10 @@ func TestHdfsCredentialBuilder(t *testing.T) {
 	g.Expect(c.Create(context.TODO(), customHdfsSecret)).NotTo(gomega.HaveOccurred())
 	g.Expect(c.Create(context.TODO(), customOnlyServiceAccount)).NotTo(gomega.HaveOccurred())
 
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 
-		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, nil, scenario.serviceAccount.Name,
 			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
 			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
 		)
@@ -766,10 +901,10 @@ func TestAzureCredentialBuilder(t *testing.T) {
 	g.Expect(c.Create(context.TODO(), customAzureSecret)).NotTo(gomega.HaveOccurred())
 	g.Expect(c.Create(context.TODO(), customOnlyServiceAccount)).NotTo(gomega.HaveOccurred())
 
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 
-		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, nil, scenario.serviceAccount.Name,
 			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
 			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
 		)
@@ -870,10 +1005,10 @@ func TestAzureStorageAccessKeyCredentialBuilder(t *testing.T) {
 	g.Expect(c.Create(context.TODO(), customAzureSecret)).NotTo(gomega.HaveOccurred())
 	g.Expect(c.Create(context.TODO(), customOnlyServiceAccount)).NotTo(gomega.HaveOccurred())
 
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 
-		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, nil, scenario.serviceAccount.Name,
 			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
 			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
 		)
@@ -898,7 +1033,7 @@ func TestAzureStorageAccessKeyCredentialBuilder(t *testing.T) {
 func TestCredentialBuilder_CreateStorageSpecSecretEnvs(t *testing.T) {
 	g := gomega.NewGomegaWithT(t)
 	namespace := "default"
-	builder := NewCredentialBulder(c, configMap)
+	builder := NewCredentialBuilder(c, configMap)
 
 	scenarios := map[string]struct {
 		secret            *v1.Secret
@@ -1393,7 +1528,7 @@ func TestCredentialBuilder_CreateStorageSpecSecretEnvs(t *testing.T) {
 		if err := c.Create(context.TODO(), tc.secret); err != nil {
 			t.Errorf("Failed to create secret %s: %v", "storage-secret", err)
 		}
-		err := builder.CreateStorageSpecSecretEnvs(namespace, tc.storageKey, tc.storageSecretName, tc.overrideParams, tc.container)
+		err := builder.CreateStorageSpecSecretEnvs(namespace, nil, tc.storageKey, tc.overrideParams, tc.container)
 		if !tc.shouldFail {
 			g.Expect(err).Should(gomega.BeNil())
 			g.Expect(tc.container).Should(tc.matcher)
diff --git a/pkg/webhook/admission/pod/agent_injector_test.go b/pkg/webhook/admission/pod/agent_injector_test.go
--- a/pkg/webhook/admission/pod/agent_injector_test.go
+++ b/pkg/webhook/admission/pod/agent_injector_test.go
@@ -1101,7 +1101,7 @@ func TestAgentInjector(t *testing.T) {
 		},
 	}
 
-	credentialBuilder := credentials.NewCredentialBulder(c, &v1.ConfigMap{
+	credentialBuilder := credentials.NewCredentialBuilder(c, &v1.ConfigMap{
 		Data: map[string]string{},
 	})
 
diff --git a/pkg/webhook/admission/pod/storage_initializer_injector_test.go b/pkg/webhook/admission/pod/storage_initializer_injector_test.go
--- a/pkg/webhook/admission/pod/storage_initializer_injector_test.go
+++ b/pkg/webhook/admission/pod/storage_initializer_injector_test.go
@@ -38,7 +38,6 @@ const (
 	StorageInitializerDefaultCPULimit                   = "1"
 	StorageInitializerDefaultMemoryRequest              = "200Mi"
 	StorageInitializerDefaultMemoryLimit                = "1Gi"
-	StorageInitializerDefaultStorageSpecSecretName      = "storage-config"
 	StorageInitializerDefaultEnableDirectPvcVolumeMount = false
 )
 
@@ -48,7 +47,6 @@ var (
 		CpuLimit:                   StorageInitializerDefaultCPULimit,
 		MemoryRequest:              StorageInitializerDefaultMemoryRequest,
 		MemoryLimit:                StorageInitializerDefaultMemoryLimit,
-		StorageSpecSecretName:      StorageInitializerDefaultStorageSpecSecretName,
 		EnableDirectPvcVolumeMount: StorageInitializerDefaultEnableDirectPvcVolumeMount,
 	}
 
@@ -349,7 +347,7 @@ func TestStorageInitializerInjector(t *testing.T) {
 
 	for name, scenario := range scenarios {
 		injector := &StorageInitializerInjector{
-			credentialBuilder: credentials.NewCredentialBulder(c, &v1.ConfigMap{
+			credentialBuilder: credentials.NewCredentialBuilder(c, &v1.ConfigMap{
 				Data: map[string]string{},
 			}),
 			config: storageInitializerConfig,
@@ -389,7 +387,7 @@ func TestStorageInitializerFailureCases(t *testing.T) {
 
 	for name, scenario := range scenarios {
 		injector := &StorageInitializerInjector{
-			credentialBuilder: credentials.NewCredentialBulder(c, &v1.ConfigMap{
+			credentialBuilder: credentials.NewCredentialBuilder(c, &v1.ConfigMap{
 				Data: map[string]string{},
 			}),
 			config: storageInitializerConfig,
@@ -488,7 +486,7 @@ func TestCustomSpecStorageUriInjection(t *testing.T) {
 
 	for name, scenario := range scenarios {
 		injector := &StorageInitializerInjector{
-			credentialBuilder: credentials.NewCredentialBulder(c, &v1.ConfigMap{
+			credentialBuilder: credentials.NewCredentialBuilder(c, &v1.ConfigMap{
 				Data: map[string]string{},
 			}),
 			config: storageInitializerConfig,
@@ -935,7 +933,7 @@ func TestCredentialInjection(t *testing.T) {
 		},
 	}
 
-	builder := credentials.NewCredentialBulder(c, configMap)
+	builder := credentials.NewCredentialBuilder(c, configMap)
 	for name, scenario := range scenarios {
 		g.Expect(c.Create(context.TODO(), scenario.sa)).NotTo(gomega.HaveOccurred())
 		g.Expect(c.Create(context.TODO(), scenario.secret)).NotTo(gomega.HaveOccurred())
@@ -998,7 +996,7 @@ func TestStorageInitializerConfigmap(t *testing.T) {
 					InitContainers: []v1.Container{
 						{
 							Name:                     "storage-initializer",
-							Image:                    "kfserving/storage-initializer@sha256:xxx",
+							Image:                    "kserve/storage-initializer@sha256:xxx",
 							Args:                     []string{"gs://foo", constants.DefaultModelLocalMountPath},
 							Resources:                resourceRequirement,
 							TerminationMessagePolicy: "FallbackToLogsOnError",
@@ -1025,16 +1023,15 @@ func TestStorageInitializerConfigmap(t *testing.T) {
 
 	for name, scenario := range scenarios {
 		injector := &StorageInitializerInjector{
-			credentialBuilder: credentials.NewCredentialBulder(c, &v1.ConfigMap{
+			credentialBuilder: credentials.NewCredentialBuilder(c, &v1.ConfigMap{
 				Data: map[string]string{},
 			}),
 			config: &StorageInitializerConfig{
-				Image:                 "kfserving/storage-initializer@sha256:xxx",
-				CpuRequest:            StorageInitializerDefaultCPURequest,
-				CpuLimit:              StorageInitializerDefaultCPULimit,
-				MemoryRequest:         StorageInitializerDefaultMemoryRequest,
-				MemoryLimit:           StorageInitializerDefaultMemoryLimit,
-				StorageSpecSecretName: StorageInitializerDefaultStorageSpecSecretName,
+				Image:         "kserve/storage-initializer@sha256:xxx",
+				CpuRequest:    StorageInitializerDefaultCPURequest,
+				CpuLimit:      StorageInitializerDefaultCPULimit,
+				MemoryRequest: StorageInitializerDefaultMemoryRequest,
+				MemoryLimit:   StorageInitializerDefaultMemoryLimit,
 			},
 		}
 		if err := injector.InjectStorageInitializer(scenario.original); err != nil {
@@ -1060,24 +1057,22 @@ func TestGetStorageInitializerConfigs(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{},
 				Data: map[string]string{
 					StorageInitializerConfigMapKeyName: `{
-						"Image":        		 "gcr.io/kfserving/storage-initializer:latest",
+						"Image":        		 "gcr.io/kserve/storage-initializer:latest",
 						"CpuRequest":   		 "100m",
 						"CpuLimit":      		 "1",
 						"MemoryRequest": 		 "200Mi",
-						"MemoryLimit":   		 "1Gi",
-						"StorageSpecSecretName": "storage-secret"
+						"MemoryLimit":   		 "1Gi"
 					}`,
 				},
 				BinaryData: map[string][]byte{},
 			},
 			matchers: []types.GomegaMatcher{
 				gomega.Equal(&StorageInitializerConfig{
-					Image:                 "gcr.io/kfserving/storage-initializer:latest",
-					CpuRequest:            "100m",
-					CpuLimit:              "1",
-					MemoryRequest:         "200Mi",
-					MemoryLimit:           "1Gi",
-					StorageSpecSecretName: "storage-secret",
+					Image:         "gcr.io/kserve/storage-initializer:latest",
+					CpuRequest:    "100m",
+					CpuLimit:      "1",
+					MemoryRequest: "200Mi",
+					MemoryLimit:   "1Gi",
 				}),
 				gomega.BeNil(),
 			},
@@ -1089,24 +1084,22 @@ func TestGetStorageInitializerConfigs(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{},
 				Data: map[string]string{
 					StorageInitializerConfigMapKeyName: `{
-						"Image":        		 "gcr.io/kfserving/storage-initializer:latest",
+						"Image":        		 "gcr.io/kserve/storage-initializer:latest",
 						"CpuRequest":   		 "100m",
 						"CpuLimit":      		 "1",
 						"MemoryRequest": 		 "200MC",
-						"MemoryLimit":   		 "1Gi",
-						"StorageSpecSecretName": "storage-secret"
+						"MemoryLimit":   		 "1Gi"
 					}`,
 				},
 				BinaryData: map[string][]byte{},
 			},
 			matchers: []types.GomegaMatcher{
 				gomega.Equal(&StorageInitializerConfig{
-					Image:                 "gcr.io/kfserving/storage-initializer:latest",
-					CpuRequest:            "100m",
-					CpuLimit:              "1",
-					MemoryRequest:         "200MC",
-					MemoryLimit:           "1Gi",
-					StorageSpecSecretName: "storage-secret",
+					Image:         "gcr.io/kserve/storage-initializer:latest",
+					CpuRequest:    "100m",
+					CpuLimit:      "1",
+					MemoryRequest: "200MC",
+					MemoryLimit:   "1Gi",
 				}),
 				gomega.HaveOccurred(),
 			},
@@ -1265,7 +1258,7 @@ func TestDirectVolumeMountForPvc(t *testing.T) {
 
 	for name, scenario := range scenarios {
 		injector := &StorageInitializerInjector{
-			credentialBuilder: credentials.NewCredentialBulder(c, &v1.ConfigMap{
+			credentialBuilder: credentials.NewCredentialBuilder(c, &v1.ConfigMap{
 				Data: map[string]string{},
 			}),
 			config: &StorageInitializerConfig{
@@ -1589,7 +1582,7 @@ func TestTransformerCollocation(t *testing.T) {
 
 	for name, scenario := range scenarios {
 		injector := &StorageInitializerInjector{
-			credentialBuilder: credentials.NewCredentialBulder(c, &v1.ConfigMap{
+			credentialBuilder: credentials.NewCredentialBuilder(c, &v1.ConfigMap{
 				Data: map[string]string{},
 			}),
 			config: scenario.storageConfig,