Skip to content

Commit b261cc5

Browse files
committed
RHAIENG-2063: Deprecate old cluster objects and add RayCluster()
1 parent 98d0ea7 commit b261cc5

58 files changed

Lines changed: 9136 additions & 402 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

demo-notebooks/guided-demos/1_cluster_job_client.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@
229229
],
230230
"metadata": {
231231
"kernelspec": {
232-
"display_name": "Python 3",
232+
"display_name": "base",
233233
"language": "python",
234234
"name": "python3"
235235
},
@@ -243,7 +243,7 @@
243243
"name": "python",
244244
"nbconvert_exporter": "python",
245245
"pygments_lexer": "ipython3",
246-
"version": "3.9.18"
246+
"version": "3.12.7"
247247
}
248248
},
249249
"nbformat": 4,

demo-notebooks/guided-demos/4_rayjob_existing_cluster.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
" job_name=\"sdk-test-job\",\n",
145145
" cluster_name=\"rayjob-cluster\",\n",
146146
" namespace=\"your-namespace\",\n",
147-
" entrypoint=\"python -c 'import time; time.sleep(20)'\",\n",
147+
" entrypoint=\"python code.py\",\n",
148148
")\n",
149149
"\n",
150150
"rayjob.submit()"

src/codeflare_sdk/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from .ray import (
2-
Cluster,
3-
ClusterConfiguration,
2+
RayCluster,
43
RayClusterStatus,
54
CodeFlareClusterStatus,
6-
RayCluster,
5+
RayClusterInfo,
6+
DEFAULT_ACCELERATORS,
7+
Cluster,
8+
ClusterConfiguration,
79
get_cluster,
810
list_all_queued,
911
list_all_clusters,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Importing everything from the kubernetes_cluster module
2+
from .kubernetes_cluster import (
3+
Authentication,
4+
KubeConfiguration,
5+
TokenAuthentication,
6+
KubeConfigFileAuthentication,
7+
_kube_api_error_handling,
8+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .auth import (
2+
Authentication,
3+
KubeConfiguration,
4+
TokenAuthentication,
5+
KubeConfigFileAuthentication,
6+
config_check,
7+
get_api_client,
8+
)
9+
10+
from .kube_api_helpers import _kube_api_error_handling
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Copyright 2022 IBM, Red Hat
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
The auth sub-module contains the definitions for the Authentication objects, which represent
17+
the methods by which a user can authenticate to their cluster(s). The abstract class, `Authentication`,
18+
contains two required methods `login()` and `logout()`. Users can use one of the existing concrete classes to
19+
authenticate to their cluster or add their own custom concrete classes here.
20+
"""
21+
22+
import abc
23+
from kubernetes import client, config
24+
import os
25+
import urllib3
26+
from .kube_api_helpers import _kube_api_error_handling
27+
28+
from typing import Optional
29+
30+
global api_client
31+
api_client = None
32+
global config_path
33+
config_path = None
34+
35+
WORKBENCH_CA_CERT_PATH = "/etc/pki/tls/custom-certs/ca-bundle.crt"
36+
37+
38+
class Authentication(metaclass=abc.ABCMeta):
39+
"""
40+
An abstract class that defines the necessary methods for authenticating to a remote environment.
41+
Specifically, this class defines the need for a `login()` and a `logout()` function.
42+
"""
43+
44+
def login(self):
45+
"""
46+
Method for logging in to a remote cluster.
47+
"""
48+
pass
49+
50+
def logout(self):
51+
"""
52+
Method for logging out of the remote cluster.
53+
"""
54+
pass
55+
56+
57+
class KubeConfiguration(metaclass=abc.ABCMeta):
58+
"""
59+
An abstract class that defines the method for loading a user defined config file using the `load_kube_config()` function
60+
"""
61+
62+
def load_kube_config(self):
63+
"""
64+
Method for setting your Kubernetes configuration to a certain file
65+
"""
66+
pass
67+
68+
def logout(self):
69+
"""
70+
Method for logging out of the remote cluster
71+
"""
72+
pass
73+
74+
75+
class TokenAuthentication(Authentication):
76+
"""
77+
`TokenAuthentication` is a subclass of `Authentication`. It can be used to authenticate to a Kubernetes
78+
cluster when the user has an API token and the API server address.
79+
"""
80+
81+
def __init__(
82+
self,
83+
token: str,
84+
server: str,
85+
skip_tls: bool = False,
86+
ca_cert_path: str = None,
87+
):
88+
"""
89+
Initialize a TokenAuthentication object that requires a value for `token`, the API Token
90+
and `server`, the API server address for authenticating to a Kubernetes cluster.
91+
"""
92+
93+
self.token = token
94+
self.server = server
95+
self.skip_tls = skip_tls
96+
self.ca_cert_path = _gen_ca_cert_path(ca_cert_path)
97+
98+
def login(self) -> str:
99+
"""
100+
This function is used to log in to a Kubernetes cluster using the user's API token and API server address.
101+
Depending on the cluster, a user can choose to login in with `--insecure-skip-tls-verify` by setting `skip_tls`
102+
to `True` or `--certificate-authority` by setting `skip_tls` to False and providing a path to a ca bundle with `ca_cert_path`.
103+
"""
104+
global config_path
105+
global api_client
106+
try:
107+
configuration = client.Configuration()
108+
configuration.api_key_prefix["authorization"] = "Bearer"
109+
configuration.host = self.server
110+
configuration.api_key["authorization"] = self.token
111+
112+
if self.skip_tls:
113+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
114+
print("Insecure request warnings have been disabled")
115+
configuration.verify_ssl = False
116+
117+
api_client = client.ApiClient(configuration)
118+
if not self.skip_tls:
119+
_client_with_cert(api_client, self.ca_cert_path)
120+
121+
client.AuthenticationApi(api_client).get_api_group()
122+
config_path = None
123+
return "Logged into %s" % self.server
124+
except client.ApiException as e:
125+
_kube_api_error_handling(e)
126+
127+
def logout(self) -> str:
128+
"""
129+
This function is used to logout of a Kubernetes cluster.
130+
"""
131+
global config_path
132+
config_path = None
133+
global api_client
134+
api_client = None
135+
return "Successfully logged out of %s" % self.server
136+
137+
138+
class KubeConfigFileAuthentication(KubeConfiguration):
139+
"""
140+
A class that defines the necessary methods for passing a user's own Kubernetes config file.
141+
Specifically this class defines the `load_kube_config()` and `config_check()` functions.
142+
"""
143+
144+
def __init__(self, kube_config_path: str = None):
145+
self.kube_config_path = kube_config_path
146+
147+
def load_kube_config(self):
148+
"""
149+
Function for loading a user's own predefined Kubernetes config file.
150+
"""
151+
global config_path
152+
global api_client
153+
try:
154+
if self.kube_config_path == None:
155+
return "Please specify a config file path"
156+
config_path = self.kube_config_path
157+
api_client = None
158+
config.load_kube_config(config_path)
159+
response = "Loaded user config file at path %s" % self.kube_config_path
160+
except config.ConfigException: # pragma: no cover
161+
config_path = None
162+
raise Exception("Please specify a config file path")
163+
return response
164+
165+
166+
def config_check() -> str:
167+
"""
168+
Check and load the Kubernetes config from the default location.
169+
170+
This function checks if a Kubernetes config file exists at the default path
171+
(`~/.kube/config`). If none is provided, it tries to load in-cluster config.
172+
If the `config_path` global variable is set by an external module (e.g., `auth.py`),
173+
this path will be used directly.
174+
175+
Returns:
176+
str:
177+
The loaded config path if successful.
178+
179+
Raises:
180+
PermissionError:
181+
If no valid credentials or config file is found.
182+
"""
183+
global config_path
184+
global api_client
185+
home_directory = os.path.expanduser("~")
186+
if config_path == None and api_client == None:
187+
if os.path.isfile("%s/.kube/config" % home_directory):
188+
try:
189+
config.load_kube_config()
190+
except Exception as e: # pragma: no cover
191+
_kube_api_error_handling(e)
192+
elif "KUBERNETES_PORT" in os.environ:
193+
try:
194+
config.load_incluster_config()
195+
except Exception as e: # pragma: no cover
196+
_kube_api_error_handling(e)
197+
else:
198+
raise PermissionError(
199+
"Action not permitted, have you put in correct/up-to-date auth credentials?"
200+
)
201+
202+
if config_path != None and api_client == None:
203+
return config_path
204+
205+
206+
def _client_with_cert(client: client.ApiClient, ca_cert_path: Optional[str] = None):
207+
client.configuration.verify_ssl = True
208+
cert_path = _gen_ca_cert_path(ca_cert_path)
209+
if cert_path is None:
210+
client.configuration.ssl_ca_cert = None
211+
elif os.path.isfile(cert_path):
212+
client.configuration.ssl_ca_cert = cert_path
213+
else:
214+
raise FileNotFoundError(f"Certificate file not found at {cert_path}")
215+
216+
217+
def _gen_ca_cert_path(ca_cert_path: Optional[str]):
218+
"""Gets the path to the default CA certificate file either through env config or default path"""
219+
if ca_cert_path is not None:
220+
return ca_cert_path
221+
elif "CF_SDK_CA_CERT_PATH" in os.environ:
222+
return os.environ.get("CF_SDK_CA_CERT_PATH")
223+
elif os.path.exists(WORKBENCH_CA_CERT_PATH):
224+
return WORKBENCH_CA_CERT_PATH
225+
else:
226+
return None
227+
228+
229+
def get_api_client() -> client.ApiClient:
230+
"""
231+
Retrieve the Kubernetes API client with the default configuration.
232+
233+
This function returns the current API client instance if already loaded,
234+
or creates a new API client with the default configuration.
235+
236+
Returns:
237+
client.ApiClient:
238+
The Kubernetes API client object.
239+
"""
240+
if api_client != None:
241+
return api_client
242+
to_return = client.ApiClient()
243+
_client_with_cert(to_return)
244+
return to_return
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2022 IBM, Red Hat
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
This sub-module exists primarily to be used internally for any Kubernetes
17+
API error handling or wrapping.
18+
"""
19+
20+
import executing
21+
from kubernetes import client, config
22+
23+
ERROR_MESSAGES = {
24+
"Not Found": "The requested resource could not be located.\n"
25+
"Please verify the resource name and namespace.",
26+
"Unauthorized": "Access to the API is unauthorized.\n"
27+
"Check your credentials or permissions.",
28+
"Forbidden": "Access denied to the Kubernetes resource.\n"
29+
"Ensure your role has sufficient permissions for this operation.",
30+
"Conflict": "A conflict occurred with the RayCluster resource.\n"
31+
"Only one RayCluster with the same name is allowed. "
32+
"Please delete or rename the existing RayCluster before creating a new one with the desired name.",
33+
}
34+
35+
36+
# private methods
37+
def _kube_api_error_handling(
38+
e: Exception, print_error: bool = True
39+
): # pragma: no cover
40+
def print_message(message: str):
41+
if print_error:
42+
print(message)
43+
44+
if isinstance(e, client.ApiException):
45+
# Retrieve message based on reason, defaulting if reason is not known
46+
message = ERROR_MESSAGES.get(
47+
e.reason, f"Unexpected API error encountered (Reason: {e.reason})"
48+
)
49+
full_message = f"{message}\nResponse: {e.body}"
50+
print_message(full_message)
51+
52+
elif isinstance(e, config.ConfigException):
53+
message = "Configuration error: Unable to load Kubernetes configuration. Verify the config file path and format."
54+
print_message(message)
55+
56+
elif isinstance(e, executing.executing.NotOneValueFound):
57+
message = "Execution error: Expected exactly one value in the operation but found none or multiple."
58+
print_message(message)
59+
60+
else:
61+
message = f"Unexpected error:\n{str(e)}"
62+
print_message(message)
63+
raise e

0 commit comments

Comments
 (0)