use std::{collections::HashMap, path::PathBuf}; use async_trait::async_trait;use serde::{Deserialize, Deserializer, Serialize, de};use util::command::Command; use crate::{ command_json::evaluate_json_command, devcontainer_api::DevContainerError, devcontainer_json::MountDefinition,}; #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "PascalCase")]pub(crate) struct DockerPs { #[serde(alias = "ID")] pub(crate) id: String,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "PascalCase")]pub(crate) struct DockerState { pub(crate) running: bool,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "PascalCase")]pub(crate) struct DockerInspect { pub(crate) id: String, pub(crate) config: DockerInspectConfig, pub(crate) mounts: Option<Vec<DockerInspectMount>>, pub(crate) state: Option<DockerState>,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]pub(crate) struct DockerConfigLabels { #[serde( default, rename = "devcontainer.metadata", deserialize_with = "deserialize_metadata" )] pub(crate) metadata: Option<Vec<HashMap<String, serde_json_lenient::Value>>>,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "PascalCase")]pub(crate) struct DockerInspectConfig { #[serde(default, deserialize_with = "deserialize_nullable_labels")] pub(crate) labels: DockerConfigLabels, #[serde(rename = "User")] pub(crate) image_user: Option<String>, #[serde(default)] pub(crate) env: Vec<String>,} impl DockerInspectConfig { pub(crate) fn env_as_map(&self) -> Result<HashMap<String, String>, DevContainerError> { let mut map = HashMap::new(); for env_var in &self.env { let Some((key, value)) = env_var.split_once('=') else { log::error!("Unable to parse {env_var} into an environment key-value"); return Err(DevContainerError::DevContainerParseFailed); }; map.insert(key.to_string(), value.to_string()); } Ok(map) }} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "PascalCase")]pub(crate) struct DockerInspectMount { pub(crate) source: String, pub(crate) destination: String,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]pub(crate) struct DockerComposeServiceBuild { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) context: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) dockerfile: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) target: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) args: Option<HashMap<String, String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) additional_contexts: Option<HashMap<String, String>>,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]pub(crate) struct DockerComposeServicePort { #[serde(deserialize_with = "deserialize_string_or_int")] pub(crate) target: String, #[serde(deserialize_with = "deserialize_string_or_int")] pub(crate) published: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) mode: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) protocol: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) host_ip: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) app_protocol: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) name: Option<String>,} fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<String, D::Error>where D: serde::Deserializer<'de>,{ use serde::Deserialize; #[derive(Deserialize)] #[serde(untagged)] enum StringOrInt { String(String), Int(u32), } match StringOrInt::deserialize(deserializer)? { StringOrInt::String(s) => Ok(s), StringOrInt::Int(b) => Ok(b.to_string()), }} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]pub(crate) struct DockerComposeService { pub(crate) image: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) entrypoint: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) cap_add: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) security_opt: Option<Vec<String>>, #[serde( skip_serializing_if = "Option::is_none", default, deserialize_with = "deserialize_labels" )] pub(crate) labels: Option<HashMap<String, String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) build: Option<DockerComposeServiceBuild>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) privileged: Option<bool>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub(crate) volumes: Vec<MountDefinition>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) env_file: Option<Vec<String>>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub(crate) ports: Vec<DockerComposeServicePort>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) network_mode: Option<String>, #[serde( default, skip_serializing_if = "Vec::is_empty", deserialize_with = "deserialize_nullable_vec" )] pub(crate) command: Vec<String>,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]pub(crate) struct DockerComposeVolume { pub(crate) name: String,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]pub(crate) struct DockerComposeConfig { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) name: Option<String>, pub(crate) services: HashMap<String, DockerComposeService>, #[serde(default)] pub(crate) volumes: HashMap<String, DockerComposeVolume>,} pub(crate) struct Docker { docker_cli: String, has_buildx: bool,} impl DockerInspect { pub(crate) fn is_running(&self) -> bool { self.state.as_ref().map_or(false, |s| s.running) }} impl Docker { pub(crate) async fn new(docker_cli: &str) -> Self { let has_buildx = if docker_cli == "podman" { false } else { let output = Command::new(docker_cli) .args(["buildx", "version"]) .output() .await; output.map(|o| o.status.success()).unwrap_or(false) }; if !has_buildx && docker_cli != "podman" { log::info!( "docker buildx not found; dev container builds will use the scratch-image fallback" ); } Self { docker_cli: docker_cli.to_string(), has_buildx, } } fn is_podman(&self) -> bool { self.docker_cli == "podman" } async fn pull_image(&self, image: &String) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); command.args(&["pull", "--", image]); let output = command.output().await.map_err(|e| { log::error!("Error pulling image: {e}"); DevContainerError::ResourceFetchFailed })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("Non-success result from docker pull: {stderr}"); return Err(DevContainerError::ResourceFetchFailed); } Ok(()) } fn create_docker_query_containers(&self, filters: Vec<String>) -> Command { let mut command = Command::new(&self.docker_cli); command.args(&["ps", "-a"]); for filter in filters { command.arg("--filter"); command.arg(filter); } command.arg("--format={{ json . }}"); command } fn create_docker_inspect(&self, id: &str) -> Command { let mut command = Command::new(&self.docker_cli); command.args(&["inspect", "--format={{json . }}", id]); command } fn create_docker_compose_config_command(&self, config_files: &Vec<PathBuf>) -> Command { let mut command = Command::new(&self.docker_cli); command.arg("compose"); for file_path in config_files { command.args(&["-f", &file_path.display().to_string()]); } command.args(&["config", "--format", "json"]); command }} #[async_trait]impl DockerClient for Docker { async fn inspect(&self, id: &String) -> Result<DockerInspect, DevContainerError> { // Try to pull the image, continue on failure; Image may be local only, id a reference to a running container self.pull_image(id).await.ok(); let command = self.create_docker_inspect(id); let Some(docker_inspect): Option<DockerInspect> = evaluate_json_command(command).await? else { log::error!("Docker inspect produced no deserializable output"); return Err(DevContainerError::CommandFailed(self.docker_cli.clone())); }; Ok(docker_inspect) } async fn get_docker_compose_config( &self, config_files: &Vec<PathBuf>, ) -> Result<Option<DockerComposeConfig>, DevContainerError> { let command = self.create_docker_compose_config_command(config_files); evaluate_json_command(command).await } async fn docker_compose_build( &self, config_files: &Vec<PathBuf>, project_name: &str, ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); if !self.is_podman() { command.env("DOCKER_BUILDKIT", "1"); } command.args(&["compose", "--project-name", project_name]); for docker_compose_file in config_files { command.args(&["-f", &docker_compose_file.display().to_string()]); } command.arg("build"); let output = command.output().await.map_err(|e| { log::error!("Error running docker compose up: {e}"); DevContainerError::CommandFailed(command.get_program().display().to_string()) })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("Non-success status from docker compose up: {}", stderr); return Err(DevContainerError::CommandFailed( command.get_program().display().to_string(), )); } Ok(()) } async fn run_docker_exec( &self, container_id: &str, remote_folder: &str, user: &str, env: &HashMap<String, String>, inner_command: Command, ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); command.args(&["exec", "-w", remote_folder, "-u", user]); for (k, v) in env.iter() { command.arg("-e"); let env_declaration = format!("{}={}", k, v); command.arg(&env_declaration); } command.arg(container_id); command.arg("sh"); let mut inner_program_script: Vec<String> = vec![inner_command.get_program().display().to_string()]; let mut args: Vec<String> = inner_command .get_args() .map(|arg| arg.display().to_string()) .collect(); inner_program_script.append(&mut args); command.args(&["-c", &inner_program_script.join(" ")]); let output = command.output().await.map_err(|e| { log::error!("Error running command {e} in container exec"); DevContainerError::ContainerNotValid(container_id.to_string()) })?; if !output.status.success() { let std_err = String::from_utf8_lossy(&output.stderr); log::error!("Command produced a non-successful output. StdErr: {std_err}"); } let std_out = String::from_utf8_lossy(&output.stdout); log::debug!("Command output:\n {std_out}"); Ok(()) } async fn start_container(&self, id: &str) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); command.args(&["start", id]); let output = command.output().await.map_err(|e| { log::error!("Error running docker start: {e}"); DevContainerError::CommandFailed(command.get_program().display().to_string()) })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("Non-success status from docker start: {stderr}"); return Err(DevContainerError::CommandFailed( command.get_program().display().to_string(), )); } Ok(()) } async fn find_process_by_filters( &self, filters: Vec<String>, ) -> Result<Option<DockerPs>, DevContainerError> { let command = self.create_docker_query_containers(filters); evaluate_json_command(command).await } fn docker_cli(&self) -> String { self.docker_cli.clone() } fn supports_compose_buildkit(&self) -> bool { self.has_buildx }} #[async_trait]pub(crate) trait DockerClient { async fn inspect(&self, id: &String) -> Result<DockerInspect, DevContainerError>; async fn get_docker_compose_config( &self, config_files: &Vec<PathBuf>, ) -> Result<Option<DockerComposeConfig>, DevContainerError>; async fn docker_compose_build( &self, config_files: &Vec<PathBuf>, project_name: &str, ) -> Result<(), DevContainerError>; async fn run_docker_exec( &self, container_id: &str, remote_folder: &str, user: &str, env: &HashMap<String, String>, inner_command: Command, ) -> Result<(), DevContainerError>; async fn start_container(&self, id: &str) -> Result<(), DevContainerError>; async fn find_process_by_filters( &self, filters: Vec<String>, ) -> Result<Option<DockerPs>, DevContainerError>; fn supports_compose_buildkit(&self) -> bool; /// This operates as an escape hatch for more custom uses of the docker API. /// See DevContainerManifest::create_docker_build as an example fn docker_cli(&self) -> String;} fn deserialize_labels<'de, D>(deserializer: D) -> Result<Option<HashMap<String, String>>, D::Error>where D: Deserializer<'de>,{ struct LabelsVisitor; impl<'de> de::Visitor<'de> for LabelsVisitor { type Value = Option<HashMap<String, String>>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a sequence of strings or a map of string key-value pairs") } fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> where A: de::SeqAccess<'de>, { let values = Vec::<String>::deserialize(de::value::SeqAccessDeserializer::new(seq))?; Ok(Some( values .iter() .filter_map(|v| { let (key, value) = v.split_once('=')?; Some((key.to_string(), value.to_string())) }) .collect(), )) } fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error> where M: de::MapAccess<'de>, { HashMap::<String, String>::deserialize(de::value::MapAccessDeserializer::new(map)) .map(|v| Some(v)) } fn visit_none<E>(self) -> Result<Self::Value, E> where E: de::Error, { Ok(None) } fn visit_unit<E>(self) -> Result<Self::Value, E> where E: de::Error, { Ok(None) } } deserializer.deserialize_any(LabelsVisitor)} fn deserialize_nullable_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>where D: Deserializer<'de>, T: Deserialize<'de>,{ Option::<Vec<T>>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())} fn deserialize_nullable_labels<'de, D>(deserializer: D) -> Result<DockerConfigLabels, D::Error>where D: Deserializer<'de>,{ Option::<DockerConfigLabels>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())} fn deserialize_metadata<'de, D>( deserializer: D,) -> Result<Option<Vec<HashMap<String, serde_json_lenient::Value>>>, D::Error>whereLines 1–500 of 1317. Load more
use std::{collections::HashMap, path::PathBuf}; use async_trait::async_trait; use serde::{Deserialize, Deserializer, Serialize, de}; use util::command::Command; use crate::{ command_json::evaluate_json_command, devcontainer_api::DevContainerError, devcontainer_json::MountDefinition, }; #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "PascalCase")] pub(crate) struct DockerPs { #[serde(alias = "ID")] pub(crate) id: String, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "PascalCase")] pub(crate) struct DockerState { pub(crate) running: bool, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "PascalCase")] pub(crate) struct DockerInspect { pub(crate) id: String, pub(crate) config: DockerInspectConfig, pub(crate) mounts: Option<Vec<DockerInspectMount>>, pub(crate) state: Option<DockerState>, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] pub(crate) struct DockerConfigLabels { #[serde( default, rename = "devcontainer.metadata", deserialize_with = "deserialize_metadata" )] pub(crate) metadata: Option<Vec<HashMap<String, serde_json_lenient::Value>>>, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "PascalCase")] pub(crate) struct DockerInspectConfig { #[serde(default, deserialize_with = "deserialize_nullable_labels")] pub(crate) labels: DockerConfigLabels, #[serde(rename = "User")] pub(crate) image_user: Option<String>, #[serde(default)] pub(crate) env: Vec<String>, } impl DockerInspectConfig { pub(crate) fn env_as_map(&self) -> Result<HashMap<String, String>, DevContainerError> { let mut map = HashMap::new(); for env_var in &self.env { let Some((key, value)) = env_var.split_once('=') else { log::error!("Unable to parse {env_var} into an environment key-value"); return Err(DevContainerError::DevContainerParseFailed); }; map.insert(key.to_string(), value.to_string()); } Ok(map) } } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "PascalCase")] pub(crate) struct DockerInspectMount { pub(crate) source: String, pub(crate) destination: String, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] pub(crate) struct DockerComposeServiceBuild { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) context: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) dockerfile: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) target: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) args: Option<HashMap<String, String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) additional_contexts: Option<HashMap<String, String>>, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] pub(crate) struct DockerComposeServicePort { #[serde(deserialize_with = "deserialize_string_or_int")] pub(crate) target: String, #[serde(deserialize_with = "deserialize_string_or_int")] pub(crate) published: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) mode: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) protocol: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) host_ip: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) app_protocol: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) name: Option<String>, } fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<String, D::Error> where D: serde::Deserializer<'de>, { use serde::Deserialize; #[derive(Deserialize)] #[serde(untagged)] enum StringOrInt { String(String), Int(u32), } match StringOrInt::deserialize(deserializer)? { StringOrInt::String(s) => Ok(s), StringOrInt::Int(b) => Ok(b.to_string()), } } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] pub(crate) struct DockerComposeService { pub(crate) image: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) entrypoint: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) cap_add: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) security_opt: Option<Vec<String>>, #[serde( skip_serializing_if = "Option::is_none", default, deserialize_with = "deserialize_labels" )] pub(crate) labels: Option<HashMap<String, String>>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) build: Option<DockerComposeServiceBuild>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) privileged: Option<bool>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub(crate) volumes: Vec<MountDefinition>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) env_file: Option<Vec<String>>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub(crate) ports: Vec<DockerComposeServicePort>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) network_mode: Option<String>, #[serde( default, skip_serializing_if = "Vec::is_empty", deserialize_with = "deserialize_nullable_vec" )] pub(crate) command: Vec<String>, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] pub(crate) struct DockerComposeVolume { pub(crate) name: String, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] pub(crate) struct DockerComposeConfig { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) name: Option<String>, pub(crate) services: HashMap<String, DockerComposeService>, #[serde(default)] pub(crate) volumes: HashMap<String, DockerComposeVolume>, } pub(crate) struct Docker { docker_cli: String, has_buildx: bool, } impl DockerInspect { pub(crate) fn is_running(&self) -> bool { self.state.as_ref().map_or(false, |s| s.running) } } impl Docker { pub(crate) async fn new(docker_cli: &str) -> Self { let has_buildx = if docker_cli == "podman" { false } else { let output = Command::new(docker_cli) .args(["buildx", "version"]) .output() .await; output.map(|o| o.status.success()).unwrap_or(false) }; if !has_buildx && docker_cli != "podman" { log::info!( "docker buildx not found; dev container builds will use the scratch-image fallback" ); } Self { docker_cli: docker_cli.to_string(), has_buildx, } } fn is_podman(&self) -> bool { self.docker_cli == "podman" } async fn pull_image(&self, image: &String) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); command.args(&["pull", "--", image]); let output = command.output().await.map_err(|e| { log::error!("Error pulling image: {e}"); DevContainerError::ResourceFetchFailed })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("Non-success result from docker pull: {stderr}"); return Err(DevContainerError::ResourceFetchFailed); } Ok(()) } fn create_docker_query_containers(&self, filters: Vec<String>) -> Command { let mut command = Command::new(&self.docker_cli); command.args(&["ps", "-a"]); for filter in filters { command.arg("--filter"); command.arg(filter); } command.arg("--format={{ json . }}"); command } fn create_docker_inspect(&self, id: &str) -> Command { let mut command = Command::new(&self.docker_cli); command.args(&["inspect", "--format={{json . }}", id]); command } fn create_docker_compose_config_command(&self, config_files: &Vec<PathBuf>) -> Command { let mut command = Command::new(&self.docker_cli); command.arg("compose"); for file_path in config_files { command.args(&["-f", &file_path.display().to_string()]); } command.args(&["config", "--format", "json"]); command } } #[async_trait] impl DockerClient for Docker { async fn inspect(&self, id: &String) -> Result<DockerInspect, DevContainerError> { // Try to pull the image, continue on failure; Image may be local only, id a reference to a running container self.pull_image(id).await.ok(); let command = self.create_docker_inspect(id); let Some(docker_inspect): Option<DockerInspect> = evaluate_json_command(command).await? else { log::error!("Docker inspect produced no deserializable output"); return Err(DevContainerError::CommandFailed(self.docker_cli.clone())); }; Ok(docker_inspect) } async fn get_docker_compose_config( &self, config_files: &Vec<PathBuf>, ) -> Result<Option<DockerComposeConfig>, DevContainerError> { let command = self.create_docker_compose_config_command(config_files); evaluate_json_command(command).await } async fn docker_compose_build( &self, config_files: &Vec<PathBuf>, project_name: &str, ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); if !self.is_podman() { command.env("DOCKER_BUILDKIT", "1"); } command.args(&["compose", "--project-name", project_name]); for docker_compose_file in config_files { command.args(&["-f", &docker_compose_file.display().to_string()]); } command.arg("build"); let output = command.output().await.map_err(|e| { log::error!("Error running docker compose up: {e}"); DevContainerError::CommandFailed(command.get_program().display().to_string()) })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("Non-success status from docker compose up: {}", stderr); return Err(DevContainerError::CommandFailed( command.get_program().display().to_string(), )); } Ok(()) } async fn run_docker_exec( &self, container_id: &str, remote_folder: &str, user: &str, env: &HashMap<String, String>, inner_command: Command, ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); command.args(&["exec", "-w", remote_folder, "-u", user]); for (k, v) in env.iter() { command.arg("-e"); let env_declaration = format!("{}={}", k, v); command.arg(&env_declaration); } command.arg(container_id); command.arg("sh"); let mut inner_program_script: Vec<String> = vec![inner_command.get_program().display().to_string()]; let mut args: Vec<String> = inner_command .get_args() .map(|arg| arg.display().to_string()) .collect(); inner_program_script.append(&mut args); command.args(&["-c", &inner_program_script.join(" ")]); let output = command.output().await.map_err(|e| { log::error!("Error running command {e} in container exec"); DevContainerError::ContainerNotValid(container_id.to_string()) })?; if !output.status.success() { let std_err = String::from_utf8_lossy(&output.stderr); log::error!("Command produced a non-successful output. StdErr: {std_err}"); } let std_out = String::from_utf8_lossy(&output.stdout); log::debug!("Command output:\n {std_out}"); Ok(()) } async fn start_container(&self, id: &str) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); command.args(&["start", id]); let output = command.output().await.map_err(|e| { log::error!("Error running docker start: {e}"); DevContainerError::CommandFailed(command.get_program().display().to_string()) })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("Non-success status from docker start: {stderr}"); return Err(DevContainerError::CommandFailed( command.get_program().display().to_string(), )); } Ok(()) } async fn find_process_by_filters( &self, filters: Vec<String>, ) -> Result<Option<DockerPs>, DevContainerError> { let command = self.create_docker_query_containers(filters); evaluate_json_command(command).await } fn docker_cli(&self) -> String { self.docker_cli.clone() } fn supports_compose_buildkit(&self) -> bool { self.has_buildx } } #[async_trait] pub(crate) trait DockerClient { async fn inspect(&self, id: &String) -> Result<DockerInspect, DevContainerError>; async fn get_docker_compose_config( &self, config_files: &Vec<PathBuf>, ) -> Result<Option<DockerComposeConfig>, DevContainerError>; async fn docker_compose_build( &self, config_files: &Vec<PathBuf>, project_name: &str, ) -> Result<(), DevContainerError>; async fn run_docker_exec( &self, container_id: &str, remote_folder: &str, user: &str, env: &HashMap<String, String>, inner_command: Command, ) -> Result<(), DevContainerError>; async fn start_container(&self, id: &str) -> Result<(), DevContainerError>; async fn find_process_by_filters( &self, filters: Vec<String>, ) -> Result<Option<DockerPs>, DevContainerError>; fn supports_compose_buildkit(&self) -> bool; /// This operates as an escape hatch for more custom uses of the docker API. /// See DevContainerManifest::create_docker_build as an example fn docker_cli(&self) -> String; } fn deserialize_labels<'de, D>(deserializer: D) -> Result<Option<HashMap<String, String>>, D::Error> where D: Deserializer<'de>, { struct LabelsVisitor; impl<'de> de::Visitor<'de> for LabelsVisitor { type Value = Option<HashMap<String, String>>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a sequence of strings or a map of string key-value pairs") } fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> where A: de::SeqAccess<'de>, { let values = Vec::<String>::deserialize(de::value::SeqAccessDeserializer::new(seq))?; Ok(Some( values .iter() .filter_map(|v| { let (key, value) = v.split_once('=')?; Some((key.to_string(), value.to_string())) }) .collect(), )) } fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error> where M: de::MapAccess<'de>, { HashMap::<String, String>::deserialize(de::value::MapAccessDeserializer::new(map)) .map(|v| Some(v)) } fn visit_none<E>(self) -> Result<Self::Value, E> where E: de::Error, { Ok(None) } fn visit_unit<E>(self) -> Result<Self::Value, E> where E: de::Error, { Ok(None) } } deserializer.deserialize_any(LabelsVisitor) } fn deserialize_nullable_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error> where D: Deserializer<'de>, T: Deserialize<'de>, { Option::<Vec<T>>::deserialize(deserializer).map(|opt| opt.unwrap_or_default()) } fn deserialize_nullable_labels<'de, D>(deserializer: D) -> Result<DockerConfigLabels, D::Error> where D: Deserializer<'de>, { Option::<DockerConfigLabels>::deserialize(deserializer).map(|opt| opt.unwrap_or_default()) } fn deserialize_metadata<'de, D>( deserializer: D, ) -> Result<Option<Vec<HashMap<String, serde_json_lenient::Value>>>, D::Error> where