feat(kube): Implement complete kubectl port-forward runtime #72
@ -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<Vec<Cluster>, 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<Cluster> = 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<Vec<PortForward>, 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<PortForward> = stmt
|
||||
.query_map([], |row| {
|
||||
let ports_str: String = row.get(5)?;
|
||||
let local_ports_str: String = row.get(6)?;
|
||||
let ports: Vec<u16> = 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<u16> = 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)
|
||||
}
|
||||
|
||||
@ -148,13 +148,20 @@ fn extract_server_url(content: &str) -> Result<String, String> {
|
||||
|
||||
#[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<String> = 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(())
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user