Skip to content

Commit 7de0e4f

Browse files
authored
[FSTORE-1479] Deduplicate hsml client (#243)
* Move hsml/client/istio * Adapt the moved hsml client * Fix istio.__init__ * Fix client.__init__ * Fix hsml client so that it is possible to do hsml.connection(...) * Add aliases to hsml.client
1 parent 85105f5 commit 7de0e4f

39 files changed

+1705
-1891
lines changed

Diff for: python/hopsworks_common/client/__init__.py

+70-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818

1919
from typing import Literal, Optional, Union
2020

21-
from hopsworks_common.client import external, hopsworks
21+
from hopsworks_common.client import external, hopsworks, istio
22+
from hopsworks_common.constants import HOSTS
2223

2324

2425
_client: Union[hopsworks.Client, external.Client, None] = None
@@ -62,13 +63,78 @@ def init(
6263

6364
def get_instance() -> Union[hopsworks.Client, external.Client]:
6465
global _client
65-
if _client:
66-
return _client
67-
raise Exception("Couldn't find client. Try reconnecting to Hopsworks.")
66+
if not _client:
67+
raise Exception("Couldn't find client. Try reconnecting to Hopsworks.")
68+
return _client
6869

6970

7071
def stop() -> None:
7172
global _client
7273
if _client:
7374
_client._close()
7475
_client = None
76+
if istio._client:
77+
istio._client._close()
78+
istio._client = None
79+
80+
81+
def is_saas_connection() -> bool:
82+
return get_instance()._host == HOSTS.APP_HOST
83+
84+
85+
_kserve_installed = None
86+
87+
88+
def set_kserve_installed(kserve_installed):
89+
global _kserve_installed
90+
_kserve_installed = kserve_installed
91+
92+
93+
def is_kserve_installed() -> bool:
94+
global _kserve_installed
95+
return _kserve_installed
96+
97+
98+
_serving_resource_limits = None
99+
100+
101+
def set_serving_resource_limits(max_resources):
102+
global _serving_resource_limits
103+
_serving_resource_limits = max_resources
104+
105+
106+
def get_serving_resource_limits():
107+
global _serving_resource_limits
108+
return _serving_resource_limits
109+
110+
111+
_serving_num_instances_limits = None
112+
113+
114+
def set_serving_num_instances_limits(num_instances_range):
115+
global _serving_num_instances_limits
116+
_serving_num_instances_limits = num_instances_range
117+
118+
119+
def get_serving_num_instances_limits():
120+
global _serving_num_instances_limits
121+
return _serving_num_instances_limits
122+
123+
124+
def is_scale_to_zero_required():
125+
# scale-to-zero is required for KServe deployments if the Hopsworks variable `kube_serving_min_num_instances`
126+
# is set to 0. Other possible values are -1 (unlimited num instances) or >1 num instances.
127+
return get_serving_num_instances_limits()[0] == 0
128+
129+
130+
_knative_domain = None
131+
132+
133+
def get_knative_domain():
134+
global _knative_domain
135+
return _knative_domain
136+
137+
138+
def set_knative_domain(knative_domain):
139+
global _knative_domain
140+
_knative_domain = knative_domain

Diff for: python/hopsworks_common/client/auth.py

+25
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
from __future__ import annotations
1818

19+
import os
20+
1921
import requests
22+
from hopsworks_common.client import exceptions
2023

2124

2225
class BearerAuth(requests.auth.AuthBase):
@@ -50,3 +53,25 @@ def __init__(self, token):
5053
def __call__(self, r):
5154
r.headers["X-API-KEY"] = self._token
5255
return r
56+
57+
58+
def get_api_key(api_key_value, api_key_file):
59+
if api_key_value is not None:
60+
return api_key_value
61+
elif api_key_file is not None:
62+
file = None
63+
if os.path.exists(api_key_file):
64+
try:
65+
file = open(api_key_file, mode="r")
66+
return file.read()
67+
finally:
68+
file.close()
69+
else:
70+
raise IOError(
71+
"Could not find api key file on path: {}".format(api_key_file)
72+
)
73+
else:
74+
raise exceptions.ExternalClientError(
75+
"Either api_key_file or api_key_value must be set when connecting to"
76+
" hopsworks from an external environment."
77+
)

Diff for: python/hopsworks_common/client/exceptions.py

+31
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
class RestAPIError(Exception):
2626
"""REST Exception encapsulating the response object and url."""
2727

28+
STATUS_CODE_BAD_REQUEST = 400
29+
STATUS_CODE_UNAUTHORIZED = 401
30+
STATUS_CODE_FORBIDDEN = 403
31+
STATUS_CODE_NOT_FOUND = 404
32+
STATUS_CODE_INTERNAL_SERVER_ERROR = 500
33+
2834
class FeatureStoreErrorCode(int, Enum):
2935
FEATURE_GROUP_COMMIT_NOT_FOUND = 270227
3036
STATISTICS_NOT_FOUND = 270228
@@ -111,6 +117,17 @@ def __init__(self, missing_argument: str) -> None:
111117
super().__init__(message)
112118

113119

120+
class HopsworksClientError(TypeError):
121+
"""Raised when hopsworks internal client cannot be initialized due to missing arguments."""
122+
123+
def __init__(self, missing_argument):
124+
message = (
125+
"{0} cannot be of type NoneType, {0} is a non-optional "
126+
"argument to connect to hopsworks from an internal environment."
127+
).format(missing_argument)
128+
super().__init__(message)
129+
130+
114131
class GitException(Exception):
115132
"""Generic git exception"""
116133

@@ -141,3 +158,17 @@ class OpenSearchException(Exception):
141158

142159
class JobExecutionException(Exception):
143160
"""Generic job executions exception"""
161+
162+
163+
class ModelRegistryException(Exception):
164+
"""Generic model registry exception"""
165+
166+
167+
class ModelServingException(Exception):
168+
"""Generic model serving exception"""
169+
170+
ERROR_CODE_SERVING_NOT_FOUND = 240000
171+
ERROR_CODE_ILLEGAL_ARGUMENT = 240001
172+
ERROR_CODE_DUPLICATED_ENTRY = 240011
173+
174+
ERROR_CODE_DEPLOYMENT_NOT_RUNNING = 250001

Diff for: python/hopsworks_common/client/istio/__init__.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#
2+
# Copyright 2022 Logical Clocks AB
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
from __future__ import annotations
18+
19+
from typing import Union
20+
21+
import hopsworks_common.client as _main
22+
from hopsworks_common.client.istio import external, hopsworks
23+
24+
25+
_client: Union[hopsworks.Client, external.Client, None] = None
26+
27+
28+
def init(host, port, project=None, api_key_value=None):
29+
global _client
30+
31+
if _client:
32+
return
33+
if isinstance(_main._client, _main.hopsworks.Client):
34+
_client = hopsworks.Client(host, port)
35+
elif isinstance(_main, _main.external.Client):
36+
_client = external.Client(host, port, project, api_key_value)
37+
38+
39+
def get_instance() -> Union[hopsworks.Client, external.Client, None]:
40+
global _client
41+
return _client

Diff for: python/hsml/client/hopsworks/base.py renamed to python/hopsworks_common/client/istio/base.py

+17-38
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,33 @@
1717
import os
1818
from abc import abstractmethod
1919

20-
from hsml.client import auth, base
20+
from hopsworks_common.client import base
21+
from hopsworks_common.client.istio.grpc.inference_client import (
22+
GRPCInferenceServerClient,
23+
)
2124

2225

2326
class Client(base.Client):
24-
TOKEN_FILE = "token.jwt"
25-
APIKEY_FILE = "api.key"
26-
REST_ENDPOINT = "REST_ENDPOINT"
27+
SERVING_API_KEY = "SERVING_API_KEY"
2728
HOPSWORKS_PUBLIC_HOST = "HOPSWORKS_PUBLIC_HOST"
29+
TOKEN_EXPIRED_MAX_RETRIES = 0
2830

29-
BASE_PATH_PARAMS = ["hopsworks-api", "api"]
31+
BASE_PATH_PARAMS = []
3032

3133
@abstractmethod
3234
def __init__(self):
33-
"""To be extended by clients."""
35+
"""To be implemented by clients."""
3436
pass
3537

3638
def _get_verify(self, verify, trust_store_path):
37-
"""Get verification method for sending HTTP requests to Hopsworks.
39+
"""Get verification method for sending inference requests to Istio.
3840
3941
Credit to https://gist.github.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c
4042
4143
:param verify: perform hostname verification, 'true' or 'false'
4244
:type verify: str
4345
:param trust_store_path: path of the truststore locally if it was uploaded manually to
44-
the external environment
46+
the external environment such as EKS or AKS
4547
:type trust_store_path: str
4648
:return: if verify is true and the truststore is provided, then return the trust store location
4749
if verify is true but the truststore wasn't provided, then return true
@@ -56,23 +58,6 @@ def _get_verify(self, verify, trust_store_path):
5658

5759
return False
5860

59-
def _get_retry(self, request, response):
60-
"""Get retry method for resending HTTP requests to Hopsworks
61-
62-
:param request: original HTTP request already sent
63-
:type request: requests.Request
64-
:param response: response of the original HTTP request
65-
:type response: requests.Response
66-
"""
67-
if response.status_code == 401 and self.REST_ENDPOINT in os.environ:
68-
# refresh token and retry request - only on hopsworks
69-
self._auth = auth.BearerAuth(self._read_jwt())
70-
# Update request with the new token
71-
request.auth = self._auth
72-
# retry request
73-
return True
74-
return False
75-
7661
def _get_host_port_pair(self):
7762
"""
7863
Removes "http or https" from the rest endpoint and returns a list
@@ -88,19 +73,6 @@ def _get_host_port_pair(self):
8873
host, port = endpoint.split(":")
8974
return host, port
9075

91-
def _read_jwt(self):
92-
"""Retrieve jwt from local container."""
93-
return self._read_file(self.TOKEN_FILE)
94-
95-
def _read_apikey(self):
96-
"""Retrieve apikey from local container."""
97-
return self._read_file(self.APIKEY_FILE)
98-
99-
def _read_file(self, secret_file):
100-
"""Retrieve secret from local container."""
101-
with open(os.path.join(self._secrets_dir, secret_file), "r") as secret:
102-
return secret.read()
103-
10476
def _close(self):
10577
"""Closes a client. Can be implemented for clean up purposes, not mandatory."""
10678
self._connected = False
@@ -109,3 +81,10 @@ def _replace_public_host(self, url):
10981
"""replace hostname to public hostname set in HOPSWORKS_PUBLIC_HOST"""
11082
ui_url = url._replace(netloc=os.environ[self.HOPSWORKS_PUBLIC_HOST])
11183
return ui_url
84+
85+
def _create_grpc_channel(self, service_hostname: str) -> GRPCInferenceServerClient:
86+
return GRPCInferenceServerClient(
87+
url=self._host + ":" + str(self._port),
88+
channel_args=(("grpc.ssl_target_name_override", service_hostname),),
89+
serving_api_key=self._auth._token,
90+
)

Diff for: python/hopsworks_common/client/istio/external.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#
2+
# Copyright 2022 Logical Clocks AB
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
import requests
18+
from hopsworks_common.client import auth
19+
from hopsworks_common.client.istio import base as istio
20+
21+
22+
class Client(istio.Client):
23+
def __init__(
24+
self,
25+
host,
26+
port,
27+
project,
28+
api_key_value,
29+
hostname_verification=None,
30+
trust_store_path=None,
31+
):
32+
"""Initializes a client in an external environment such as AWS Sagemaker."""
33+
self._host = host
34+
self._port = port
35+
self._base_url = "http://" + self._host + ":" + str(self._port)
36+
self._project_name = project
37+
38+
self._auth = auth.ApiKeyAuth(api_key_value)
39+
40+
self._session = requests.session()
41+
self._connected = True
42+
self._verify = self._get_verify(hostname_verification, trust_store_path)
43+
44+
self._cert_key = None
45+
46+
def _close(self):
47+
"""Closes a client."""
48+
self._connected = False
49+
50+
def _replace_public_host(self, url):
51+
"""no need to replace as we are already in external client"""
52+
return url
53+
54+
@property
55+
def host(self):
56+
return self._host

0 commit comments

Comments
 (0)