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
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:
parent
b6453b0f75
commit
7b77511bdb
@ -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(())
|
||||
@ -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<String> = 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(())
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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