kdebug 0.3.3__tar.gz → 0.4.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,3 +1,7 @@
1
+ backups/
2
+ temp/
3
+ .vscode/
4
+
1
5
  # Byte-compiled / optimized / DLL files
2
6
  __pycache__/
3
7
  *.py[codz]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kdebug
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Universal Kubernetes Debug Container Utility
5
5
  Project-URL: Homepage, https://github.com/jessegoodier/kdebug
6
6
  Project-URL: Repository, https://github.com/jessegoodier/kdebug
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kdebug"
7
- version = "0.3.3"
7
+ version = "0.4.0"
8
8
  description = "Universal Kubernetes Debug Container Utility"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,4 @@
1
- from importlib.metadata import version, PackageNotFoundError
1
+ from importlib.metadata import PackageNotFoundError, version
2
2
 
3
3
  try:
4
4
  __version__ = version("kdebug")
@@ -138,6 +138,19 @@ def get_current_namespace() -> str:
138
138
  return output if output else "default"
139
139
 
140
140
 
141
+ def validate_cluster_connection(namespace: str) -> Optional[str]:
142
+ """Validate kubectl can connect to the cluster and namespace exists.
143
+
144
+ Returns None on success, or an error message string on failure.
145
+ """
146
+ cmd = f"{kubectl_base_cmd()} get namespace {namespace} -o name"
147
+ print_debug_command(cmd)
148
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
149
+ if result.returncode != 0:
150
+ return result.stderr.strip()
151
+ return None
152
+
153
+
141
154
  def get_pod_by_name(pod_name: str, namespace: str) -> Optional[Dict]:
142
155
  """Get pod information by name."""
143
156
  print(
@@ -450,6 +463,12 @@ def select_pod(args) -> Optional[Dict]:
450
463
  """Select a pod based on provided arguments."""
451
464
  namespace = args.namespace or get_current_namespace()
452
465
 
466
+ # Validate cluster connection and namespace before proceeding
467
+ error = validate_cluster_connection(namespace)
468
+ if error:
469
+ print(f"{colorize('✗ Error:', Colors.RED)} {error}")
470
+ return None
471
+
453
472
  # Direct pod selection
454
473
  if args.pod:
455
474
  return get_pod_by_name(args.pod, namespace)
@@ -474,12 +493,12 @@ def select_pod(args) -> Optional[Dict]:
474
493
  return pods[0]
475
494
 
476
495
  # Interactive mode - no pod or controller specified
477
- print(f"\n{colorize('Starting interactive pod selection...', Colors.BRIGHT_CYAN)}")
496
+ print(f"\n{colorize('Starting interactive pod selection...', Colors.CYAN)}")
478
497
 
479
498
  # Direct pod selection via TUI
480
499
  pod_name = select_pod_interactive(namespace)
481
500
  if not pod_name:
482
- print(f"\n{colorize('Selection cancelled', Colors.YELLOW)}")
501
+ print(f"\n{colorize('Selection cancelled', Colors.CYAN)}")
483
502
  return None
484
503
 
485
504
  return {"name": pod_name, "namespace": namespace}
@@ -682,6 +701,42 @@ def check_pod_security_context(pod_name: str, namespace: str) -> Dict:
682
701
  return {"can_run_as_root": True, "reason": "Unable to parse pod spec"}
683
702
 
684
703
 
704
+ def get_container_run_as_user(
705
+ pod_name: str, namespace: str, target_container: Optional[str]
706
+ ) -> Optional[int]:
707
+ """Detect the runAsUser UID from the target container or pod security context."""
708
+ cmd = f"{kubectl_base_cmd()} get pod {pod_name} -n {namespace} -o json"
709
+ output = run_command(cmd, check=False)
710
+
711
+ if not output:
712
+ return None
713
+
714
+ try:
715
+ pod_data = json.loads(output)
716
+ spec = pod_data.get("spec", {})
717
+
718
+ # Check container-level securityContext first (overrides pod-level)
719
+ if target_container:
720
+ for container in spec.get("containers", []):
721
+ if container.get("name") == target_container:
722
+ container_uid = container.get("securityContext", {}).get(
723
+ "runAsUser"
724
+ )
725
+ if container_uid is not None:
726
+ return int(container_uid)
727
+ break
728
+
729
+ # Fall back to pod-level securityContext
730
+ pod_uid = spec.get("securityContext", {}).get("runAsUser")
731
+ if pod_uid is not None:
732
+ return int(pod_uid)
733
+
734
+ except (json.JSONDecodeError, ValueError, TypeError):
735
+ pass
736
+
737
+ return None
738
+
739
+
685
740
  def launch_debug_container(
686
741
  pod_name: str,
687
742
  namespace: str,
@@ -689,6 +744,7 @@ def launch_debug_container(
689
744
  target_container: Optional[str],
690
745
  existing_containers: List[str],
691
746
  as_root: bool = False,
747
+ run_as_user: Optional[int] = None,
692
748
  ) -> Optional[str]:
693
749
  """Launch a debug container attached to the pod and return its name."""
694
750
  print(f"Launching debug container for pod {colorize(pod_name, Colors.CYAN)}...")
@@ -706,12 +762,16 @@ def launch_debug_container(
706
762
  file=sys.stderr,
707
763
  )
708
764
  print(f"{colorize('Tip:', Colors.CYAN)} Try without --as-root flag\n")
709
-
710
- if existing_containers:
765
+ elif run_as_user is not None:
711
766
  print(
712
- f"Existing ephemeral containers: {colorize(', '.join(existing_containers), Colors.BRIGHT_BLACK)}"
767
+ f"Running as UID {colorize(str(run_as_user), Colors.CYAN)} (matching target container)"
713
768
  )
714
769
 
770
+ # if existing_containers:
771
+ # print(
772
+ # f"Existing ephemeral containers: {colorize(', '.join(existing_containers), Colors.BRIGHT_BLACK)}"
773
+ # )
774
+
715
775
  # Build kubectl debug command
716
776
  cmd_parts = [
717
777
  f"nohup {kubectl_base_cmd()} debug -i --tty",
@@ -731,6 +791,10 @@ def launch_debug_container(
731
791
 
732
792
  if as_root:
733
793
  cmd_parts.append('--custom=<(echo \'{"securityContext":{"runAsUser":0}}\')')
794
+ elif run_as_user is not None:
795
+ cmd_parts.append(
796
+ f'--custom=<(echo \'{{"securityContext":{{"runAsUser":{run_as_user}}}}}\')'
797
+ )
734
798
 
735
799
  cmd_parts.extend(
736
800
  [
@@ -777,13 +841,12 @@ def exec_interactive(
777
841
  ) -> int:
778
842
  """Execute an interactive command in the debug container."""
779
843
  print(f"\n{colorize('=' * 60, Colors.BLUE)}")
780
- print(
781
- f"{colorize('Starting interactive session', Colors.BOLD)} in pod {colorize(pod_name, Colors.CYAN)}"
782
- )
844
+ print(f"{colorize('Starting interactive session', Colors.BOLD)} in:")
845
+ print(f"Pod: {colorize(pod_name, Colors.CYAN)}")
783
846
  print(f"Container: {colorize(container_name, Colors.CYAN)}")
784
- print(f"Command: {colorize(cmd, Colors.YELLOW)}")
847
+ print(f"Command: {colorize(cmd, Colors.CYAN)}")
785
848
  if cd_into:
786
- print(f"Directory: {colorize(cd_into, Colors.MAGENTA)}")
849
+ print(f"Directory: {colorize(cd_into, Colors.CYAN)}")
787
850
  print(f"{colorize('=' * 60, Colors.BLUE)}\n")
788
851
 
789
852
  # If cd_into is specified, wrap command to cd first
@@ -1022,7 +1085,7 @@ def parse_controller_arg(value: str) -> Tuple[str, str]:
1022
1085
  controller_type, controller_name = value.split("/", 1)
1023
1086
  if not controller_name:
1024
1087
  raise argparse.ArgumentTypeError(
1025
- f"Missing controller name after '/'. Expected TYPE/NAME (e.g. sts/myapp)."
1088
+ "Missing controller name after '/'. Expected TYPE/NAME (e.g. sts/myapp)."
1026
1089
  )
1027
1090
  if controller_type.lower() not in CONTROLLER_ALIASES:
1028
1091
  valid_types = ", ".join(sorted(CONTROLLER_ALIASES.keys()))
@@ -1179,15 +1242,13 @@ Usage:
1179
1242
  )
1180
1243
 
1181
1244
  print(f"\n{colorize('=' * 60, Colors.BLUE)}")
1245
+ print(f"{colorize('Namespace:', Colors.BOLD)} {colorize(namespace, Colors.CYAN)}")
1182
1246
  print(f"{colorize('Target Pod:', Colors.BOLD)} {colorize(pod_name, Colors.CYAN)}")
1183
- print(
1184
- f"{colorize('Namespace:', Colors.BOLD)} {colorize(namespace, Colors.MAGENTA)}"
1185
- )
1186
1247
  print(
1187
1248
  f"{colorize('Target Container:', Colors.BOLD)} {colorize(target_container, Colors.CYAN)}"
1188
1249
  )
1189
1250
  print(
1190
- f"{colorize('Debug Image:', Colors.BOLD)} {colorize(args.debug_image, Colors.BRIGHT_BLACK)}"
1251
+ f"{colorize('Debug Image:', Colors.BOLD)} {colorize(args.debug_image, Colors.CYAN)}"
1191
1252
  )
1192
1253
  print(f"{colorize('=' * 60, Colors.BLUE)}\n")
1193
1254
 
@@ -1201,7 +1262,12 @@ Usage:
1201
1262
  f"Found existing ephemeral containers: {colorize(', '.join(existing_containers), Colors.BRIGHT_BLACK)}"
1202
1263
  )
1203
1264
  # For simplicity, we'll create a new one. In production, you might want to reuse.
1204
- print(f"{colorize('Creating new debug container...', Colors.YELLOW)}")
1265
+ print(f"{colorize('Creating new debug container...', Colors.MAGENTA)}")
1266
+
1267
+ # Detect target container UID for the debug container
1268
+ run_as_user = None
1269
+ if not args.as_root:
1270
+ run_as_user = get_container_run_as_user(pod_name, namespace, target_container)
1205
1271
 
1206
1272
  # Launch debug container
1207
1273
  debug_container = launch_debug_container(
@@ -1211,6 +1277,7 @@ Usage:
1211
1277
  target_container,
1212
1278
  existing_containers,
1213
1279
  args.as_root,
1280
+ run_as_user,
1214
1281
  )
1215
1282
 
1216
1283
  if not debug_container:
File without changes