feat(kubernetes): add database persistence for clusters and port_forwards
All checks were successful
Test / frontend-tests (pull_request) Successful in 1m33s
Test / frontend-typecheck (pull_request) Successful in 1m41s
PR Review Automation / review (pull_request) Successful in 3m57s
Test / rust-fmt-check (pull_request) Successful in 11m13s
Test / rust-clippy (pull_request) Successful in 12m50s
Test / rust-tests (pull_request) Successful in 14m29s

- Add load_clusters and load_port_forwards commands to db.rs
- Update remove_cluster to delete from database
- Update delete_port_forward to delete from database
- Add Cluster and PortForward imports to db.rs
- Add load commands to lib.rs generate_handler
- Fix formatting issues
This commit is contained in:
Shaun Arman 2026-06-06 19:29:42 -05:00
parent b6453b0f75
commit 7b77511bdb
4 changed files with 119 additions and 11 deletions

View File

@ -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)
}

View File

@ -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) {

View File

@ -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,