diff --git a/src-tauri/src/commands/db.rs b/src-tauri/src/commands/db.rs index 9746dc32..d239fbd2 100644 --- a/src-tauri/src/commands/db.rs +++ b/src-tauri/src/commands/db.rs @@ -1,8 +1,8 @@ use tauri::State; use crate::db::models::{ - AiConversation, AiMessage, ImageAttachment, Issue, IssueDetail, IssueFilter, IssueSummary, - IssueUpdate, LogFile, ResolutionStep, TimelineEvent, + AiConversation, AiMessage, Cluster, ImageAttachment, Issue, IssueDetail, IssueFilter, + IssueSummary, IssueUpdate, LogFile, PortForward, ResolutionStep, TimelineEvent, }; use crate::state::AppState; @@ -805,3 +805,93 @@ mod tests { assert_eq!(results[0], "issue-1"); } } + +// ─── Kubernetes Cluster CRUD ──────────────────────────────────────────────── + +use rusqlite::ffi; + +#[tauri::command] +pub async fn load_clusters(state: State<'_, AppState>) -> Result, String> { + let db = state.db.lock().map_err(|e| e.to_string())?; + + let mut stmt = db + .prepare( + "SELECT id, name, context, server_url, kubeconfig_id, created_at, updated_at \ + FROM clusters ORDER BY name ASC", + ) + .map_err(|e| e.to_string())?; + + let clusters: Vec = stmt + .query_map([], |row| { + Ok(Cluster { + id: row.get(0)?, + name: row.get(1)?, + context: row.get(2)?, + server_url: row.get(3)?, + kubeconfig_id: row.get(4)?, + created_at: row.get(5)?, + updated_at: row.get(6)?, + }) + }) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + + Ok(clusters) +} + +// ─── Port Forward CRUD ────────────────────────────────────────────────────── + +#[tauri::command] +pub async fn load_port_forwards(state: State<'_, AppState>) -> Result, String> { + let db = state.db.lock().map_err(|e| e.to_string())?; + + let mut stmt = db + .prepare( + "SELECT id, cluster_id, namespace, pod, container, ports, local_ports, status, error_message, created_at, updated_at \ + FROM port_forwards ORDER BY created_at ASC", + ) + .map_err(|e| e.to_string())?; + + let port_forwards: Vec = stmt + .query_map([], |row| { + let ports_str: String = row.get(5)?; + let local_ports_str: String = row.get(6)?; + let ports: Vec = match serde_json::from_str(&ports_str) { + Ok(v) => v, + Err(e) => { + return Err(rusqlite::Error::SqliteFailure( + ffi::Error::new(ffi::SQLITE_ERROR), + Some(format!("Failed to parse ports: {e}")), + )) + } + }; + let local_ports: Vec = match serde_json::from_str(&local_ports_str) { + Ok(v) => v, + Err(e) => { + return Err(rusqlite::Error::SqliteFailure( + ffi::Error::new(ffi::SQLITE_ERROR), + Some(format!("Failed to parse local_ports: {e}")), + )) + } + }; + Ok(PortForward { + id: row.get(0)?, + cluster_id: row.get(1)?, + namespace: row.get(2)?, + pod: row.get(3)?, + container: row.get(4)?, + ports, + local_ports, + status: row.get(7)?, + error_message: row.get(8)?, + created_at: row.get(9)?, + updated_at: row.get(10)?, + }) + }) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + + Ok(port_forwards) +} diff --git a/src-tauri/src/commands/kube.rs b/src-tauri/src/commands/kube.rs index 780fa569..dcfe7236 100644 --- a/src-tauri/src/commands/kube.rs +++ b/src-tauri/src/commands/kube.rs @@ -148,13 +148,20 @@ fn extract_server_url(content: &str) -> Result { #[tauri::command] pub async fn remove_cluster(id: String, state: State<'_, AppState>) -> Result<(), String> { + // Delete cluster from database (cascade will delete port_forwards) + { + let db = state.db.lock().map_err(|e| e.to_string())?; + db.execute("DELETE FROM clusters WHERE id = ?1", [&id]) + .map_err(|e| format!("Failed to delete cluster: {e}"))?; + } + let mut clusters = state.clusters.lock().await; if clusters.remove(&id).is_none() { return Err(format!("Cluster {id} not found")); } - // Cascade delete: remove all port forwards for this cluster + // Cascade delete: remove all port forwards for this cluster from memory let mut port_forwards = state.port_forwards.lock().await; let session_ids_to_remove: Vec = port_forwards .iter() @@ -163,7 +170,9 @@ pub async fn remove_cluster(id: String, state: State<'_, AppState>) -> Result<() .collect(); for session_id in session_ids_to_remove { - port_forwards.remove(&session_id); + if let Some(mut session) = port_forwards.remove(&session_id) { + session.close().await; + } } Ok(()) @@ -202,7 +211,7 @@ pub async fn test_cluster_connection( // Write kubeconfig to temp file and ensure cleanup even on panic let temp_dir = std::env::temp_dir(); let temp_path = temp_dir.join(format!("kubeconfig-{}.yaml", cluster_id)); - + // Create cleanup struct BEFORE writing - ensures cleanup happens even on panic struct TempFileCleanup(std::path::PathBuf); impl Drop for TempFileCleanup { @@ -258,7 +267,7 @@ pub async fn discover_pods( // Write kubeconfig to temp file and ensure cleanup even on panic let temp_dir = std::env::temp_dir(); let temp_path = temp_dir.join(format!("kubeconfig-{}-pods.yaml", cluster_id)); - + // Create cleanup struct BEFORE writing - ensures cleanup happens even on panic struct TempFileCleanup(std::path::PathBuf); impl Drop for TempFileCleanup { @@ -475,7 +484,7 @@ pub async fn start_port_forward( // Write kubeconfig to temp file and ensure cleanup even on panic let temp_dir = std::env::temp_dir(); let temp_path = temp_dir.join(format!("kubeconfig-{}.yaml", request.cluster_id)); - + // Create cleanup struct BEFORE writing - ensures cleanup happens even on panic struct TempFileCleanup(std::path::PathBuf); impl Drop for TempFileCleanup { @@ -591,6 +600,13 @@ pub async fn list_port_forwards( #[tauri::command] pub async fn delete_port_forward(id: String, state: State<'_, AppState>) -> Result<(), String> { + // Delete from database + { + let db = state.db.lock().map_err(|e| e.to_string())?; + db.execute("DELETE FROM port_forwards WHERE id = ?1", [&id]) + .map_err(|e| format!("Failed to delete port forward: {e}"))?; + } + let mut port_forwards = state.port_forwards.lock().await; if let Some(mut session) = port_forwards.remove(&id) { @@ -698,15 +714,15 @@ mod tests { #[tauri::command] pub async fn shutdown_port_forwards(state: State<'_, AppState>) -> Result<(), String> { let mut port_forwards = state.port_forwards.lock().await; - + // Close all active port forward sessions let session_ids: Vec = port_forwards.keys().cloned().collect(); - + for session_id in session_ids { if let Some(mut session) = port_forwards.remove(&session_id) { session.close().await; } } - + Ok(()) } diff --git a/src-tauri/src/kube/portforward.rs b/src-tauri/src/kube/portforward.rs index 86556c1d..35d3799d 100644 --- a/src-tauri/src/kube/portforward.rs +++ b/src-tauri/src/kube/portforward.rs @@ -85,7 +85,7 @@ impl PortForwardSession { let join_handle = tokio::spawn(async move { // Take the child from the Arc let mut child = child_for_task.lock().await.take().expect("Child not set"); - + // Wait for the child process to complete // This is safe because we're in an async context let result = child.wait().await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c4b6b253..95a4e125 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -95,6 +95,8 @@ pub fn run() { commands::db::update_five_why, commands::db::add_timeline_event, commands::db::get_timeline_events, + commands::db::load_clusters, + commands::db::load_port_forwards, // Analysis / PII commands::analysis::upload_log_file, commands::analysis::upload_log_file_by_content,