Skip to content

Commit ddc6ea4

Browse files
committed
feat(context): auto-reload on kubeconfig change
Adds a file watcher to the `~/.kube/config` file. When a change is detected (e.g., by using `kubectx`), the Kubernetes client configuration is automatically reloaded. This ensures that the monitoring data displayed is always from the currently active context without requiring a manual restart of the tool. This is implemented using the `watchdog` library, which is added as a new dependency.
1 parent 2101289 commit ddc6ea4

File tree

2 files changed

+85
-14
lines changed

2 files changed

+85
-14
lines changed

kubernetes_monitoring.py

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,59 @@
4646
from rich.table import Table
4747
from rich.text import Text
4848

49+
try:
50+
from watchdog.events import FileSystemEventHandler
51+
from watchdog.observers import Observer
52+
except ImportError:
53+
Observer = None # type: ignore
54+
FileSystemEventHandler = None # type: ignore
55+
4956
try:
5057
from urllib3.exceptions import ConnectTimeoutError, MaxRetryError, ReadTimeoutError
5158
except ImportError:
5259
ConnectTimeoutError = MaxRetryError = ReadTimeoutError = Exception # type: ignore
5360

5461
console = Console()
5562

63+
# 컨텍스트 변경 감지를 위한 전역 플래그
64+
CONTEXT_CONFIG_NEEDS_RELOAD = False
65+
66+
67+
# Kubeconfig 변경을 감지하는 핸들러
68+
if FileSystemEventHandler:
69+
70+
class KubeConfigChangeHandler(FileSystemEventHandler):
71+
"""Kubeconfig 파일 변경을 감지하여 플래그를 설정."""
72+
73+
def __init__(self, file_path: Path):
74+
self.file_path = file_path
75+
76+
def on_modified(self, event: Any) -> None:
77+
if not event.is_directory and Path(event.src_path) == self.file_path:
78+
global CONTEXT_CONFIG_NEEDS_RELOAD
79+
CONTEXT_CONFIG_NEEDS_RELOAD = True
80+
console.print(
81+
"\n[bold yellow]Kubeconfig 변경 감지. 다음 갱신 시 컨텍스트를 다시 로드합니다.[/bold yellow]"
82+
)
83+
84+
85+
def start_kube_config_watcher() -> None:
86+
"""Kubeconfig 파일 감시자를 백그라운드 스레드에서 시작."""
87+
if not Observer:
88+
return
89+
90+
kube_config_path = Path(os.path.expanduser("~/.kube/config"))
91+
if not kube_config_path.is_file():
92+
return
93+
94+
event_handler = KubeConfigChangeHandler(kube_config_path)
95+
observer = Observer()
96+
observer.schedule(event_handler, str(kube_config_path.parent), recursive=False)
97+
observer.daemon = True
98+
observer.start()
99+
100+
101+
56102
# 노드그룹 라벨을 변수로 분리 (기본값: node.kubernetes.io/app)
57103
NODE_GROUP_LABEL = "node.kubernetes.io/app"
58104

@@ -867,21 +913,28 @@ async def _graceful_shutdown(timeout: float = 10.0) -> None:
867913
globals()["_async_graceful_shutdown"] = _graceful_shutdown # for advanced usage
868914

869915

870-
def load_kube_config() -> None:
871-
"""kube config 로드 (예외처리 포함)"""
916+
def reload_kube_config_if_changed(force: bool = False) -> bool:
917+
"""kube config 변경이 감지되었거나 강제 실행 시 리로드 후 True 반환."""
918+
global CONTEXT_CONFIG_NEEDS_RELOAD
919+
if not force and not CONTEXT_CONFIG_NEEDS_RELOAD:
920+
return False
921+
872922
try:
923+
config.kube_config.KubeConfigLoader.cleanup_and_reset()
873924
config.load_kube_config()
925+
CONTEXT_CONFIG_NEEDS_RELOAD = False
926+
console.print("\n[bold green]Kubeconfig를 다시 로드했습니다.[/bold green]")
927+
return True
874928
except Exception as e:
875-
print(f"Error loading kube config: {e}")
876-
sys.exit(1)
929+
console.print(f"\n[bold red]Kubeconfig 리로드 실패: {e}[/bold red]")
930+
return False
877931

878932

879933
def choose_namespace() -> Optional[str]:
880934
"""
881935
클러스터의 모든 namespace 목록을 표시하고, 사용자가 index로 선택
882936
아무 입력도 없으면 전체(namespace 전체) 조회
883937
"""
884-
load_kube_config()
885938
v1 = client.CoreV1Api()
886939
try:
887940
ns_list: V1NamespaceList = v1.list_namespace(
@@ -947,7 +1000,6 @@ def choose_node_group() -> Optional[str]:
9471000
클러스터의 모든 노드 그룹 목록(NODE_GROUP_LABEL로부터) 표시 후, 사용자가 index로 선택
9481001
아무 입력도 없으면 필터링하지 않음
9491002
"""
950-
load_kube_config()
9511003
v1 = client.CoreV1Api()
9521004
try:
9531005
nodes = v1.list_node().items
@@ -1059,7 +1111,6 @@ def watch_event_monitoring() -> None:
10591111
tail_num_raw = get_tail_lines("몇 줄씩 확인할까요? (예: 20): ")
10601112
tail_limit = _parse_tail_count(tail_num_raw)
10611113

1062-
load_kube_config()
10631114
v1 = client.CoreV1Api()
10641115
field_selector = "type!=Normal" if event_choice == "2" else None
10651116
if ns:
@@ -1075,6 +1126,9 @@ def watch_event_monitoring() -> None:
10751126
with Live(console=console, auto_refresh=False) as live:
10761127
tracker = LiveFrameTracker(live)
10771128
while True:
1129+
if reload_kube_config_if_changed():
1130+
v1 = client.CoreV1Api()
1131+
10781132
try:
10791133
if ns:
10801134
response = v1.list_namespaced_event(
@@ -1296,7 +1350,7 @@ def view_restarted_container_logs() -> None:
12961350
최근 재시작된 컨테이너 목록에서 선택하여 이전 컨테이너의 로그 확인
12971351
"""
12981352
console.print("\n[2] 재시작된 컨테이너 확인 및 로그 조회", style="bold blue")
1299-
load_kube_config()
1353+
reload_kube_config_if_changed()
13001354
v1 = client.CoreV1Api()
13011355
ns = choose_namespace()
13021356
pods = get_pods(v1, ns)
@@ -1379,7 +1433,6 @@ def watch_pod_monitoring_by_creation() -> None:
13791433
tail_num_raw = get_tail_lines("몇 줄씩 확인할까요? (예: 20): ")
13801434
tail_limit = _parse_tail_count(tail_num_raw)
13811435

1382-
load_kube_config()
13831436
v1 = client.CoreV1Api()
13841437
if ns:
13851438
command_descriptor = (
@@ -1397,6 +1450,8 @@ def watch_pod_monitoring_by_creation() -> None:
13971450
with Live(console=console, auto_refresh=False) as live:
13981451
tracker = LiveFrameTracker(live)
13991452
while True:
1453+
if reload_kube_config_if_changed():
1454+
v1 = client.CoreV1Api()
14001455
try:
14011456
if ns:
14021457
response = v1.list_namespaced_pod(
@@ -1665,7 +1720,6 @@ def watch_non_running_pod() -> None:
16651720
tail_num_raw = get_tail_lines("몇 줄씩 확인할까요? (예: 20): ")
16661721
tail_limit = _parse_tail_count(tail_num_raw)
16671722

1668-
load_kube_config()
16691723
v1 = client.CoreV1Api()
16701724
if ns:
16711725
command_descriptor = (
@@ -1682,6 +1736,8 @@ def watch_non_running_pod() -> None:
16821736
with Live(console=console, auto_refresh=False) as live:
16831737
tracker = LiveFrameTracker(live)
16841738
while True:
1739+
if reload_kube_config_if_changed():
1740+
v1 = client.CoreV1Api()
16851741
try:
16861742
if ns:
16871743
response = v1.list_namespaced_pod(
@@ -1936,13 +1992,14 @@ def watch_pod_counts() -> None:
19361992
)
19371993
ns = choose_namespace()
19381994
console.print("\n(Ctrl+C로 중지 후 메뉴로 돌아갑니다.)", style="bold yellow")
1939-
load_kube_config()
19401995
v1 = client.CoreV1Api()
19411996
try:
19421997
with suppress_terminal_echo():
19431998
with Live(console=console, auto_refresh=False) as live:
19441999
tracker = LiveFrameTracker(live)
19452000
while True:
2001+
if reload_kube_config_if_changed():
2002+
v1 = client.CoreV1Api()
19462003
pods = get_pods(v1, ns)
19472004
total = len(pods)
19482005
normal = sum(
@@ -2018,7 +2075,6 @@ def watch_node_monitoring_by_creation() -> None:
20182075
tail_num_raw = get_tail_lines("몇 줄씩 확인할까요? (예: 20): ")
20192076
tail_limit = _parse_tail_count(tail_num_raw)
20202077

2021-
load_kube_config()
20222078
v1 = client.CoreV1Api()
20232079
command_descriptor = (
20242080
"Python client: CoreV1Api.list_node (sorted by creationTimestamp)"
@@ -2034,6 +2090,8 @@ def watch_node_monitoring_by_creation() -> None:
20342090
with Live(console=console, auto_refresh=False) as live:
20352091
tracker = LiveFrameTracker(live)
20362092
while True:
2093+
if reload_kube_config_if_changed():
2094+
v1 = client.CoreV1Api()
20372095
try:
20382096
response = v1.list_node(_request_timeout=API_REQUEST_TIMEOUT)
20392097
nodes = list(getattr(response, "items", []) or [])
@@ -2260,7 +2318,6 @@ def watch_unhealthy_nodes() -> None:
22602318
tail_num_raw = get_tail_lines("몇 줄씩 확인할까요? (예: 20): ")
22612319
tail_limit = _parse_tail_count(tail_num_raw)
22622320

2263-
load_kube_config()
22642321
v1 = client.CoreV1Api()
22652322
command_descriptor = "Python client: CoreV1Api.list_node (exclude Ready)"
22662323
if filter_nodegroup:
@@ -2274,6 +2331,8 @@ def watch_unhealthy_nodes() -> None:
22742331
with Live(console=console, auto_refresh=False) as live:
22752332
tracker = LiveFrameTracker(live)
22762333
while True:
2334+
if reload_kube_config_if_changed():
2335+
v1 = client.CoreV1Api()
22772336
try:
22782337
response = v1.list_node(_request_timeout=API_REQUEST_TIMEOUT)
22792338
nodes = list(getattr(response, "items", []) or [])
@@ -2651,7 +2710,6 @@ def watch_pod_resources() -> None:
26512710
if filter_choice.startswith("y"):
26522711
filter_nodegroup = choose_node_group() or ""
26532712

2654-
load_kube_config()
26552713
v1 = client.CoreV1Api()
26562714
node_filter: Optional[Set[str]] = None
26572715
if filter_nodegroup:
@@ -2669,6 +2727,16 @@ def watch_pod_resources() -> None:
26692727
with Live(console=console, auto_refresh=False) as live:
26702728
tracker = LiveFrameTracker(live)
26712729
while True:
2730+
if reload_kube_config_if_changed():
2731+
v1 = client.CoreV1Api()
2732+
if filter_nodegroup:
2733+
node_filter = _collect_nodes_for_group(v1, filter_nodegroup)
2734+
if not node_filter:
2735+
console.print(
2736+
"[bold red]NodeGroup 필터에 해당하는 노드를 찾을 수 없습니다. 필터를 리셋합니다.[/bold red]"
2737+
)
2738+
filter_nodegroup = "" # Reset filter
2739+
26722740
metrics, error, kubectl_cmd = _get_kubectl_top_pod(namespace)
26732741
if error:
26742742
frame_key = _make_frame_key("error", error)
@@ -2896,6 +2964,8 @@ def main() -> None:
28962964
"""
28972965
메인 함수 실행
28982966
"""
2967+
start_kube_config_watcher()
2968+
reload_kube_config_if_changed(force=True) # 초기 강제 로드
28992969
try:
29002970
while True:
29012971
choice = main_menu()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies = [
1111
"kubernetes>=32.0.1",
1212
"tabulate>=0.9.0",
1313
"rich>=13.0.0",
14+
"watchdog>=2.3.1",
1415
]
1516

1617
[project.optional-dependencies]

0 commit comments

Comments
 (0)