diff --git a/k8s_tool/k8s_client.py b/k8s_tool/k8s_client.py index 597f4ce..dd263ad 100644 --- a/k8s_tool/k8s_client.py +++ b/k8s_tool/k8s_client.py @@ -339,6 +339,59 @@ class K8sClient: console.print(f"[red]Error scaling deployment:[/red] {e}") return False + def get_deployment_yaml(self, namespace: str, name: str) -> Optional[str]: + """Get deployment as YAML string.""" + try: + from kubernetes import utils + + deployment = self.apps_v1.read_namespaced_deployment(name, namespace) + + # Convert to dict and remove managed fields and status + deployment_dict = deployment.to_dict() + + # Remove fields that shouldn't be edited + if 'metadata' in deployment_dict: + deployment_dict['metadata'].pop('managed_fields', None) + deployment_dict['metadata'].pop('resource_version', None) + deployment_dict['metadata'].pop('uid', None) + deployment_dict['metadata'].pop('self_link', None) + deployment_dict['metadata'].pop('creation_timestamp', None) + deployment_dict['metadata'].pop('generation', None) + + # Remove status + deployment_dict.pop('status', None) + + # Convert to YAML + import yaml + yaml_str = yaml.dump(deployment_dict, default_flow_style=False, sort_keys=False) + return yaml_str + except ApiException as e: + console.print(f"[red]Error reading deployment:[/red] {e}") + return None + + def update_deployment_yaml(self, namespace: str, name: str, yaml_str: str) -> bool: + """Update deployment from YAML string.""" + try: + import yaml + + # Parse YAML + deployment_dict = yaml.safe_load(yaml_str) + + # Update deployment + self.apps_v1.patch_namespaced_deployment( + name=name, + namespace=namespace, + body=deployment_dict + ) + console.print(f"[green]✓[/green] Deployment {name} updated successfully") + return True + except ApiException as e: + console.print(f"[red]Error updating deployment:[/red] {e}") + return False + except yaml.YAMLError as e: + console.print(f"[red]Error parsing YAML:[/red] {e}") + return False + def get_pod_logs(self, namespace: str, pod_name: str, container: Optional[str] = None, tail_lines: int = 100) -> Optional[str]: """Get logs from pod.""" diff --git a/k8s_tool/main.py b/k8s_tool/main.py index 11d1079..10d2ad3 100644 --- a/k8s_tool/main.py +++ b/k8s_tool/main.py @@ -80,6 +80,7 @@ class K8sTool: "List Deployments", "Restart Deployment", "Scale Deployment", + "Edit Deployment", "View ConfigMaps", "Edit ConfigMap", "View Pod Logs", @@ -104,6 +105,8 @@ class K8sTool: self._restart_deployment() elif action == "Scale Deployment": self._scale_deployment() + elif action == "Edit Deployment": + self._edit_deployment() elif action == "View ConfigMaps": self._view_configmaps() elif action == "Edit ConfigMap": @@ -134,14 +137,14 @@ class K8sTool: 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:", + # Use autocomplete for filtering + selected = questionary.autocomplete( + "Select namespace (type to filter):", choices=choices, - style=custom_style + style=custom_style, + match_middle=True ).ask() if selected: @@ -554,6 +557,88 @@ class K8sTool: if replicas: self.k8s_client.scale_deployment(self.current_namespace, dep_name, int(replicas)) + def _edit_deployment(self): + """Edit 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 edit:", + choices=dep_names, + style=custom_style + ).ask() + + if not dep_name: + return + + # Get deployment YAML + deployment_yaml = self.k8s_client.get_deployment_yaml(self.current_namespace, dep_name) + if deployment_yaml: + self._edit_deployment_yaml(dep_name, deployment_yaml) + + def _edit_deployment_yaml(self, dep_name: str, current_yaml: str): + """Edit deployment YAML in text editor.""" + # Get editor from environment or use default + editor = os.environ.get('EDITOR', os.environ.get('VISUAL', 'vi')) + + # Create temporary file with deployment YAML + with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as tf: + temp_file = tf.name + tf.write(current_yaml) + + 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_yaml = f.read() + # Validate YAML + yaml.safe_load(new_yaml) + 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_deployment_yaml(dep_name, current_yaml) + return + + # Show confirmation + console.print("\n[bold]Deployment YAML has been modified[/bold]") + console.print("[yellow]Warning:[/yellow] Editing deployment directly can be risky.") + + # Confirm update + if questionary.confirm( + f"Apply changes to deployment '{dep_name}'?", + style=custom_style, + default=False + ).ask(): + self.k8s_client.update_deployment_yaml(self.current_namespace, dep_name, new_yaml) + + finally: + # Clean up temp file + try: + os.unlink(temp_file) + except Exception: + pass + def _view_configmaps(self): """View ConfigMaps.""" if not self._ensure_namespace_selected():