Repository: kserve/kserve

Issue:
Support credentials for Azure Blobs
/kind feature

**Describe the solution you'd like**
1. Credentials builder should support exposing azure blob credentials to model initializer
2. Downloader should read and use credentials


**Anything else you would like to add:**
Here is how az creds are setup for kubeflow:  https://github.com/kubeflow/kubeflow/pull/2676


Discussion:
/assign krabhishek8260 
@rakelkar: GitHub didn't allow me to assign the following users: krabhishek8260.

Note that only [kubeflow members](https://github.com/orgs/kubeflow/people) and repo collaborators can be assigned and that issues/PRs can only have 10 assignees at the same time.
For more information please see [the contributor guide](https://git.k8s.io/community/contributors/guide/#issue-assignment-in-github)

<details>

In response to [this](https://github.com/kubeflow/kfserving/issues/217#issuecomment-507540127):

>/assign krabhishek8260 


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
@krabhishek8260 cut an issue for you :-)
/area azure

Resolving patch:
diff --git a/python/kfserving/kfserving/storage.py b/python/kfserving/kfserving/storage.py
--- a/python/kfserving/kfserving/storage.py
+++ b/python/kfserving/kfserving/storage.py
@@ -101,14 +101,15 @@ def _download_gcs(uri, temp_dir: str):
                 blob.download_to_filename(dest_path)
 
     @staticmethod
-    def _download_blob(uri, out_dir: str):
+    def _download_blob(uri, out_dir: str): # pylint: disable=too-many-locals
         match = re.search(_BLOB_RE, uri)
         account_name = match.group(1)
         storage_url = match.group(2)
         container_name, prefix = storage_url.split("/", 1)
 
         logging.info("Connecting to BLOB account: %s, contianer: %s", account_name, container_name)
-        block_blob_service = BlockBlobService(account_name=account_name)
+        token = Storage._get_azure_storage_token()
+        block_blob_service = BlockBlobService(account_name=account_name, token_credential=token)
         blobs = block_blob_service.list_blobs(container_name, prefix=prefix)
 
         for blob in blobs:
@@ -122,6 +123,33 @@ def _download_blob(uri, out_dir: str):
             logging.info("Downloading: %s", dest_path)
             block_blob_service.get_blob_to_path(container_name, blob.name, dest_path)
 
+    @staticmethod
+    def _get_azure_storage_token():
+        tenant_id = os.getenv("AZ_TENANT_ID", "")
+        client_id = os.getenv("AZ_CLIENT_ID", "")
+        client_secret = os.getenv("AZ_CLIENT_SECRET", "")
+        subscription_id = os.getenv("AZ_SUBSCRIPTION_ID", "")
+
+        if tenant_id == "" or client_id == "" or client_secret == "" or subscription_id == "":
+            return None
+
+        # note the SP must have "Storage Blob Data Owner" perms for this to work
+        import adal
+        from azure.storage.common import TokenCredential
+
+        authority_url = "https://login.microsoftonline.com/" + tenant_id
+
+        context = adal.AuthenticationContext(authority_url)
+
+        token = context.acquire_token_with_client_credentials(
+            "https://storage.azure.com/",
+            client_id,
+            client_secret)
+
+        token_credential = TokenCredential(token["accessToken"])
+
+        return token_credential
+
     @staticmethod
     def _download_local(uri, out_dir=None):
         local_path = uri.replace(_LOCAL_PREFIX, "", 1)


Test patch:
diff --git a/pkg/controller/kfservice/resources/credentials/azure/azure_secret_test.go b/pkg/controller/kfservice/resources/credentials/azure/azure_secret_test.go
new file mode 100644
--- /dev/null
+++ b/pkg/controller/kfservice/resources/credentials/azure/azure_secret_test.go
@@ -0,0 +1,95 @@
+/*
+Copyright 2019 kubeflow.org.
+
+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 azure
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestAzureSecret(t *testing.T) {
+	scenarios := map[string]struct {
+		config   AzureConfig
+		secret   *v1.Secret
+		expected []v1.EnvVar
+	}{
+		"AzureSecretEnvs": {
+			secret: &v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "azcreds",
+				},
+			},
+			expected: []v1.EnvVar{
+				{
+					Name: AzureSubscriptionId,
+					ValueFrom: &v1.EnvVarSource{
+						SecretKeyRef: &v1.SecretKeySelector{
+							LocalObjectReference: v1.LocalObjectReference{
+								Name: "azcreds",
+							},
+							Key: AzureSubscriptionId,
+						},
+					},
+				},
+				{
+					Name: AzureTenantId,
+					ValueFrom: &v1.EnvVarSource{
+						SecretKeyRef: &v1.SecretKeySelector{
+							LocalObjectReference: v1.LocalObjectReference{
+								Name: "azcreds",
+							},
+							Key: AzureTenantId,
+						},
+					},
+				},
+				{
+					Name: AzureClientId,
+					ValueFrom: &v1.EnvVarSource{
+						SecretKeyRef: &v1.SecretKeySelector{
+							LocalObjectReference: v1.LocalObjectReference{
+								Name: "azcreds",
+							},
+							Key: AzureClientId,
+						},
+					},
+				},
+				{
+					Name: AzureClientSecret,
+					ValueFrom: &v1.EnvVarSource{
+						SecretKeyRef: &v1.SecretKeySelector{
+							LocalObjectReference: v1.LocalObjectReference{
+								Name: "azcreds",
+							},
+							Key: AzureClientSecret,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for name, scenario := range scenarios {
+		envs := BuildSecretEnvs(scenario.secret, &scenario.config)
+
+		if diff := cmp.Diff(scenario.expected, envs); diff != "" {
+			t.Errorf("Test %q unexpected result (-want +got): %v", name, diff)
+		}
+	}
+}
diff --git a/pkg/controller/kfservice/resources/credentials/service_account_credentials_test.go b/pkg/controller/kfservice/resources/credentials/service_account_credentials_test.go
--- a/pkg/controller/kfservice/resources/credentials/service_account_credentials_test.go
+++ b/pkg/controller/kfservice/resources/credentials/service_account_credentials_test.go
@@ -37,7 +37,8 @@ var configMap = &v1.ConfigMap{
 			"s3" : {
 				"s3AccessKeyIDName": "awsAccessKeyID",
 				"s3SecretAccessKeyName": "awsSecretAccessKey"
-			}
+			},
+			"azure" : {"azureSecretName": "azcreds"}
 	    }`,
 	},
 }