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/__init__.py +6 -0
- kdebug/cli.py +1469 -0
- kdebug-0.2.0.dist-info/METADATA +318 -0
- kdebug-0.2.0.dist-info/RECORD +6 -0
- kdebug-0.2.0.dist-info/WHEEL +4 -0
- kdebug-0.2.0.dist-info/entry_points.txt +2 -0
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
|