summaryrefslogtreecommitdiffhomepage
path: root/tools/unitctl/unit-client-rs/src/unitd_docker.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tools/unitctl/unit-client-rs/src/unitd_docker.rs')
-rw-r--r--tools/unitctl/unit-client-rs/src/unitd_docker.rs456
1 files changed, 456 insertions, 0 deletions
diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs
new file mode 100644
index 00000000..2b9e0c7d
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs
@@ -0,0 +1,456 @@
+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<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,
+ {
+ // 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<UnitdContainer> {
+ if let Ok(docker) = Docker::connect_with_local_defaults() {
+ match docker.list_containers::<String>(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::<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 {
+ 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!("<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))
+ }
+}
+
+/* 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<Vec<String>, 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::<String, String>(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::<String>(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::<StartContainerOptions<String>>,
+ )
+ .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!(
+ "<container>:/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()
+ );
+ }
+}