tftsr-devops_investigation/src-tauri/src/commands/shell.rs

235 lines
7.2 KiB
Rust
Raw Normal View History

// Shell Command Execution Tauri Commands
//
// This module provides Tauri commands for the frontend to:
// - Manage kubeconfig files (upload, list, activate, delete)
// - Respond to shell command approval requests
// - List command execution history
// - Check kubectl installation status
use crate::shell::KubeconfigInfo;
use crate::state::{AppState, ApprovalResponse};
use rusqlite::params;
use serde::{Deserialize, Serialize};
use tauri::State;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandExecution {
pub id: String,
pub command: String,
pub tier: i32,
pub approval_status: String,
pub exit_code: Option<i32>,
pub stdout: Option<String>,
pub stderr: Option<String>,
pub execution_time_ms: Option<i64>,
pub executed_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KubectlStatus {
pub installed: bool,
pub path: Option<String>,
pub version: Option<String>,
}
#[tauri::command]
pub async fn upload_kubeconfig(
name: String,
content: String,
state: State<'_, AppState>,
) -> Result<String, String> {
// Generate ID
let id = uuid::Uuid::now_v7().to_string();
// Parse kubeconfig to extract context
let contexts = crate::shell::kubeconfig::parse_kubeconfig_contexts(&content)?;
let context = contexts
.first()
.ok_or_else(|| "No contexts found in kubeconfig".to_string())?;
// Encrypt content
let encrypted_content = crate::integrations::auth::encrypt_token(&content)?;
// Store in database
{
let db = state.db.lock().map_err(|e| e.to_string())?;
db.execute(
"INSERT INTO kubeconfig_files (id, name, encrypted_content, context, cluster_url, is_active)
VALUES (?1, ?2, ?3, ?4, ?5, 0)",
params![&id, &name, &encrypted_content, &context.name, &context.cluster_url],
).map_err(|e| format!("Failed to store kubeconfig: {e}"))?;
}
Ok(id)
}
#[tauri::command]
pub fn list_kubeconfigs(state: State<'_, AppState>) -> Result<Vec<KubeconfigInfo>, String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
let mut stmt = db
.prepare("SELECT id, name, context, cluster_url, is_active FROM kubeconfig_files ORDER BY uploaded_at DESC")
.map_err(|e| format!("Failed to prepare statement: {e}"))?;
let configs = stmt
.query_map([], |row| {
Ok(KubeconfigInfo {
id: row.get(0)?,
name: row.get(1)?,
context: row.get(2)?,
cluster_url: row.get(3)?,
is_active: row.get::<_, i32>(4)? != 0,
})
})
.map_err(|e| format!("Failed to query kubeconfigs: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Failed to collect results: {e}"))?;
Ok(configs)
}
#[tauri::command]
pub fn activate_kubeconfig(id: String, state: State<'_, AppState>) -> Result<(), String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
// Deactivate all configs
db.execute("UPDATE kubeconfig_files SET is_active = 0", [])
.map_err(|e| format!("Failed to deactivate configs: {e}"))?;
// Activate the specified config
db.execute(
"UPDATE kubeconfig_files SET is_active = 1 WHERE id = ?1",
params![&id],
)
.map_err(|e| format!("Failed to activate config: {e}"))?;
Ok(())
}
#[tauri::command]
pub fn delete_kubeconfig(id: String, state: State<'_, AppState>) -> Result<(), String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
db.execute("DELETE FROM kubeconfig_files WHERE id = ?1", params![&id])
.map_err(|e| format!("Failed to delete kubeconfig: {e}"))?;
Ok(())
}
#[tauri::command]
pub async fn respond_to_shell_approval(
approval_id: String,
decision: String, // "deny", "allow_once", "allow_session"
state: State<'_, AppState>,
) -> Result<(), String> {
// Retrieve the pending approval channel
let sender = {
let mut approvals = state.pending_approvals.lock().await;
approvals.remove(&approval_id)
};
if let Some(sender) = sender {
let approved = decision != "deny";
let response = ApprovalResponse { approved, decision };
// Send response
sender
.send(response)
.map_err(|_| "Failed to send approval response".to_string())?;
Ok(())
} else {
Err("Approval request not found or already responded to".to_string())
}
}
#[tauri::command]
pub fn list_command_executions(
issue_id: Option<String>,
state: State<'_, AppState>,
) -> Result<Vec<CommandExecution>, String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
let (query, params_vec): (String, Vec<String>) = if let Some(issue_id) = issue_id {
(
"SELECT id, command, tier, approval_status, exit_code, stdout, stderr, execution_time_ms, executed_at
FROM command_executions
WHERE issue_id = ?1
ORDER BY executed_at DESC
LIMIT 100".to_string(),
vec![issue_id],
)
} else {
(
"SELECT id, command, tier, approval_status, exit_code, stdout, stderr, execution_time_ms, executed_at
FROM command_executions
ORDER BY executed_at DESC
LIMIT 100".to_string(),
vec![],
)
};
let mut stmt = db
.prepare(&query)
.map_err(|e| format!("Failed to prepare statement: {e}"))?;
let params_refs: Vec<&dyn rusqlite::ToSql> = params_vec
.iter()
.map(|s| s as &dyn rusqlite::ToSql)
.collect();
let executions = stmt
.query_map(params_refs.as_slice(), |row| {
Ok(CommandExecution {
id: row.get(0)?,
command: row.get(1)?,
tier: row.get(2)?,
approval_status: row.get(3)?,
exit_code: row.get(4)?,
stdout: row.get(5)?,
stderr: row.get(6)?,
execution_time_ms: row.get(7)?,
executed_at: row.get(8)?,
})
})
.map_err(|e| format!("Failed to query executions: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Failed to collect results: {e}"))?;
Ok(executions)
}
#[tauri::command]
pub async fn check_kubectl_installed(_state: State<'_, AppState>) -> Result<KubectlStatus, String> {
match crate::shell::kubectl::locate_kubectl() {
Ok(path) => {
// Try to get version
let version = tokio::process::Command::new(&path)
.arg("version")
.arg("--client")
.arg("--output=json")
.output()
.await
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout).ok()
} else {
None
}
});
Ok(KubectlStatus {
installed: true,
path: Some(path.to_string_lossy().to_string()),
version,
})
}
Err(_) => Ok(KubectlStatus {
installed: false,
path: None,
version: None,
}),
}
}