kdebug 0.5.0__tar.gz → 0.6.1__tar.gz
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-0.5.0 → kdebug-0.6.1}/PKG-INFO +3 -2
- {kdebug-0.5.0 → kdebug-0.6.1}/pyproject.toml +3 -2
- kdebug-0.6.1/src/kdebug/backup.py +245 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/src/kdebug/cli.py +274 -589
- kdebug-0.6.1/src/kdebug/debug.py +88 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/.gitignore +0 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/README.md +0 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/src/kdebug/__init__.py +0 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/src/kdebug/completions/__init__.py +0 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/src/kdebug/completions/_kdebug +0 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/src/kdebug/completions/kdebug.bash +0 -0
- {kdebug-0.5.0 → kdebug-0.6.1}/src/kdebug/completions/kdebug.fish +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kdebug
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: TUI for Kubernetes Debug Containers
|
|
5
5
|
Project-URL: Homepage, https://github.com/jessegoodier/kdebug
|
|
6
6
|
Project-URL: Repository, https://github.com/jessegoodier/kdebug
|
|
7
7
|
Author: Jesse Goodier
|
|
@@ -14,6 +14,7 @@ Classifier: Operating System :: MacOS
|
|
|
14
14
|
Classifier: Operating System :: POSIX :: Linux
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: rich>=14.0
|
|
17
18
|
Description-Content-Type: text/markdown
|
|
18
19
|
|
|
19
20
|
# kdebug - Universal Kubernetes Debug and File Copy Container Utility
|
|
@@ -4,13 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kdebug"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.6.1"
|
|
8
|
+
description = "TUI for Kubernetes Debug Containers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
12
|
authors = [{ name = "Jesse Goodier" }]
|
|
13
13
|
keywords = ["kubernetes", "debug", "kubectl", "cli"]
|
|
14
|
+
dependencies = ["rich>=14.0"]
|
|
14
15
|
classifiers = [
|
|
15
16
|
"Development Status :: 4 - Beta",
|
|
16
17
|
"Environment :: Console",
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""kdebug.backup - Backup subcommand implementation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import rich.box as box
|
|
9
|
+
from rich.markup import escape
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
import kdebug.cli as _cli
|
|
14
|
+
|
|
15
|
+
BACKUP_LOCAL_PATH_DEFAULT = "./backups/{namespace}/{date}_{pod}"
|
|
16
|
+
_BACKUP_TEMPLATE_VARS = {"namespace", "pod", "date", "container"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_local_path_template(template: str) -> Optional[str]:
|
|
20
|
+
"""Validate that a local-path template only uses known variables.
|
|
21
|
+
|
|
22
|
+
Returns None on success, or an error message string on failure.
|
|
23
|
+
"""
|
|
24
|
+
unknown = set()
|
|
25
|
+
for match in re.finditer(r"\{(\w+)\}", template):
|
|
26
|
+
var = match.group(1)
|
|
27
|
+
if var not in _BACKUP_TEMPLATE_VARS:
|
|
28
|
+
unknown.add(var)
|
|
29
|
+
if unknown:
|
|
30
|
+
available = ", ".join(f"{{{v}}}" for v in sorted(_BACKUP_TEMPLATE_VARS))
|
|
31
|
+
return (
|
|
32
|
+
f"Unknown template variable(s): {', '.join(f'{{{v}}}' for v in sorted(unknown))}. "
|
|
33
|
+
f"Available variables: {available}"
|
|
34
|
+
)
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def expand_local_path(
|
|
39
|
+
template: str, namespace: str, pod_name: str, container_name: str
|
|
40
|
+
) -> str:
|
|
41
|
+
"""Expand a local-path template with actual values."""
|
|
42
|
+
date_string = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
43
|
+
return template.format_map(
|
|
44
|
+
{
|
|
45
|
+
"namespace": namespace,
|
|
46
|
+
"pod": pod_name,
|
|
47
|
+
"date": date_string,
|
|
48
|
+
"container": container_name,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_backup(
|
|
54
|
+
pod_name: str,
|
|
55
|
+
namespace: str,
|
|
56
|
+
container_name: str,
|
|
57
|
+
container_path: str,
|
|
58
|
+
local_path_template: str,
|
|
59
|
+
compress: bool = False,
|
|
60
|
+
tar_excludes: Optional[List[str]] = None,
|
|
61
|
+
) -> bool:
|
|
62
|
+
"""Create a backup of the specified path and copy it locally."""
|
|
63
|
+
# Validate template before doing anything
|
|
64
|
+
template_error = validate_local_path_template(local_path_template)
|
|
65
|
+
if template_error:
|
|
66
|
+
_cli.err_console.print(f"[error]✗ Error:[/] {escape(template_error)}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Expand the local path template
|
|
70
|
+
local_path = expand_local_path(
|
|
71
|
+
local_path_template, namespace, pod_name, container_name
|
|
72
|
+
)
|
|
73
|
+
if compress:
|
|
74
|
+
local_path += ".tar.gz"
|
|
75
|
+
|
|
76
|
+
t = Text()
|
|
77
|
+
t.append("Pod: ", "bold")
|
|
78
|
+
t.append(f"{pod_name}\n", "pod")
|
|
79
|
+
t.append("Container: ", "bold")
|
|
80
|
+
t.append(f"{container_name}\n", "container")
|
|
81
|
+
t.append("Container path: ", "bold")
|
|
82
|
+
t.append(f"{container_path}\n", "namespace")
|
|
83
|
+
t.append("Local path: ", "bold")
|
|
84
|
+
t.append(f"{local_path}\n", "namespace")
|
|
85
|
+
t.append("Mode: ", "bold")
|
|
86
|
+
if compress:
|
|
87
|
+
t.append("Compressed (tar.gz)", "warning")
|
|
88
|
+
else:
|
|
89
|
+
t.append("Direct copy (uncompressed)", "warning")
|
|
90
|
+
_cli.console.print(
|
|
91
|
+
Panel(
|
|
92
|
+
t,
|
|
93
|
+
title="[menu_title]Creating backup[/]",
|
|
94
|
+
border_style="border",
|
|
95
|
+
box=box.HEAVY,
|
|
96
|
+
padding=(0, 2),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Verify the container path exists in the container using ls
|
|
101
|
+
_cli.console.print("[warning]Verifying container path exists...[/]")
|
|
102
|
+
verify_cmd = (
|
|
103
|
+
f"{_cli.kubectl_base_cmd()} exec {pod_name} "
|
|
104
|
+
f"-n {namespace} "
|
|
105
|
+
f"-c {container_name} "
|
|
106
|
+
f"-- ls -d /proc/1/root{container_path} 2>/dev/null"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
result = _cli.run_command(verify_cmd, check=False)
|
|
110
|
+
if not result or result.strip() == "":
|
|
111
|
+
_cli.err_console.print(
|
|
112
|
+
f"[error]✗ Error:[/] Path [namespace]{escape(container_path)}[/] does not exist in container"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Try to provide helpful context by checking parent directory
|
|
116
|
+
parent_dir = os.path.dirname(container_path)
|
|
117
|
+
if parent_dir and parent_dir != "/":
|
|
118
|
+
_cli.console.print(
|
|
119
|
+
f"[warning]Checking parent directory:[/] {escape(parent_dir)}"
|
|
120
|
+
)
|
|
121
|
+
parent_cmd = (
|
|
122
|
+
f"{_cli.kubectl_base_cmd()} exec {pod_name} "
|
|
123
|
+
f"-n {namespace} "
|
|
124
|
+
f"-c {container_name} "
|
|
125
|
+
f"-- ls -la /proc/1/root{parent_dir} 2>/dev/null | head -20"
|
|
126
|
+
)
|
|
127
|
+
parent_result = _cli.run_command(parent_cmd, check=False)
|
|
128
|
+
if parent_result:
|
|
129
|
+
_cli.console.print(f"[dim]Contents:[/]\n{escape(parent_result)}")
|
|
130
|
+
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
_cli.console.print(f"[success]✓[/] Path exists: {escape(result.strip())}")
|
|
134
|
+
|
|
135
|
+
# Create parent directories for local path
|
|
136
|
+
local_dir = os.path.dirname(local_path)
|
|
137
|
+
if local_dir:
|
|
138
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
if compress:
|
|
141
|
+
# Compressed backup using tar.gz.
|
|
142
|
+
# The debug container accesses the target container's filesystem via
|
|
143
|
+
# /proc/1/root, so use -C to make tar treat that as the root.
|
|
144
|
+
container_path_rel = container_path.lstrip("/") or "."
|
|
145
|
+
exclude_flags = " ".join(
|
|
146
|
+
f"--exclude={p.lstrip('/')}" for p in (tar_excludes or [])
|
|
147
|
+
)
|
|
148
|
+
exclude_str = f" {exclude_flags}" if exclude_flags else ""
|
|
149
|
+
|
|
150
|
+
# Show source size (with excludes applied) before running tar
|
|
151
|
+
du_result = _cli.run_command(
|
|
152
|
+
f"{_cli.kubectl_base_cmd()} exec {pod_name} -n {namespace} -c {container_name} "
|
|
153
|
+
f"-- /bin/sh -c 'du -sh{exclude_str} /proc/1/root/{container_path_rel}'",
|
|
154
|
+
check=False,
|
|
155
|
+
)
|
|
156
|
+
if du_result:
|
|
157
|
+
_cli.console.print(
|
|
158
|
+
f"Source size before compression: [info]{escape(du_result.split()[0])}[/]"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
_cli.console.print("[warning]Creating tar.gz archive...[/]")
|
|
162
|
+
backup_cmd = f"tar czf /tmp/kdebug-backup.tar.gz /proc/1/root/{container_path_rel} {exclude_str}"
|
|
163
|
+
|
|
164
|
+
cmd = (
|
|
165
|
+
f"{_cli.kubectl_base_cmd()} exec {pod_name} "
|
|
166
|
+
f"-n {namespace} "
|
|
167
|
+
f"-c {container_name} "
|
|
168
|
+
f"-- /bin/bash -c '{backup_cmd}'"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
result = _cli.run_command(cmd, check=True)
|
|
172
|
+
|
|
173
|
+
if result is None:
|
|
174
|
+
_cli.err_console.print("[error]✗[/] Backup command failed")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
_cli.console.print("[success]✓[/] Backup archive created")
|
|
178
|
+
|
|
179
|
+
# Show archive size
|
|
180
|
+
size_result = _cli.run_command(
|
|
181
|
+
f"{_cli.kubectl_base_cmd()} exec {pod_name} -n {namespace} -c {container_name} "
|
|
182
|
+
f"-- ls -lh /tmp/kdebug-backup.tar.gz 2>/dev/null | awk '{{print $5}}'",
|
|
183
|
+
check=False,
|
|
184
|
+
)
|
|
185
|
+
if size_result:
|
|
186
|
+
_cli.console.print(
|
|
187
|
+
f"Archive size to download: [info]{escape(size_result.strip())}[/]"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Copy backup to local machine
|
|
191
|
+
_cli.console.print("[warning]Copying backup to local machine...[/]")
|
|
192
|
+
|
|
193
|
+
cmd = (
|
|
194
|
+
f"{_cli.kubectl_base_cmd()} cp "
|
|
195
|
+
f"-n {namespace} "
|
|
196
|
+
f"-c {container_name} "
|
|
197
|
+
f"{pod_name}:/tmp/kdebug-backup.tar.gz "
|
|
198
|
+
f"{local_path}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
result = _cli.run_command(cmd, check=True)
|
|
202
|
+
|
|
203
|
+
if result is None:
|
|
204
|
+
_cli.err_console.print("[error]✗[/] Failed to copy backup")
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
_cli.console.print(
|
|
208
|
+
f"[success]✓[/] Backup saved to: [success]{escape(local_path)}[/]"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Cleanup remote backup file
|
|
212
|
+
cleanup_cmd = f"{_cli.kubectl_base_cmd()} exec {pod_name} -n {namespace} -c {container_name} -- rm -f /tmp/kdebug-backup.tar.gz"
|
|
213
|
+
_cli.run_command(cleanup_cmd, check=False)
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
# Direct copy without compression
|
|
217
|
+
du_result = _cli.run_command(
|
|
218
|
+
f"{_cli.kubectl_base_cmd()} exec {pod_name} -n {namespace} -c {container_name} "
|
|
219
|
+
f"-- du -sh /proc/1/root{container_path}",
|
|
220
|
+
check=False,
|
|
221
|
+
)
|
|
222
|
+
if du_result:
|
|
223
|
+
_cli.console.print(f"Source size: [info]{escape(du_result.split()[0])}[/]")
|
|
224
|
+
|
|
225
|
+
_cli.console.print("[warning]Copying files directly (uncompressed)...[/]")
|
|
226
|
+
|
|
227
|
+
cmd = (
|
|
228
|
+
f"{_cli.kubectl_base_cmd()} cp "
|
|
229
|
+
f"-n {namespace} "
|
|
230
|
+
f"-c {container_name} "
|
|
231
|
+
f"{pod_name}:/proc/1/root{container_path} "
|
|
232
|
+
f"{local_path}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
result = _cli.run_command(cmd, check=False)
|
|
236
|
+
|
|
237
|
+
if result is None:
|
|
238
|
+
_cli.err_console.print("[error]✗[/] Failed to copy backup")
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
_cli.console.print(
|
|
242
|
+
f"[success]✓[/] Backup saved to: [success]{escape(local_path)}[/]"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return True
|