"""Main application entry point.""" import sys import os import subprocess import tempfile import yaml from typing import Optional, Dict import questionary from questionary import Style from rich.console import Console from rich.panel import Panel from rich.syntax import Syntax from k8s_tool.k8s_client import K8sClient from k8s_tool.config import ConfigManager console = Console() # Custom style for questionary prompts custom_style = Style([ ('qmark', 'fg:#673ab7 bold'), ('question', 'bold'), ('answer', 'fg:#f44336 bold'), ('pointer', 'fg:#673ab7 bold'), ('highlighted', 'fg:#673ab7 bold'), ('selected', 'fg:#cc5454'), ('separator', 'fg:#cc5454'), ('instruction', ''), ('text', ''), ]) class K8sTool: """Main application class.""" def __init__(self): """Initialize the application.""" self.k8s_client = K8sClient() self.config = ConfigManager() self.current_namespace: Optional[str] = None def run(self): """Run the main application loop.""" console.print(Panel.fit( "[bold cyan]K8s Tool[/bold cyan]\n" "[dim]Interactive kubectl helper[/dim]", border_style="cyan" )) while True: try: self._main_menu() except KeyboardInterrupt: console.print("\n[yellow]Goodbye! 👋[/yellow]") sys.exit(0) except Exception as e: console.print(f"[red]Error:[/red] {e}") if questionary.confirm("Continue?", style=custom_style).ask(): continue else: break def _main_menu(self): """Display main menu.""" namespace_info = f"[cyan]{self.current_namespace}[/cyan]" if self.current_namespace else "[dim]not selected[/dim]" is_favorite = self.config.is_favorite(self.current_namespace) if self.current_namespace else False fav_indicator = " ⭐" if is_favorite else "" console.print(f"\n[bold]Current namespace:[/bold] {namespace_info}{fav_indicator}") choices = [ "Select Namespace", "Manage Favorites", "List Deployments", "Restart Deployment", "Scale Deployment", "View ConfigMaps", "Edit ConfigMap", "View Pod Logs", "Exit" ] action = questionary.select( "What would you like to do?", choices=choices, style=custom_style ).ask() if action == "Select Namespace": self._select_namespace() elif action == "Manage Favorites": self._manage_favorites() elif action == "List Deployments": self._list_deployments() elif action == "Restart Deployment": self._restart_deployment() elif action == "Scale Deployment": self._scale_deployment() elif action == "View ConfigMaps": self._view_configmaps() elif action == "Edit ConfigMap": self._edit_configmaps() elif action == "View Pod Logs": self._view_pod_logs() elif action == "Exit": console.print("[yellow]Goodbye! 👋[/yellow]") sys.exit(0) def _select_namespace(self): """Select namespace.""" console.print("[dim]Fetching namespaces...[/dim]") namespaces = self.k8s_client.get_namespaces() if not namespaces: console.print("[red]No namespaces found[/red]") return # Get favorites favorites = self.config.get_favorites() # Separate favorites and non-favorites fav_namespaces = [ns for ns in namespaces if ns in favorites] other_namespaces = [ns for ns in namespaces if ns not in favorites] # Create choices with stars for favorites choices = [] if fav_namespaces: choices.extend([f"⭐ {ns}" for ns in fav_namespaces]) if other_namespaces: choices.append(questionary.Separator("─" * 40)) choices.extend(other_namespaces) selected = questionary.select( "Select namespace:", choices=choices, style=custom_style ).ask() if selected: # Remove star prefix if present namespace = selected.replace("⭐ ", "") self.current_namespace = namespace is_fav = self.config.is_favorite(namespace) fav_text = " (favorite)" if is_fav else "" console.print(f"[green]✓[/green] Namespace set to: [cyan]{namespace}[/cyan]{fav_text}") def _manage_favorites(self): """Manage favorite namespaces.""" choices = [] if self.current_namespace: if self.config.is_favorite(self.current_namespace): choices.append(f"Remove '{self.current_namespace}' from favorites") else: choices.append(f"Add '{self.current_namespace}' to favorites") choices.extend([ "View all favorites", "Add namespace to favorites", "Remove namespace from favorites", "Clear all favorites", "Back to main menu" ]) action = questionary.select( "Favorites management:", choices=choices, style=custom_style ).ask() if not action or action == "Back to main menu": return if action.startswith("Add '") and action.endswith("' to favorites"): # Add current namespace self.config.add_favorite(self.current_namespace) elif action.startswith("Remove '") and action.endswith("' from favorites"): # Remove current namespace self.config.remove_favorite(self.current_namespace) elif action == "View all favorites": self._view_favorites() elif action == "Add namespace to favorites": self._add_namespace_to_favorites() elif action == "Remove namespace from favorites": self._remove_namespace_from_favorites() elif action == "Clear all favorites": if questionary.confirm("Are you sure you want to clear all favorites?", style=custom_style, default=False).ask(): self.config.clear_favorites() def _view_favorites(self): """View all favorite namespaces.""" favorites = self.config.get_favorites() if not favorites: console.print("[yellow]No favorites yet[/yellow]") return console.print("\n[bold]Favorite namespaces:[/bold]") for ns in favorites: indicator = " [dim](current)[/dim]" if ns == self.current_namespace else "" console.print(f" ⭐ [cyan]{ns}[/cyan]{indicator}") console.print() def _add_namespace_to_favorites(self): """Add a namespace to favorites.""" console.print("[dim]Fetching namespaces...[/dim]") namespaces = self.k8s_client.get_namespaces() if not namespaces: console.print("[red]No namespaces found[/red]") return # Filter out already favorite namespaces favorites = self.config.get_favorites() available = [ns for ns in namespaces if ns not in favorites] if not available: console.print("[yellow]All namespaces are already in favorites[/yellow]") return namespace = questionary.select( "Select namespace to add to favorites:", choices=available, style=custom_style ).ask() if namespace: self.config.add_favorite(namespace) def _remove_namespace_from_favorites(self): """Remove a namespace from favorites.""" favorites = self.config.get_favorites() if not favorites: console.print("[yellow]No favorites to remove[/yellow]") return namespace = questionary.select( "Select namespace to remove from favorites:", choices=favorites, style=custom_style ).ask() if namespace: self.config.remove_favorite(namespace) def _ensure_namespace_selected(self) -> bool: """Ensure namespace is selected.""" if not self.current_namespace: console.print("[yellow]Please select a namespace first[/yellow]") self._select_namespace() return self.current_namespace is not None return True def _list_deployments(self): """List deployments in current namespace.""" if not self._ensure_namespace_selected(): return console.print(f"[dim]Fetching deployments in {self.current_namespace}...[/dim]") deployments = self.k8s_client.get_deployments(self.current_namespace) if not deployments: console.print("[yellow]No deployments found[/yellow]") return self.k8s_client.display_deployments_table(deployments) if questionary.confirm("View pods for a deployment?", style=custom_style, default=False).ask(): dep_names = [dep['name'] for dep in deployments] dep_name = questionary.select( "Select deployment:", choices=dep_names, style=custom_style ).ask() if dep_name: self._view_deployment_pods(dep_name) def _view_deployment_pods(self, deployment_name: str): """View pods for a deployment.""" label_selector = f"app={deployment_name}" console.print(f"[dim]Fetching pods for deployment {deployment_name}...[/dim]") pods = self.k8s_client.get_pods(self.current_namespace, label_selector=label_selector) if not pods: # Try without label selector console.print("[yellow]No pods found with label selector, fetching all pods...[/yellow]") pods = self.k8s_client.get_pods(self.current_namespace) if pods: self.k8s_client.display_pods_table(pods) else: console.print("[yellow]No pods found[/yellow]") def _restart_deployment(self): """Restart a deployment.""" if not self._ensure_namespace_selected(): return console.print(f"[dim]Fetching deployments in {self.current_namespace}...[/dim]") deployments = self.k8s_client.get_deployments(self.current_namespace) if not deployments: console.print("[yellow]No deployments found[/yellow]") return dep_names = [dep['name'] for dep in deployments] dep_name = questionary.select( "Select deployment to restart:", choices=dep_names, style=custom_style ).ask() if not dep_name: return confirm = questionary.confirm( f"Are you sure you want to restart {dep_name}?", style=custom_style, default=False ).ask() if confirm: self.k8s_client.restart_deployment(self.current_namespace, dep_name) def _scale_deployment(self): """Scale a deployment.""" if not self._ensure_namespace_selected(): return console.print(f"[dim]Fetching deployments in {self.current_namespace}...[/dim]") deployments = self.k8s_client.get_deployments(self.current_namespace) if not deployments: console.print("[yellow]No deployments found[/yellow]") return dep_names = [f"{dep['name']} (current: {dep['replicas']})" for dep in deployments] selected = questionary.select( "Select deployment to scale:", choices=dep_names, style=custom_style ).ask() if not selected: return dep_name = selected.split(" (current:")[0] replicas = questionary.text( "Enter number of replicas:", validate=lambda text: text.isdigit() and int(text) >= 0, style=custom_style ).ask() if replicas: self.k8s_client.scale_deployment(self.current_namespace, dep_name, int(replicas)) def _view_configmaps(self): """View ConfigMaps.""" if not self._ensure_namespace_selected(): return console.print(f"[dim]Fetching ConfigMaps in {self.current_namespace}...[/dim]") configmaps = self.k8s_client.get_configmaps(self.current_namespace) if not configmaps: console.print("[yellow]No ConfigMaps found[/yellow]") return self.k8s_client.display_configmaps_table(configmaps) if questionary.confirm("View ConfigMap data?", style=custom_style, default=False).ask(): cm_names = [cm['name'] for cm in configmaps] cm_name = questionary.select( "Select ConfigMap:", choices=cm_names, style=custom_style ).ask() if cm_name: data = self.k8s_client.get_configmap_data(self.current_namespace, cm_name) if data: console.print(f"\n[bold]ConfigMap:[/bold] [cyan]{cm_name}[/cyan]\n") for key, value in data.items(): console.print(f"[bold cyan]--- {key} ---[/bold cyan]") console.print(Syntax(value, "yaml", line_numbers=False, background_color="default")) console.print() def _edit_configmaps(self): """Edit a ConfigMap.""" if not self._ensure_namespace_selected(): return console.print(f"[dim]Fetching ConfigMaps in {self.current_namespace}...[/dim]") configmaps = self.k8s_client.get_configmaps(self.current_namespace) if not configmaps: console.print("[yellow]No ConfigMaps found[/yellow]") return cm_names = [cm['name'] for cm in configmaps] cm_name = questionary.select( "Select ConfigMap to edit:", choices=cm_names, style=custom_style ).ask() if cm_name: data = self.k8s_client.get_configmap_data(self.current_namespace, cm_name) if data: self._edit_configmap(cm_name, data) def _edit_configmap(self, cm_name: str, current_data: Dict[str, str]): """Edit ConfigMap data in text editor.""" # Get editor from environment or use default editor = os.environ.get('EDITOR', os.environ.get('VISUAL', 'vi')) # Create temporary file with ConfigMap data with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as tf: temp_file = tf.name # Write current data as YAML yaml.dump(current_data, tf, default_flow_style=False, allow_unicode=True) try: # Get modification time before editing mtime_before = os.path.getmtime(temp_file) # Open editor subprocess.call([editor, temp_file]) # Check if file was modified mtime_after = os.path.getmtime(temp_file) if mtime_after == mtime_before: console.print("[yellow]No changes made[/yellow]") return # Read edited data with open(temp_file, 'r') as f: try: new_data = yaml.safe_load(f) except yaml.YAMLError as e: console.print(f"[red]Error parsing YAML:[/red] {e}") if questionary.confirm("Retry editing?", style=custom_style, default=False).ask(): self._edit_configmap(cm_name, current_data) return # Validate that new_data is a dict if not isinstance(new_data, dict): console.print("[red]Error:[/red] ConfigMap data must be a dictionary") if questionary.confirm("Retry editing?", style=custom_style, default=False).ask(): self._edit_configmap(cm_name, current_data) return # Convert all values to strings (ConfigMap requirement) new_data = {k: str(v) if not isinstance(v, str) else v for k, v in new_data.items()} # Show diff console.print("\n[bold]Changes:[/bold]") removed_keys = set(current_data.keys()) - set(new_data.keys()) added_keys = set(new_data.keys()) - set(current_data.keys()) modified_keys = {k for k in current_data.keys() & new_data.keys() if current_data[k] != new_data[k]} if removed_keys: console.print(f"[red]Removed keys:[/red] {', '.join(removed_keys)}") if added_keys: console.print(f"[green]Added keys:[/green] {', '.join(added_keys)}") if modified_keys: console.print(f"[yellow]Modified keys:[/yellow] {', '.join(modified_keys)}") if not (removed_keys or added_keys or modified_keys): console.print("[yellow]No changes detected[/yellow]") return # Confirm update if questionary.confirm( f"Apply changes to ConfigMap '{cm_name}'?", style=custom_style, default=False ).ask(): self.k8s_client.update_configmap(self.current_namespace, cm_name, new_data) finally: # Clean up temp file try: os.unlink(temp_file) except Exception: pass def _view_pod_logs(self): """View pod logs.""" if not self._ensure_namespace_selected(): return console.print(f"[dim]Fetching pods in {self.current_namespace}...[/dim]") pods = self.k8s_client.get_pods(self.current_namespace) if not pods: console.print("[yellow]No pods found[/yellow]") return pod_names = [f"{pod['name']} ({pod['status']})" for pod in pods] selected = questionary.select( "Select pod:", choices=pod_names, style=custom_style ).ask() if not selected: return pod_name = selected.split(" (")[0] # Check if pod has multiple containers containers = self.k8s_client.get_pod_containers(self.current_namespace, pod_name) container = None if len(containers) > 1: container = questionary.select( "Select container:", choices=containers, style=custom_style ).ask() elif len(containers) == 1: container = containers[0] tail_lines = questionary.text( "Number of lines to show (default: 100):", default="100", validate=lambda text: text.isdigit() and int(text) > 0, style=custom_style ).ask() if tail_lines: logs = self.k8s_client.get_pod_logs( self.current_namespace, pod_name, container=container, tail_lines=int(tail_lines) ) if logs: title = f"[bold cyan]Logs: {pod_name}" if container: title += f" [{container}]" title += "[/bold cyan]" console.print(f"\n{title}\n") console.print(Syntax(logs, "log", line_numbers=False, background_color="default")) console.print() def main(): """Main entry point.""" try: app = K8sTool() app.run() except KeyboardInterrupt: console.print("\n[yellow]Goodbye! 👋[/yellow]") sys.exit(0) except Exception as e: console.print(f"[red]Fatal error:[/red] {e}") sys.exit(1) if __name__ == "__main__": main()