kdebug 0.5.0__tar.gz → 0.6.0__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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kdebug
3
- Version: 0.5.0
4
- Summary: Universal Kubernetes Debug Container Utility
3
+ Version: 0.6.0
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.5.0"
8
- description = "Universal Kubernetes Debug Container Utility"
7
+ version = "0.6.0"
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