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

494 lines
16 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;
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
/// RAII guard for a temp kubeconfig file. Removes the file when dropped
/// unless `disarm()` has been called — used on the error path of session
/// start so the file isn't leaked if kubectl resolution or session
/// registration fails after we've written it. On the success path we call
/// `disarm()` and the PTY session itself becomes responsible for the file's
/// lifetime (it lives in `std::env::temp_dir()` which is OS-cleaned).
struct KubeconfigGuard {
path: Option<std::path::PathBuf>,
}
impl KubeconfigGuard {
fn new(path: std::path::PathBuf) -> Self {
Self { path: Some(path) }
}
/// Transfer ownership: caller is now responsible for the file.
/// Returns the path string for use with the PTY session.
fn disarm(mut self) -> String {
let path = self.path.take().expect("KubeconfigGuard already disarmed");
path.to_string_lossy().into_owned()
}
}
impl Drop for KubeconfigGuard {
fn drop(&mut self) {
if let Some(path) = self.path.take() {
if let Err(e) = std::fs::remove_file(&path) {
if e.kind() != std::io::ErrorKind::NotFound {
tracing::warn!("Failed to remove temp kubeconfig {}: {}", path.display(), e);
}
}
}
}
}
#[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();
fix(kube): use current-context for kubectl auth; fix SelectValue label display ## kubectl credentials still failing after --context fix Root cause: both extract_context() (kube.rs) and upload_kubeconfig() (shell.rs) ignored the kubeconfig's current-context field and always picked contexts[0] from the contexts array. If a kubeconfig has multiple contexts and current-context points to entry N>0, we silently used the wrong context — one that may have empty or expired credentials — causing the 401 "the server has asked for the client to provide credentials" error on every kubectl call. Fixes: - extract_context(): read current-context field first; fall back to contexts[0] only when current-context is absent or empty. - extract_current_context_name(): new helper in kubeconfig.rs using the same line-scanner approach as parse_kubeconfig_contexts (no extra dependencies). - upload_kubeconfig(): use current-context to select the matching context entry when storing context name in kubeconfig_files; falls back to first entry. NOTE: existing kubeconfig rows in the database have the old (wrong) context stored. Re-uploading kubeconfig files after deploying this build will fix them. ## Cluster dropdown still showing UUID Root cause: SelectValue rendered ctx.value (the raw UUID passed to SelectItem's value prop) instead of the display label (SelectItem's children). The custom Select component had no mechanism to mirror a selected item's children into the trigger area. Fix: Select now builds a value→label Map by walking the children tree at render time (collectLabels). The map is memoised on children. SelectValue reads the display label from the map; if found, shows the label; otherwise falls back to the raw value so existing behaviour is preserved for callers that don't need it.
2026-06-08 00:40:53 +00:00
// Parse kubeconfig to extract context.
// Use current-context to select the right entry — the user's kubeconfig may
// have multiple contexts and current-context is the one kubectl defaults to.
let contexts = crate::shell::kubeconfig::parse_kubeconfig_contexts(&content)?;
fix(kube): use current-context for kubectl auth; fix SelectValue label display ## kubectl credentials still failing after --context fix Root cause: both extract_context() (kube.rs) and upload_kubeconfig() (shell.rs) ignored the kubeconfig's current-context field and always picked contexts[0] from the contexts array. If a kubeconfig has multiple contexts and current-context points to entry N>0, we silently used the wrong context — one that may have empty or expired credentials — causing the 401 "the server has asked for the client to provide credentials" error on every kubectl call. Fixes: - extract_context(): read current-context field first; fall back to contexts[0] only when current-context is absent or empty. - extract_current_context_name(): new helper in kubeconfig.rs using the same line-scanner approach as parse_kubeconfig_contexts (no extra dependencies). - upload_kubeconfig(): use current-context to select the matching context entry when storing context name in kubeconfig_files; falls back to first entry. NOTE: existing kubeconfig rows in the database have the old (wrong) context stored. Re-uploading kubeconfig files after deploying this build will fix them. ## Cluster dropdown still showing UUID Root cause: SelectValue rendered ctx.value (the raw UUID passed to SelectItem's value prop) instead of the display label (SelectItem's children). The custom Select component had no mechanism to mirror a selected item's children into the trigger area. Fix: Select now builds a value→label Map by walking the children tree at render time (collectLabels). The map is memoised on children. SelectValue reads the display label from the map; if found, shows the label; otherwise falls back to the raw value so existing behaviour is preserved for callers that don't need it.
2026-06-08 00:40:53 +00:00
let current_context_name = crate::shell::kubeconfig::extract_current_context_name(&content);
let context = if let Some(ref current) = current_context_name {
contexts
.iter()
.find(|c| &c.name == current)
.or_else(|| contexts.first())
.ok_or_else(|| "No contexts found in kubeconfig".to_string())?
} else {
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,
}),
}
}
fix(classifier): fix 3 safety bugs, extract const arrays, make tier UI dynamic Bug 1 — Dead multi-word tier3 entries / missing single-token commands parse_single_command() extracts only the first token as `command`, so multi-word entries like "kill -9", "init 0", "service stop" in the tier3 array never matched. Adding the single-token forms "kill", "pkill", "killall", "init" to TIER3_COMMANDS ensures these commands are always denied. Removed all dead multi-word entries. Bug 2 — systemctl Tier 1 special case was dead code systemctl was not in tier1_general, so the block that was supposed to auto-execute `systemctl status` never ran. Moved systemctl handling into its own block (TIER1_SYSTEMCTL_SUBCOMMANDS / TIER2_SYSTEMCTL_SUBCOMMANDS) evaluated before the general tier checks. status, is-active, is-enabled, list-units, list-unit-files → Tier 1; all others → Tier 2. Bug 3 — ldapmodify / ldapdelete / ldapadd misclassified as Tier 1 Both appeared in the old tier1_general and tier2_general arrays; the tier1 check ran first, so LDAP write operations auto-executed. Removed them from tier1. ldapsearch (read-only) remains Tier 1. Dynamic Safety Architecture UI Extracted all tier classification arrays to module-level pub const slices (TIER3_COMMANDS, TIER1_KUBECTL_SUBCOMMANDS, etc.) so both the classifier logic and a new get_classifier_rules() Tauri command share a single source of truth. ShellExecution.tsx now calls getClassifierRulesCmd() on mount and renders the actual command lists in collapsible per-tier cards — any change to the const arrays is automatically reflected in the UI with no manual documentation update needed. Also fixes the cargo fmt CI failure introduced in the previous commit (ClusterClient::new call reformatted to a single line).
2026-06-07 23:15:42 +00:00
/// Return the live classifier rule lists so the UI can render them dynamically.
/// The data derives directly from the module-level const arrays in classifier.rs,
/// so any addition or removal there is automatically reflected in the UI.
#[tauri::command]
pub fn get_classifier_rules() -> crate::shell::classifier::ClassifierRules {
crate::shell::classifier::CommandClassifier::get_rules()
}
// ═══════════════════════════════════════════════════════════════════════════
// PTY Session Management Commands
// ═══════════════════════════════════════════════════════════════════════════
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PtySessionInfo {
pub id: String,
pub cluster_id: String,
pub namespace: String,
pub pod: String,
pub container: Option<String>,
pub session_type: String,
pub created_at: String,
}
/// Start an interactive kubectl exec session with PTY support
#[tauri::command]
pub async fn start_pty_exec_session(
app: tauri::AppHandle,
state: State<'_, AppState>,
cluster_id: String,
namespace: String,
pod: String,
container: Option<String>,
) -> Result<String, String> {
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
// Get active kubeconfig — the guard ensures the temp file is removed
// if anything between here and `disarm()` fails.
let kubeconfig_guard: Option<KubeconfigGuard> = {
let db = state.db.lock().map_err(|e| e.to_string())?;
let mut stmt = db
.prepare("SELECT encrypted_content FROM kubeconfig_files WHERE is_active = 1 LIMIT 1")
.map_err(|e| format!("Failed to query active kubeconfig: {e}"))?;
let encrypted: Option<String> = stmt.query_row([], |row| row.get(0)).ok();
if let Some(enc) = encrypted {
let content = crate::integrations::auth::decrypt_token(&enc)
.map_err(|e| format!("Failed to decrypt kubeconfig: {e}"))?;
// Write to temp file
let temp_path =
std::env::temp_dir().join(format!("kubeconfig-{}.yaml", uuid::Uuid::now_v7()));
std::fs::write(&temp_path, content)
.map_err(|e| format!("Failed to write kubeconfig: {e}"))?;
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
Some(KubeconfigGuard::new(temp_path))
} else {
None
}
};
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
// Locate kubectl — if this fails, the guard cleans up the temp kubeconfig.
let kubectl_path =
crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?;
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
// Transfer ownership: PTY session now owns the temp file's lifetime.
let kubeconfig_path = kubeconfig_guard.map(|g| g.disarm());
// Start session
let params = crate::shell::session::SessionParams {
cluster_id,
namespace,
pod,
container,
kubectl_path: kubectl_path.to_string_lossy().to_string(),
kubeconfig_path,
};
let session_id = state
.pty_sessions
.start_exec_session(app, params)
.await
.map_err(|e| format!("Failed to start exec session: {e}"))?;
Ok(session_id)
}
/// Start an interactive kubectl attach session with PTY support
#[tauri::command]
pub async fn start_pty_attach_session(
app: tauri::AppHandle,
state: State<'_, AppState>,
cluster_id: String,
namespace: String,
pod: String,
container: Option<String>,
) -> Result<String, String> {
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
// Get active kubeconfig — the guard ensures the temp file is removed
// if anything between here and `disarm()` fails.
let kubeconfig_guard: Option<KubeconfigGuard> = {
let db = state.db.lock().map_err(|e| e.to_string())?;
let mut stmt = db
.prepare("SELECT encrypted_content FROM kubeconfig_files WHERE is_active = 1 LIMIT 1")
.map_err(|e| format!("Failed to query active kubeconfig: {e}"))?;
let encrypted: Option<String> = stmt.query_row([], |row| row.get(0)).ok();
if let Some(enc) = encrypted {
let content = crate::integrations::auth::decrypt_token(&enc)
.map_err(|e| format!("Failed to decrypt kubeconfig: {e}"))?;
// Write to temp file
let temp_path =
std::env::temp_dir().join(format!("kubeconfig-{}.yaml", uuid::Uuid::now_v7()));
std::fs::write(&temp_path, content)
.map_err(|e| format!("Failed to write kubeconfig: {e}"))?;
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
Some(KubeconfigGuard::new(temp_path))
} else {
None
}
};
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
// Locate kubectl — if this fails, the guard cleans up the temp kubeconfig.
let kubectl_path =
crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?;
fix(security): address automated code review findings BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
2026-06-09 23:08:58 +00:00
// Transfer ownership: PTY session now owns the temp file's lifetime.
let kubeconfig_path = kubeconfig_guard.map(|g| g.disarm());
// Start session
let params = crate::shell::session::SessionParams {
cluster_id,
namespace,
pod,
container,
kubectl_path: kubectl_path.to_string_lossy().to_string(),
kubeconfig_path,
};
let session_id = state
.pty_sessions
.start_attach_session(app, params)
.await
.map_err(|e| format!("Failed to start attach session: {e}"))?;
Ok(session_id)
}
/// Send stdin data to a PTY session
#[tauri::command]
pub async fn send_pty_stdin(
state: State<'_, AppState>,
session_id: String,
data: Vec<u8>,
) -> Result<(), String> {
state
.pty_sessions
.send_stdin(&session_id, data)
.await
.map_err(|e| format!("Failed to send stdin: {e}"))
}
/// Resize a PTY session
#[tauri::command]
pub async fn resize_pty_session(
state: State<'_, AppState>,
session_id: String,
rows: u16,
cols: u16,
) -> Result<(), String> {
state
.pty_sessions
.resize_session(&session_id, rows, cols)
.await
.map_err(|e| format!("Failed to resize session: {e}"))
}
/// Terminate a PTY session
#[tauri::command]
pub async fn terminate_pty_session(
state: State<'_, AppState>,
session_id: String,
) -> Result<(), String> {
state
.pty_sessions
.terminate_session(&session_id)
.await
.map_err(|e| format!("Failed to terminate session: {e}"))
}
/// List all active PTY sessions
#[tauri::command]
pub async fn list_pty_sessions(state: State<'_, AppState>) -> Result<Vec<PtySessionInfo>, String> {
let sessions = state.pty_sessions.list_sessions().await;
Ok(sessions
.into_iter()
.map(|s| PtySessionInfo {
id: s.id,
cluster_id: s.cluster_id,
namespace: s.namespace,
pod: s.pod,
container: s.container,
session_type: match s.session_type {
crate::shell::SessionType::Exec => "exec".to_string(),
crate::shell::SessionType::Attach => "attach".to_string(),
},
created_at: s.created_at.to_rfc3339(),
})
.collect())
}