diff --git a/crates/dev_container/src/devcontainer_json.rs b/crates/dev_container/src/devcontainer_json.rs index 6033ca29f0..c8573b6b9c 100644 --- a/crates/dev_container/src/devcontainer_json.rs +++ b/crates/dev_container/src/devcontainer_json.rs @@ -232,7 +232,7 @@ pub(crate) struct DevContainer { #[serde(default, deserialize_with = "deserialize_string_or_array")] pub(crate) docker_compose_file: Option>, pub(crate) service: Option, - run_services: Option>, + pub(crate) run_services: Option>, pub(crate) initialize_command: Option, pub(crate) on_create_command: Option, pub(crate) update_content_command: Option, diff --git a/crates/dev_container/src/devcontainer_manifest.rs b/crates/dev_container/src/devcontainer_manifest.rs index cc8fbedb14..0c38538657 100644 --- a/crates/dev_container/src/devcontainer_manifest.rs +++ b/crates/dev_container/src/devcontainer_manifest.rs @@ -1058,7 +1058,11 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true let project_name = self.project_name().await?; self.docker_client - .docker_compose_build(&docker_compose_resources.files, &project_name) + .docker_compose_build( + &docker_compose_resources.files, + &project_name, + dev_container.run_services.as_ref(), + ) .await?; ( self.docker_client @@ -1151,7 +1155,11 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true let project_name = self.project_name().await?; self.docker_client - .docker_compose_build(&docker_compose_resources.files, &project_name) + .docker_compose_build( + &docker_compose_resources.files, + &project_name, + dev_container.run_services.as_ref(), + ) .await?; ( @@ -1785,6 +1793,9 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true command.args(&["-f", &docker_compose_file.display().to_string()]); } command.args(&["up", "-d"]); + if let Some(run_services) = self.dev_container().run_services.as_ref() { + command.args(run_services); + } let output = self .command_runner @@ -4783,6 +4794,111 @@ ENV DOCKER_BUILDKIT=1 ); } + #[gpui::test] + async fn test_spawns_only_requested_compose_services(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + env_logger::try_init().ok(); + let given_devcontainer_contents = r#" + { + "name": "Devcontainer and PostgreSQL", + "dockerComposeFile": "docker-compose.yml", + "service": "devcontainer", + "runServices": ["devcontainer", "db"], + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "updateRemoteUserUID": false + } + "#; + let (test_dependencies, mut devcontainer_manifest) = + init_default_devcontainer_manifest(cx, given_devcontainer_contents) + .await + .unwrap(); + + test_dependencies + .fs + .atomic_write( + PathBuf::from(TEST_PROJECT_PATH).join(".devcontainer/docker-compose.yml"), + r#" +version: '3.8' + +x-base: &base + build: + context: . + dockerfile: Dockerfile + env_file: + - .env + +volumes: + postgres-data: + +services: + app: + <<: *base + ports: + - "3000:3000" + + devcontainer: + <<: *base + ports: + - "3000:3000" + volumes: + - ../..:/workspaces:cached + + db: + image: postgres:14.1 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + - .env + "# + .trim() + .to_string(), + ) + .await + .unwrap(); + + test_dependencies + .fs + .atomic_write( + PathBuf::from(TEST_PROJECT_PATH).join(".devcontainer/Dockerfile"), + r#" +FROM mcr.microsoft.com/devcontainers/rust:2-1-bookworm + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -y install clang lld \ +&& apt-get autoremove -y && apt-get clean -y + "# + .trim() + .to_string(), + ) + .await + .unwrap(); + + devcontainer_manifest.parse_nonremote_vars().unwrap(); + let _devcontainer_up = devcontainer_manifest.build_and_run().await.unwrap(); + + let docker_commands = test_dependencies + .command_runner + .commands_by_program("docker"); + let compose_up = docker_commands + .iter() + .find(|c| { + c.args.first().map(String::as_str) == Some("compose") + && c.args.iter().any(|a| a == "up") + }) + .expect("docker compose up command recorded"); + assert!( + compose_up.args.ends_with(&[ + "up".to_string(), + "-d".to_string(), + "devcontainer".to_string(), + "db".to_string(), + ]), + "compose up should target only the requested service, got: {:?}", + compose_up.args + ); + } + #[cfg(not(target_os = "windows"))] #[gpui::test] async fn test_spawns_devcontainer_with_docker_compose_and_podman(cx: &mut TestAppContext) { @@ -6067,6 +6183,19 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de return Ok(Some(DockerComposeConfig { name: None, services: HashMap::from([ + ( + "devcontainer".to_string(), + DockerComposeService { + image: Some("test_image:latest".to_string()), + volumes: vec![MountDefinition { + source: Some("../..".to_string()), + target: "/workspaces".to_string(), + mount_type: Some("bind".to_string()), + }], + command: vec!["sleep".to_string(), "infinity".to_string()], + ..Default::default() + }, + ), ( "app".to_string(), DockerComposeService { @@ -6193,6 +6322,7 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de &self, _config_files: &Vec, _project_name: &str, + _services: Option<&Vec>, ) -> Result<(), DevContainerError> { Ok(()) } diff --git a/crates/dev_container/src/docker.rs b/crates/dev_container/src/docker.rs index cccbb4373e..6a583681ac 100644 --- a/crates/dev_container/src/docker.rs +++ b/crates/dev_container/src/docker.rs @@ -291,6 +291,7 @@ impl DockerClient for Docker { &self, config_files: &Vec, project_name: &str, + services: Option<&Vec>, ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); if !self.is_podman() { @@ -301,6 +302,9 @@ impl DockerClient for Docker { command.args(&["-f", &docker_compose_file.display().to_string()]); } command.arg("build"); + if let Some(services) = services { + command.args(services); + } let output = command.output().await.map_err(|e| { log::error!("Error running docker compose up: {e}"); @@ -457,6 +461,7 @@ pub(crate) trait DockerClient { &self, config_files: &Vec, project_name: &str, + services: Option<&Vec>, ) -> Result<(), DevContainerError>; async fn run_docker_exec( &self,