diff --git a/crates/dev_container/src/devcontainer_api.rs b/crates/dev_container/src/devcontainer_api.rs index dae7b61b1f..6e7a2a336b 100644 --- a/crates/dev_container/src/devcontainer_api.rs +++ b/crates/dev_container/src/devcontainer_api.rs @@ -19,7 +19,7 @@ use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate}; struct DevContainerUp { _outcome: String, container_id: String, - _remote_user: String, + remote_user: String, remote_workspace_folder: String, } @@ -156,6 +156,7 @@ pub async fn start_dev_container( Ok(DevContainerUp { container_id, remote_workspace_folder, + remote_user, .. }) => { let project_name = match devcontainer_read_configuration( @@ -180,6 +181,7 @@ pub async fn start_dev_container( name: project_name, container_id: container_id, use_podman, + remote_user, }; Ok((connection, remote_workspace_folder)) @@ -563,7 +565,7 @@ mod tests { up.container_id, "826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a" ); - assert_eq!(up._remote_user, "vscode"); + assert_eq!(up.remote_user, "vscode"); assert_eq!(up.remote_workspace_folder, "/workspaces/zed"); let json_in_plaintext = r#"[2026-01-22T16:19:08.802Z] @devcontainers/cli 0.80.1. Node.js v22.21.1. darwin 24.6.0 arm64. @@ -574,7 +576,7 @@ mod tests { up.container_id, "826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a" ); - assert_eq!(up._remote_user, "vscode"); + assert_eq!(up.remote_user, "vscode"); assert_eq!(up.remote_workspace_folder, "/workspaces/zed"); } } diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 96b6981577..9d6199786c 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -90,6 +90,7 @@ impl From for RemoteConnectionOptions { Connection::DevContainer(conn) => { RemoteConnectionOptions::Docker(DockerConnectionOptions { name: conn.name, + remote_user: conn.remote_user, container_id: conn.container_id, upload_binary_over_docker_exec: false, use_podman: conn.use_podman, diff --git a/crates/remote/src/transport/docker.rs b/crates/remote/src/transport/docker.rs index 713e532704..7a8c22e0fc 100644 --- a/crates/remote/src/transport/docker.rs +++ b/crates/remote/src/transport/docker.rs @@ -33,6 +33,7 @@ use crate::{ pub struct DockerConnectionOptions { pub name: String, pub container_id: String, + pub remote_user: String, pub upload_binary_over_docker_exec: bool, pub use_podman: bool, } @@ -115,16 +116,39 @@ impl DockerExecConnection { { Ok(shell) => match shell.trim() { "" => { - log::error!("$SHELL is not set, falling back to {default_shell}"); - default_shell.to_owned() + log::info!("$SHELL is not set, checking passwd for user"); + } + shell => { + return shell.to_owned(); } - shell => shell.to_owned(), }, Err(e) => { - log::error!("Failed to get shell: {e}"); - default_shell.to_owned() + log::error!("Failed to get $SHELL: {e}. Checking passwd for user"); } } + + match self + .run_docker_exec( + "sh", + None, + &Default::default(), + &["-c", "getent passwd \"$(id -un)\" | cut -d: -f7"], + ) + .await + { + Ok(shell) => match shell.trim() { + "" => { + log::info!("No shell found in passwd, falling back to {default_shell}"); + } + shell => { + return shell.to_owned(); + } + }, + Err(e) => { + log::info!("Error getting shell from passwd: {e}. Falling back to {default_shell}"); + } + } + default_shell.to_owned() } async fn check_remote_platform(&self) -> Result { @@ -370,6 +394,55 @@ impl DockerExecConnection { Ok(()) } + async fn upload_and_chown( + docker_cli: String, + connection_options: DockerConnectionOptions, + src_path: String, + dst_path: String, + ) -> Result<()> { + let mut command = util::command::new_smol_command(&docker_cli); + command.arg("cp"); + command.arg("-a"); + command.arg(&src_path); + command.arg(format!("{}:{}", connection_options.container_id, dst_path)); + + let output = command.output().await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + log::debug!("failed to upload via docker cp {src_path} -> {dst_path}: {stderr}",); + anyhow::bail!( + "failed to upload via docker cp {} -> {}: {}", + src_path, + dst_path, + stderr, + ); + } + + let mut chown_command = util::command::new_smol_command(&docker_cli); + chown_command.arg("exec"); + chown_command.arg(connection_options.container_id); + chown_command.arg("chown"); + chown_command.arg(format!( + "{}:{}", + connection_options.remote_user, connection_options.remote_user, + )); + chown_command.arg(&dst_path); + + let output = chown_command.output().await?; + + if output.status.success() { + return Ok(()); + } + + let stderr = String::from_utf8_lossy(&output.stderr); + log::debug!("failed to change ownership for via chown: {stderr}",); + anyhow::bail!( + "failed to change ownership for zed_remote_server via chown: {}", + stderr, + ); + } + async fn upload_file( &self, src_path: &Path, @@ -380,32 +453,15 @@ impl DockerExecConnection { let src_path_display = src_path.display().to_string(); let dest_path_str = dest_path.display(self.path_style()); + let full_server_path = format!("{}/{}", remote_dir_for_server, dest_path_str); - let mut command = util::command::new_smol_command(self.docker_cli()); - command.arg("cp"); - command.arg("-a"); - command.arg(&src_path_display); - command.arg(format!( - "{}:{}/{}", - &self.connection_options.container_id, remote_dir_for_server, dest_path_str - )); - - let output = command.output().await?; - - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!( - "failed to upload file via docker cp {src_path_display} -> {dest_path_str}: {stderr}", - ); - anyhow::bail!( - "failed to upload file via docker cp {} -> {}: {}", + Self::upload_and_chown( + self.docker_cli().to_string(), + self.connection_options.clone(), src_path_display, - dest_path_str, - stderr, - ); + full_server_path, + ) + .await } async fn run_docker_command( @@ -440,6 +496,9 @@ impl DockerExecConnection { None => vec![], }; + args.push("-u".to_string()); + args.push(self.connection_options.remote_user.clone()); + for (k, v) in env.iter() { args.push("-e".to_string()); let env_declaration = format!("{}={}", k, v); @@ -581,6 +640,8 @@ impl RemoteConnection for DockerExecConnection { } docker_args.extend([ + "-u".to_string(), + self.connection_options.remote_user.to_string(), "-w".to_string(), self.remote_dir_for_server.clone(), "-i".to_string(), @@ -641,24 +702,14 @@ impl RemoteConnection for DockerExecConnection { let dest_path_str = dest_path.to_string(); let src_path_display = src_path.display().to_string(); - let mut command = util::command::new_smol_command(self.docker_cli()); - command.arg("cp"); - command.arg("-a"); // Archive mode is required to assign the file ownership to the default docker exec user - command.arg(src_path_display); - command.arg(format!( - "{}:{}", - self.connection_options.container_id, dest_path_str - )); + let upload_task = Self::upload_and_chown( + self.docker_cli().to_string(), + self.connection_options.clone(), + src_path_display, + dest_path_str, + ); - cx.background_spawn(async move { - let output = command.output().await?; - - if output.status.success() { - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to upload directory")) - } - }) + cx.background_spawn(upload_task) } async fn kill(&self) -> Result<()> { @@ -706,7 +757,11 @@ impl RemoteConnection for DockerExecConnection { inner_program.push("-l".to_string()); }; - let mut docker_args = vec!["exec".to_string()]; + let mut docker_args = vec![ + "exec".to_string(), + "-u".to_string(), + self.connection_options.remote_user.clone(), + ]; if let Some(parsed_working_dir) = parsed_working_dir { docker_args.push("-w".to_string()); diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 86c58dc540..99870b5b1c 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -988,6 +988,7 @@ pub struct RemoteSettingsContent { )] pub struct DevContainerConnection { pub name: String, + pub remote_user: String, pub container_id: String, pub use_podman: bool, } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 1c41c904ed..08ea880fd5 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1425,7 +1425,7 @@ impl WorkspaceDb { options: RemoteConnectionOptions, ) -> Result { let kind; - let mut user = None; + let user: Option; let mut host = None; let mut port = None; let mut distro = None; @@ -1448,12 +1448,14 @@ impl WorkspaceDb { kind = RemoteConnectionKind::Docker; container_id = Some(options.container_id); name = Some(options.name); - use_podman = Some(options.use_podman) + use_podman = Some(options.use_podman); + user = Some(options.remote_user); } #[cfg(any(test, feature = "test-support"))] RemoteConnectionOptions::Mock(options) => { kind = RemoteConnectionKind::Ssh; host = Some(format!("mock-{}", options.id)); + user = Some(format!("mock-user-{}", options.id)); } } Self::get_or_create_remote_connection_query( @@ -1691,6 +1693,7 @@ impl WorkspaceDb { Some(RemoteConnectionOptions::Docker(DockerConnectionOptions { container_id: container_id?, name: name?, + remote_user: user?, upload_binary_over_docker_exec: false, use_podman: use_podman?, }))