Skip to content

Commit e66e744

Browse files
✨ Add support for containers
1 parent adfb413 commit e66e744

File tree

7 files changed

+1029
-445
lines changed

7 files changed

+1029
-445
lines changed

debugwand/cli.py

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,14 @@
66
import typer
77
from rich.console import Console
88

9+
from debugwand import container as container_ops
10+
from debugwand import kubernetes as k8s
911
from debugwand.operations import (
10-
copy_to_pod,
1112
detect_reload_mode,
12-
exec_command_in_pod,
1313
find_process_using_port,
14-
find_replacement_pod,
15-
get_and_select_pod_handler,
16-
get_and_select_process_handler,
17-
get_pods_for_service_handler,
1814
is_port_available,
1915
kill_process,
20-
list_all_processes_with_details_handler,
21-
list_python_processes_with_details,
22-
monitor_worker_pid,
2316
prepare_debugpy_script,
24-
select_pid,
25-
select_pod,
26-
wait_for_new_pod,
2717
)
2818
from debugwand.types import PodInfo, ProcessInfo
2919
from debugwand.ui import (
@@ -55,14 +45,12 @@ def pods(
5545
),
5646
):
5747
if with_pids:
58-
pod_list = get_pods_for_service_handler(namespace, service)
48+
pod_list = k8s.get_pods_for_service_handler(namespace, service)
5949

6050
# Collect all pod-process pairs
6151
pod_processes: list[tuple[PodInfo, list[ProcessInfo]]] = []
6252
for pod in pod_list:
63-
processes: list[ProcessInfo] | None = (
64-
list_all_processes_with_details_handler(pod)
65-
)
53+
processes: list[ProcessInfo] | None = k8s.list_python_processes_handler(pod)
6654
if processes:
6755
pod_processes.append((pod, processes))
6856

@@ -73,7 +61,7 @@ def pods(
7361
typer.echo("❌ No running pods with Python processes found.", err=True)
7462
raise typer.Exit(code=1)
7563
else:
76-
pod_list = get_pods_for_service_handler(namespace, service)
64+
pod_list = k8s.get_pods_for_service_handler(namespace, service)
7765

7866
render_pods_table(pod_list)
7967

@@ -87,26 +75,26 @@ def inject(
8775
script: str = typer.Option(..., "--script", "-c", help="The script to execute."),
8876
):
8977
typer.echo(f"Executing script '{script}' in the selected pod...")
90-
pod_list = get_pods_for_service_handler(namespace, service)
91-
pod = select_pod(pod_list)
78+
pod_list = k8s.get_pods_for_service_handler(namespace, service)
79+
pod = k8s.select_pod(pod_list)
9280

93-
processes: list[ProcessInfo] | None = list_all_processes_with_details_handler(pod)
81+
processes: list[ProcessInfo] | None = k8s.list_python_processes_handler(pod)
9482
if not processes:
9583
typer.echo(
9684
"❌ No running Python processes found in the selected pod.", err=True
9785
)
9886
raise typer.Exit(code=1)
99-
pid = select_pid(processes)
87+
pid = k8s.get_and_select_process(pod, None)
10088

10189
# Copy the user's script into the pod
10290
script_basename = os.path.basename(script)
103-
copy_to_pod(pod, script, f"/tmp/{script_basename}")
91+
k8s.copy_to_pod(pod, script, f"/tmp/{script_basename}")
10492

10593
# Copy the attacher script into the pod (used to inject and run the user's script)
10694
attacher_path = Path(__file__).parent / "attacher.py"
107-
copy_to_pod(pod, str(attacher_path), "/tmp/attacher.py")
95+
k8s.copy_to_pod(pod, str(attacher_path), "/tmp/attacher.py")
10896

109-
exec_command_in_pod(
97+
k8s.exec_command(
11098
pod=pod,
11199
command=[
112100
"python3",
@@ -125,17 +113,17 @@ def _inject_debugpy_into_pod(pod: PodInfo, pid: int, script_path: str) -> None:
125113

126114
# Copy the attacher script into the pod
127115
attacher_path = Path(__file__).parent / "attacher.py"
128-
copy_to_pod(pod, str(attacher_path), "/tmp/attacher.py")
116+
k8s.copy_to_pod(pod, str(attacher_path), "/tmp/attacher.py")
129117

130118
# Copy debugpy script into the pod
131-
copy_to_pod(pod, script_path, f"/tmp/{script_basename}")
119+
k8s.copy_to_pod(pod, script_path, f"/tmp/{script_basename}")
132120

133121
print_step(
134122
f"Injecting debugpy into PID [cyan bold]{pid}[/cyan bold] in pod [blue]{pod.name}[/blue]..."
135123
)
136124
# Run injection synchronously to capture errors
137125
try:
138-
exec_command_in_pod(
126+
k8s.exec_command(
139127
pod=pod,
140128
command=[
141129
"python3",
@@ -274,7 +262,7 @@ def _monitor_and_handle_reload_mode(
274262
Returns (final_pid, should_break) where should_break indicates if we should exit the loop.
275263
"""
276264
try:
277-
processes = list_python_processes_with_details(pod)
265+
processes = k8s.list_python_processes(pod)
278266
is_reload, _ = detect_reload_mode(processes) if processes else (False, None)
279267
except Exception:
280268
# Pod might be slow to respond while debugger is attached, or pod died
@@ -290,7 +278,7 @@ def _monitor_and_handle_reload_mode(
290278
# Monitor for worker PID changes while keeping port-forward alive
291279
while port_forward_proc.poll() is None:
292280
try:
293-
new_pid = monitor_worker_pid(pod, pid)
281+
new_pid = k8s.monitor_worker_pid(pod, pid)
294282
except Exception as e:
295283
# Pod might have died or become unresponsive
296284
print_info(
@@ -313,10 +301,12 @@ def _monitor_and_handle_reload_mode(
313301
try:
314302
reinject_script_path = prepare_debugpy_script(port=port, wait=False)
315303
reinject_basename = os.path.basename(reinject_script_path)
316-
copy_to_pod(pod, reinject_script_path, f"/tmp/{reinject_basename}")
304+
k8s.copy_to_pod(
305+
pod, reinject_script_path, f"/tmp/{reinject_basename}"
306+
)
317307

318308
# Run injection in background (non-blocking)
319-
exec_command_in_pod(
309+
k8s.exec_command(
320310
pod=pod,
321311
command=[
322312
"python3",
@@ -363,12 +353,12 @@ def _attempt_reconnect(
363353
"""
364354
print_step("Connection lost, attempting to reconnect...")
365355
try:
366-
new_pod = find_replacement_pod(pod, service, namespace)
356+
new_pod = k8s.find_replacement_pod(pod, service, namespace)
367357
if not new_pod:
368358
print_info("Could not find replacement pod, waiting...")
369-
new_pod = wait_for_new_pod(service, namespace)
359+
new_pod = k8s.wait_for_new_pod(service, namespace)
370360

371-
new_pid = get_and_select_process_handler(pod=new_pod, pid=None)
361+
new_pid = k8s.get_and_select_process_handler(pod=new_pod, pid=None)
372362
print_success(f"Reconnected to new pod: {new_pod.name}")
373363
print_info("App is running - connect your debugger to continue debugging")
374364
return new_pod, new_pid
@@ -381,7 +371,7 @@ def _cleanup_injected_files(pod: PodInfo, script_basename: str) -> None:
381371
"""Clean up temporary files injected into the pod."""
382372
try:
383373
print_step("Cleaning up injected files in the pod...", prefix="🧹")
384-
exec_command_in_pod(
374+
k8s.exec_command(
385375
pod=pod,
386376
command=["rm", "-f", f"/tmp/{script_basename}", "/tmp/attacher.py"],
387377
silent_errors=True,
@@ -391,23 +381,67 @@ def _cleanup_injected_files(pod: PodInfo, script_basename: str) -> None:
391381
pass
392382

393383

394-
@app.command(help="Start remote debugging session in a Python process within a pod.")
384+
@app.command(
385+
help="Start remote debugging session in a Python process within a pod or container."
386+
)
395387
def debug(
396388
namespace: str = typer.Option(
397-
..., "--namespace", "-n", help="The namespace to use."
389+
None,
390+
"--namespace",
391+
"-n",
392+
help="The Kubernetes namespace to use.",
393+
rich_help_panel="Kubernetes Options",
394+
),
395+
service: str = typer.Option(
396+
None,
397+
"--service",
398+
"-s",
399+
help="The Kubernetes service to use.",
400+
rich_help_panel="Kubernetes Options",
401+
),
402+
container: str = typer.Option(
403+
None,
404+
"--container",
405+
"-c",
406+
help="Container name or ID to debug.",
407+
rich_help_panel="Container Options",
398408
),
399-
service: str = typer.Option(..., "--service", "-s", help="The service to use."),
400409
port: int = typer.Option(
401-
5679, "--port", "-p", help="The local port to forward for debugging."
410+
5679,
411+
"--port",
412+
"-p",
413+
help="The port for debugpy to listen on.",
414+
rich_help_panel="Common Options",
402415
),
403416
pid: int = typer.Option(
404417
None,
405418
"--pid",
406419
help="The PID of the Python process to debug. If not provided, you will be prompted to select.",
420+
rich_help_panel="Common Options",
407421
),
408422
):
409-
pod = get_and_select_pod_handler(service=service, namespace=namespace)
410-
pid = get_and_select_process_handler(pod=pod, pid=pid)
423+
if container:
424+
# Container mode - error if k8s options are also provided
425+
if namespace or service:
426+
typer.echo(
427+
"❌ Cannot use --namespace or --service with --container.",
428+
err=True,
429+
)
430+
raise typer.Exit(1)
431+
container_ops.debug(container, port, pid)
432+
return
433+
elif namespace and service:
434+
# Kubernetes mode
435+
pass
436+
else:
437+
typer.echo(
438+
"❌ Either --container or both --namespace and --service are required.",
439+
err=True,
440+
)
441+
raise typer.Exit(1)
442+
443+
pod = k8s.get_and_select_pod_handler(service=service, namespace=namespace)
444+
pid = k8s.get_and_select_process_handler(pod=pod, pid=pid)
411445

412446
# Prepare debugpy script on local filesystem (wait=False means app continues immediately)
413447
temp_script_path = prepare_debugpy_script(port=port, wait=False)

0 commit comments

Comments
 (0)