feature/freelens-parity-complete #87

Merged
sarman merged 16 commits from feature/freelens-parity-complete into master 2026-06-10 01:06:11 +00:00
5 changed files with 121 additions and 42 deletions
Showing only changes of commit 0603910c1f - Show all commits

View File

@ -6268,7 +6268,10 @@ fn parse_crds_json(json_str: &str) -> Result<Vec<CrdInfo>, String> {
.filter_map(|ver| {
let version_name = ver.get("name").and_then(|n| n.as_str())?.to_string();
let served = ver.get("served").and_then(|s| s.as_bool()).unwrap_or(true);
let storage = ver.get("storage").and_then(|s| s.as_bool()).unwrap_or(false);
let storage = ver
.get("storage")
.and_then(|s| s.as_bool())
.unwrap_or(false);
// Parse printer columns for this version
let printer_columns: Vec<PrinterColumn> = ver
@ -6277,11 +6280,26 @@ fn parse_crds_json(json_str: &str) -> Result<Vec<CrdInfo>, String> {
.map(|cols| {
cols.iter()
.filter_map(|col| {
let col_name = col.get("name").and_then(|n| n.as_str())?.to_string();
let json_path = col.get("jsonPath").and_then(|j| j.as_str())?.to_string();
let column_type = col.get("type").and_then(|t| t.as_str()).unwrap_or("string").to_string();
let description = col.get("description").and_then(|d| d.as_str()).map(|s| s.to_string());
let priority = col.get("priority").and_then(|p| p.as_i64()).unwrap_or(0) as i32;
let col_name =
col.get("name").and_then(|n| n.as_str())?.to_string();
let json_path = col
.get("jsonPath")
.and_then(|j| j.as_str())?
.to_string();
let column_type = col
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("string")
.to_string();
let description = col
.get("description")
.and_then(|d| d.as_str())
.map(|s| s.to_string());
let priority = col
.get("priority")
.and_then(|p| p.as_i64())
.unwrap_or(0)
as i32;
Some(PrinterColumn {
name: col_name,

View File

@ -286,16 +286,15 @@ pub async fn start_pty_exec_session(
.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();
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()));
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}"))?;
@ -306,8 +305,8 @@ pub async fn start_pty_exec_session(
};
// Locate kubectl
let kubectl_path = crate::shell::kubectl::locate_kubectl()
.map_err(|e| format!("kubectl not found: {e}"))?;
let kubectl_path =
crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?;
// Start session
let params = crate::shell::session::SessionParams {
@ -345,16 +344,15 @@ pub async fn start_pty_attach_session(
.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();
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()));
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}"))?;
@ -365,8 +363,8 @@ pub async fn start_pty_attach_session(
};
// Locate kubectl
let kubectl_path = crate::shell::kubectl::locate_kubectl()
.map_err(|e| format!("kubectl not found: {e}"))?;
let kubectl_path =
crate::shell::kubectl::locate_kubectl().map_err(|e| format!("kubectl not found: {e}"))?;
// Start session
let params = crate::shell::session::SessionParams {

View File

@ -50,12 +50,13 @@ impl PtySession {
.spawn_command(cmd)
.context("Failed to spawn command in PTY")?;
debug!("PTY session spawned: {} (PID: {:?})", command, child.process_id());
debug!(
"PTY session spawned: {} (PID: {:?})",
command,
child.process_id()
);
Ok(Self {
pair,
child,
})
Ok(Self { pair, child })
}
/// Spawn kubectl exec session
@ -126,7 +127,11 @@ impl PtySession {
/// Write data to PTY stdin
pub fn write(&mut self, data: &[u8]) -> Result<()> {
let mut writer = self.pair.master.take_writer().context("PTY writer unavailable")?;
let mut writer = self
.pair
.master
.take_writer()
.context("PTY writer unavailable")?;
writer
.write_all(data)
.context("Failed to write to PTY stdin")?;
@ -136,7 +141,11 @@ impl PtySession {
/// Read available data from PTY stdout/stderr (non-blocking)
pub fn read(&mut self) -> Result<Vec<u8>> {
let mut reader = self.pair.master.try_clone_reader().context("PTY reader unavailable")?;
let mut reader = self
.pair
.master
.try_clone_reader()
.context("PTY reader unavailable")?;
let mut buffer = vec![0u8; 4096];
// Non-blocking read with timeout
@ -175,12 +184,16 @@ impl PtySession {
/// Kill the child process
pub fn kill(&mut self) -> Result<()> {
self.child.kill().context("Failed to kill PTY child process")
self.child
.kill()
.context("Failed to kill PTY child process")
}
/// Wait for the child process to exit
pub fn wait(&mut self) -> Result<portable_pty::ExitStatus> {
self.child.wait().context("Failed to wait for PTY child process")
self.child
.wait()
.context("Failed to wait for PTY child process")
}
}
@ -214,8 +227,10 @@ mod tests {
let output_str = String::from_utf8_lossy(&output);
// Should contain "hello"
assert!(output_str.contains("hello") || output_str.is_empty(),
"Expected output to contain 'hello' or be empty (timing issue)");
assert!(
output_str.contains("hello") || output_str.is_empty(),
"Expected output to contain 'hello' or be empty (timing issue)"
);
}
#[test]
@ -241,8 +256,10 @@ mod tests {
// Output should contain our test data (cat echoes it back)
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains("test input") || output_str.is_empty(),
"Expected output to contain 'test input' or be empty (timing issue)");
assert!(
output_str.contains("test input") || output_str.is_empty(),
"Expected output to contain 'test input' or be empty (timing issue)"
);
}
#[test]
@ -303,7 +320,9 @@ mod tests {
let output_str = String::from_utf8_lossy(&output);
// Should contain our test value
assert!(output_str.contains("test_value") || output_str.is_empty(),
"Expected output to contain 'test_value' or be empty (timing issue)");
assert!(
output_str.contains("test_value") || output_str.is_empty(),
"Expected output to contain 'test_value' or be empty (timing issue)"
);
}
}

View File

@ -263,9 +263,7 @@ impl SessionManager {
/// Send stdin data to a session
pub async fn send_stdin(&self, session_id: &str, data: Vec<u8>) -> Result<()> {
let sessions = self.sessions.read().await;
let session = sessions
.get(session_id)
.context("Session not found")?;
let session = sessions.get(session_id).context("Session not found")?;
session
.stdin_tx
@ -278,9 +276,7 @@ impl SessionManager {
/// Resize a session's PTY
pub async fn resize_session(&self, session_id: &str, rows: u16, cols: u16) -> Result<()> {
let sessions = self.sessions.read().await;
let session = sessions
.get(session_id)
.context("Session not found")?;
let session = sessions.get(session_id).context("Session not found")?;
session
.control_tx
@ -293,9 +289,7 @@ impl SessionManager {
/// Terminate a session
pub async fn terminate_session(&self, session_id: &str) -> Result<()> {
let sessions = self.sessions.read().await;
let session = sessions
.get(session_id)
.context("Session not found")?;
let session = sessions.get(session_id).context("Session not found")?;
session
.control_tx

View File

@ -1513,3 +1513,53 @@ export const listCrdsCmd = (clusterId: string) =>
export const listCustomResourcesCmd = (clusterId: string, group: string, version: string, resource: string, namespace: string) =>
invoke<CustomResourceInfo[]>("list_custom_resources", { clusterId, group, version, resource, namespace });
// ─── PTY Terminal Commands ────────────────────────────────────────────────────
export interface PtySessionInfo {
session_id: string;
cluster_id: string;
namespace: string;
pod_name: string;
container_name: string | null;
session_type: "exec" | "attach";
}
export const startPtyExecSessionCmd = (
clusterId: string,
namespace: string,
podName: string,
containerName: string | null,
shell: string
) =>
invoke<string>("start_pty_exec_session", {
clusterId,
namespace,
podName,
containerName,
shell,
});
export const startPtyAttachSessionCmd = (
clusterId: string,
namespace: string,
podName: string,
containerName: string | null
) =>
invoke<string>("start_pty_attach_session", {
clusterId,
namespace,
podName,
containerName,
});
export const sendPtyStdinCmd = (sessionId: string, data: string) =>
invoke<void>("send_pty_stdin", { sessionId, data });
export const resizePtySessionCmd = (sessionId: string, rows: number, cols: number) =>
invoke<void>("resize_pty_session", { sessionId, rows, cols });
export const terminatePtySessionCmd = (sessionId: string) =>
invoke<void>("terminate_pty_session", { sessionId });
export const listPtySessionsCmd = () => invoke<PtySessionInfo[]>("list_pty_sessions", {});