k8s-tool/k8s_tool/main.py
2025-10-25 21:17:27 +03:00

466 lines
15 KiB
Python

"""Main application entry point."""
import sys
from typing import Optional
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",
"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 == "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 _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()