fix(shell): delay KubeconfigGuard disarm until after PTY session starts
Some checks failed
Test / frontend-typecheck (pull_request) Successful in 1m40s
Test / frontend-tests (pull_request) Successful in 1m42s
PR Review Automation / review (pull_request) Has been cancelled
Test / rust-fmt-check (pull_request) Has been cancelled
Test / rust-clippy (pull_request) Has been cancelled
Test / rust-tests (pull_request) Has been cancelled

Add `path_str()` to `KubeconfigGuard` so the path can be passed to
`SessionParams` without consuming the guard. Both `start_pty_exec_session`
and `start_pty_attach_session` now hold the guard live until
`start_exec/attach_session` returns `Ok`, then disarm it. Previously
`disarm()` was called before the session-start call, meaning a kubeconfig
temp file would leak if PTY spawn or session registration failed after the
guard was consumed.
This commit is contained in:
Shaun Arman 2026-06-09 20:00:50 -05:00
parent 9ae89bf487
commit e15374bdd3

View File

@ -27,6 +27,15 @@ impl KubeconfigGuard {
Self { path: Some(path) } Self { path: Some(path) }
} }
/// Return the path as a string without transferring ownership.
fn path_str(&self) -> String {
self.path
.as_ref()
.expect("KubeconfigGuard path already taken")
.to_string_lossy()
.into_owned()
}
/// Transfer ownership: caller is now responsible for the file. /// Transfer ownership: caller is now responsible for the file.
/// Returns the path string for use with the PTY session. /// Returns the path string for use with the PTY session.
fn disarm(mut self) -> String { fn disarm(mut self) -> String {
@ -344,8 +353,9 @@ pub async fn start_pty_exec_session(
let kubectl_path = let kubectl_path =
crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?; crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?;
// Transfer ownership: PTY session now owns the temp file's lifetime. // Obtain path string without disarming; the guard remains active so the
let kubeconfig_path = kubeconfig_guard.map(|g| g.disarm()); // file is cleaned up if session start fails below.
let kubeconfig_path = kubeconfig_guard.as_ref().map(|g| g.path_str());
// Start session // Start session
let params = crate::shell::session::SessionParams { let params = crate::shell::session::SessionParams {
@ -363,6 +373,13 @@ pub async fn start_pty_exec_session(
.await .await
.map_err(|e| format!("Failed to start exec session: {e}"))?; .map_err(|e| format!("Failed to start exec session: {e}"))?;
// Session started — disarm the guard so the file outlives this function.
// The PTY process needs the kubeconfig for the full session duration;
// temp dir is OS-cleaned on reboot.
if let Some(g) = kubeconfig_guard {
g.disarm();
}
Ok(session_id) Ok(session_id)
} }
@ -406,8 +423,9 @@ pub async fn start_pty_attach_session(
let kubectl_path = let kubectl_path =
crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?; crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?;
// Transfer ownership: PTY session now owns the temp file's lifetime. // Obtain path string without disarming; the guard remains active so the
let kubeconfig_path = kubeconfig_guard.map(|g| g.disarm()); // file is cleaned up if session start fails below.
let kubeconfig_path = kubeconfig_guard.as_ref().map(|g| g.path_str());
// Start session // Start session
let params = crate::shell::session::SessionParams { let params = crate::shell::session::SessionParams {
@ -425,6 +443,11 @@ pub async fn start_pty_attach_session(
.await .await
.map_err(|e| format!("Failed to start attach session: {e}"))?; .map_err(|e| format!("Failed to start attach session: {e}"))?;
// Session started — disarm the guard so the file outlives this function.
if let Some(g) = kubeconfig_guard {
g.disarm();
}
Ok(session_id) Ok(session_id)
} }