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());
}
}