From 85d03d5122ee75d8b56a80d863da5e15d98dfd9f Mon Sep 17 00:00:00 2001 From: KyleBarton Date: Mon, 2 Feb 2026 16:43:51 -0800 Subject: [PATCH] Use remote user from devcontainer CLI and respect shell in passwd (#48230) Closes #46252 Uses the `remoteUser` property returned from the devcontainer CLI so that settings are respected. Additionally, checks for a default shell in `passwd` if `$SHELL` is not set. Release Notes: - Fixed remote_user and shell inconsistencies from within dev containers --- crates/dev_container/src/devcontainer_api.rs | 8 +- .../recent_projects/src/remote_connections.rs | 1 + crates/remote/src/transport/docker.rs | 145 ++++++++++++------ .../settings_content/src/settings_content.rs | 1 + crates/workspace/src/persistence.rs | 7 +- 5 files changed, 112 insertions(+), 50 deletions(-) diff --git a/crates/dev_container/src/devcontainer_api.rs b/crates/dev_container/src/devcontainer_api.rs index dae7b61b1f684837fa2cbea0d3e1796ccaa6253a..6e7a2a336bfa231ed668df5bb6046c7b05af9636 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 96b6981577507b3e9024889dc8f144fdc8f4f0f1..9d6199786cb6f251a792fa32e8caccd9351d00d3 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 713e532704063a244b94a7f2fb0fbd5a47727f23..7a8c22e0fc4389c6702260ac94623349c51a6609 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(); + } + }, + Err(e) => { + 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(); } - shell => shell.to_owned(), }, Err(e) => { - log::error!("Failed to get shell: {e}"); - default_shell.to_owned() + 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,44 +394,76 @@ impl DockerExecConnection { Ok(()) } - async fn upload_file( - &self, - src_path: &Path, - dest_path: &RelPath, - remote_dir_for_server: &str, + async fn upload_and_chown( + docker_cli: String, + connection_options: DockerConnectionOptions, + src_path: String, + dst_path: String, ) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - - let src_path_display = src_path.display().to_string(); - let dest_path_str = dest_path.display(self.path_style()); - - let mut command = util::command::new_smol_command(self.docker_cli()); + let mut command = util::command::new_smol_command(&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 - )); + 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 upload file via docker cp {src_path_display} -> {dest_path_str}: {stderr}", - ); + log::debug!("failed to change ownership for via chown: {stderr}",); anyhow::bail!( - "failed to upload file via docker cp {} -> {}: {}", - src_path_display, - dest_path_str, + "failed to change ownership for zed_remote_server via chown: {}", stderr, ); } + async fn upload_file( + &self, + src_path: &Path, + dest_path: &RelPath, + remote_dir_for_server: &str, + ) -> Result<()> { + log::debug!("uploading file {:?} to {:?}", src_path, dest_path); + + 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); + + Self::upload_and_chown( + self.docker_cli().to_string(), + self.connection_options.clone(), + src_path_display, + full_server_path, + ) + .await + } + async fn run_docker_command( &self, subcommand: &str, @@ -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 - )); - - cx.background_spawn(async move { - let output = command.output().await?; + let upload_task = Self::upload_and_chown( + self.docker_cli().to_string(), + self.connection_options.clone(), + src_path_display, + dest_path_str, + ); - 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 86c58dc5400e441bdca9fe8c33c4a66adef6464e..99870b5b1c3da2c405936c10c6fad06cc7499b2a 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 1c41c904edf94c0ad79e7beda2e116ed3148993f..08ea880fd573b608400613d54bfec47b3984f260 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?, }))