Skip to content

Commit 4062b13

Browse files
authored
[dy] Show the load balancer ingress host if there is no host in the ingress rules (mage-ai#4575)
* [dy] Don't return url when host is None * [dy] Check the status field * [dy] Add unit test
1 parent 5746745 commit 4062b13

File tree

3 files changed

+109
-1
lines changed

3 files changed

+109
-1
lines changed

mage_ai/cluster_manager/kubernetes/workload_manager.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
)
4444
from mage_ai.settings import MAGE_SETTINGS_ENVIRONMENT_VARIABLES
4545
from mage_ai.shared.array import find
46+
from mage_ai.shared.hash import safe_dig
4647

4748

4849
class WorkloadManager:
@@ -183,7 +184,6 @@ def format_workloads(
183184
pass
184185
if ip:
185186
workload['ip'] = f'{ip}:{port}'
186-
187187
elif stateful_set and stateful_set.spec.replicas == 0:
188188
workload['status'] = 'STOPPED'
189189
else:
@@ -515,6 +515,18 @@ def get_url_from_ingress(self, ingress_name: str, workload_name: str) -> str:
515515
rule = ingress.spec.rules[0]
516516
host = rule.host
517517

518+
if host is None:
519+
ingress_dict = ingress.to_dict()
520+
lb_ingress = safe_dig(ingress_dict, ['status', 'load_balancer', 'ingress[0]']) or {}
521+
if 'hostname' in lb_ingress:
522+
host = lb_ingress['hostname']
523+
# Alternatively, check for `ip` if `hostname` is not available
524+
elif 'ip' in lb_ingress:
525+
host = lb_ingress['ip']
526+
527+
if host is None:
528+
return None
529+
518530
tls_enabled = False
519531
try:
520532
tls = ingress.spec.tls[0]

mage_ai/shared/hash.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,53 @@ def _build(obj, key):
3838
return obj.get(key)
3939
else:
4040
return obj
41+
42+
return reduce(_build, arr, obj_arg)
43+
44+
45+
def safe_dig(obj_arg, arr_or_string):
46+
"""
47+
Safely retrieves nested values from a dictionary or list using a dot-separated path.
48+
49+
Args:
50+
obj_arg: The object (dictionary or list) to navigate.
51+
arr_or_string (str or list): A dot-separated path string or a list of
52+
keys/indexes.
53+
54+
Returns:
55+
The value retrieved from the nested structure, or None if any intermediate
56+
key/index is missing or the object is None.
57+
"""
58+
if isinstance(arr_or_string, str):
59+
arr_or_string = arr_or_string.split('.')
60+
arr = list(map(str.strip, arr_or_string))
61+
62+
def _build(obj, key):
63+
# Return None if the object is None or not a dictionary
64+
if obj is None or not isinstance(obj, dict) and not isinstance(obj, list):
65+
return None
66+
67+
tup = re.split(r'\[(\d+)\]$', key)
68+
if len(tup) >= 2:
69+
key, index = filter(lambda x: x, tup)
70+
index = int(index) if index else None
71+
if key and index is not None:
72+
if key not in obj:
73+
return None # Return None if the key is not present
74+
return (
75+
obj[key][index]
76+
if isinstance(obj[key], list) and len(obj[key]) > index
77+
else None
78+
)
79+
elif index is not None:
80+
return (
81+
obj[index] if isinstance(obj, list) and len(obj) > index else None
82+
)
83+
elif isinstance(obj, dict):
84+
return obj.get(key)
85+
else:
86+
return None
87+
4188
return reduce(_build, arr, obj_arg)
4289

4390

@@ -93,6 +140,7 @@ def _build(obj, key):
93140
if include_blank_values or val is not None:
94141
obj[key] = val
95142
return obj
143+
96144
return reduce(_build, keys, {})
97145

98146

@@ -111,6 +159,7 @@ def _build(obj, item):
111159
obj[val] = []
112160
obj[val].append(item)
113161
return obj
162+
114163
return reduce(_build, arr, {})
115164

116165

@@ -141,6 +190,7 @@ def _replace_nan_value(v):
141190
if isinstance(v, float) and math.isnan(v):
142191
return None
143192
return v
193+
144194
return {k: _replace_nan_value(v) for k, v in d.items()}
145195

146196

mage_ai/tests/shared/test_hash.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
camel_case_keys_to_snake_case,
33
combine_into,
44
get_json_value,
5+
safe_dig,
56
set_value,
67
)
78
from mage_ai.tests.base_test import TestCase
@@ -107,3 +108,48 @@ def test_combine_into(self):
107108
),
108109
ice=3,
109110
))
111+
112+
def test_safe_dig_dict_navigation(self):
113+
data = {
114+
'foo': {
115+
'bar': {
116+
'baz': 42
117+
}
118+
}
119+
}
120+
self.assertEqual(safe_dig(data, 'foo.bar.baz'), 42)
121+
122+
def test_safe_dig_list_navigation(self):
123+
data = {
124+
'foo': [
125+
{'bar': 42},
126+
{'bar': 43}
127+
]
128+
}
129+
self.assertEqual(safe_dig(data, 'foo[0].bar'), 42)
130+
self.assertEqual(safe_dig(data, 'foo[1].bar'), 43)
131+
self.assertEqual(safe_dig(data, ['foo[0]', 'bar']), 42)
132+
self.assertEqual(safe_dig(data, ['foo[1]', 'bar']), 43)
133+
134+
def test_safe_dig_missing_key(self):
135+
data = {
136+
'foo': {
137+
'bar': {
138+
'baz': 42
139+
}
140+
}
141+
}
142+
self.assertIsNone(safe_dig(data, 'foo.bar.baz.qux'))
143+
self.assertIsNone(safe_dig(data, ['foo', 'bar', 'baz', 'qux']))
144+
145+
def test_safe_dig_none_value(self):
146+
data = None
147+
self.assertIsNone(safe_dig(data, 'foo.bar.baz'))
148+
self.assertIsNone(safe_dig(data, ['foo', 'bar', 'baz']))
149+
150+
def test_safe_dig_invalid_index(self):
151+
data = {
152+
'foo': [1, 2, 3]
153+
}
154+
self.assertIsNone(safe_dig(data, 'foo[3]'))
155+
self.assertIsNone(safe_dig(data, ['foo[3]']))

0 commit comments

Comments
 (0)