Skip to content

Commit 740087f

Browse files
authored
➖ delete python-client from dependencies and include as module (#7)
1 parent 3ba26bf commit 740087f

6 files changed

Lines changed: 224 additions & 37 deletions

File tree

dagster_ray/kuberay/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import logging
2+
3+
# Group, Version, Plural
4+
GROUP = "ray.io"
5+
VERSION = "v1alpha1"
6+
PLURAL = "rayclusters"
7+
KIND = "RayCluster"
8+
9+
# log level
10+
LOGLEVEL = logging.INFO

dagster_ray/kuberay/ray_cluster_api.py

Lines changed: 204 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
import logging
12
import time
23
from typing import Any, Optional
34

45
from kubernetes import client, config
56
from kubernetes.client import ApiException
6-
from python_client import constants
7-
from python_client.kuberay_cluster_api import RayClusterApi, log
87

8+
import dagster_ray.kuberay.constants as constants
99

10-
class PatchedRayClusterApi(RayClusterApi):
10+
log = logging.getLogger(__name__)
11+
12+
13+
if logging.getLevelName(log.level) == "NOTSET":
14+
logging.basicConfig(format="%(asctime)s %(message)s", level=constants.LOGLEVEL)
15+
16+
17+
class RayClusterApi:
1118
"""
19+
Taken from https://github.com/ray-project/kuberay/blob/master/clients/python-client/python_client/kuberay_cluster_api.py
20+
This project is not published to PyPI, so it's impossible to install it as a dependency.
21+
1222
List of modifications to the original RayClusterApi:
1323
- allow passing config_file and context to __init__\
1424
- fixed get_ray_cluster_status hard-querying 'status' field which is not always present
25+
26+
RayClusterApi provides APIs to list, get, create, build, update, delete rayclusters.
27+
28+
Methods:
29+
- list_ray_clusters(k8s_namespace: str = "default", async_req: bool = False) -> Any:
30+
- get_ray_cluster(name: str, k8s_namespace: str = "default") -> Any:
31+
- create_ray_cluster(body: Any, k8s_namespace: str = "default") -> Any:
32+
- delete_ray_cluster(name: str, k8s_namespace: str = "default") -> bool:
33+
- patch_ray_cluster(name: str, ray_patch: Any, k8s_namespace: str = "default") -> Any:
1534
"""
1635

1736
def __init__(self, config_file: Optional[str], context: Optional[str] = None):
@@ -48,3 +67,185 @@ def get_ray_cluster_status(
4867

4968
log.info("raycluster {} status not set yet, timing out...".format(name))
5069
return None
70+
71+
def __del__(self):
72+
self.api = None
73+
self.kube_config = None
74+
75+
def list_ray_clusters(
76+
self, k8s_namespace: str = "default", label_selector: str = "", async_req: bool = False
77+
) -> Any:
78+
"""List Ray clusters in a given namespace.
79+
80+
Parameters:
81+
- k8s_namespace (str, optional): The namespace in which to list the Ray clusters. Defaults to "default".
82+
- async_req (bool, optional): Whether to make the request asynchronously. Defaults to False.
83+
84+
Returns:
85+
Any: The custom resource for Ray clusters in the specified namespace, or None if not found.
86+
87+
Raises:
88+
ApiException: If there was an error fetching the custom resource.
89+
"""
90+
try:
91+
resource: Any = self.api.list_namespaced_custom_object(
92+
group=constants.GROUP,
93+
version=constants.VERSION,
94+
plural=constants.PLURAL,
95+
namespace=k8s_namespace,
96+
label_selector=label_selector,
97+
async_req=async_req,
98+
)
99+
if "items" in resource:
100+
return resource
101+
return None
102+
except ApiException as e:
103+
if e.status == 404:
104+
log.error("raycluster resource is not found. error = {}".format(e))
105+
return None
106+
else:
107+
log.error("error fetching custom resource: {}".format(e))
108+
return None
109+
110+
def get_ray_cluster(self, name: str, k8s_namespace: str = "default") -> Any:
111+
"""Get a specific Ray cluster in a given namespace.
112+
113+
Parameters:
114+
- name (str): The name of the Ray cluster custom resource. Defaults to "".
115+
- k8s_namespace (str, optional): The namespace in which to retrieve the Ray cluster. Defaults to "default".
116+
117+
Returns:
118+
Any: The custom resource for the specified Ray cluster, or None if not found.
119+
120+
Raises:
121+
ApiException: If there was an error fetching the custom resource.
122+
"""
123+
try:
124+
resource: Any = self.api.get_namespaced_custom_object(
125+
group=constants.GROUP,
126+
version=constants.VERSION,
127+
plural=constants.PLURAL,
128+
name=name,
129+
namespace=k8s_namespace,
130+
)
131+
return resource
132+
except ApiException as e:
133+
if e.status == 404:
134+
log.error("raycluster resource is not found. error = {}".format(e))
135+
return None
136+
else:
137+
log.error("error fetching custom resource: {}".format(e))
138+
return None
139+
140+
def wait_until_ray_cluster_running(
141+
self, name: str, k8s_namespace: str = "default", timeout: int = 60, delay_between_attempts: int = 5
142+
) -> bool:
143+
"""Get a specific Ray cluster in a given namespace.
144+
145+
Parameters:
146+
- name (str): The name of the Ray cluster custom resource. Defaults to "".
147+
- k8s_namespace (str, optional): The namespace in which to retrieve the Ray cluster. Defaults to "default".
148+
- timeout (int, optional): The duration in seconds after which we stop trying to get status. Defaults to 60 seconds.
149+
- delay_between_attempts (int, optional): The duration in seconds to wait between attempts to get status if not set. Defaults to 5 seconds.
150+
151+
152+
153+
Returns:
154+
Bool: True if the raycluster status is Running, False otherwise.
155+
156+
"""
157+
status = self.get_ray_cluster_status(name, k8s_namespace, timeout, delay_between_attempts)
158+
159+
# TODO: once we add State to Status, we should check for that as well <if status and status["state"] == "Running":>
160+
if status and status["head"] and status["head"]["serviceIP"]:
161+
return True
162+
163+
log.info(
164+
"raycluster {} status is not running yet, current status is {}".format(
165+
name, status["state"] if status else "unknown"
166+
)
167+
)
168+
return False
169+
170+
def create_ray_cluster(self, body: Any, k8s_namespace: str = "default") -> Any:
171+
"""Create a new Ray cluster custom resource.
172+
173+
Parameters:
174+
- body (Any): The data of the custom resource to create.
175+
- k8s_namespace (str, optional): The namespace in which to create the custom resource. Defaults to "default".
176+
177+
Returns:
178+
Any: The created custom resource, or None if it already exists or there was an error.
179+
"""
180+
try:
181+
resource: Any = self.api.create_namespaced_custom_object(
182+
group=constants.GROUP,
183+
version=constants.VERSION,
184+
plural=constants.PLURAL,
185+
body=body,
186+
namespace=k8s_namespace,
187+
)
188+
return resource
189+
except ApiException as e:
190+
if e.status == 409:
191+
log.error("raycluster resource already exists. error = {}".format(e.reason))
192+
return None
193+
else:
194+
log.error("error creating custom resource: {}".format(e))
195+
return None
196+
197+
def delete_ray_cluster(self, name: str, k8s_namespace: str = "default") -> bool:
198+
"""Delete a Ray cluster custom resource.
199+
200+
Parameters:
201+
- name (str): The name of the Ray cluster custom resource to delete.
202+
- k8s_namespace (str, optional): The namespace in which the Ray cluster exists. Defaults to "default".
203+
204+
Returns:
205+
Any: The deleted custom resource, or None if already deleted or there was an error.
206+
"""
207+
try:
208+
resource: Any = self.api.delete_namespaced_custom_object(
209+
group=constants.GROUP,
210+
version=constants.VERSION,
211+
plural=constants.PLURAL,
212+
name=name,
213+
namespace=k8s_namespace,
214+
)
215+
return resource
216+
except ApiException as e:
217+
if e.status == 404:
218+
log.error("raycluster custom resource already deleted. error = {}".format(e.reason))
219+
return None
220+
else:
221+
log.error("error deleting the raycluster custom resource: {}".format(e.reason))
222+
return None
223+
224+
def patch_ray_cluster(self, name: str, ray_patch: Any, k8s_namespace: str = "default") -> Any:
225+
"""Patch an existing Ray cluster custom resource.
226+
227+
Parameters:
228+
- name (str): The name of the Ray cluster custom resource to be patched.
229+
- ray_patch (Any): The patch data for the Ray cluster.
230+
- k8s_namespace (str, optional): The namespace in which the Ray cluster exists. Defaults to "default".
231+
232+
Returns:
233+
bool: True if the patch was successful, False otherwise.
234+
"""
235+
try:
236+
# we patch the existing raycluster with the new config
237+
self.api.patch_namespaced_custom_object(
238+
group=constants.GROUP,
239+
version=constants.VERSION,
240+
plural=constants.PLURAL,
241+
name=name,
242+
body=ray_patch,
243+
namespace=k8s_namespace,
244+
)
245+
except ApiException as e:
246+
log.error("raycluster `{}` failed to patch, with error: {}".format(name, e))
247+
return False
248+
else:
249+
log.info("raycluster `%s` is patched successfully", name)
250+
251+
return True

dagster_ray/kuberay/resources.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414

1515
# yes, `python-client` is actually the KubeRay package name
1616
# https://github.com/ray-project/kuberay/issues/2078
17-
from python_client import kuberay_cluster_api
18-
1917
from dagster_ray.kuberay.configs import DEFAULT_DEPLOYMENT_NAME, RayClusterConfig
20-
from dagster_ray.kuberay.ray_cluster_api import PatchedRayClusterApi
18+
from dagster_ray.kuberay.ray_cluster_api import RayClusterApi
2119

2220
if sys.version_info >= (3, 11):
2321
from typing import Self
@@ -33,12 +31,12 @@
3331
class KubeRayAPI(ConfigurableResource):
3432
kubeconfig_file: Optional[str] = None
3533

36-
_kuberay_api: PatchedRayClusterApi = PrivateAttr()
34+
_kuberay_api: RayClusterApi = PrivateAttr()
3735
_k8s_api: client.CustomObjectsApi = PrivateAttr()
3836
_k8s_core_api: client.CoreV1Api = PrivateAttr()
3937

4038
@property
41-
def kuberay(self) -> kuberay_cluster_api.RayClusterApi:
39+
def kuberay(self) -> RayClusterApi:
4240
if self._kuberay_api is None:
4341
raise ValueError("KubeRayAPI not initialized")
4442
return self._kuberay_api
@@ -58,7 +56,7 @@ def k8s_core(self) -> client.CoreV1Api:
5856
def setup_for_execution(self, context: InitResourceContext) -> None:
5957
self._load_kubeconfig(self.kubeconfig_file)
6058

61-
self._kuberay_api = PatchedRayClusterApi(config_file=self.kubeconfig_file)
59+
self._kuberay_api = RayClusterApi(config_file=self.kubeconfig_file)
6260
self._k8s_api = client.CustomObjectsApi()
6361
self._k8s_core_api = client.CoreV1Api()
6462

poetry.lock

Lines changed: 2 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ license = "Apache-2.0"
2929
python = ">=3.8.1,<3.13"
3030
pyyaml = ">=4.0.0"
3131
kubernetes = ">=20.0.0" # no idea what's a good lower bound
32-
33-
# FIXME: switch to PyPI once the package is published
34-
# https://github.com/ray-project/kuberay/issues/2078
35-
python-client = { git = "https://github.com/ray-project/kuberay.git", subdirectory = "clients/python-client", optional = true }
3632
tenacity = ">=8.0.0"
3733
ray = {extras = ["all"], version = ">=2.7.0"}
3834
dagster = ">=1.6.0"
@@ -140,4 +136,5 @@ exclude = [
140136
"dist",
141137
"node_modules",
142138
"venv",
139+
"dagster_ray/kuberay/ray_cluster_api.py" # taken from https://github.com/ray-project/kuberay/tree/master/clients/python-client/python_client
143140
]

tests/test_kuberay.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from dagster_ray.kuberay import KubeRayAPI, KubeRayCluster, RayClusterConfig, cleanup_kuberay_clusters
1818
from dagster_ray.kuberay.configs import DEFAULT_HEAD_GROUP_SPEC, DEFAULT_WORKER_GROUP_SPECS
1919
from dagster_ray.kuberay.ops import CleanupKuberayClustersConfig
20-
from dagster_ray.kuberay.ray_cluster_api import PatchedRayClusterApi
20+
from dagster_ray.kuberay.ray_cluster_api import RayClusterApi
2121
from tests import ROOT_DIR
2222

2323

@@ -230,7 +230,7 @@ def my_asset(context: AssetExecutionContext, ray_cluster: RayResource) -> None:
230230
resources={"ray_cluster": ray_cluster_resource},
231231
)
232232

233-
kuberay_api = PatchedRayClusterApi(config_file=str(k8s_with_raycluster.kubeconfig))
233+
kuberay_api = RayClusterApi(config_file=str(k8s_with_raycluster.kubeconfig))
234234

235235
# make sure the RayCluster is cleaned up
236236

@@ -257,7 +257,7 @@ def my_asset(ray_cluster: RayResource) -> None:
257257
resources={"ray_cluster": ray_cluster_resource_skip_cleanup},
258258
)
259259

260-
kuberay_api = PatchedRayClusterApi(config_file=str(k8s_with_raycluster.kubeconfig))
260+
kuberay_api = RayClusterApi(config_file=str(k8s_with_raycluster.kubeconfig))
261261

262262
assert (
263263
len(

0 commit comments

Comments
 (0)