Something went wrong. Try again.
TRAMP-style remote editing for nushell: an ssh agent and a nu plugin, in Rust
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984//! Automatic deployment of the `tramp-agent` binary into Docker/Kubernetes//! containers.//!//! This module extends the agent deployment concept (see [`super::deploy`])//! to exec-based backends. Instead of uploading via SFTP, the agent binary//! is copied into containers using `docker cp` / `kubectl cp` (or a base64//! exec fallback), then started as an interactive process with piped//! stdin/stdout for MsgPack-RPC communication.//!//! ## Supported flows//!//! | Backend | Copy method | Start method |//! |------------|---------------------------|-------------------------------------------|//! | Docker | `docker cp` | `docker exec -i <ctr> /tmp/tramp-agent` |//! | Kubernetes | `kubectl cp` (or base64) | `kubectl exec -i <pod> -- /tmp/...` |//!//! ## Chained paths//!//! For standalone containers (local runner), the agent process is spawned as//! a local `tokio::process::Child` with piped stdio.//!//! For chained paths (e.g. `/ssh:host|docker:ctr:/path`), the parent SSH//! backend already benefits from the RPC agent, so the Docker commands//! executed through it are already fast. Agent deployment *inside* the//! container through a remote runner is not yet supported (Phase 6).
use std::process::Stdio;use std::sync::Arc;use std::time::Duration;
use tokio::process::Child;
use super::deploy::{self, RemoteTarget};use super::rpc::RpcBackend;use super::rpc_client::RpcClient;use super::runner::CommandRunner;use super::{Backend, ExecResult};use crate::errors::{TrampError, TrampResult};
/// The version of the agent we expect — must match the plugin's version.const AGENT_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Remote path inside the container where the agent is stored.const CONTAINER_AGENT_DIR: &str = "/tmp/tramp-agent-dir";
/// Agent binary name inside the container.const CONTAINER_AGENT_BIN: &str = "tramp-agent";
/// Full path to the agent binary inside the container.pub const CONTAINER_AGENT_PATH: &str = "/tmp/tramp-agent-dir/tramp-agent";
/// Timeout for agent ping after startup.const AGENT_PING_TIMEOUT: Duration = Duration::from_secs(5);
// ---------------------------------------------------------------------------// Container kind// ---------------------------------------------------------------------------
/// The kind of exec backend we're deploying into.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum ContainerKind { Docker, Kubernetes,}
/// Parameters needed to identify a container for agent deployment.#[derive(Debug, Clone)]pub struct ContainerTarget { /// Docker or Kubernetes. pub kind: ContainerKind, /// Container name (Docker) or pod name (Kubernetes). pub name: String, /// Optional user for Docker (`--user`). pub user: Option<String>, /// Optional container name within a K8s pod (`-c`). pub k8s_container: Option<String>,}
// ---------------------------------------------------------------------------// Architecture detection// ---------------------------------------------------------------------------
/// Detect the container's OS and architecture by running `uname -sm` inside it.pub async fn detect_container_target( runner: &dyn CommandRunner, target: &ContainerTarget,) -> TrampResult<RemoteTarget> { let result = run_in_container(runner, target, "uname", &["-sm"]).await?;
if result.exit_code != 0 { let stderr = String::from_utf8_lossy(&result.stderr); return Err(TrampError::Internal(format!( "`uname -sm` failed in container {}: {}", target.name, stderr.trim() ))); }
let text = String::from_utf8_lossy(&result.stdout); let text = text.trim();
let parts: Vec<&str> = text.split_whitespace().collect(); if parts.len() < 2 { return Err(TrampError::Internal(format!( "unexpected `uname -sm` output from container {}: {text:?}", target.name, ))); }
let os_raw = parts[0].to_lowercase(); let arch_raw = parts[1].to_lowercase();
let os = match os_raw.as_str() { "linux" => "linux", "darwin" => "darwin", "freebsd" => "freebsd", other => { return Err(TrampError::Internal(format!( "unsupported container OS: {other}" ))); } };
let arch = match arch_raw.as_str() { "x86_64" | "amd64" => "x86_64", "aarch64" | "arm64" => "aarch64", "armv7l" | "armhf" => "armv7", other => { return Err(TrampError::Internal(format!( "unsupported container architecture: {other}" ))); } };
let triple = match (arch, os) { ("x86_64", "linux") => "x86_64-unknown-linux-musl", ("aarch64", "linux") => "aarch64-unknown-linux-musl", ("armv7", "linux") => "armv7-unknown-linux-musleabihf", ("x86_64", "darwin") => "x86_64-apple-darwin", ("aarch64", "darwin") => "aarch64-apple-darwin", ("x86_64", "freebsd") => "x86_64-unknown-freebsd", _ => { return Err(TrampError::Internal(format!( "no agent binary available for {arch}-{os}" ))); } };
Ok(RemoteTarget { os: os.to_string(), arch: arch.to_string(), triple: triple.to_string(), })}
// ---------------------------------------------------------------------------// Agent presence check// ---------------------------------------------------------------------------
/// Check if the agent is already deployed and executable in the container/// **with the correct version**.////// Runs the agent with `--version` inside the container and verifies the/// output matches the plugin's `CARGO_PKG_VERSION`. Returns `true` only/// when the binary exists, is executable, and reports the same version.pub async fn is_agent_deployed_in_container( runner: &dyn CommandRunner, target: &ContainerTarget,) -> bool { let result = run_in_container( runner, target, "sh", &[ "-c", &format!( "test -x {CONTAINER_AGENT_PATH} && {CONTAINER_AGENT_PATH} --version 2>/dev/null || echo MISSING" ), ], ) .await;
match result { Ok(r) => { if r.exit_code != 0 { return false; } let text = String::from_utf8_lossy(&r.stdout); let text = text.trim(); if text.contains("MISSING") { return false; } // The agent prints "tramp-agent <version>" on --version. let expected = format!("tramp-agent {AGENT_VERSION}"); if text == expected { true } else { eprintln!( "tramp: container agent version mismatch (got {text:?}, expected {expected:?}), will re-deploy" ); false } } Err(_) => false, }}
// ---------------------------------------------------------------------------// Upload methods// ---------------------------------------------------------------------------
/// Upload the agent binary into a Docker container using `docker cp`.////// This runs `docker cp <local_path> <container>:<remote_path>` on the/// host (via the runner).pub async fn upload_agent_docker( runner: &dyn CommandRunner, container: &str, agent_bytes: &[u8],) -> TrampResult<()> { // Write the agent binary to a temporary file on the host first. let tmp_path = write_temp_agent(agent_bytes)?; let tmp_str = tmp_path.to_string_lossy();
// Ensure the target directory exists inside the container. let mkdir_result = runner .run( "docker", &["exec", container, "mkdir", "-p", CONTAINER_AGENT_DIR], ) .await?; if mkdir_result.exit_code != 0 { cleanup_temp(&tmp_path); let stderr = String::from_utf8_lossy(&mkdir_result.stderr); return Err(TrampError::Internal(format!( "failed to create agent dir in container {container}: {}", stderr.trim() ))); }
// Copy the binary into the container. let dest = format!("{container}:{CONTAINER_AGENT_PATH}"); let result = runner.run("docker", &["cp", &tmp_str, &dest]).await;
cleanup_temp(&tmp_path);
let result = result?; if result.exit_code != 0 { let stderr = String::from_utf8_lossy(&result.stderr); return Err(TrampError::Internal(format!( "docker cp failed for {container}: {}", stderr.trim() ))); }
// Make executable. let chmod_result = runner .run( "docker", &["exec", container, "chmod", "755", CONTAINER_AGENT_PATH], ) .await?; if chmod_result.exit_code != 0 { let stderr = String::from_utf8_lossy(&chmod_result.stderr); return Err(TrampError::Internal(format!( "chmod failed in container {container}: {}", stderr.trim() ))); }
Ok(())}
/// Upload the agent binary into a Kubernetes pod using `kubectl cp`.////// Falls back to base64 exec if `kubectl cp` fails (some minimal containers/// lack `tar`).pub async fn upload_agent_kubernetes( runner: &dyn CommandRunner, pod: &str, k8s_container: Option<&str>, agent_bytes: &[u8],) -> TrampResult<()> { // Try kubectl cp first (requires tar in the container). let tmp_path = write_temp_agent(agent_bytes)?; let tmp_str = tmp_path.to_string_lossy();
let dest = format!("{pod}:{CONTAINER_AGENT_PATH}");
// Ensure the target directory exists inside the pod. let mut mkdir_args = vec!["exec"]; if let Some(c) = k8s_container { mkdir_args.extend_from_slice(&["-c", c]); } mkdir_args.extend_from_slice(&[pod, "--", "mkdir", "-p", CONTAINER_AGENT_DIR]); let _ = runner.run("kubectl", &mkdir_args).await; // best-effort
let mut cp_args = vec!["cp", &tmp_str, &dest]; if let Some(c) = k8s_container { cp_args.extend_from_slice(&["-c", c]); }
let cp_result = runner.run("kubectl", &cp_args).await; cleanup_temp(&tmp_path);
let try_base64 = match cp_result { Ok(r) => r.exit_code != 0, Err(_) => true, };
if try_base64 { // Fallback: pipe the binary via base64 through kubectl exec. upload_agent_base64(runner, pod, k8s_container, agent_bytes).await?; }
// Make executable. let mut chmod_args: Vec<&str> = vec!["exec"]; if let Some(c) = k8s_container { chmod_args.extend_from_slice(&["-c", c]); } chmod_args.extend_from_slice(&[pod, "--", "chmod", "755", CONTAINER_AGENT_PATH]); let chmod_result = runner.run("kubectl", &chmod_args).await?; if chmod_result.exit_code != 0 { let stderr = String::from_utf8_lossy(&chmod_result.stderr); return Err(TrampError::Internal(format!( "chmod failed in pod {pod}: {}", stderr.trim() ))); }
Ok(())}
/// Upload the agent binary via base64 encoding through exec.////// This works even in minimal containers that lack `tar`.async fn upload_agent_base64( runner: &dyn CommandRunner, pod: &str, k8s_container: Option<&str>, agent_bytes: &[u8],) -> TrampResult<()> { use base64::Engine; let encoded = base64::engine::general_purpose::STANDARD.encode(agent_bytes);
let script = format!( "mkdir -p {CONTAINER_AGENT_DIR} && base64 -d > {CONTAINER_AGENT_PATH} <<'__TRAMP_AGENT_EOF__'\n{encoded}\n__TRAMP_AGENT_EOF__" );
let mut args: Vec<&str> = vec!["exec"]; if let Some(c) = k8s_container { args.extend_from_slice(&["-c", c]); } args.extend_from_slice(&[pod, "--", "sh", "-c", &script]);
let result = runner.run("kubectl", &args).await?; if result.exit_code != 0 { let stderr = String::from_utf8_lossy(&result.stderr); return Err(TrampError::Internal(format!( "base64 agent upload failed in pod {pod}: {}", stderr.trim() ))); }
Ok(())}
/// Upload the agent binary via base64 into a Docker container.////// Used when the runner is remote and `docker cp` won't work (since we/// can't access the local filesystem from the remote host).pub async fn upload_agent_docker_base64( runner: &dyn CommandRunner, container: &str, agent_bytes: &[u8],) -> TrampResult<()> { use base64::Engine; let encoded = base64::engine::general_purpose::STANDARD.encode(agent_bytes);
let script = format!( "mkdir -p {CONTAINER_AGENT_DIR} && base64 -d > {CONTAINER_AGENT_PATH} <<'__TRAMP_AGENT_EOF__'\n{encoded}\n__TRAMP_AGENT_EOF__\nchmod 755 {CONTAINER_AGENT_PATH}" );
let result = runner .run("docker", &["exec", container, "sh", "-c", &script]) .await?;
if result.exit_code != 0 { let stderr = String::from_utf8_lossy(&result.stderr); return Err(TrampError::Internal(format!( "base64 agent upload failed in container {container}: {}", stderr.trim() ))); }
Ok(())}
// ---------------------------------------------------------------------------// Agent process — local spawn// ---------------------------------------------------------------------------
/// A handle to a `tramp-agent` process running inside a container.////// The process is started via `docker exec -i` or `kubectl exec -i`, with/// stdin/stdout piped through the local `tokio::process::Child`.////// Dropping this handle kills the child process.pub struct ContainerAgentProcess { child: Child,}
impl ContainerAgentProcess { /// Take the stdin writer for sending RPC requests. pub fn take_stdin(&mut self) -> Option<tokio::process::ChildStdin> { self.child.stdin.take() }
/// Take the stdout reader for reading RPC responses. pub fn take_stdout(&mut self) -> Option<tokio::process::ChildStdout> { self.child.stdout.take() }}
/// Start the agent inside a Docker container as a local interactive process.////// Spawns `docker exec -i <container> /tmp/tramp-agent-dir/tramp-agent` with/// piped stdin/stdout.pub fn start_agent_docker( container: &str, user: Option<&str>,) -> TrampResult<ContainerAgentProcess> { let mut cmd = tokio::process::Command::new("docker"); cmd.arg("exec").arg("-i"); if let Some(u) = user { cmd.arg("--user").arg(u); } cmd.arg(container).arg(CONTAINER_AGENT_PATH); cmd.stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null());
let child = cmd.spawn().map_err(|e| { TrampError::Internal(format!( "failed to start agent in container {container}: {e}" )) })?;
Ok(ContainerAgentProcess { child })}
/// Start the agent inside a Kubernetes pod as a local interactive process.////// Spawns `kubectl exec -i [-c container] <pod> -- /tmp/tramp-agent-dir/tramp-agent`/// with piped stdin/stdout.pub fn start_agent_kubernetes( pod: &str, k8s_container: Option<&str>,) -> TrampResult<ContainerAgentProcess> { let mut cmd = tokio::process::Command::new("kubectl"); cmd.arg("exec").arg("-i"); if let Some(c) = k8s_container { cmd.arg("-c").arg(c); } cmd.arg(pod).arg("--").arg(CONTAINER_AGENT_PATH); cmd.stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null());
let child = cmd .spawn() .map_err(|e| TrampError::Internal(format!("failed to start agent in pod {pod}: {e}")))?;
Ok(ContainerAgentProcess { child })}
// ---------------------------------------------------------------------------// High-level deployment flow// ---------------------------------------------------------------------------
/// Outcome of the container agent deployment attempt.pub enum ExecDeployResult { /// The agent is running inside the container, wrapped in an `RpcBackend`. Ready(Arc<dyn Backend>), /// Deployment failed — reason string for diagnostics. Fallback(String),}
/// Attempt to deploy and start the `tramp-agent` inside a container.////// This is the main entry point for exec backend agent deployment./// It follows the same decision tree as SSH agent deployment:////// 1. Detect container arch via `uname -sm`/// 2. Check if agent is already deployed/// 3. Find cached binary locally → upload into container/// 4. Start the agent as an interactive process/// 5. Ping to verify it responds////// On any failure, returns `ExecDeployResult::Fallback`.////// **Note:** This only works for standalone containers (local runner)./// For chained paths, the parent backend's agent already provides/// performance benefits.pub async fn deploy_and_start_in_container( runner: &dyn CommandRunner, target: &ContainerTarget,) -> ExecDeployResult { let remote_target = match detect_container_target(runner, target).await { Ok(t) => t, Err(e) => return ExecDeployResult::Fallback(format!("arch detection failed: {e}")), };
let needs_upload = !is_agent_deployed_in_container(runner, target).await;
if needs_upload { let agent_bytes = match deploy::find_cached_agent(&remote_target) { Some(path) => match std::fs::read(&path) { Ok(data) => data, Err(e) => { return ExecDeployResult::Fallback(format!( "failed to read cached agent at {}: {e}", path.display() )); } }, None => { return ExecDeployResult::Fallback(format!( "no cached agent binary for {} (container: {})", remote_target.triple, target.name, )); } };
let upload_result = match target.kind { ContainerKind::Docker => upload_agent_docker(runner, &target.name, &agent_bytes).await, ContainerKind::Kubernetes => { upload_agent_kubernetes( runner, &target.name, target.k8s_container.as_deref(), &agent_bytes, ) .await } };
if let Err(e) = upload_result { return ExecDeployResult::Fallback(format!("agent upload failed: {e}")); } }
let mut agent = match target.kind { ContainerKind::Docker => match start_agent_docker(&target.name, target.user.as_deref()) { Ok(a) => a, Err(e) => return ExecDeployResult::Fallback(format!("agent start failed: {e}")), }, ContainerKind::Kubernetes => { match start_agent_kubernetes(&target.name, target.k8s_container.as_deref()) { Ok(a) => a, Err(e) => return ExecDeployResult::Fallback(format!("agent start failed: {e}")), } } };
let stdin = agent.take_stdin(); let stdout = agent.take_stdout();
let (stdin, stdout) = match (stdin, stdout) { (Some(w), Some(r)) => (w, r), _ => { return ExecDeployResult::Fallback( "agent started but stdin/stdout not available".into(), ); } };
let client = RpcClient::new(stdout, stdin);
let ping_result = tokio::time::timeout(AGENT_PING_TIMEOUT, client.ping()).await; match ping_result { Ok(Ok(())) => {} Ok(Err(e)) => { return ExecDeployResult::Fallback(format!("agent ping failed: {e}")); } Err(_) => { return ExecDeployResult::Fallback("agent ping timed out".into()); } }
let host = match target.kind { ContainerKind::Docker => format!("docker:{}", target.name), ContainerKind::Kubernetes => format!("k8s:{}", target.name), };
ExecDeployResult::Ready(Arc::new(RpcBackend::new(client, host)))}
// ---------------------------------------------------------------------------// Helpers// ---------------------------------------------------------------------------
/// Run a command inside a container via the appropriate exec mechanism.async fn run_in_container( runner: &dyn CommandRunner, target: &ContainerTarget, program: &str, args: &[&str],) -> TrampResult<ExecResult> { match target.kind { ContainerKind::Docker => { let mut full_args: Vec<&str> = vec!["exec"]; if let Some(ref u) = target.user { full_args.extend_from_slice(&["--user", u]); } full_args.push(&target.name); full_args.push(program); full_args.extend_from_slice(args); runner.run("docker", &full_args).await } ContainerKind::Kubernetes => { let mut full_args: Vec<&str> = vec!["exec"]; if let Some(ref c) = target.k8s_container { full_args.extend_from_slice(&["-c", c]); } full_args.push(&target.name); full_args.push("--"); full_args.push(program); full_args.extend_from_slice(args); runner.run("kubectl", &full_args).await } }}
/// Write the agent binary to a temporary file on the local filesystem.////// Returns the path to the temporary file. The caller is responsible for/// cleaning it up via [`cleanup_temp`].fn write_temp_agent(agent_bytes: &[u8]) -> TrampResult<std::path::PathBuf> { let tmp_dir = std::env::temp_dir(); let tmp_path = tmp_dir.join(format!("tramp-agent-upload-{}", std::process::id())); std::fs::write(&tmp_path, agent_bytes).map_err(|e| { TrampError::Internal(format!( "failed to write temp agent to {}: {e}", tmp_path.display() )) })?;
// Make the temp file readable (needed for docker cp). #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let perms = std::fs::Permissions::from_mode(0o644); let _ = std::fs::set_permissions(&tmp_path, perms); }
Ok(tmp_path)}
/// Remove a temporary agent file.fn cleanup_temp(path: &std::path::Path) { let _ = std::fs::remove_file(path);}
/// Check whether the runner is a local runner (i.e. not behind another/// backend).////// Agent deployment inside containers through a remote runner is not/// currently supported — the `docker cp` / `kubectl cp` commands expect/// local filesystem access. For remote runners, we fall back to base64/// upload or skip deployment entirely.pub fn is_local_runner(_runner: &dyn CommandRunner) -> bool { // We use a trait-object downcast check. `LocalRunner` is a unit struct, // so we check if the runner's description matches. Since we can't // downcast trait objects directly without `Any`, we use a simpler // heuristic: try to detect `RemoteRunner` by running a no-op. // // For now, we rely on the caller (VFS layer) to pass this information // explicitly based on whether a parent backend exists. // // This function is a placeholder that always returns true — the VFS // layer gates on `parent.is_none()` before calling deploy. true}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*;
#[test] fn container_agent_path_is_consistent() { assert_eq!( CONTAINER_AGENT_PATH, format!("{CONTAINER_AGENT_DIR}/{CONTAINER_AGENT_BIN}") ); }
#[test] fn container_target_debug() { let target = ContainerTarget { kind: ContainerKind::Docker, name: "mycontainer".into(), user: Some("root".into()), k8s_container: None, }; let dbg = format!("{target:?}"); assert!(dbg.contains("Docker")); assert!(dbg.contains("mycontainer")); assert!(dbg.contains("root")); }
#[test] fn container_target_kubernetes() { let target = ContainerTarget { kind: ContainerKind::Kubernetes, name: "mypod".into(), user: None, k8s_container: Some("app".into()), }; let dbg = format!("{target:?}"); assert!(dbg.contains("Kubernetes")); assert!(dbg.contains("mypod")); assert!(dbg.contains("app")); }
#[test] fn write_and_cleanup_temp() { let data = b"fake agent binary"; let path = write_temp_agent(data).unwrap(); assert!(path.exists()); let read_back = std::fs::read(&path).unwrap(); assert_eq!(read_back, data); cleanup_temp(&path); assert!(!path.exists()); }
#[test] fn cleanup_temp_nonexistent_is_noop() { // Should not panic. cleanup_temp(std::path::Path::new( "/tmp/nonexistent-tramp-agent-test-12345", )); }
// Mock runner for unit tests. struct MockRunner { responses: std::sync::Mutex<Vec<ExecResult>>, }
impl MockRunner { fn new(responses: Vec<ExecResult>) -> Self { Self { responses: std::sync::Mutex::new(responses), } } }
#[async_trait::async_trait] impl CommandRunner for MockRunner { async fn run(&self, _program: &str, _args: &[&str]) -> TrampResult<ExecResult> { let mut responses = self.responses.lock().unwrap(); if responses.is_empty() { Ok(ExecResult { stdout: bytes::Bytes::new(), stderr: bytes::Bytes::from("no more mock responses"), exit_code: 1, }) } else { Ok(responses.remove(0)) } }
async fn run_with_stdin( &self, program: &str, args: &[&str], _stdin_data: &[u8], ) -> TrampResult<ExecResult> { self.run(program, args).await } }
#[tokio::test] async fn detect_container_target_linux_x86_64() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from("Linux x86_64\n"), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "test".into(), user: None, k8s_container: None, };
let result = detect_container_target(&runner, &target).await.unwrap(); assert_eq!(result.os, "linux"); assert_eq!(result.arch, "x86_64"); assert_eq!(result.triple, "x86_64-unknown-linux-musl"); }
#[tokio::test] async fn detect_container_target_linux_aarch64() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from("Linux aarch64\n"), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Kubernetes, name: "mypod".into(), user: None, k8s_container: Some("app".into()), };
let result = detect_container_target(&runner, &target).await.unwrap(); assert_eq!(result.os, "linux"); assert_eq!(result.arch, "aarch64"); assert_eq!(result.triple, "aarch64-unknown-linux-musl"); }
#[tokio::test] async fn detect_container_target_arm64_alias() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from("Linux arm64\n"), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "test".into(), user: None, k8s_container: None, };
let result = detect_container_target(&runner, &target).await.unwrap(); assert_eq!(result.arch, "aarch64"); }
#[tokio::test] async fn detect_container_target_failure() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::new(), stderr: bytes::Bytes::from("exec failed"), exit_code: 1, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "test".into(), user: None, k8s_container: None, };
let result = detect_container_target(&runner, &target).await; assert!(result.is_err()); }
#[tokio::test] async fn is_agent_deployed_positive() { let version_output = format!("tramp-agent {}\n", env!("CARGO_PKG_VERSION")); let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from(version_output), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "test".into(), user: None, k8s_container: None, };
assert!(is_agent_deployed_in_container(&runner, &target).await); }
#[tokio::test] async fn is_agent_deployed_negative() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::new(), stderr: bytes::Bytes::new(), exit_code: 1, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "test".into(), user: None, k8s_container: None, };
assert!(!is_agent_deployed_in_container(&runner, &target).await); }
#[tokio::test] async fn is_agent_deployed_version_mismatch() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from("tramp-agent 0.0.0-fake\n"), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "test".into(), user: None, k8s_container: None, };
assert!(!is_agent_deployed_in_container(&runner, &target).await); }
#[tokio::test] async fn is_agent_deployed_missing() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from("MISSING\n"), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "test".into(), user: None, k8s_container: None, };
assert!(!is_agent_deployed_in_container(&runner, &target).await); }
#[tokio::test] async fn run_in_container_docker_builds_correct_args() { // This test verifies the function doesn't panic and delegates properly. let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from("hello\n"), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Docker, name: "mycontainer".into(), user: Some("www".into()), k8s_container: None, };
let result = run_in_container(&runner, &target, "echo", &["hello"]) .await .unwrap(); assert_eq!(result.exit_code, 0); assert_eq!(String::from_utf8_lossy(&result.stdout).trim(), "hello"); }
#[tokio::test] async fn run_in_container_kubernetes_builds_correct_args() { let runner = MockRunner::new(vec![ExecResult { stdout: bytes::Bytes::from("world\n"), stderr: bytes::Bytes::new(), exit_code: 0, }]);
let target = ContainerTarget { kind: ContainerKind::Kubernetes, name: "mypod".into(), user: None, k8s_container: Some("sidecar".into()), };
let result = run_in_container(&runner, &target, "echo", &["world"]) .await .unwrap(); assert_eq!(result.exit_code, 0); }}