@@ -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