kdebug 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kdebug/cli.py ADDED
@@ -0,0 +1,1469 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ kdebug.py - Universal Kubernetes Debug Container Utility
5
+
6
+ A utility for launching ephemeral debug containers in Kubernetes pods with
7
+ interactive shell access and backup capabilities.
8
+
9
+ Usage Examples:
10
+ # Interactive session with controller
11
+ ./kdebug.py -n kubecost --controller sts --controller-name aggregator --container aggregator --cmd bash
12
+
13
+ # Interactive session with direct pod
14
+ ./kdebug.py -n kubecost --pod aggregator-0 --container aggregator
15
+
16
+ # Backup mode
17
+ ./kdebug.py -n kubecost --pod aggregator-0 --container aggregator --backup /var/configs
18
+
19
+ # Using deployment
20
+ ./kdebug.py -n myapp --controller deployment --controller-name frontend --cmd sh
21
+ """
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import subprocess
27
+ import sys
28
+ import termios
29
+ import time
30
+ import tty
31
+ from datetime import datetime
32
+ from typing import Dict, List, Optional, Tuple
33
+
34
+ from kdebug import __version__
35
+
36
+ # Global debug flag
37
+ DEBUG_MODE = False
38
+
39
+ # Global kubectl options (set after argument parsing)
40
+ KUBECTL_CONTEXT = None
41
+ KUBECTL_KUBECONFIG = None
42
+
43
+ # ANSI Color codes (kubecolor-style)
44
+
45
+
46
+ class Colors:
47
+ # Basic colors
48
+ RESET = "\033[0m"
49
+ BOLD = "\033[1m"
50
+ DIM = "\033[2m"
51
+
52
+ # Foreground colors
53
+ BLACK = "\033[30m"
54
+ RED = "\033[31m"
55
+ GREEN = "\033[32m"
56
+ YELLOW = "\033[33m"
57
+ BLUE = "\033[34m"
58
+ MAGENTA = "\033[35m"
59
+ CYAN = "\033[36m"
60
+ WHITE = "\033[37m"
61
+
62
+ # Bright foreground colors
63
+ BRIGHT_BLACK = "\033[90m"
64
+ BRIGHT_RED = "\033[91m"
65
+ BRIGHT_GREEN = "\033[92m"
66
+ BRIGHT_YELLOW = "\033[93m"
67
+ BRIGHT_BLUE = "\033[94m"
68
+ BRIGHT_MAGENTA = "\033[95m"
69
+ BRIGHT_CYAN = "\033[96m"
70
+ BRIGHT_WHITE = "\033[97m"
71
+
72
+
73
+ def colorize(text: str, color: str) -> str:
74
+ """Wrap text with color codes."""
75
+ return f"{color}{text}{Colors.RESET}"
76
+
77
+
78
+ # Controller type aliases
79
+ CONTROLLER_ALIASES = {
80
+ "deployment": "Deployment",
81
+ "deploy": "Deployment",
82
+ "statefulset": "StatefulSet",
83
+ "sts": "StatefulSet",
84
+ "daemonset": "DaemonSet",
85
+ "ds": "DaemonSet",
86
+ }
87
+
88
+
89
+ def kubectl_base_cmd() -> str:
90
+ """Return kubectl command with global options."""
91
+ parts = ["kubectl"]
92
+ if KUBECTL_KUBECONFIG:
93
+ parts.append(f"--kubeconfig={KUBECTL_KUBECONFIG}")
94
+ if KUBECTL_CONTEXT:
95
+ parts.append(f"--context={KUBECTL_CONTEXT}")
96
+ return " ".join(parts)
97
+
98
+
99
+ def print_debug_command(cmd: str):
100
+ """Print command in a nice format when debug mode is enabled."""
101
+ if DEBUG_MODE:
102
+ print(f"\n{'─' * 60}")
103
+ print("🔍 DEBUG: Executing command:")
104
+ print(f"{'─' * 60}")
105
+ print(f"{cmd}")
106
+ print(f"{'─' * 60}\n")
107
+
108
+
109
+ def run_command(cmd: str, check: bool = True, use_bash: bool = False) -> Optional[str]:
110
+ """Run a shell command and return the output."""
111
+ print_debug_command(cmd)
112
+ try:
113
+ if use_bash:
114
+ # Use bash explicitly for commands that need bash features like process substitution
115
+ result = subprocess.run(
116
+ ["bash", "-c", cmd], capture_output=True, text=True, check=check
117
+ )
118
+ else:
119
+ result = subprocess.run(
120
+ cmd, shell=True, capture_output=True, text=True, check=check
121
+ )
122
+ return result.stdout.strip()
123
+ except subprocess.CalledProcessError as e:
124
+ print(f"Error running command: {cmd}", file=sys.stderr)
125
+ print(f"Error: {e.stderr}", file=sys.stderr)
126
+ if check:
127
+ return None
128
+ raise
129
+
130
+
131
+ def get_current_namespace() -> str:
132
+ """Get the current namespace from kubectl context."""
133
+ cmd = (
134
+ f"{kubectl_base_cmd()} config view --minify --output 'jsonpath={{..namespace}}'"
135
+ )
136
+ output = run_command(cmd, check=False)
137
+ return output if output else "default"
138
+
139
+
140
+ def get_pod_by_name(pod_name: str, namespace: str) -> Optional[Dict]:
141
+ """Get pod information by name."""
142
+ print(
143
+ f"Looking up pod {colorize(pod_name, Colors.CYAN)} in namespace {colorize(namespace, Colors.MAGENTA)}..."
144
+ )
145
+
146
+ cmd = f"{kubectl_base_cmd()} get pod {pod_name} -n {namespace} -o json"
147
+ output = run_command(cmd, check=False)
148
+
149
+ if not output:
150
+ print(
151
+ f"{colorize('✗ Error:', Colors.RED)} Pod '{pod_name}' not found in namespace '{namespace}'",
152
+ file=sys.stderr,
153
+ )
154
+ return None
155
+
156
+ try:
157
+ pod_data = json.loads(output)
158
+ return {
159
+ "name": pod_data.get("metadata", {}).get("name", ""),
160
+ "namespace": namespace,
161
+ }
162
+ except json.JSONDecodeError as e:
163
+ print(f"Error parsing pod JSON: {e}", file=sys.stderr)
164
+ return None
165
+
166
+
167
+ def get_pods_by_controller(
168
+ controller_type: str, controller_name: str, namespace: str
169
+ ) -> List[Dict]:
170
+ """Get all pods owned by a specific controller using owner references."""
171
+ # Normalize controller type
172
+ controller_kind = CONTROLLER_ALIASES.get(controller_type.lower())
173
+ if not controller_kind:
174
+ print(f"Error: Unknown controller type '{controller_type}'", file=sys.stderr)
175
+ print(
176
+ f"Supported types: {', '.join(CONTROLLER_ALIASES.keys())}", file=sys.stderr
177
+ )
178
+ return []
179
+
180
+ print(
181
+ f"Searching for pods from {colorize(controller_kind, Colors.YELLOW)} {colorize(controller_name, Colors.CYAN)} in namespace {colorize(namespace, Colors.MAGENTA)}..."
182
+ )
183
+
184
+ # Get all pods in the namespace
185
+ cmd = f"{kubectl_base_cmd()} get pods -n {namespace} -o json"
186
+ output = run_command(cmd, check=False)
187
+
188
+ if not output:
189
+ print("Error: Failed to get pods", file=sys.stderr)
190
+ return []
191
+
192
+ try:
193
+ pods_data = json.loads(output)
194
+ except json.JSONDecodeError as e:
195
+ print(f"Error parsing pods JSON: {e}", file=sys.stderr)
196
+ return []
197
+
198
+ matching_pods = []
199
+
200
+ for pod in pods_data.get("items", []):
201
+ pod_name = pod.get("metadata", {}).get("name", "")
202
+ owner_refs = pod.get("metadata", {}).get("ownerReferences", [])
203
+ pod_matched = False
204
+
205
+ # Check direct ownership (works for StatefulSet, DaemonSet)
206
+ for ref in owner_refs:
207
+ if (
208
+ ref.get("kind") == controller_kind
209
+ and ref.get("name") == controller_name
210
+ ):
211
+ matching_pods.append({"name": pod_name, "namespace": namespace})
212
+ pod_matched = True
213
+ break
214
+
215
+ # For Deployments, check if owned by a ReplicaSet that belongs to our Deployment
216
+ if controller_kind == "Deployment" and not pod_matched:
217
+ for ref in owner_refs:
218
+ if ref.get("kind") == "ReplicaSet":
219
+ rs_name = ref.get("name", "")
220
+ # ReplicaSet names typically start with deployment name
221
+ if rs_name.startswith(controller_name + "-"):
222
+ matching_pods.append({"name": pod_name, "namespace": namespace})
223
+ pod_matched = True
224
+ break
225
+
226
+ return matching_pods
227
+
228
+
229
+ def get_all_controllers(namespace: str) -> Dict[str, List[Dict]]:
230
+ """Get all controllers (deployments, statefulsets, daemonsets) in namespace."""
231
+ controllers = {"Deployment": [], "StatefulSet": [], "DaemonSet": []}
232
+
233
+ for controller_type in ["deployment", "statefulset", "daemonset"]:
234
+ cmd = f"{kubectl_base_cmd()} get {controller_type} -n {namespace} -o json"
235
+ output = run_command(cmd, check=False)
236
+
237
+ if output:
238
+ try:
239
+ data = json.loads(output)
240
+ for item in data.get("items", []):
241
+ name = item.get("metadata", {}).get("name", "")
242
+ replicas = item.get("spec", {}).get("replicas", 0)
243
+ ready = item.get("status", {}).get("readyReplicas", 0)
244
+
245
+ controller_kind = CONTROLLER_ALIASES.get(
246
+ controller_type, controller_type
247
+ )
248
+ controllers[controller_kind].append(
249
+ {
250
+ "name": name,
251
+ "type": controller_type,
252
+ "kind": controller_kind,
253
+ "replicas": replicas,
254
+ "ready": ready,
255
+ }
256
+ )
257
+ except json.JSONDecodeError:
258
+ pass
259
+
260
+ return controllers
261
+
262
+
263
+ def get_all_pods(namespace: str) -> List[Dict]:
264
+ """Get all pods in namespace with their status."""
265
+ cmd = f"{kubectl_base_cmd()} get pods -n {namespace} -o json"
266
+ output = run_command(cmd, check=False)
267
+
268
+ if not output:
269
+ return []
270
+
271
+ try:
272
+ data = json.loads(output)
273
+ pods = []
274
+ for item in data.get("items", []):
275
+ name = item.get("metadata", {}).get("name", "")
276
+ status = item.get("status", {}).get("phase", "Unknown")
277
+ pods.append({"name": name, "status": status, "namespace": namespace})
278
+ return pods
279
+ except json.JSONDecodeError:
280
+ return []
281
+
282
+
283
+ def read_key() -> str:
284
+ """Read a single keypress from stdin."""
285
+ fd = sys.stdin.fileno()
286
+ old_settings = termios.tcgetattr(fd)
287
+ try:
288
+ tty.setraw(fd)
289
+ ch = sys.stdin.read(1)
290
+ # Handle arrow keys (escape sequences)
291
+ if ch == "\x1b":
292
+ ch2 = sys.stdin.read(1)
293
+ if ch2 == "[":
294
+ ch3 = sys.stdin.read(1)
295
+ if ch3 == "A":
296
+ return "up"
297
+ elif ch3 == "B":
298
+ return "down"
299
+ return ch
300
+ finally:
301
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
302
+
303
+
304
+ def display_menu(
305
+ title: str, items: List[str], selected_idx: int, show_numbers: bool = True
306
+ ):
307
+ """Display a colorful menu with the selected item highlighted."""
308
+ # Clear screen
309
+ print("\033[2J\033[H", end="")
310
+
311
+ # Print title
312
+ print(f"\n{colorize('═' * 70, Colors.BLUE)}")
313
+ print(f"{colorize(title, Colors.BOLD + Colors.BRIGHT_CYAN)}")
314
+ print(f"{colorize('═' * 70, Colors.BLUE)}\n")
315
+
316
+ # Print items
317
+ for idx, item in enumerate(items):
318
+ if idx == selected_idx:
319
+ # Highlighted item
320
+ prefix = "▶ " if show_numbers else " "
321
+ number = f"{idx + 1}. " if show_numbers else ""
322
+ print(
323
+ f"{colorize(prefix + number + item, Colors.BOLD + Colors.BRIGHT_GREEN)}"
324
+ )
325
+ else:
326
+ # Normal item
327
+ prefix = " "
328
+ number = f"{idx + 1}. " if show_numbers else ""
329
+ print(f"{colorize(prefix + number + item, Colors.WHITE)}")
330
+
331
+ # Print quit option as selectable item
332
+ quit_idx = len(items)
333
+ if selected_idx == quit_idx:
334
+ print(f"\n{colorize('▶ q. Quit', Colors.BOLD + Colors.BRIGHT_GREEN)}")
335
+ else:
336
+ print(f"\n {colorize('q.', Colors.WHITE)} {colorize('Quit', Colors.CYAN)}")
337
+
338
+ # Print instructions
339
+ print(f"\n{colorize('─' * 70, Colors.DIM)}")
340
+ if show_numbers:
341
+ print(
342
+ f"{colorize('Use ↑/↓ arrows or numbers to select, Enter to confirm', Colors.BRIGHT_BLACK)}"
343
+ )
344
+ else:
345
+ print(
346
+ f"{colorize('Use ↑/↓ arrows to select, Enter to confirm', Colors.BRIGHT_BLACK)}"
347
+ )
348
+ print(f"{colorize('─' * 70, Colors.DIM)}\n")
349
+
350
+
351
+ def interactive_menu(
352
+ title: str, items: List[str], show_numbers: bool = True
353
+ ) -> Optional[int]:
354
+ """Display an interactive menu and return the selected index."""
355
+ if not items:
356
+ print(f"{colorize('✗ Error:', Colors.RED)} No items to display")
357
+ return None
358
+
359
+ selected_idx = 0
360
+ quit_idx = len(items) # Quit is one position after last item
361
+
362
+ while True:
363
+ display_menu(title, items, selected_idx, show_numbers)
364
+
365
+ key = read_key()
366
+
367
+ if key == "up":
368
+ selected_idx = (selected_idx - 1) % (len(items) + 1)
369
+ elif key == "down":
370
+ selected_idx = (selected_idx + 1) % (len(items) + 1)
371
+ elif key == "\r" or key == "\n": # Enter
372
+ if selected_idx == quit_idx:
373
+ return None # Quit selected
374
+ return selected_idx
375
+ elif key == "q" or key == "Q":
376
+ return None
377
+ elif key.isdigit() and show_numbers:
378
+ num = int(key)
379
+ if 1 <= num <= len(items):
380
+ selected_idx = num - 1
381
+ return selected_idx
382
+
383
+
384
+ def select_controller_interactive(namespace: str) -> Optional[Tuple[str, str]]:
385
+ """Interactive TUI for selecting a controller."""
386
+ print(f"\n{colorize('Fetching controllers...', Colors.YELLOW)}")
387
+ controllers = get_all_controllers(namespace)
388
+
389
+ # Flatten controllers into menu items
390
+ menu_items = []
391
+ controller_map = []
392
+
393
+ for kind in ["Deployment", "StatefulSet", "DaemonSet"]:
394
+ for ctrl in controllers[kind]:
395
+ status = (
396
+ f"{ctrl['ready']}/{ctrl['replicas']}" if ctrl["replicas"] > 0 else "N/A"
397
+ )
398
+ menu_items.append(
399
+ f"{colorize(kind, Colors.YELLOW)} {colorize(ctrl['name'], Colors.CYAN)} "
400
+ f"({colorize(status, Colors.GREEN if ctrl['ready'] == ctrl['replicas'] else Colors.YELLOW)})"
401
+ )
402
+ controller_map.append((ctrl["type"], ctrl["name"]))
403
+
404
+ if not menu_items:
405
+ print(
406
+ f"{colorize('✗ Error:', Colors.RED)} No controllers found in namespace {colorize(namespace, Colors.MAGENTA)}"
407
+ )
408
+ return None
409
+
410
+ title = f"Select Controller in namespace: {colorize(namespace, Colors.MAGENTA)}"
411
+ selected_idx = interactive_menu(title, menu_items)
412
+
413
+ if selected_idx is None:
414
+ return None
415
+
416
+ return controller_map[selected_idx]
417
+
418
+
419
+ def select_pod_interactive(namespace: str) -> Optional[str]:
420
+ """Interactive TUI for selecting a pod."""
421
+ print(f"\n{colorize('Fetching pods...', Colors.YELLOW)}")
422
+ pods = get_all_pods(namespace)
423
+
424
+ if not pods:
425
+ print(
426
+ f"{colorize('✗ Error:', Colors.RED)} No pods found in namespace {colorize(namespace, Colors.MAGENTA)}"
427
+ )
428
+ return None
429
+
430
+ # Create menu items
431
+ menu_items = []
432
+ for pod in pods:
433
+ status_color = Colors.GREEN if pod["status"] == "Running" else Colors.YELLOW
434
+ menu_items.append(
435
+ f"{colorize(pod['name'], Colors.CYAN)} "
436
+ f"({colorize(pod['status'], status_color)})"
437
+ )
438
+
439
+ title = f"Select Pod in namespace: {colorize(namespace, Colors.MAGENTA)}"
440
+ selected_idx = interactive_menu(title, menu_items)
441
+
442
+ if selected_idx is None:
443
+ return None
444
+
445
+ return pods[selected_idx]["name"]
446
+
447
+
448
+ def select_pod(args) -> Optional[Dict]:
449
+ """Select a pod based on provided arguments."""
450
+ namespace = args.namespace or get_current_namespace()
451
+
452
+ # Direct pod selection
453
+ if args.pod:
454
+ return get_pod_by_name(args.pod, namespace)
455
+
456
+ # Controller-based selection
457
+ if args.controller:
458
+ if not args.controller_name:
459
+ print(
460
+ "Error: --controller-name is required when using --controller",
461
+ file=sys.stderr,
462
+ )
463
+ return None
464
+
465
+ pods = get_pods_by_controller(args.controller, args.controller_name, namespace)
466
+
467
+ if not pods:
468
+ print(
469
+ f"No pods found for {args.controller} '{args.controller_name}'",
470
+ file=sys.stderr,
471
+ )
472
+ return None
473
+
474
+ if len(pods) > 1:
475
+ print(
476
+ f"Found {colorize(str(len(pods)), Colors.YELLOW)} pods, selecting first one: {colorize(pods[0]['name'], Colors.CYAN)}"
477
+ )
478
+
479
+ return pods[0]
480
+
481
+ # Interactive mode - no pod or controller specified
482
+ print(f"\n{colorize('Starting interactive pod selection...', Colors.BRIGHT_CYAN)}")
483
+
484
+ # Direct pod selection via TUI
485
+ pod_name = select_pod_interactive(namespace)
486
+ if not pod_name:
487
+ print(f"\n{colorize('Selection cancelled', Colors.YELLOW)}")
488
+ return None
489
+
490
+ return {"name": pod_name, "namespace": namespace}
491
+
492
+
493
+ def get_pod_containers(pod_name: str, namespace: str) -> Dict[str, List[str]]:
494
+ """Get all containers from a pod, separated by type."""
495
+ cmd = f"{kubectl_base_cmd()} get pod {pod_name} -n {namespace} -o json"
496
+ output = run_command(cmd)
497
+
498
+ if not output:
499
+ return {"containers": [], "init_containers": [], "ephemeral_containers": []}
500
+
501
+ try:
502
+ pod_data = json.loads(output)
503
+ except json.JSONDecodeError as e:
504
+ print(f"Error parsing pod JSON: {e}", file=sys.stderr)
505
+ return {"containers": [], "init_containers": [], "ephemeral_containers": []}
506
+
507
+ spec = pod_data.get("spec", {})
508
+
509
+ containers = [
510
+ container.get("name")
511
+ for container in spec.get("containers", [])
512
+ if container.get("name")
513
+ ]
514
+
515
+ init_containers = [
516
+ container.get("name")
517
+ for container in spec.get("initContainers", [])
518
+ if container.get("name")
519
+ ]
520
+
521
+ ephemeral_containers = [
522
+ container.get("name")
523
+ for container in spec.get("ephemeralContainers", [])
524
+ if container.get("name")
525
+ ]
526
+
527
+ return {
528
+ "containers": containers,
529
+ "init_containers": init_containers,
530
+ "ephemeral_containers": ephemeral_containers,
531
+ }
532
+
533
+
534
+ def get_existing_ephemeral_containers(pod_name: str, namespace: str) -> List[str]:
535
+ """Get list of existing ephemeral container names."""
536
+ container_info = get_pod_containers(pod_name, namespace)
537
+ return container_info["ephemeral_containers"]
538
+
539
+
540
+ def wait_for_container_running(
541
+ pod_name: str, namespace: str, container_name: str, timeout: int = 60
542
+ ) -> bool:
543
+ """Poll until the container is in running state or timeout."""
544
+ print(
545
+ f"Waiting for container {colorize(container_name, Colors.CYAN)} to be running..."
546
+ )
547
+
548
+ # Known failure states that should immediately fail
549
+ failure_states = {
550
+ "ImagePullBackOff",
551
+ "ErrImagePull",
552
+ "CrashLoopBackOff",
553
+ "CreateContainerError",
554
+ "InvalidImageName",
555
+ "CreateContainerConfigError",
556
+ }
557
+
558
+ start_time = time.time()
559
+ last_reason = None
560
+
561
+ while time.time() - start_time < timeout:
562
+ cmd = f"{kubectl_base_cmd()} get pod {pod_name} -n {namespace} -o json"
563
+ output = run_command(cmd)
564
+
565
+ if not output:
566
+ time.sleep(2)
567
+ continue
568
+
569
+ try:
570
+ pod_data = json.loads(output)
571
+ ephemeral_statuses = pod_data.get("status", {}).get(
572
+ "ephemeralContainerStatuses", []
573
+ )
574
+
575
+ for status in ephemeral_statuses:
576
+ if status.get("name") == container_name:
577
+ state = status.get("state", {})
578
+
579
+ # Check if running
580
+ if "running" in state:
581
+ print(
582
+ f"{colorize('✓', Colors.GREEN)} Container {colorize(container_name, Colors.CYAN)} is {colorize('running', Colors.GREEN)}"
583
+ )
584
+ return True
585
+
586
+ # Check if waiting
587
+ elif "waiting" in state:
588
+ waiting_info = state.get("waiting", {})
589
+ reason = waiting_info.get("reason", "Unknown")
590
+ message = waiting_info.get("message", "")
591
+
592
+ # Check for immediate failure states
593
+ if reason in failure_states:
594
+ print(
595
+ f"{colorize('✗', Colors.RED)} Container failed to start: {colorize(reason, Colors.RED)}",
596
+ file=sys.stderr,
597
+ )
598
+ if message:
599
+ print(
600
+ f"{colorize('Error details:', Colors.RED)} {message}",
601
+ file=sys.stderr,
602
+ )
603
+ return False
604
+
605
+ # Show progress for transient states
606
+ if reason != last_reason:
607
+ print(
608
+ f"Container status: {colorize(reason, Colors.YELLOW)}"
609
+ )
610
+ last_reason = reason
611
+
612
+ # Check if terminated
613
+ elif "terminated" in state:
614
+ terminated_info = state.get("terminated", {})
615
+ reason = terminated_info.get("reason", "Unknown")
616
+ exit_code = terminated_info.get("exitCode", "N/A")
617
+ message = terminated_info.get("message", "")
618
+
619
+ print(
620
+ f"{colorize('✗', Colors.RED)} Container terminated: {colorize(reason, Colors.RED)} (exit code: {colorize(str(exit_code), Colors.RED)})",
621
+ file=sys.stderr,
622
+ )
623
+ if message:
624
+ print(
625
+ f"{colorize('Error details:', Colors.RED)} {message}",
626
+ file=sys.stderr,
627
+ )
628
+ return False
629
+
630
+ # Container exists but no state info yet
631
+ else:
632
+ if last_reason != "NoState":
633
+ print("Container status: Initializing...")
634
+ last_reason = "NoState"
635
+
636
+ except json.JSONDecodeError as e:
637
+ print(f"Warning: Failed to parse pod JSON: {e}", file=sys.stderr)
638
+
639
+ time.sleep(2)
640
+
641
+ print(
642
+ f"{colorize('✗', Colors.RED)} Timeout ({timeout}s) waiting for container to start",
643
+ file=sys.stderr,
644
+ )
645
+ if last_reason:
646
+ print(
647
+ f"Last known status: {colorize(last_reason, Colors.YELLOW)}",
648
+ file=sys.stderr,
649
+ )
650
+ return False
651
+
652
+
653
+ def check_pod_security_context(pod_name: str, namespace: str) -> Dict:
654
+ """Check the pod's security context to see if running as root is allowed."""
655
+ cmd = f"{kubectl_base_cmd()} get pod {pod_name} -n {namespace} -o json"
656
+ output = run_command(cmd, check=False)
657
+
658
+ if not output:
659
+ return {"can_run_as_root": True, "reason": "Unable to check"}
660
+
661
+ try:
662
+ pod_data = json.loads(output)
663
+ spec = pod_data.get("spec", {})
664
+
665
+ # Check pod-level security context
666
+ pod_security_context = spec.get("securityContext", {})
667
+ run_as_non_root = pod_security_context.get("runAsNonRoot", False)
668
+
669
+ # Check if there's a runAsUser set at pod level
670
+ pod_run_as_user = pod_security_context.get("runAsUser")
671
+
672
+ # Check container-level security contexts
673
+ containers = spec.get("containers", [])
674
+ for container in containers:
675
+ container_security = container.get("securityContext", {})
676
+ container_run_as_non_root = container_security.get("runAsNonRoot", False)
677
+
678
+ if container_run_as_non_root or run_as_non_root:
679
+ return {
680
+ "can_run_as_root": False,
681
+ "reason": "Pod has runAsNonRoot policy enabled",
682
+ }
683
+
684
+ return {"can_run_as_root": True, "reason": "No restrictions found"}
685
+
686
+ except json.JSONDecodeError:
687
+ return {"can_run_as_root": True, "reason": "Unable to parse pod spec"}
688
+
689
+
690
+ def launch_debug_container(
691
+ pod_name: str,
692
+ namespace: str,
693
+ debug_image: str,
694
+ target_container: Optional[str],
695
+ existing_containers: List[str],
696
+ as_root: bool = False,
697
+ ) -> Optional[str]:
698
+ """Launch a debug container attached to the pod and return its name."""
699
+ print(f"Launching debug container for pod {colorize(pod_name, Colors.CYAN)}...")
700
+
701
+ # Check if running as root is possible when requested
702
+ if as_root:
703
+ security_check = check_pod_security_context(pod_name, namespace)
704
+ if not security_check["can_run_as_root"]:
705
+ print(
706
+ f"{colorize('⚠ Warning:', Colors.YELLOW)} {security_check['reason']}",
707
+ file=sys.stderr,
708
+ )
709
+ print(
710
+ f"{colorize('The --as-root flag will likely fail.', Colors.YELLOW)} Proceeding anyway...",
711
+ file=sys.stderr,
712
+ )
713
+ print(f"{colorize('Tip:', Colors.CYAN)} Try without --as-root flag\n")
714
+
715
+ if existing_containers:
716
+ print(
717
+ f"Existing ephemeral containers: {colorize(', '.join(existing_containers), Colors.BRIGHT_BLACK)}"
718
+ )
719
+
720
+ # Build kubectl debug command
721
+ cmd_parts = [
722
+ f"nohup {kubectl_base_cmd()} debug -i --tty",
723
+ pod_name,
724
+ f"--namespace={namespace}",
725
+ ]
726
+
727
+ if target_container:
728
+ cmd_parts.append(f"--target={target_container}")
729
+
730
+ cmd_parts.extend(
731
+ [
732
+ "--share-processes",
733
+ "--profile=general",
734
+ ]
735
+ )
736
+
737
+ if as_root:
738
+ cmd_parts.append('--custom=<(echo \'{"securityContext":{"runAsUser":0}}\')')
739
+
740
+ cmd_parts.extend(
741
+ [
742
+ f"--image={debug_image}",
743
+ "-- sleep 1440 > /dev/null 2>&1 &",
744
+ ]
745
+ )
746
+
747
+ cmd = " ".join(cmd_parts)
748
+ run_command(cmd, check=False, use_bash=True)
749
+
750
+ # Give kubectl a moment to register the debug container
751
+ time.sleep(2)
752
+
753
+ # Get the new list of ephemeral containers
754
+ new_containers = get_existing_ephemeral_containers(pod_name, namespace)
755
+
756
+ # Find the newly created container
757
+ new_container_names = [
758
+ name for name in new_containers if name not in existing_containers
759
+ ]
760
+
761
+ if not new_container_names:
762
+ print(
763
+ "Error: Could not identify newly created debug container", file=sys.stderr
764
+ )
765
+ return None
766
+
767
+ debug_container = new_container_names[0]
768
+ print(
769
+ f"{colorize('✓', Colors.GREEN)} Created debug container: {colorize(debug_container, Colors.CYAN)}"
770
+ )
771
+
772
+ # Wait for the container to actually be running
773
+ if not wait_for_container_running(pod_name, namespace, debug_container):
774
+ print("Error: Debug container failed to start", file=sys.stderr)
775
+ return None
776
+
777
+ return debug_container
778
+
779
+
780
+ def exec_interactive(
781
+ pod_name: str, namespace: str, container_name: str, cmd: str, cd_into: str
782
+ ) -> int:
783
+ """Execute an interactive command in the debug container."""
784
+ print(f"\n{colorize('=' * 60, Colors.BLUE)}")
785
+ print(
786
+ f"{colorize('Starting interactive session', Colors.BOLD)} in pod {colorize(pod_name, Colors.CYAN)}"
787
+ )
788
+ print(f"Container: {colorize(container_name, Colors.CYAN)}")
789
+ print(f"Command: {colorize(cmd, Colors.YELLOW)}")
790
+ if cd_into:
791
+ print(f"Directory: {colorize(cd_into, Colors.MAGENTA)}")
792
+ print(f"{colorize('=' * 60, Colors.BLUE)}\n")
793
+
794
+ # If cd_into is specified, wrap command to cd first
795
+ if cd_into:
796
+ if cmd == "bash":
797
+ cmd = f"bash -c 'cd /proc/1/root{cd_into} && exec bash'"
798
+ elif cmd == "sh":
799
+ cmd = f"sh -c 'cd /proc/1/root{cd_into} && exec sh'"
800
+ else:
801
+ # For custom commands, prepend cd
802
+ cmd = f"bash -c 'cd /proc/1/root{cd_into} && {cmd}'"
803
+
804
+ # Build kubectl command - handle complex commands with shell
805
+ kubectl_cmd = ["kubectl"]
806
+ if KUBECTL_KUBECONFIG:
807
+ kubectl_cmd.extend(["--kubeconfig", KUBECTL_KUBECONFIG])
808
+ if KUBECTL_CONTEXT:
809
+ kubectl_cmd.extend(["--context", KUBECTL_CONTEXT])
810
+ kubectl_cmd.extend(
811
+ [
812
+ "exec",
813
+ "-it",
814
+ pod_name,
815
+ "-n",
816
+ namespace,
817
+ "-c",
818
+ container_name,
819
+ "--",
820
+ ]
821
+ )
822
+
823
+ # Split the command if it's a simple command, otherwise use sh -c
824
+ if cmd.startswith("bash -c") or cmd.startswith("sh -c"):
825
+ # For complex commands, we need to use shell
826
+ kubectl_cmd.extend(["sh", "-c", cmd])
827
+ else:
828
+ # For simple commands, just append
829
+ kubectl_cmd.append(cmd)
830
+
831
+ print_debug_command(" ".join(kubectl_cmd))
832
+
833
+ try:
834
+ # Use subprocess.run without capture_output for interactive TTY
835
+ result = subprocess.run(kubectl_cmd)
836
+ return result.returncode
837
+ except KeyboardInterrupt:
838
+ print("\n\nInterrupted by user")
839
+ return 130
840
+ except Exception as e:
841
+ print(f"Error executing interactive command: {e}", file=sys.stderr)
842
+ return 1
843
+
844
+
845
+ def create_backup(
846
+ pod_name: str,
847
+ namespace: str,
848
+ container_name: str,
849
+ backup_path: str,
850
+ compress: bool = False,
851
+ ) -> bool:
852
+ """Create a backup of the specified path and copy it locally."""
853
+ print(f"\n{colorize('=' * 60, Colors.BLUE)}")
854
+ print(
855
+ f"{colorize('Creating backup', Colors.BOLD)} from pod {colorize(pod_name, Colors.CYAN)}"
856
+ )
857
+ print(f"Path: {colorize(backup_path, Colors.MAGENTA)}")
858
+ if compress:
859
+ print(f"Mode: {colorize('Compressed (tar.gz)', Colors.YELLOW)}")
860
+ else:
861
+ print(f"Mode: {colorize('Direct copy (uncompressed)', Colors.YELLOW)}")
862
+ print(f"{colorize('=' * 60, Colors.BLUE)}\n")
863
+
864
+ # Verify the backup path exists in the container using ls
865
+ print(f"{colorize('Verifying backup path exists...', Colors.YELLOW)}")
866
+ verify_cmd = (
867
+ f"{kubectl_base_cmd()} exec {pod_name} "
868
+ f"-n {namespace} "
869
+ f"-c {container_name} "
870
+ f"-- ls -d {backup_path} 2>/dev/null"
871
+ )
872
+
873
+ result = run_command(verify_cmd, check=False)
874
+ if not result or result.strip() == "":
875
+ print(
876
+ f"{colorize('✗ Error:', Colors.RED)} Path {colorize(backup_path, Colors.MAGENTA)} does not exist in container",
877
+ file=sys.stderr,
878
+ )
879
+
880
+ # Try to provide helpful context by checking parent directory
881
+ parent_dir = os.path.dirname(backup_path)
882
+ if parent_dir and parent_dir != "/":
883
+ print(
884
+ f"{colorize('Checking parent directory:', Colors.YELLOW)} {parent_dir}"
885
+ )
886
+ parent_cmd = (
887
+ f"{kubectl_base_cmd()} exec {pod_name} "
888
+ f"-n {namespace} "
889
+ f"-c {container_name} "
890
+ f"-- ls -la {parent_dir} 2>/dev/null | head -20"
891
+ )
892
+ parent_result = run_command(parent_cmd, check=False)
893
+ if parent_result:
894
+ print(f"{colorize('Contents:', Colors.BRIGHT_BLACK)}\n{parent_result}")
895
+
896
+ return False
897
+
898
+ print(f"{colorize('✓', Colors.GREEN)} Path exists: {result.strip()}")
899
+
900
+ # Create backup directory if it doesn't exist
901
+ backup_dir = f"./backups/{namespace}"
902
+ os.makedirs(backup_dir, exist_ok=True)
903
+
904
+ date_string = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
905
+
906
+ if compress:
907
+ # Compressed backup using tar.gz
908
+ print(f"{colorize('Creating tar.gz archive...', Colors.YELLOW)}")
909
+ backup_cmd = f"tar czf /tmp/kdebug-backup.tar.gz {backup_path}"
910
+
911
+ cmd = (
912
+ f"{kubectl_base_cmd()} exec {pod_name} "
913
+ f"-n {namespace} "
914
+ f"-c {container_name} "
915
+ f"-- /bin/bash -c '{backup_cmd}'"
916
+ )
917
+
918
+ result = run_command(cmd, check=False)
919
+
920
+ if result is None:
921
+ print(f"{colorize('✗', Colors.RED)} Backup command failed", file=sys.stderr)
922
+ return False
923
+
924
+ print(f"{colorize('✓', Colors.GREEN)} Backup archive created")
925
+
926
+ # Copy backup to local machine
927
+ print(f"{colorize('Copying backup to local machine...', Colors.YELLOW)}")
928
+
929
+ local_filename = f"{backup_dir}/{date_string}_{pod_name}.tar.gz"
930
+
931
+ cmd = (
932
+ f"{kubectl_base_cmd()} cp "
933
+ f"-n {namespace} "
934
+ f"-c {container_name} "
935
+ f"{pod_name}:/tmp/kdebug-backup.tar.gz "
936
+ f"{local_filename}"
937
+ )
938
+
939
+ result = run_command(cmd, check=False)
940
+
941
+ if result is None:
942
+ print(f"{colorize('✗', Colors.RED)} Failed to copy backup", file=sys.stderr)
943
+ return False
944
+
945
+ print(
946
+ f"{colorize('✓', Colors.GREEN)} Backup saved to: {colorize(local_filename, Colors.GREEN)}"
947
+ )
948
+
949
+ # Cleanup remote backup file
950
+ cleanup_cmd = f"{kubectl_base_cmd()} exec {pod_name} -n {namespace} -c {container_name} -- rm -f /tmp/kdebug-backup.tar.gz"
951
+ run_command(cleanup_cmd, check=False)
952
+
953
+ else:
954
+ # Direct copy without compression
955
+ print(f"{colorize('Copying files directly (uncompressed)...', Colors.YELLOW)}")
956
+
957
+ # Determine if backup_path is a file or directory for naming
958
+ local_filename = f"{backup_dir}/{date_string}_{pod_name}"
959
+
960
+ cmd = (
961
+ f"{kubectl_base_cmd()} cp "
962
+ f"-n {namespace} "
963
+ f"-c {container_name} "
964
+ f"{pod_name}:{backup_path} "
965
+ f"{local_filename}"
966
+ )
967
+
968
+ result = run_command(cmd, check=False)
969
+
970
+ if result is None:
971
+ print(f"{colorize('✗', Colors.RED)} Failed to copy backup", file=sys.stderr)
972
+ return False
973
+
974
+ print(
975
+ f"{colorize('✓', Colors.GREEN)} Backup saved to: {colorize(local_filename, Colors.GREEN)}"
976
+ )
977
+
978
+ return True
979
+
980
+
981
+ def cleanup_debug_container(
982
+ pod_name: str, namespace: str, debug_container: str
983
+ ) -> bool:
984
+ """Attempt to clean up the debug container."""
985
+ print(f"\n{colorize('Cleaning up debug container...', Colors.YELLOW)}")
986
+
987
+ # Kill the sleep process in the debug container
988
+ cmd = (
989
+ f"{kubectl_base_cmd()} exec {pod_name} "
990
+ f"-n {namespace} "
991
+ f"-c {debug_container} "
992
+ f"-- /bin/bash -c 'kill -9 1' 2>/dev/null || true"
993
+ )
994
+
995
+ run_command(cmd, check=False)
996
+
997
+ print(f"{colorize('✓', Colors.GREEN)} Debug container cleanup initiated")
998
+ return True
999
+
1000
+
1001
+ def generate_bash_completion() -> str:
1002
+ """Generate bash completion script."""
1003
+ return """# kdebug bash completion
1004
+ # Source this file or add to ~/.bashrc:
1005
+ # source <(kdebug --completions bash)
1006
+ # Or:
1007
+ # source /path/to/completions/kdebug.bash
1008
+
1009
+ _kdebug_get_contexts() {
1010
+ kubectl config get-contexts -o name 2>/dev/null
1011
+ }
1012
+
1013
+ _kdebug_get_namespaces() {
1014
+ local kubectl_args=$(_kdebug_get_kubectl_args)
1015
+ kubectl $kubectl_args get namespaces -o jsonpath='{.items[*].metadata.name}' 2>/dev/null
1016
+ }
1017
+
1018
+ _kdebug_get_pods() {
1019
+ local ns="${1:-default}"
1020
+ local kubectl_args=$(_kdebug_get_kubectl_args)
1021
+ kubectl $kubectl_args get pods -n "$ns" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null
1022
+ }
1023
+
1024
+ _kdebug_get_controllers() {
1025
+ local ns="${1:-default}"
1026
+ local controller_type="$2"
1027
+ local kubectl_args=$(_kdebug_get_kubectl_args)
1028
+ case "$controller_type" in
1029
+ deployment|deploy)
1030
+ kubectl $kubectl_args get deployments -n "$ns" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null
1031
+ ;;
1032
+ statefulset|sts)
1033
+ kubectl $kubectl_args get statefulsets -n "$ns" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null
1034
+ ;;
1035
+ daemonset|ds)
1036
+ kubectl $kubectl_args get daemonsets -n "$ns" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null
1037
+ ;;
1038
+ esac
1039
+ }
1040
+
1041
+ _kdebug_get_kubectl_args() {
1042
+ local i args=""
1043
+ for ((i=1; i < ${#COMP_WORDS[@]}; i++)); do
1044
+ case "${COMP_WORDS[i]}" in
1045
+ --context)
1046
+ if [[ $((i+1)) -lt ${#COMP_WORDS[@]} ]]; then
1047
+ args="$args --context=${COMP_WORDS[$((i+1))]}"
1048
+ fi
1049
+ ;;
1050
+ --context=*)
1051
+ args="$args ${COMP_WORDS[i]}"
1052
+ ;;
1053
+ --kubeconfig)
1054
+ if [[ $((i+1)) -lt ${#COMP_WORDS[@]} ]]; then
1055
+ args="$args --kubeconfig=${COMP_WORDS[$((i+1))]}"
1056
+ fi
1057
+ ;;
1058
+ --kubeconfig=*)
1059
+ args="$args ${COMP_WORDS[i]}"
1060
+ ;;
1061
+ esac
1062
+ done
1063
+ echo "$args"
1064
+ }
1065
+
1066
+ _kdebug_get_namespace_from_args() {
1067
+ local i
1068
+ for ((i=1; i < ${#COMP_WORDS[@]}; i++)); do
1069
+ case "${COMP_WORDS[i]}" in
1070
+ -n|--namespace)
1071
+ if [[ $((i+1)) -lt ${#COMP_WORDS[@]} ]]; then
1072
+ echo "${COMP_WORDS[$((i+1))]}"
1073
+ return
1074
+ fi
1075
+ ;;
1076
+ -n=*|--namespace=*)
1077
+ echo "${COMP_WORDS[i]#*=}"
1078
+ return
1079
+ ;;
1080
+ esac
1081
+ done
1082
+ local kubectl_args=$(_kdebug_get_kubectl_args)
1083
+ kubectl $kubectl_args config view --minify -o jsonpath='{..namespace}' 2>/dev/null || echo "default"
1084
+ }
1085
+
1086
+ _kdebug_get_controller_from_args() {
1087
+ local i
1088
+ for ((i=1; i < ${#COMP_WORDS[@]}; i++)); do
1089
+ case "${COMP_WORDS[i]}" in
1090
+ --controller)
1091
+ if [[ $((i+1)) -lt ${#COMP_WORDS[@]} ]]; then
1092
+ echo "${COMP_WORDS[$((i+1))]}"
1093
+ return
1094
+ fi
1095
+ ;;
1096
+ --controller=*)
1097
+ echo "${COMP_WORDS[i]#*=}"
1098
+ return
1099
+ ;;
1100
+ esac
1101
+ done
1102
+ }
1103
+
1104
+ _kdebug() {
1105
+ local cur prev words cword
1106
+ _init_completion || return
1107
+
1108
+ local opts="--pod --controller --controller-name -n --namespace --context --kubeconfig
1109
+ --container --debug-image --cmd --cd-into --backup --compress --as-root
1110
+ --debug --completions -V --version --help -h"
1111
+
1112
+ local controller_types="deployment deploy statefulset sts daemonset ds"
1113
+
1114
+ case "$prev" in
1115
+ -n|--namespace)
1116
+ COMPREPLY=($(compgen -W "$(_kdebug_get_namespaces)" -- "$cur"))
1117
+ return
1118
+ ;;
1119
+ --context)
1120
+ COMPREPLY=($(compgen -W "$(_kdebug_get_contexts)" -- "$cur"))
1121
+ return
1122
+ ;;
1123
+ --kubeconfig)
1124
+ _filedir
1125
+ return
1126
+ ;;
1127
+ --pod)
1128
+ local ns=$(_kdebug_get_namespace_from_args)
1129
+ COMPREPLY=($(compgen -W "$(_kdebug_get_pods "$ns")" -- "$cur"))
1130
+ return
1131
+ ;;
1132
+ --controller)
1133
+ COMPREPLY=($(compgen -W "$controller_types" -- "$cur"))
1134
+ return
1135
+ ;;
1136
+ --controller-name)
1137
+ local ns=$(_kdebug_get_namespace_from_args)
1138
+ local ct=$(_kdebug_get_controller_from_args)
1139
+ if [[ -n "$ct" ]]; then
1140
+ COMPREPLY=($(compgen -W "$(_kdebug_get_controllers "$ns" "$ct")" -- "$cur"))
1141
+ fi
1142
+ return
1143
+ ;;
1144
+ --container|--debug-image|--cmd|--cd-into|--backup)
1145
+ # These take arbitrary values, no completion
1146
+ return
1147
+ ;;
1148
+ --completions)
1149
+ COMPREPLY=($(compgen -W "bash zsh" -- "$cur"))
1150
+ return
1151
+ ;;
1152
+ esac
1153
+
1154
+ if [[ "$cur" == -* ]]; then
1155
+ COMPREPLY=($(compgen -W "$opts" -- "$cur"))
1156
+ return
1157
+ fi
1158
+ }
1159
+
1160
+ complete -F _kdebug kdebug
1161
+ """
1162
+
1163
+
1164
+ def generate_zsh_completion() -> str:
1165
+ """Generate zsh completion script."""
1166
+ return """#compdef kdebug
1167
+ # kdebug zsh completion
1168
+ # Install: kdebug --completions zsh > ~/.zsh/completions/_kdebug
1169
+ # Or: source <(kdebug --completions zsh)
1170
+
1171
+ _kdebug_kubectl_args() {
1172
+ local args=""
1173
+ [[ -n "${opt_args[--context]}" ]] && args="$args --context=${opt_args[--context]}"
1174
+ [[ -n "${opt_args[--kubeconfig]}" ]] && args="$args --kubeconfig=${opt_args[--kubeconfig]}"
1175
+ echo "$args"
1176
+ }
1177
+
1178
+ _kdebug_contexts() {
1179
+ local -a contexts
1180
+ contexts=(${(f)"$(kubectl config get-contexts -o name 2>/dev/null)"})
1181
+ _describe 'context' contexts
1182
+ }
1183
+
1184
+ _kdebug_namespaces() {
1185
+ local kubectl_args=$(_kdebug_kubectl_args)
1186
+ local -a namespaces
1187
+ namespaces=(${(f)"$(kubectl $kubectl_args get namespaces -o jsonpath='{range .items[*]}{.metadata.name}{"\\n"}{end}' 2>/dev/null)"})
1188
+ _describe 'namespace' namespaces
1189
+ }
1190
+
1191
+ _kdebug_pods() {
1192
+ local ns="${opt_args[-n]:-${opt_args[--namespace]:-default}}"
1193
+ local kubectl_args=$(_kdebug_kubectl_args)
1194
+ local -a pods
1195
+ pods=(${(f)"$(kubectl $kubectl_args get pods -n "$ns" -o jsonpath='{range .items[*]}{.metadata.name}{"\\n"}{end}' 2>/dev/null)"})
1196
+ _describe 'pod' pods
1197
+ }
1198
+
1199
+ _kdebug_controller_names() {
1200
+ local ns="${opt_args[-n]:-${opt_args[--namespace]:-default}}"
1201
+ local ct="${opt_args[--controller]:-deployment}"
1202
+ local kubectl_args=$(_kdebug_kubectl_args)
1203
+ local resource
1204
+ case "$ct" in
1205
+ deployment|deploy) resource="deployments" ;;
1206
+ statefulset|sts) resource="statefulsets" ;;
1207
+ daemonset|ds) resource="daemonsets" ;;
1208
+ *) resource="deployments" ;;
1209
+ esac
1210
+ local -a names
1211
+ names=(${(f)"$(kubectl $kubectl_args get "$resource" -n "$ns" -o jsonpath='{range .items[*]}{.metadata.name}{"\\n"}{end}' 2>/dev/null)"})
1212
+ _describe 'controller name' names
1213
+ }
1214
+
1215
+ _kdebug() {
1216
+ local -a args
1217
+ args=(
1218
+ '(-V --version)'{-V,--version}'[Show version and exit]'
1219
+ '(-h --help)'{-h,--help}'[Show help message]'
1220
+ '--pod[Pod name for direct selection]:pod:_kdebug_pods'
1221
+ '--controller[Controller type]:type:(deployment deploy statefulset sts daemonset ds)'
1222
+ '--controller-name[Controller name]:name:_kdebug_controller_names'
1223
+ '(-n --namespace)'{-n,--namespace}'[Kubernetes namespace]:namespace:_kdebug_namespaces'
1224
+ '--context[Kubernetes context to use]:context:_kdebug_contexts'
1225
+ '--kubeconfig[Path to kubeconfig file]:path:_files'
1226
+ '--container[Target container for process namespace sharing]:container:'
1227
+ '--debug-image[Debug container image]:image:'
1228
+ '--cmd[Command to run in debug container]:command:'
1229
+ '--cd-into[Change to directory on start]:directory:_files -/'
1230
+ '--backup[Copy path from pod to local backups]:path:'
1231
+ '--compress[Compress backup as tar.gz]'
1232
+ '--as-root[Run debug container as root]'
1233
+ '--debug[Show kubectl commands being executed]'
1234
+ '--completions[Output shell completion script]:shell:(bash zsh)'
1235
+ )
1236
+ _arguments -s $args
1237
+ }
1238
+
1239
+ _kdebug "$@"
1240
+ """
1241
+
1242
+
1243
+ class KdebugHelpFormatter(argparse.RawDescriptionHelpFormatter):
1244
+ """Custom formatter for consistent help alignment."""
1245
+
1246
+ def __init__(self, prog, indent_increment=2, max_help_position=30, width=80):
1247
+ super().__init__(prog, indent_increment, max_help_position, width)
1248
+
1249
+
1250
+ def main():
1251
+ """Main function to orchestrate the debug container utility."""
1252
+ global DEBUG_MODE
1253
+
1254
+ parser = argparse.ArgumentParser(
1255
+ prog="kdebug",
1256
+ description="""Launch ephemeral debug containers in Kubernetes pods.
1257
+
1258
+ Usage:
1259
+ kdebug [options] Interactive TUI mode
1260
+ kdebug --pod POD [options] Direct pod selection
1261
+ kdebug --controller TYPE --controller-name NAME Controller selection""",
1262
+ formatter_class=KdebugHelpFormatter,
1263
+ epilog="""Examples:
1264
+ kdebug # Interactive TUI
1265
+ kdebug -n prod --pod api-0 # Direct pod
1266
+ kdebug --controller sts --controller-name db # StatefulSet pod
1267
+ kdebug --pod web-0 --backup /app/config # Backup files""",
1268
+ )
1269
+
1270
+ # Version flag
1271
+ parser.add_argument(
1272
+ "-V", "--version", action="version", version=f"%(prog)s {__version__}"
1273
+ )
1274
+
1275
+ # Target selection arguments
1276
+ target_group = parser.add_argument_group("Target Selection")
1277
+ target_group.add_argument(
1278
+ "--pod", metavar="NAME", help="Pod name for direct selection"
1279
+ )
1280
+ target_group.add_argument(
1281
+ "--controller",
1282
+ choices=list(CONTROLLER_ALIASES.keys()),
1283
+ metavar="TYPE",
1284
+ help="Controller type: deployment, sts, ds (or full names)",
1285
+ )
1286
+ target_group.add_argument(
1287
+ "--controller-name",
1288
+ metavar="NAME",
1289
+ help="Controller name (required with --controller)",
1290
+ )
1291
+
1292
+ # Options arguments
1293
+ options_group = parser.add_argument_group("Options")
1294
+ options_group.add_argument(
1295
+ "-n",
1296
+ "--namespace",
1297
+ metavar="NS",
1298
+ help="Kubernetes namespace (default: current context)",
1299
+ )
1300
+ options_group.add_argument(
1301
+ "--context", metavar="NAME", help="Kubernetes context to use"
1302
+ )
1303
+ options_group.add_argument(
1304
+ "--kubeconfig", metavar="PATH", help="Path to kubeconfig file"
1305
+ )
1306
+ options_group.add_argument(
1307
+ "--container",
1308
+ metavar="NAME",
1309
+ help="Target container for process namespace sharing",
1310
+ )
1311
+ options_group.add_argument(
1312
+ "--debug-image",
1313
+ metavar="IMAGE",
1314
+ default="ghcr.io/jessegoodier/toolbox:latest",
1315
+ help="Debug image (default: ghcr.io/jessegoodier/toolbox:latest)",
1316
+ )
1317
+
1318
+ # Operations arguments
1319
+ ops_group = parser.add_argument_group("Operations")
1320
+ ops_group.add_argument(
1321
+ "--cmd",
1322
+ metavar="CMD",
1323
+ default="bash",
1324
+ help="Command to run in debug container (default: bash)",
1325
+ )
1326
+ ops_group.add_argument(
1327
+ "--cd-into",
1328
+ metavar="DIR",
1329
+ help="Change to directory on start (via /proc/1/root)",
1330
+ )
1331
+ ops_group.add_argument(
1332
+ "--backup",
1333
+ metavar="PATH",
1334
+ help="Copy path from pod to local ./backups/ directory",
1335
+ )
1336
+ ops_group.add_argument(
1337
+ "--compress",
1338
+ action="store_true",
1339
+ help="Compress backup as tar.gz (requires --backup)",
1340
+ )
1341
+ ops_group.add_argument(
1342
+ "--as-root", action="store_true", help="Run debug container as root (UID 0)"
1343
+ )
1344
+
1345
+ # Utility arguments
1346
+ util_group = parser.add_argument_group("Utility")
1347
+ util_group.add_argument(
1348
+ "--debug", action="store_true", help="Show kubectl commands being executed"
1349
+ )
1350
+ util_group.add_argument(
1351
+ "--completions",
1352
+ choices=["bash", "zsh"],
1353
+ metavar="SHELL",
1354
+ help="Output shell completion script",
1355
+ )
1356
+
1357
+ args = parser.parse_args()
1358
+
1359
+ # Handle --completions early
1360
+ if args.completions:
1361
+ if args.completions == "bash":
1362
+ print(generate_bash_completion())
1363
+ else:
1364
+ print(generate_zsh_completion())
1365
+ sys.exit(0)
1366
+
1367
+ # Set debug mode and kubectl global options
1368
+ DEBUG_MODE = args.debug
1369
+ global KUBECTL_CONTEXT, KUBECTL_KUBECONFIG
1370
+ KUBECTL_CONTEXT = args.context
1371
+ KUBECTL_KUBECONFIG = args.kubeconfig
1372
+
1373
+ # Validate arguments - allow interactive mode if no pod/controller specified
1374
+ if args.controller and not args.controller_name:
1375
+ parser.error("--controller-name is required when using --controller")
1376
+
1377
+ # Select pod
1378
+ pod = select_pod(args)
1379
+ if not pod:
1380
+ sys.exit(1)
1381
+
1382
+ pod_name = pod["name"]
1383
+ namespace = pod["namespace"]
1384
+
1385
+ # Auto-select container if not specified
1386
+ target_container = args.container
1387
+ if not target_container:
1388
+ container_info = get_pod_containers(pod_name, namespace)
1389
+ regular_containers = container_info["containers"]
1390
+
1391
+ if not regular_containers:
1392
+ print("Error: No regular containers found in pod", file=sys.stderr)
1393
+ sys.exit(1)
1394
+
1395
+ target_container = regular_containers[0]
1396
+ print(
1397
+ f"No --container specified, auto-selecting first non-ephemeral container: {colorize(target_container, Colors.CYAN)}"
1398
+ )
1399
+
1400
+ print(f"\n{colorize('=' * 60, Colors.BLUE)}")
1401
+ print(f"{colorize('Target Pod:', Colors.BOLD)} {colorize(pod_name, Colors.CYAN)}")
1402
+ print(
1403
+ f"{colorize('Namespace:', Colors.BOLD)} {colorize(namespace, Colors.MAGENTA)}"
1404
+ )
1405
+ print(
1406
+ f"{colorize('Target Container:', Colors.BOLD)} {colorize(target_container, Colors.CYAN)}"
1407
+ )
1408
+ print(
1409
+ f"{colorize('Debug Image:', Colors.BOLD)} {colorize(args.debug_image, Colors.BRIGHT_BLACK)}"
1410
+ )
1411
+ print(f"{colorize('=' * 60, Colors.BLUE)}\n")
1412
+
1413
+ # Get existing ephemeral containers
1414
+ existing_containers = get_existing_ephemeral_containers(pod_name, namespace)
1415
+
1416
+ # Check if we can reuse an existing debug container
1417
+ debug_container = None
1418
+ if existing_containers:
1419
+ print(
1420
+ f"Found existing ephemeral containers: {colorize(', '.join(existing_containers), Colors.BRIGHT_BLACK)}"
1421
+ )
1422
+ # For simplicity, we'll create a new one. In production, you might want to reuse.
1423
+ print(f"{colorize('Creating new debug container...', Colors.YELLOW)}")
1424
+
1425
+ # Launch debug container
1426
+ debug_container = launch_debug_container(
1427
+ pod_name,
1428
+ namespace,
1429
+ args.debug_image,
1430
+ target_container,
1431
+ existing_containers,
1432
+ args.as_root,
1433
+ )
1434
+
1435
+ if not debug_container:
1436
+ print("Failed to launch debug container", file=sys.stderr)
1437
+ sys.exit(1)
1438
+
1439
+ exit_code = 0
1440
+ try:
1441
+ # Execute operation
1442
+ if args.backup:
1443
+ # Backup mode
1444
+ success = create_backup(
1445
+ pod_name, namespace, debug_container, args.backup, args.compress
1446
+ )
1447
+ exit_code = 0 if success else 1
1448
+ else:
1449
+ # Interactive mode
1450
+ exit_code = exec_interactive(
1451
+ pod_name, namespace, debug_container, args.cmd, cd_into=args.cd_into
1452
+ )
1453
+ except KeyboardInterrupt:
1454
+ print(f"\n{colorize('Interrupted by user', Colors.YELLOW)}")
1455
+ exit_code = 130
1456
+ except Exception as e:
1457
+ print(f"{colorize('✗ Error:', Colors.RED)} {e}", file=sys.stderr)
1458
+ exit_code = 1
1459
+ finally:
1460
+ # Cleanup - runs after backup/interactive session completes
1461
+ cleanup_debug_container(pod_name, namespace, debug_container)
1462
+
1463
+ sys.exit(exit_code)
1464
+
1465
+
1466
+ if __name__ == "__main__":
1467
+ main()
1468
+
1469
+ # Made with Bob