use std::collections::HashMap; use std::fs::read_to_string; use std::io::stderr; use std::path::{PathBuf, MAIN_SEPARATOR}; use crate::control_socket_address::ControlSocket; use crate::futures::StreamExt; use crate::unit_client::UnitClientError; use crate::unitd_process::UnitdProcess; use bollard::container::{Config, ListContainersOptions, StartContainerOptions}; use bollard::image::CreateImageOptions; use bollard::models::{ContainerCreateResponse, ContainerSummary, HostConfig, Mount, MountTypeEnum}; use bollard::secret::ContainerInspectResponse; use bollard::Docker; use regex::Regex; use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; use pbr::ProgressBar; #[derive(Clone, Debug)] pub struct UnitdContainer { pub container_id: Option, pub container_image: String, pub command: Option, pub mounts: HashMap, pub platform: String, details: Option, } impl From<&ContainerSummary> for UnitdContainer { fn from(ctr: &ContainerSummary) -> Self { // we assume paths from the docker api are absolute // they certainly have to be later... let mut mounts = HashMap::new(); if let Some(mts) = &ctr.mounts { for i in mts { if let Some(ref src) = i.source { if let Some(ref dest) = i.destination { mounts.insert(PathBuf::from(dest.clone()), PathBuf::from(src.clone())); } } } } UnitdContainer { container_id: ctr.id.clone(), container_image: format!( "{} (docker)", ctr.image.clone().unwrap_or(String::from("unknown container")), ), command: ctr.command.clone(), mounts: mounts, platform: String::from("Docker"), details: None, } } } impl From<&UnitdContainer> for UnitdProcess { fn from(ctr: &UnitdContainer) -> Self { let version = ctr.details.as_ref().and_then(|details| { details.config.as_ref().and_then(|conf| { conf.labels.as_ref().and_then(|labels| { labels .get("org.opencontainers.image.version") .and_then(|version| Some(version.clone())) }) }) }); let command = ctr.command.clone().and_then(|cmd| { Some(format!( "{}{} [{}{}]", "unit: main v", version.or(Some(String::from(""))).unwrap(), ctr.container_image, ctr.rewrite_socket( cmd.strip_prefix("/usr/local/bin/docker-entrypoint.sh") .or_else(|| Some("")) .unwrap() .to_string() ) )) }); let mut cmds = vec![]; let _ = command.map_or((), |cmd| cmds.push(cmd)); UnitdProcess { all_cmds: cmds, binary_name: ctr.container_image.clone(), process_id: ctr .details .as_ref() .and_then(|details| { details .state .as_ref() .and_then(|state| state.pid.and_then(|pid| Some(pid.clone() as u64))) }) .or(Some(0 as u64)) .unwrap(), executable_path: None, environ: vec![], working_dir: ctr.details.as_ref().and_then(|details| { details.config.as_ref().and_then(|conf| { Some( PathBuf::from( conf.working_dir .as_ref() .map_or(String::new(), |dir| ctr.host_path(dir.clone())), ) .into_boxed_path(), ) }) }), child_pids: vec![], user: None, effective_user: None, container: Some(ctr.clone()), } } } impl Serialize for UnitdContainer { fn serialize(&self, serializer: S) -> Result where S: Serializer, { // 5 = fields to serialize let mut state = serializer.serialize_map(Some(5))?; state.serialize_entry("container_id", &self.container_id)?; state.serialize_entry("container_image", &self.container_image)?; state.serialize_entry("command", &self.command)?; state.serialize_entry("mounts", &self.mounts)?; state.serialize_entry("platform", &self.platform)?; state.end() } } impl UnitdContainer { pub async fn find_unitd_containers() -> Vec { if let Ok(docker) = Docker::connect_with_local_defaults() { match docker.list_containers::(None).await { Err(e) => { eprintln!("{}", e); vec![] } Ok(summary) => { let unitd_command_re = Regex::new(r"^(.* )?unitd( .*)?$").unwrap(); // cant do this functionally because of the async call let mut mapped = vec![]; for ctr in summary { if unitd_command_re.is_match(&ctr.clone().command.or(Some(String::new())).unwrap()) { let mut c = UnitdContainer::from(&ctr); if let Some(names) = ctr.names { if names.len() > 0 { let name = names[0].strip_prefix("/").or(Some(names[0].as_str())).unwrap(); if let Ok(cir) = docker.inspect_container(name, None).await { c.details = Some(cir); } } } mapped.push(c); } } mapped } } } else { vec![] } } pub fn host_path(&self, container_path: String) -> String { let cp = PathBuf::from(container_path); // get only possible mount points // sort to deepest mountpoint first // assumed deepest possible mount point takes precedence let mut keys = self .mounts .clone() .into_keys() .filter(|mp| cp.as_path().starts_with(mp)) .collect::>(); keys.sort_by_key(|a| 0 as isize - a.ancestors().count() as isize); // either return translated path or original prefixed with "container" if keys.len() > 0 { let mut matches = self.mounts[&keys[0]].clone().join( cp.as_path() .strip_prefix(keys[0].clone()) .expect("error checking path prefix"), ); /* Observed on M1 Mac that Docker on OSX * adds a bunch of garbage to the mount path * converting it into a useless directory * that doesnt actually exist */ if cfg!(target_os = "macos") { let mut abs = PathBuf::from(String::from(MAIN_SEPARATOR)); let m = matches .strip_prefix("/host_mnt/private") .unwrap_or(matches.strip_prefix("/host_mnt").unwrap_or(matches.as_path())); // make it absolute again abs.push(m); matches = abs; } matches.to_string_lossy().to_string() } else { format!(":{}", cp.display()) } } pub fn rewrite_socket(&self, command: String) -> String { command .split(" ") .map(|tok| { if tok.starts_with("unix:") { format!( "unix:{}", self.host_path(tok.strip_prefix("unix:").unwrap().to_string()) ) } else { tok.to_string() } }) .collect::>() .join(" ") } pub fn container_is_running(&self) -> Option { self.details .as_ref() .and_then(|details| details.state.as_ref().and_then(|state| state.running)) } } /* deploys a new docker image of tag $image_tag. * mounts $socket to /var/run in the new container. * mounts $application read only to /www. * new container is on host network. * * ON SUCCESS returns vector of warnings from Docker API * ON FAILURE returns wrapped error from Docker API */ pub async fn deploy_new_container( socket: ControlSocket, application: &String, application_read_only: bool, image: &String, ) -> Result, UnitClientError> { match Docker::connect_with_local_defaults() { Ok(docker) => { let mut mounts = vec![]; // if a unix socket is specified, mounts its directory if socket.is_local_socket() { let mount_path = PathBuf::from(socket.clone()).as_path().to_string_lossy().to_string(); mounts.push(Mount { typ: Some(MountTypeEnum::BIND), source: Some(mount_path), target: Some("/var/run".to_string()), ..Default::default() }); } // mount application dir mounts.push(Mount { typ: Some(MountTypeEnum::BIND), source: Some(application.clone()), target: Some("/www".to_string()), read_only: Some(application_read_only), ..Default::default() }); let mut pb = ProgressBar::on(stderr(), 10); let mut totals = HashMap::new(); let mut stream = docker.create_image( Some(CreateImageOptions { from_image: image.as_str(), ..Default::default() }), None, None, ); while let Some(res) = stream.next().await { if let Ok(info) = res { if let Some(id) = info.id { if let Some(_) = totals.get_mut(&id) { if let Some(delta) = info.progress_detail.and_then(|detail| detail.current) { pb.add(delta as u64); } } else { if let Some(total) = info.progress_detail.and_then(|detail| detail.total) { totals.insert(id, total); pb.total += total as u64; } } } } } pb.finish(); // create the new unit container let resp: ContainerCreateResponse; let host_conf = HostConfig { mounts: Some(mounts), network_mode: Some("host".to_string()), ..Default::default() }; let mut container_conf = Config { image: Some(image.clone()), ..Default::default() }; if let ControlSocket::TcpSocket(ref uri) = socket { let port = uri.port_u16().or(Some(80)).unwrap(); // override port container_conf.cmd = Some(vec![ "unitd".to_string(), "--no-daemon".to_string(), "--control".to_string(), format!("{}:{}", uri.host().unwrap(), port), ]); } container_conf.host_config = Some(host_conf); match docker.create_container::(None, container_conf).await { Err(err) => { return Err(UnitClientError::UnitdDockerError { message: err.to_string(), }) } Ok(response) => resp = response, } // create container gives us an ID // but start container requires a name let mut list_container_filters = HashMap::new(); list_container_filters.insert("id".to_string(), vec![resp.id]); match docker .list_containers::(Some(ListContainersOptions { all: true, limit: None, size: false, filters: list_container_filters, })) .await { // somehow our container doesnt exist Err(e) => Err(UnitClientError::UnitdDockerError { message: e.to_string() }), // here it is! Ok(info) => { if info.len() < 1 { return Err(UnitClientError::UnitdDockerError { message: "couldnt find new container".to_string(), }); } else if info[0].names.is_none() || info[0].names.clone().unwrap().len() < 1 { return Err(UnitClientError::UnitdDockerError { message: "new container has no name".to_string(), }); } // start our container match docker .start_container( info[0].names.clone().unwrap()[0].strip_prefix(MAIN_SEPARATOR).unwrap(), None::>, ) .await { Err(err) => Err(UnitClientError::UnitdDockerError { message: err.to_string(), }), Ok(_) => Ok(resp.warnings), } } } } Err(e) => Err(UnitClientError::UnitdDockerError { message: e.to_string() }), } } /* Returns either 64 char docker container ID or None */ pub fn pid_is_dockerized(pid: u64) -> bool { let cg_filepath = format!("/proc/{}/cgroup", pid); match read_to_string(cg_filepath) { Err(e) => { eprintln!("{}", e); false } Ok(contents) => { let docker_re = Regex::new(r"docker-([a-zA-Z0-9]{64})").unwrap(); docker_re.is_match(contents.as_str()) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_path_translation() { let mut mounts = HashMap::new(); mounts.insert("/1/2/3/4/5/6/7".into(), "/0".into()); mounts.insert("/root".into(), "/1".into()); mounts.insert("/root/mid".into(), "/2".into()); mounts.insert("/root/mid/child".into(), "/3".into()); mounts.insert("/mid/child".into(), "/4".into()); mounts.insert("/child".into(), "/5".into()); mounts.insert("/var".into(), "/host_mnt/private/6".into()); mounts.insert("/var/var".into(), "/host_mnt/7".into()); let ctr = UnitdContainer { container_id: None, container_image: String::from(""), command: None, platform: "test".to_string(), details: None, mounts: mounts, }; assert_eq!( "/3/c2/test".to_string(), ctr.host_path("/root/mid/child/c2/test".to_string()) ); assert_eq!( ":/path/to/conf".to_string(), ctr.host_path("/path/to/conf".to_string()) ); if cfg!(target_os = "macos") { assert_eq!("/6/test".to_string(), ctr.host_path("/var/test".to_string())); assert_eq!("/7/test".to_string(), ctr.host_path("/var/var/test".to_string())); } } #[test] fn test_unix_sock_path_translate() { let mut mounts = HashMap::new(); mounts.insert("/var/run".into(), "/tmp".into()); let ctr = UnitdContainer { container_id: None, container_image: String::from(""), command: None, platform: "test".to_string(), details: None, mounts: mounts, }; assert_eq!( ctr.rewrite_socket("unitd --no-daemon --control unix:/var/run/control.unit.sock".to_string()), "unitd --no-daemon --control unix:/tmp/control.unit.sock".to_string() ); } }