@@ -477,8 +477,19 @@ def cluster_uri(self) -> str:
477477 def cluster_dashboard_uri (self ) -> str :
478478 """
479479 Returns a string containing the cluster's dashboard URI.
480+ Tries HTTPRoute first (RHOAI v3.0+), then falls back to OpenShift Routes or Ingresses.
480481 """
481482 config_check ()
483+
484+ # Try HTTPRoute first (RHOAI v3.0+)
485+ # This will return None if HTTPRoute is not found (pre-v3.0 or Kind clusters)
486+ httproute_url = _get_dashboard_url_from_httproute (
487+ self .config .name , self .config .namespace
488+ )
489+ if httproute_url :
490+ return httproute_url
491+
492+ # Fall back to OpenShift Routes (pre-v3.0) or Ingresses (Kind)
482493 if _is_openshift_cluster ():
483494 try :
484495 api_instance = client .CustomObjectsApi (get_api_client ())
@@ -1005,45 +1016,51 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]:
10051016 status = RayClusterStatus .UNKNOWN
10061017 config_check ()
10071018 dashboard_url = None
1008- if _is_openshift_cluster ():
1009- try :
1010- api_instance = client .CustomObjectsApi (get_api_client ())
1011- routes = api_instance .list_namespaced_custom_object (
1012- group = "route.openshift.io" ,
1013- version = "v1" ,
1014- namespace = rc ["metadata" ]["namespace" ],
1015- plural = "routes" ,
1016- )
1017- except Exception as e : # pragma: no cover
1018- return _kube_api_error_handling (e )
10191019
1020- for route in routes ["items" ]:
1021- rc_name = rc ["metadata" ]["name" ]
1022- if route ["metadata" ]["name" ] == f"ray-dashboard-{ rc_name } " or route [
1023- "metadata"
1024- ]["name" ].startswith (f"{ rc_name } -ingress" ):
1025- protocol = "https" if route ["spec" ].get ("tls" ) else "http"
1026- dashboard_url = f"{ protocol } ://{ route ['spec' ]['host' ]} "
1027- else :
1028- try :
1029- api_instance = client .NetworkingV1Api (get_api_client ())
1030- ingresses = api_instance .list_namespaced_ingress (
1031- rc ["metadata" ]["namespace" ]
1032- )
1033- except Exception as e : # pragma no cover
1034- return _kube_api_error_handling (e )
1035- for ingress in ingresses .items :
1036- annotations = ingress .metadata .annotations
1037- protocol = "http"
1038- if (
1039- ingress .metadata .name == f"ray-dashboard-{ rc ['metadata' ]['name' ]} "
1040- or ingress .metadata .name .startswith (f"{ rc ['metadata' ]['name' ]} -ingress" )
1041- ):
1042- if annotations == None :
1043- protocol = "http"
1044- elif "route.openshift.io/termination" in annotations :
1045- protocol = "https"
1046- dashboard_url = f"{ protocol } ://{ ingress .spec .rules [0 ].host } "
1020+ # Try HTTPRoute first (RHOAI v3.0+)
1021+ rc_name = rc ["metadata" ]["name" ]
1022+ rc_namespace = rc ["metadata" ]["namespace" ]
1023+ dashboard_url = _get_dashboard_url_from_httproute (rc_name , rc_namespace )
1024+
1025+ # Fall back to OpenShift Routes or Ingresses if HTTPRoute not found
1026+ if not dashboard_url :
1027+ if _is_openshift_cluster ():
1028+ try :
1029+ api_instance = client .CustomObjectsApi (get_api_client ())
1030+ routes = api_instance .list_namespaced_custom_object (
1031+ group = "route.openshift.io" ,
1032+ version = "v1" ,
1033+ namespace = rc_namespace ,
1034+ plural = "routes" ,
1035+ )
1036+ except Exception as e : # pragma: no cover
1037+ return _kube_api_error_handling (e )
1038+
1039+ for route in routes ["items" ]:
1040+ if route ["metadata" ]["name" ] == f"ray-dashboard-{ rc_name } " or route [
1041+ "metadata"
1042+ ]["name" ].startswith (f"{ rc_name } -ingress" ):
1043+ protocol = "https" if route ["spec" ].get ("tls" ) else "http"
1044+ dashboard_url = f"{ protocol } ://{ route ['spec' ]['host' ]} "
1045+ break
1046+ else :
1047+ try :
1048+ api_instance = client .NetworkingV1Api (get_api_client ())
1049+ ingresses = api_instance .list_namespaced_ingress (rc_namespace )
1050+ except Exception as e : # pragma no cover
1051+ return _kube_api_error_handling (e )
1052+ for ingress in ingresses .items :
1053+ annotations = ingress .metadata .annotations
1054+ protocol = "http"
1055+ if (
1056+ ingress .metadata .name == f"ray-dashboard-{ rc_name } "
1057+ or ingress .metadata .name .startswith (f"{ rc_name } -ingress" )
1058+ ):
1059+ if annotations == None :
1060+ protocol = "http"
1061+ elif "route.openshift.io/termination" in annotations :
1062+ protocol = "https"
1063+ dashboard_url = f"{ protocol } ://{ ingress .spec .rules [0 ].host } "
10471064
10481065 (
10491066 head_extended_resources ,
@@ -1133,3 +1150,80 @@ def _is_openshift_cluster():
11331150 return False
11341151 except Exception as e : # pragma: no cover
11351152 return _kube_api_error_handling (e )
1153+
1154+
1155+ # Get dashboard URL from HTTPRoute (RHOAI v3.0+)
1156+ def _get_dashboard_url_from_httproute (
1157+ cluster_name : str , namespace : str
1158+ ) -> Optional [str ]:
1159+ """
1160+ Attempts to get the Ray dashboard URL from an HTTPRoute resource.
1161+ This is used for RHOAI v3.0+ clusters that use Gateway API.
1162+
1163+ Args:
1164+ cluster_name: Name of the Ray cluster
1165+ namespace: Namespace of the Ray cluster
1166+
1167+ Returns:
1168+ Dashboard URL if HTTPRoute is found, None otherwise
1169+ """
1170+ try :
1171+ config_check ()
1172+ api_instance = client .CustomObjectsApi (get_api_client ())
1173+
1174+ # Try to get HTTPRoute for this Ray cluster
1175+ try :
1176+ httproute = api_instance .get_namespaced_custom_object (
1177+ group = "gateway.networking.k8s.io" ,
1178+ version = "v1" ,
1179+ namespace = namespace ,
1180+ plural = "httproutes" ,
1181+ name = cluster_name ,
1182+ )
1183+ except client .exceptions .ApiException as e :
1184+ if e .status == 404 :
1185+ # HTTPRoute not found - this is expected for pre-v3.0 or Kind clusters
1186+ return None
1187+ raise
1188+
1189+ # Get the Gateway reference from HTTPRoute
1190+ parent_refs = httproute .get ("spec" , {}).get ("parentRefs" , [])
1191+ if not parent_refs :
1192+ return None
1193+
1194+ gateway_ref = parent_refs [0 ]
1195+ gateway_name = gateway_ref .get ("name" )
1196+ gateway_namespace = gateway_ref .get ("namespace" )
1197+
1198+ if not gateway_name or not gateway_namespace :
1199+ return None
1200+
1201+ # Get the Gateway to retrieve the hostname
1202+ gateway = api_instance .get_namespaced_custom_object (
1203+ group = "gateway.networking.k8s.io" ,
1204+ version = "v1" ,
1205+ namespace = gateway_namespace ,
1206+ plural = "gateways" ,
1207+ name = gateway_name ,
1208+ )
1209+
1210+ # Extract hostname from Gateway listeners
1211+ listeners = gateway .get ("spec" , {}).get ("listeners" , [])
1212+ if not listeners :
1213+ return None
1214+
1215+ hostname = listeners [0 ].get ("hostname" )
1216+ if not hostname :
1217+ return None
1218+
1219+ # Construct the dashboard URL using RHOAI v3.0+ Gateway API pattern
1220+ # The HTTPRoute existence confirms v3.0+, so we use the standard path pattern
1221+ # Format: https://{hostname}/ray/{namespace}/{cluster-name}
1222+ protocol = "https" # Gateway API uses HTTPS
1223+ dashboard_url = f"{ protocol } ://{ hostname } /ray/{ namespace } /{ cluster_name } "
1224+
1225+ return dashboard_url
1226+
1227+ except Exception as e : # pragma: no cover
1228+ # If any error occurs, return None to fall back to OpenShift Route
1229+ return None
0 commit comments