@@ -74,33 +74,56 @@ def kbhit(self) -> bool: ...
7474 def getwch (self ) -> str : ...
7575
7676
77+ def _normalize_attr_key (key : Any ) -> str :
78+ """Attribute-style 접근을 위한 키 정규화."""
79+ if isinstance (key , str ):
80+ text = key
81+ else :
82+ text = str (key )
83+ return "" .join (ch for ch in text if ch .isalnum ()).lower ()
84+
85+
7786class AttrDict :
7887 """dict를 속성 접근 방식으로 다룰 수 있게 감싸는 래퍼."""
7988
80- __slots__ = ("_data" ,)
89+ __slots__ = ("_data" , "_key_map" )
8190
8291 def __init__ (self , data : Dict [str , Any ]) -> None :
8392 self ._data = data
93+ key_map : Dict [str , str ] = {}
94+ for original_key in data :
95+ normalized = _normalize_attr_key (original_key )
96+ key_map .setdefault (normalized , original_key )
97+ self ._key_map = key_map
8498
8599 def __getattr__ (self , item : str ) -> Any :
86- if item not in self ._data :
100+ actual_key = self ._key_map .get (_normalize_attr_key (item ))
101+ if actual_key is None or actual_key not in self ._data :
87102 return None
88- value = self ._data [item ]
103+ value = self ._data [actual_key ]
89104 wrapped = _wrap_kubectl_value (value )
90105 if wrapped is not value :
91- self ._data [item ] = wrapped
106+ self ._data [actual_key ] = wrapped
92107 return wrapped
93108
94109 def __getitem__ (self , key : str ) -> Any :
95- return self .__getattr__ (key )
110+ actual_key = self ._key_map .get (_normalize_attr_key (key ))
111+ if actual_key is None or actual_key not in self ._data :
112+ raise KeyError (key )
113+ value = self ._data [actual_key ]
114+ wrapped = _wrap_kubectl_value (value )
115+ if wrapped is not value :
116+ self ._data [actual_key ] = wrapped
117+ return wrapped
96118
97119 def get (self , key : str , default : Any = None ) -> Any :
98- if key not in self ._data :
120+ actual_key = self ._key_map .get (_normalize_attr_key (key ))
121+ if actual_key is None or actual_key not in self ._data :
99122 return default
100- value = self ._data [key ]
123+ value = self ._data [actual_key ]
101124 wrapped = _wrap_kubectl_value (value )
102125 if wrapped is not value :
103- self ._data [key ] = wrapped
126+ self ._data [actual_key ] = wrapped
104127 return wrapped
105128
106129 def to_dict (self ) -> Dict [str , Any ]:
@@ -189,6 +212,69 @@ class SnapshotPayload:
189212 command : Optional [str ]
190213
191214
215+ @dataclass (frozen = True )
216+ class PodContainerSummary :
217+ """Pod 컨테이너 Ready/Restart 지표 요약."""
218+
219+ ready : int
220+ total : int
221+ restarts : int
222+
223+
224+ @dataclass (frozen = True )
225+ class NodeGroupInfo :
226+ """NodeGroup 라벨 값과 해당 노드 목록."""
227+
228+ value : str
229+ nodes : Tuple [str , ...]
230+
231+ @property
232+ def label (self ) -> str :
233+ return f"{ NODE_GROUP_LABEL } ={ self .value } "
234+
235+ @property
236+ def node_count (self ) -> int :
237+ return len (self .nodes )
238+
239+
240+ def _summarize_pod_containers (pod : AttrDict ) -> PodContainerSummary :
241+ """kubectl JSON Pod 객체에서 Ready/총 컨테이너/재시작 횟수를 계산."""
242+ status = getattr (pod , "status" , None )
243+ spec = getattr (pod , "spec" , None )
244+ container_statuses = list (getattr (status , "container_statuses" , None ) or [])
245+ ready = sum (1 for item in container_statuses if getattr (item , "ready" , False ))
246+ total = (
247+ len (container_statuses )
248+ if container_statuses
249+ else len (getattr (spec , "containers" , []) or [])
250+ )
251+ restarts = sum (
252+ int (getattr (item , "restart_count" , 0 )) for item in container_statuses
253+ )
254+ return PodContainerSummary (ready = ready , total = total , restarts = restarts )
255+
256+
257+ def _extract_node_group_infos (nodes : Sequence [AttrDict ]) -> List [NodeGroupInfo ]:
258+ """노드 목록에서 NodeGroup 라벨 정보를 정리해 반환."""
259+ node_groups : Dict [str , Set [str ]] = {}
260+ for node in nodes :
261+ metadata = getattr (node , "metadata" , None )
262+ labels = getattr (metadata , "labels" , None ) or {}
263+ value = labels .get (NODE_GROUP_LABEL )
264+ if not value :
265+ continue
266+ node_name = getattr (metadata , "name" , "" ) or ""
267+ if not node_name :
268+ continue
269+ group_nodes = node_groups .setdefault (str (value ), set ())
270+ group_nodes .add (str (node_name ))
271+ infos = [
272+ NodeGroupInfo (value = value , nodes = tuple (sorted (node_names )))
273+ for value , node_names in node_groups .items ()
274+ ]
275+ return sorted (infos , key = lambda info : info .value .lower ())
276+
277+
192278def _ensure_datetime (value : Optional [datetime .datetime ]) -> Optional [datetime .datetime ]:
193279 """datetime 또는 ISO 문자열 입력을 UTC datetime으로 정규화."""
194280 if value is None :
@@ -1129,23 +1215,28 @@ def choose_node_group() -> Optional[str]:
11291215 console .print (f"명령어: { command } " , style = "dim" )
11301216 return None
11311217
1132- node_groups : Set [str ] = set ()
1133- for node in getattr (payload , "items" , []) or []:
1134- labels = getattr (getattr (node , "metadata" , None ), "labels" , None )
1135- if labels and NODE_GROUP_LABEL in labels :
1136- value = labels [NODE_GROUP_LABEL ]
1137- if value :
1138- node_groups .add (str (value ))
1139-
1140- sorted_groups = sorted (node_groups )
1141- if not sorted_groups :
1218+ items = list (getattr (payload , "items" , []) or [])
1219+ node_group_infos = _extract_node_group_infos (items )
1220+ if not node_group_infos :
11421221 print ("노드 그룹이 존재하지 않습니다." )
11431222 return None
11441223 table = Table (show_header = True , header_style = "bold magenta" , box = box .ROUNDED )
11451224 table .add_column ("Index" , style = "bold green" , width = 5 )
1146- table .add_column ("Node Group" , overflow = "fold" )
1147- for idx , ng in enumerate (sorted_groups , start = 1 ):
1148- table .add_row (str (idx ), ng )
1225+ table .add_column ("Label" , overflow = "fold" )
1226+ table .add_column ("Node Count" , justify = "right" )
1227+ table .add_column ("Sample Nodes" , overflow = "fold" )
1228+ for idx , info in enumerate (node_group_infos , start = 1 ):
1229+ sample_nodes = ", " .join (info .nodes [:3 ])
1230+ if len (info .nodes ) > 3 :
1231+ sample_nodes = f"{ sample_nodes } , ..."
1232+ if not sample_nodes :
1233+ sample_nodes = "-"
1234+ table .add_row (
1235+ str (idx ),
1236+ info .label ,
1237+ str (info .node_count ),
1238+ sample_nodes ,
1239+ )
11491240 console .print ("\n === Available Node Groups ===" , style = "bold green" )
11501241 console .print (table )
11511242
@@ -1158,10 +1249,15 @@ def choose_node_group() -> Optional[str]:
11581249 print ("숫자로 입력해주세요. 필터링하지 않음으로 진행합니다." )
11591250 return None
11601251 index = int (selection )
1161- if index < 1 or index > len (sorted_groups ):
1252+ if index < 1 or index > len (node_group_infos ):
11621253 print ("유효하지 않은 번호입니다. 필터링하지 않음으로 진행합니다." )
11631254 return None
1164- chosen_ng = sorted_groups [index - 1 ]
1255+ chosen_info = node_group_infos [index - 1 ]
1256+ console .print (
1257+ f"선택한 NodeGroup 라벨: { chosen_info .label } (노드 { chosen_info .node_count } 개)" ,
1258+ style = "bold green" ,
1259+ )
1260+ chosen_ng = chosen_info .value
11651261 return chosen_ng
11661262
11671263
@@ -1599,27 +1695,12 @@ def watch_pod_monitoring_by_creation() -> None:
15991695 creation = _format_timestamp (
16001696 getattr (metadata , "creation_timestamp" , None )
16011697 )
1602- container_statuses = list (
1603- getattr (status , "container_statuses" , None ) or []
1604- )
1605- ready_count = sum (
1606- 1
1607- for item in container_statuses
1608- if getattr (item , "ready" , False )
1609- )
1610- total_containers = (
1611- len (container_statuses )
1612- if container_statuses
1613- else len (getattr (spec , "containers" , []) or [])
1614- )
1698+ summary = _summarize_pod_containers (pod )
16151699 ready_display = _format_ready_ratio (
1616- ready_count ,
1617- total_containers ,
1618- )
1619- restarts = sum (
1620- int (getattr (item , "restart_count" , 0 ))
1621- for item in container_statuses
1700+ summary .ready ,
1701+ summary .total ,
16221702 )
1703+ restarts = summary .restarts
16231704 phase = getattr (status , "phase" , "" ) or "-"
16241705 pod_ip = getattr (status , "pod_ip" , "" ) or "-"
16251706 node_name = getattr (spec , "node_name" , "" ) or "-"
@@ -1794,27 +1875,12 @@ def _is_non_running(pod: AttrDict) -> bool:
17941875 creation = _format_timestamp (
17951876 getattr (metadata , "creation_timestamp" , None )
17961877 )
1797- container_statuses = list (
1798- getattr (status , "container_statuses" , None ) or []
1799- )
1800- ready_count = sum (
1801- 1
1802- for item in container_statuses
1803- if getattr (item , "ready" , False )
1804- )
1805- total_containers = (
1806- len (container_statuses )
1807- if container_statuses
1808- else len (getattr (spec , "containers" , []) or [])
1809- )
1878+ summary = _summarize_pod_containers (pod )
18101879 ready_display = _format_ready_ratio (
1811- ready_count ,
1812- total_containers ,
1813- )
1814- restarts = sum (
1815- int (getattr (item , "restart_count" , 0 ))
1816- for item in container_statuses
1880+ summary .ready ,
1881+ summary .total ,
18171882 )
1883+ restarts = summary .restarts
18181884 phase = getattr (status , "phase" , "" ) or "-"
18191885 pod_ip = getattr (status , "pod_ip" , "" ) or "-"
18201886 node_name = getattr (spec , "node_name" , "" ) or "-"
0 commit comments