@@ -991,14 +991,22 @@ def test_get_dashboard_url_from_httproute(mocker):
991991 },
992992 }
993993
994- # Mock the CustomObjectsApi to return HTTPRoute and Gateway
995- def mock_get_namespaced_custom_object (group , version , namespace , plural , name ):
994+ # Mock list_cluster_custom_object to return HTTPRoute (cluster-wide search)
995+ def mock_list_cluster_custom_object (group , version , plural , label_selector ):
996996 if plural == "httproutes" :
997- return mock_httproute
998- elif plural == "gateways" :
997+ return {"items" : [mock_httproute ]}
998+ raise Exception ("Unexpected plural" )
999+
1000+ # Mock get_namespaced_custom_object to return Gateway
1001+ def mock_get_namespaced_custom_object (group , version , namespace , plural , name ):
1002+ if plural == "gateways" :
9991003 return mock_gateway
10001004 raise Exception ("Unexpected plural" )
10011005
1006+ mocker .patch (
1007+ "kubernetes.client.CustomObjectsApi.list_cluster_custom_object" ,
1008+ side_effect = mock_list_cluster_custom_object ,
1009+ )
10021010 mocker .patch (
10031011 "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object" ,
10041012 side_effect = mock_get_namespaced_custom_object ,
@@ -1011,15 +1019,24 @@ def mock_get_namespaced_custom_object(group, version, namespace, plural, name):
10111019 )
10121020 assert result == expected_url , f"Expected { expected_url } , got { result } "
10131021
1014- # Test HTTPRoute not found (404) - should return None
1015- def mock_404_error (group , version , namespace , plural , name ):
1016- error = client .exceptions .ApiException (status = 404 )
1017- error .status = 404
1018- raise error
1022+ # Test HTTPRoute not found - should return None
1023+ def mock_list_cluster_empty (group , version , plural , label_selector ):
1024+ if plural == "httproutes" :
1025+ return {"items" : []}
1026+ raise Exception ("Unexpected plural" )
1027+
1028+ def mock_list_namespaced_empty (group , version , namespace , plural , label_selector ):
1029+ if plural == "httproutes" :
1030+ return {"items" : []}
1031+ raise Exception ("Unexpected plural" )
10191032
10201033 mocker .patch (
1021- "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object" ,
1022- side_effect = mock_404_error ,
1034+ "kubernetes.client.CustomObjectsApi.list_cluster_custom_object" ,
1035+ side_effect = mock_list_cluster_empty ,
1036+ )
1037+ mocker .patch (
1038+ "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object" ,
1039+ side_effect = mock_list_namespaced_empty ,
10231040 )
10241041
10251042 result = _get_dashboard_url_from_httproute ("nonexistent-cluster" , "test-ns" )
@@ -1031,14 +1048,14 @@ def mock_404_error(group, version, namespace, plural, name):
10311048 "spec" : {"parentRefs" : []}, # Empty parentRefs
10321049 }
10331050
1034- def mock_httproute_no_parents_fn (group , version , namespace , plural , name ):
1051+ def mock_list_cluster_no_parents (group , version , plural , label_selector ):
10351052 if plural == "httproutes" :
1036- return mock_httproute_no_parents
1053+ return { "items" : [ mock_httproute_no_parents ]}
10371054 raise Exception ("Unexpected plural" )
10381055
10391056 mocker .patch (
1040- "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object " ,
1041- side_effect = mock_httproute_no_parents_fn ,
1057+ "kubernetes.client.CustomObjectsApi.list_cluster_custom_object " ,
1058+ side_effect = mock_list_cluster_no_parents ,
10421059 )
10431060
10441061 result = _get_dashboard_url_from_httproute ("test-cluster" , "test-ns" )
@@ -1059,14 +1076,14 @@ def mock_httproute_no_parents_fn(group, version, namespace, plural, name):
10591076 },
10601077 }
10611078
1062- def mock_httproute_no_name_fn (group , version , namespace , plural , name ):
1079+ def mock_list_cluster_no_name (group , version , plural , label_selector ):
10631080 if plural == "httproutes" :
1064- return mock_httproute_no_name
1081+ return { "items" : [ mock_httproute_no_name ]}
10651082 raise Exception ("Unexpected plural" )
10661083
10671084 mocker .patch (
1068- "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object " ,
1069- side_effect = mock_httproute_no_name_fn ,
1085+ "kubernetes.client.CustomObjectsApi.list_cluster_custom_object " ,
1086+ side_effect = mock_list_cluster_no_name ,
10701087 )
10711088
10721089 result = _get_dashboard_url_from_httproute ("test-cluster" , "test-ns" )
@@ -1087,14 +1104,14 @@ def mock_httproute_no_name_fn(group, version, namespace, plural, name):
10871104 },
10881105 }
10891106
1090- def mock_httproute_no_namespace_fn (group , version , namespace , plural , name ):
1107+ def mock_list_cluster_no_namespace (group , version , plural , label_selector ):
10911108 if plural == "httproutes" :
1092- return mock_httproute_no_namespace
1109+ return { "items" : [ mock_httproute_no_namespace ]}
10931110 raise Exception ("Unexpected plural" )
10941111
10951112 mocker .patch (
1096- "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object " ,
1097- side_effect = mock_httproute_no_namespace_fn ,
1113+ "kubernetes.client.CustomObjectsApi.list_cluster_custom_object " ,
1114+ side_effect = mock_list_cluster_no_namespace ,
10981115 )
10991116
11001117 result = _get_dashboard_url_from_httproute ("test-cluster" , "test-ns" )
@@ -1183,6 +1200,75 @@ def mock_403_error(group, version, namespace, plural, name):
11831200 result is None
11841201 ), "Should return None when non-404 exception occurs (caught by outer handler)"
11851202
1203+ # Real-world scenario: Cluster-wide permissions denied, falls back to namespace search
1204+ # This simulates a regular data scientist without cluster-admin permissions
1205+ call_count = {"cluster_wide" : 0 , "namespaced" : 0 }
1206+
1207+ def mock_list_cluster_permission_denied (group , version , plural , label_selector ):
1208+ call_count ["cluster_wide" ] += 1
1209+ # Simulate permission denied for cluster-wide search
1210+ error = client .exceptions .ApiException (status = 403 )
1211+ error .status = 403
1212+ raise error
1213+
1214+ def mock_list_namespaced_success (group , version , namespace , plural , label_selector ):
1215+ call_count ["namespaced" ] += 1
1216+ # First namespace fails, second succeeds (simulates opendatahub deployment)
1217+ if namespace == "redhat-ods-applications" :
1218+ return {"items" : []}
1219+ elif namespace == "opendatahub" :
1220+ return {"items" : [mock_httproute ]}
1221+ return {"items" : []}
1222+
1223+ mocker .patch (
1224+ "kubernetes.client.CustomObjectsApi.list_cluster_custom_object" ,
1225+ side_effect = mock_list_cluster_permission_denied ,
1226+ )
1227+ mocker .patch (
1228+ "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object" ,
1229+ side_effect = mock_list_namespaced_success ,
1230+ )
1231+ mocker .patch (
1232+ "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object" ,
1233+ side_effect = mock_get_namespaced_custom_object ,
1234+ )
1235+
1236+ result = _get_dashboard_url_from_httproute ("test-cluster" , "test-ns" )
1237+ expected_url = (
1238+ "https://data-science-gateway.apps.example.com/ray/test-ns/test-cluster"
1239+ )
1240+ assert result == expected_url , f"Expected { expected_url } , got { result } "
1241+ assert call_count ["cluster_wide" ] == 1 , "Should try cluster-wide search first"
1242+ assert (
1243+ call_count ["namespaced" ] >= 2
1244+ ), "Should fall back to namespace search and try multiple namespaces"
1245+
1246+ # Real-world scenario: Gateway not found (404) - should return None
1247+ # This can happen if Gateway was deleted but HTTPRoute still exists
1248+ def mock_list_cluster_with_httproute (group , version , plural , label_selector ):
1249+ if plural == "httproutes" :
1250+ return {"items" : [mock_httproute ]}
1251+ raise Exception ("Unexpected plural" )
1252+
1253+ def mock_get_gateway_404 (group , version , namespace , plural , name ):
1254+ if plural == "gateways" :
1255+ error = client .exceptions .ApiException (status = 404 )
1256+ error .status = 404
1257+ raise error
1258+ raise Exception ("Unexpected plural" )
1259+
1260+ mocker .patch (
1261+ "kubernetes.client.CustomObjectsApi.list_cluster_custom_object" ,
1262+ side_effect = mock_list_cluster_with_httproute ,
1263+ )
1264+ mocker .patch (
1265+ "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object" ,
1266+ side_effect = mock_get_gateway_404 ,
1267+ )
1268+
1269+ result = _get_dashboard_url_from_httproute ("test-cluster" , "test-ns" )
1270+ assert result is None , "Should return None when Gateway not found (404)"
1271+
11861272
11871273def test_cluster_dashboard_uri_httproute_first (mocker ):
11881274 """
0 commit comments