playbook_api/systems/
docker.rs

1use std::ffi::{CString, OsStr};
2use std::collections::HashMap;
3use std::path::Path;
4use regex::Regex;
5use nix::unistd::{fork, execvp, ForkResult};
6use nix::sys::wait::{waitpid, WaitStatus};
7use colored::Colorize;
8use ymlctx::context::{Context, CtxObj};
9use crate::{TaskError, TaskErrorSource};
10use super::Infrastructure;
11
12/// Local docker service
13pub struct Docker;
14
15impl Infrastructure for Docker {
16    fn start<I>(&self, ctx_docker: Context, cmd: I) -> Result<String, TaskError>
17      where I: IntoIterator, I::Item: AsRef<std::ffi::OsStr>
18    {
19        start(ctx_docker, cmd)
20    }
21}
22
23pub fn inside_docker() -> bool {
24    let status = std::process::Command::new("grep").args(&["-q", "docker", "/proc/1/cgroup"])
25        .status().expect("I/O error");
26    match status.code() {
27        Some(code) => code==0,
28        None => unreachable!()
29    }
30}
31
32pub fn start<I, S>(ctx_docker: Context, cmd: I) -> Result<String, TaskError>
33  where I: IntoIterator<Item = S>, S: AsRef<OsStr>
34{
35    let username;
36    let output = std::process::Command::new("id").output().unwrap();
37    let mut id_stdout = String::from_utf8_lossy(&output.stdout).into_owned();
38    let newline_len = id_stdout.trim_right().len();
39    id_stdout.truncate(newline_len);
40    let rule = Regex::new(r"^uid=(?P<uid>[0-9]+)(\((?P<user>\w+)\))? gid=(?P<gid>[0-9]+)(\((?P<group>\w+)\))?").unwrap();
41    if let Some(caps) = rule.captures(&id_stdout) {
42        username = caps.name("user").unwrap().as_str().to_owned();
43    }
44    else {
45        return Err(TaskError { msg: String::from("Failed to identify the user."), src: TaskErrorSource::Internal });
46    }
47    let mut userinfo = HashMap::new();
48    crate::copy_user_info(&mut userinfo, &username);
49    let home = format!("/home/{}", &username);
50    let mut docker_run: Vec<String> = ["docker", "run", "--init", "--rm"].iter().map(|&s| {s.to_owned()}).collect();
51    if let Some(CtxObj::Bool(interactive)) = ctx_docker.get("interactive") {
52        if *interactive {
53            docker_run.push(String::from("-it"));
54        }
55        else {
56            // * PyO3 & Python don't print without either -u or -t
57            docker_run.push(String::from("-t"));
58        }
59    }
60    else {
61        docker_run.push(String::from("-it"));
62    }
63    docker_run.push(String::from("--cap-drop=ALL"));
64    if let Some(CtxObj::Str(runtime)) = ctx_docker.get("runtime") {
65        docker_run.push(format!("--runtime={}", runtime));
66    }
67    if let Some(CtxObj::Str(ipc_namespace)) = ctx_docker.get("ipc") {
68        docker_run.push(String::from("--ipc"));
69        docker_run.push(ipc_namespace.to_owned());
70    }
71    if let Some(CtxObj::Str(net_namespace)) = ctx_docker.get("network") {
72        docker_run.push(String::from("--network"));
73        docker_run.push(net_namespace.to_owned());
74    }
75    docker_run.push(String::from("-v"));
76    docker_run.push(format!("{}:{}/current-ro:ro", std::env::current_dir().unwrap().to_str().unwrap(), &home));
77    docker_run.push(String::from("-w"));
78    docker_run.push(format!("{}/current-ro", &home));
79    if let Some(CtxObj::Array(volumes)) = ctx_docker.get("volumes") {
80        for v in volumes {
81            if let CtxObj::Str(vol) = v {
82                if let Some(i) = vol.find(":") {
83                    let (src, dst) = vol.split_at(i);
84                    let suffix = if dst.ends_with(":ro") || dst.ends_with(":rw") || dst.ends_with(":z") || dst.ends_with(":Z") { "" } else { ":ro" };
85                    if let Ok(src) = Path::new(src).canonicalize() {
86                        docker_run.push(String::from("-v"));
87                        docker_run.push(format!("{}{}{}", src.to_str().unwrap(), dst, suffix));
88                    }
89                }
90            }
91        }
92    }
93    if let Some(CtxObj::Array(ports)) = ctx_docker.get("ports") {
94        for p in ports {
95            if let CtxObj::Str(port_map) = p {
96                docker_run.push(String::from("-p"));
97                docker_run.push(port_map.to_owned());
98            }
99        }
100    }
101    if let Some(CtxObj::Bool(gui)) = ctx_docker.get("gui") {
102        if *gui {
103            // TODO verify permissions
104            docker_run.extend::<Vec<String>>([
105                "--network", "host", "-e", "DISPLAY", "-v", "/tmp/.X11-unix:/tmp/.X11-unix:rw",
106                "-v", &format!("{}/.Xauthority:{}/.Xauthority:ro", userinfo["home_dir"], home), 
107            ].iter().map(|&s| {s.to_owned()}).collect());
108        }
109    }
110    if let Some(CtxObj::Array(envs)) = ctx_docker.get("environment") {
111        for v in envs {
112            if let CtxObj::Str(var) = v {
113                docker_run.push(String::from("-e"));
114                docker_run.push(var.to_owned());
115            }
116        }
117    }
118    if let Some(CtxObj::Str(impersonate)) = ctx_docker.get("impersonate") {
119        if impersonate == "dynamic" {
120            docker_run.push(String::from("--cap-add=SETUID"));
121            docker_run.push(String::from("--cap-add=SETGID"));
122            docker_run.push(String::from("--cap-add=CHOWN")); // TODO possibility to restrict this?
123            docker_run.push(String::from("-u"));
124            docker_run.push(String::from("root"));
125            docker_run.push(String::from("-e"));
126            docker_run.push(format!("IMPERSONATE={}", &id_stdout));
127            docker_run.push(String::from("--entrypoint"));
128            docker_run.push(String::from("/usr/bin/playbook"));
129        }
130        else {
131            docker_run.push(String::from("-u"));
132            docker_run.push(impersonate.to_owned());
133        }
134    }
135    else {
136        docker_run.push(String::from("-u"));
137        docker_run.push(format!("{}:{}", userinfo["uid"], userinfo["gid"]));
138    }
139    if let Some(CtxObj::Str(name)) = ctx_docker.get("name") {
140        docker_run.push(format!("--name={}", name));
141    }
142    if let Some(CtxObj::Str(image_name)) = ctx_docker.get("image") {
143        docker_run.push(image_name.to_owned());
144    }
145    else {
146        return Err(TaskError {  msg: String::from("The Docker image specification was invalid."), src: TaskErrorSource::Internal });
147    }
148    docker_run.extend::<Vec<String>>(cmd.into_iter().map(|s| {s.as_ref().to_str().unwrap().to_owned()}).collect());
149    let docker_cmd = crate::format_cmd(docker_run.clone());
150    info!("{}", &docker_cmd);
151    #[cfg(feature = "ci_only")] // Let's see the docker command during testing.
152    println!("{}", &docker_cmd);
153    let docker_linux: Vec<CString> = docker_run.iter().map(|s| {CString::new(s as &str).unwrap()}).collect();
154    match fork() {
155        Ok(ForkResult::Child) => {
156            match execvp(&CString::new("docker").unwrap(), &docker_linux) {
157                Ok(_void) => unreachable!(),
158                Err(e) => Err(TaskError { msg: format!("Failed to issue the Docker command. {}", e), src: TaskErrorSource::NixError(e) }),
159            }
160        },
161        Ok(ForkResult::Parent { child, .. }) => {
162            match waitpid(child, None) {
163                Ok(status) => match status {
164                    WaitStatus::Exited(_, exit_code) => {
165                        if exit_code == 0 { Ok(docker_cmd) }
166                        else {
167                            Err(TaskError {
168                                msg: format!("The container has returned a non-zero exit code ({}).", exit_code.to_string().red()),
169                                src: TaskErrorSource::ExitCode(exit_code)
170                            })
171                        }
172                    },
173                    WaitStatus::Signaled(_, sig, _core_dump) => {
174                        Err(TaskError {
175                            msg: format!("The container has received a signal ({:?}).", sig),
176                            src: TaskErrorSource::Signal(sig)
177                        })
178                    },
179                    WaitStatus::Stopped(_, _sig) => unreachable!(),
180                    WaitStatus::Continued(_) => unreachable!(),
181                    WaitStatus::StillAlive => unreachable!(),
182                    _ => unimplemented!()
183                },
184                Err(e) => Err(TaskError { msg: String::from("Failed to keep track of the child process."), src: TaskErrorSource::NixError(e) })
185            }
186        },
187        Err(e) => Err(TaskError { msg: String::from("Failed to spawn a new process."), src: TaskErrorSource::NixError(e) }),
188    }
189}