Repository: kserve/kserve

Issue:
Allow using SSL between transformer and predictor
/kind feature
Currently the transformer is hard-coded to [use `http` to communicate with the predictor](https://github.com/kserve/kserve/blob/master/python/kserve/kserve/model.py#L36). We should add an extra `use_ssl` property to the `Model` that will enable connecting to the predictor via HTTPS. By default this property would be `false`.


Discussion:

Resolving patch:
diff --git a/python/custom_transformer/model.py b/python/custom_transformer/model.py
--- a/python/custom_transformer/model.py
+++ b/python/custom_transformer/model.py
@@ -53,10 +53,11 @@ def image_transform(model_name, data):
 
 
 class ImageTransformer(Model):
-    def __init__(self, name: str, predictor_host: str, protocol: str):
+    def __init__(self, name: str, predictor_host: str, protocol: str, use_ssl: bool):
         super().__init__(name)
         self.predictor_host = predictor_host
         self.protocol = protocol
+        self.use_ssl = use_ssl
         self.ready = True
 
     def preprocess(self, payload: Union[Dict, InferRequest], headers: Dict[str, str] = None) \
@@ -103,9 +104,12 @@ def postprocess(self, infer_response: Union[Dict, ModelInferResponse], headers:
 parser.add_argument(
     "--model_name", help="The name that the model is served under."
 )
+parser.add_argument(
+    "--use_ssl", help="Use ssl for connecting to the predictor", action='store_true'
+)
 args, _ = parser.parse_known_args()
 
 if __name__ == "__main__":
     model = ImageTransformer(args.model_name, predictor_host=args.predictor_host,
-                             protocol=args.protocol)
+                             protocol=args.protocol, use_ssl=args.use_ssl)
     ModelServer().start([model])
diff --git a/python/kserve/kserve/model.py b/python/kserve/kserve/model.py
--- a/python/kserve/kserve/model.py
+++ b/python/kserve/kserve/model.py
@@ -32,10 +32,10 @@
 
 from .errors import InvalidInput
 
-PREDICTOR_URL_FORMAT = "http://{0}/v1/models/{1}:predict"
-EXPLAINER_URL_FORMAT = "http://{0}/v1/models/{1}:explain"
-PREDICTOR_V2_URL_FORMAT = "http://{0}/v2/models/{1}/infer"
-EXPLAINER_V2_URL_FORMAT = "http://{0}/v2/models/{1}/explain"
+PREDICTOR_URL_FORMAT = "{0}://{1}/v1/models/{2}:predict"
+EXPLAINER_URL_FORMAT = "{0}://{1}/v1/models/{2}:explain"
+PREDICTOR_V2_URL_FORMAT = "{0}://{1}/v2/models/{2}/infer"
+EXPLAINER_V2_URL_FORMAT = "{0}://{1}/v2/models/{2}/explain"
 
 
 class ModelType(Enum):
@@ -74,6 +74,7 @@ def __init__(self, name: str):
         self._http_client_instance = None
         self._grpc_client_stub = None
         self.enable_latency_logging = False
+        self.use_ssl = False
 
     async def __call__(self, body: Union[Dict, CloudEvent, InferRequest],
                        model_type: ModelType = ModelType.PREDICTOR,
@@ -139,10 +140,14 @@ def _http_client(self):
     @property
     def _grpc_client(self):
         if self._grpc_client_stub is None:
-            # requires appending ":80" to the predictor host for gRPC to work
+            # requires appending the port to the predictor host for gRPC to work
             if ":" not in self.predictor_host:
-                self.predictor_host = self.predictor_host + ":80"
-            _channel = grpc.aio.insecure_channel(self.predictor_host)
+                port = 443 if self.use_ssl else 80
+                self.predictor_host = f"{self.predictor_host}:{port}"
+            if self.use_ssl:
+                _channel = grpc.aio.secure_channel(self.predictor_host, grpc.ssl_channel_credentials())
+            else:
+                _channel = grpc.aio.insecure_channel(self.predictor_host)
             self._grpc_client_stub = grpc_predict_v2_pb2_grpc.GRPCInferenceServiceStub(_channel)
         return self._grpc_client_stub
 
@@ -232,9 +237,10 @@ def postprocess(self, response: Union[Dict, InferResponse, ModelInferResponse],
         return response
 
     async def _http_predict(self, payload: Union[Dict, InferRequest], headers: Dict[str, str] = None) -> Dict:
-        predict_url = PREDICTOR_URL_FORMAT.format(self.predictor_host, self.name)
+        protocol = "https" if self.use_ssl else "http"
+        predict_url = PREDICTOR_URL_FORMAT.format(protocol, self.predictor_host, self.name)
         if self.protocol == PredictorProtocol.REST_V2.value:
-            predict_url = PREDICTOR_V2_URL_FORMAT.format(self.predictor_host, self.name)
+            predict_url = PREDICTOR_V2_URL_FORMAT.format(protocol, self.predictor_host, self.name)
 
         # Adjusting headers. Inject content type if not exist.
         # Also, removing host, as the header is the one passed to transformer and contains transformer's host
@@ -311,9 +317,11 @@ async def explain(self, payload: Dict, headers: Dict[str, str] = None) -> Dict:
         """
         if self.explainer_host is None:
             raise NotImplementedError("Could not find explainer_host.")
-        explain_url = EXPLAINER_URL_FORMAT.format(self.explainer_host, self.name)
+
+        protocol = "https" if self.use_ssl else "http"
+        explain_url = EXPLAINER_URL_FORMAT.format(protocol, self.explainer_host, self.name)
         if self.protocol == PredictorProtocol.REST_V2.value:
-            explain_url = EXPLAINER_V2_URL_FORMAT.format(self.explainer_host, self.name)
+            explain_url = EXPLAINER_V2_URL_FORMAT.format(protocol, self.explainer_host, self.name)
         response = await self._http_client.post(
             url=explain_url,
             timeout=self.timeout,


Test patch: