playbook_api/systems/
docker.rs1use 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
12pub 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 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 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")); 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")] 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}