|
| 1 | +import logging |
1 | 2 | import time |
2 | 3 | from typing import Any, Optional |
3 | 4 |
|
4 | 5 | from kubernetes import client, config |
5 | 6 | from kubernetes.client import ApiException |
6 | | -from python_client import constants |
7 | | -from python_client.kuberay_cluster_api import RayClusterApi, log |
8 | 7 |
|
| 8 | +import dagster_ray.kuberay.constants as constants |
9 | 9 |
|
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: |
11 | 18 | """ |
| 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 | +
|
12 | 22 | List of modifications to the original RayClusterApi: |
13 | 23 | - allow passing config_file and context to __init__\ |
14 | 24 | - 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: |
15 | 34 | """ |
16 | 35 |
|
17 | 36 | def __init__(self, config_file: Optional[str], context: Optional[str] = None): |
@@ -48,3 +67,185 @@ def get_ray_cluster_status( |
48 | 67 |
|
49 | 68 | log.info("raycluster {} status not set yet, timing out...".format(name)) |
50 | 69 | 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 |
0 commit comments