Skip to content

Commit 538e780

Browse files
task(RHOAIENG-35928): Add HTTPRoute detection
1 parent afe21b3 commit 538e780

File tree

2 files changed

+486
-46
lines changed

2 files changed

+486
-46
lines changed

src/codeflare_sdk/ray/cluster/cluster.py

Lines changed: 132 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,19 @@ def cluster_uri(self) -> str:
495495
def cluster_dashboard_uri(self) -> str:
496496
"""
497497
Returns a string containing the cluster's dashboard URI.
498+
Tries HTTPRoute first (RHOAI v3.0+), then falls back to OpenShift Routes or Ingresses.
498499
"""
499500
config_check()
501+
502+
# Try HTTPRoute first (RHOAI v3.0+)
503+
# This will return None if HTTPRoute is not found (SDK v0.31.1 and below or Kind clusters)
504+
httproute_url = _get_dashboard_url_from_httproute(
505+
self.config.name, self.config.namespace
506+
)
507+
if httproute_url:
508+
return httproute_url
509+
510+
# Fall back to OpenShift Routes (pre-v3.0) or Ingresses (Kind)
500511
if _is_openshift_cluster():
501512
try:
502513
api_instance = client.CustomObjectsApi(get_api_client())
@@ -1001,45 +1012,51 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]:
10011012
status = RayClusterStatus.UNKNOWN
10021013
config_check()
10031014
dashboard_url = None
1004-
if _is_openshift_cluster():
1005-
try:
1006-
api_instance = client.CustomObjectsApi(get_api_client())
1007-
routes = api_instance.list_namespaced_custom_object(
1008-
group="route.openshift.io",
1009-
version="v1",
1010-
namespace=rc["metadata"]["namespace"],
1011-
plural="routes",
1012-
)
1013-
except Exception as e: # pragma: no cover
1014-
return _kube_api_error_handling(e)
10151015

1016-
for route in routes["items"]:
1017-
rc_name = rc["metadata"]["name"]
1018-
if route["metadata"]["name"] == f"ray-dashboard-{rc_name}" or route[
1019-
"metadata"
1020-
]["name"].startswith(f"{rc_name}-ingress"):
1021-
protocol = "https" if route["spec"].get("tls") else "http"
1022-
dashboard_url = f"{protocol}://{route['spec']['host']}"
1023-
else:
1024-
try:
1025-
api_instance = client.NetworkingV1Api(get_api_client())
1026-
ingresses = api_instance.list_namespaced_ingress(
1027-
rc["metadata"]["namespace"]
1028-
)
1029-
except Exception as e: # pragma no cover
1030-
return _kube_api_error_handling(e)
1031-
for ingress in ingresses.items:
1032-
annotations = ingress.metadata.annotations
1033-
protocol = "http"
1034-
if (
1035-
ingress.metadata.name == f"ray-dashboard-{rc['metadata']['name']}"
1036-
or ingress.metadata.name.startswith(f"{rc['metadata']['name']}-ingress")
1037-
):
1038-
if annotations == None:
1039-
protocol = "http"
1040-
elif "route.openshift.io/termination" in annotations:
1041-
protocol = "https"
1042-
dashboard_url = f"{protocol}://{ingress.spec.rules[0].host}"
1016+
# Try HTTPRoute first (RHOAI v3.0+)
1017+
rc_name = rc["metadata"]["name"]
1018+
rc_namespace = rc["metadata"]["namespace"]
1019+
dashboard_url = _get_dashboard_url_from_httproute(rc_name, rc_namespace)
1020+
1021+
# Fall back to OpenShift Routes or Ingresses if HTTPRoute not found
1022+
if not dashboard_url:
1023+
if _is_openshift_cluster():
1024+
try:
1025+
api_instance = client.CustomObjectsApi(get_api_client())
1026+
routes = api_instance.list_namespaced_custom_object(
1027+
group="route.openshift.io",
1028+
version="v1",
1029+
namespace=rc_namespace,
1030+
plural="routes",
1031+
)
1032+
except Exception as e: # pragma: no cover
1033+
return _kube_api_error_handling(e)
1034+
1035+
for route in routes["items"]:
1036+
if route["metadata"]["name"] == f"ray-dashboard-{rc_name}" or route[
1037+
"metadata"
1038+
]["name"].startswith(f"{rc_name}-ingress"):
1039+
protocol = "https" if route["spec"].get("tls") else "http"
1040+
dashboard_url = f"{protocol}://{route['spec']['host']}"
1041+
break
1042+
else:
1043+
try:
1044+
api_instance = client.NetworkingV1Api(get_api_client())
1045+
ingresses = api_instance.list_namespaced_ingress(rc_namespace)
1046+
except Exception as e: # pragma no cover
1047+
return _kube_api_error_handling(e)
1048+
for ingress in ingresses.items:
1049+
annotations = ingress.metadata.annotations
1050+
protocol = "http"
1051+
if (
1052+
ingress.metadata.name == f"ray-dashboard-{rc_name}"
1053+
or ingress.metadata.name.startswith(f"{rc_name}-ingress")
1054+
):
1055+
if annotations == None:
1056+
protocol = "http"
1057+
elif "route.openshift.io/termination" in annotations:
1058+
protocol = "https"
1059+
dashboard_url = f"{protocol}://{ingress.spec.rules[0].host}"
10431060

10441061
(
10451062
head_extended_resources,
@@ -1129,3 +1146,80 @@ def _is_openshift_cluster():
11291146
return False
11301147
except Exception as e: # pragma: no cover
11311148
return _kube_api_error_handling(e)
1149+
1150+
1151+
# Get dashboard URL from HTTPRoute (RHOAI v3.0+)
1152+
def _get_dashboard_url_from_httproute(
1153+
cluster_name: str, namespace: str
1154+
) -> Optional[str]:
1155+
"""
1156+
Attempts to get the Ray dashboard URL from an HTTPRoute resource.
1157+
This is used for RHOAI v3.0+ clusters that use Gateway API.
1158+
1159+
Args:
1160+
cluster_name: Name of the Ray cluster
1161+
namespace: Namespace of the Ray cluster
1162+
1163+
Returns:
1164+
Dashboard URL if HTTPRoute is found, None otherwise
1165+
"""
1166+
try:
1167+
config_check()
1168+
api_instance = client.CustomObjectsApi(get_api_client())
1169+
1170+
# Try to get HTTPRoute for this Ray cluster
1171+
try:
1172+
httproute = api_instance.get_namespaced_custom_object(
1173+
group="gateway.networking.k8s.io",
1174+
version="v1",
1175+
namespace=namespace,
1176+
plural="httproutes",
1177+
name=cluster_name,
1178+
)
1179+
except client.exceptions.ApiException as e:
1180+
if e.status == 404:
1181+
# HTTPRoute not found - this is expected for SDK v0.31.1 and below or Kind clusters
1182+
return None
1183+
raise
1184+
1185+
# Get the Gateway reference from HTTPRoute
1186+
parent_refs = httproute.get("spec", {}).get("parentRefs", [])
1187+
if not parent_refs:
1188+
return None
1189+
1190+
gateway_ref = parent_refs[0]
1191+
gateway_name = gateway_ref.get("name")
1192+
gateway_namespace = gateway_ref.get("namespace")
1193+
1194+
if not gateway_name or not gateway_namespace:
1195+
return None
1196+
1197+
# Get the Gateway to retrieve the hostname
1198+
gateway = api_instance.get_namespaced_custom_object(
1199+
group="gateway.networking.k8s.io",
1200+
version="v1",
1201+
namespace=gateway_namespace,
1202+
plural="gateways",
1203+
name=gateway_name,
1204+
)
1205+
1206+
# Extract hostname from Gateway listeners
1207+
listeners = gateway.get("spec", {}).get("listeners", [])
1208+
if not listeners:
1209+
return None
1210+
1211+
hostname = listeners[0].get("hostname")
1212+
if not hostname:
1213+
return None
1214+
1215+
# Construct the dashboard URL using RHOAI v3.0+ Gateway API pattern
1216+
# The HTTPRoute existence confirms v3.0+, so we use the standard path pattern
1217+
# Format: https://{hostname}/ray/{namespace}/{cluster-name}
1218+
protocol = "https" # Gateway API uses HTTPS
1219+
dashboard_url = f"{protocol}://{hostname}/ray/{namespace}/{cluster_name}"
1220+
1221+
return dashboard_url
1222+
1223+
except Exception as e: # pragma: no cover
1224+
# If any error occurs, return None to fall back to OpenShift Route
1225+
return None

0 commit comments

Comments
 (0)