summaryrefslogblamecommitdiffhomepage
path: root/tools/unitctl/unit-client-rs/src/unitd_docker.rs
blob: d5028afcc524a16e3e99191a20bf6964ffe1a0e7 (plain) (tree)

























































































































































































































































































                                                                                                               
use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::PathBuf;

use crate::unitd_process::UnitdProcess;

use bollard::secret::ContainerInspectResponse;
use regex::Regex;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};

use bollard::{models::ContainerSummary, Docker};

#[derive(Clone, Debug)]
pub struct UnitdContainer {
    pub container_id: Option<String>,
    pub container_image: String,
    pub command: Option<String>,
    pub mounts: HashMap<PathBuf, PathBuf>,
    pub platform: String,
    details: Option<ContainerInspectResponse>,
}

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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        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<UnitdContainer> {
        if let Ok(docker) = Docker::connect_with_local_defaults() {
            match docker.list_containers::<String>(None).await {
                Err(e) => {
                    eprintln!("{}", e);
                    vec![]
                }
                Ok(summary) => {
                    // cant do this functionally because of the async call
                    let mut mapped = vec![];
                    for ctr in summary {
                        if ctr.clone().image.or(Some(String::new())).unwrap().contains("unit") {
                            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::<Vec<_>>();
        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 {
            self.mounts[&keys[0]]
                .clone()
                .join(
                    cp.as_path()
                        .strip_prefix(keys[0].clone())
                        .expect("error checking path prefix"),
                )
                .to_string_lossy()
                .to_string()
        } else {
            format!("<container>:{}", 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::<Vec<_>>()
            .join(" ")
    }

    pub fn container_is_running(&self) -> Option<bool> {
        self.details
            .as_ref()
            .and_then(|details| details.state.as_ref().and_then(|state| state.running))
    }
}

/* 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());

        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!(
            "<container>:/path/to/conf".to_string(),
            ctr.host_path("/path/to/conf".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());

    }
}