1use std::{
4 ffi::OsString,
5 process::{Command, ExitStatus, Stdio},
6};
7
8use shared_child::SharedChild;
9
10use crate::{Error, Result};
11
12pub enum Network
15{
16 Bridge,
18 Host,
20 None,
22}
23
24pub enum Ipc
26{
27 None,
29 Host
31}
32
33fn generate_name(prefix: String) -> String
41{
42 use rand::Rng;
43 const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
44 let mut rng = rand::rng();
45 let rand_string: String = (0..10)
46 .map(|_| {
47 let idx = rng.random_range(0..CHARSET.len());
48 CHARSET[idx] as char
49 })
50 .collect();
51 format!("{}-{}", prefix, rand_string)
52}
53
54pub struct Configurator
56{
57 image: String,
58 capture_stdio: bool,
59 interactive: bool,
60 tty: bool,
61 daemon: bool,
62 privileged: bool,
63 x11_forwarding: bool,
64 network: Network,
65 mounts: Vec<(String, String)>,
66 env: Vec<(String, String)>,
67 username: Option<String>,
68 work_directory: Option<String>,
69 clean_up: bool,
70 command: Vec<String>,
71 name: String,
72 port_mappings: Vec<(u16, u16)>,
73 ipc: Ipc,
74 ipc_size: Option<String>
75}
76
77impl Configurator
78{
79 pub fn create(self) -> Container
81 {
82 Container {
83 config: self,
84 process: None,
85 }
86 }
87 pub fn set_command(mut self, command: impl IntoIterator<Item = impl Into<String>>) -> Self
89 {
90 self.command = command
91 .into_iter()
92 .map(|x| -> String { x.into() })
93 .collect();
94 self
95 }
96 pub fn set_capture_stdio(mut self, capture: bool) -> Self
98 {
99 self.capture_stdio = capture;
100 self
101 }
102 pub fn set_interactive(mut self, interactive: bool) -> Self
104 {
105 self.interactive = interactive;
106 self
107 }
108 pub fn set_tty(mut self, tty: bool) -> Self
110 {
111 self.tty = tty;
112 self
113 }
114 #[deprecated(since = "0.2.4", note = "please use `set_privileged` instead")]
116 pub fn set_privilged(mut self, privileged: bool) -> Self
117 {
118 self.privileged = privileged;
119 self
120 }
121 pub fn set_privileged(mut self, privileged: bool) -> Self
123 {
124 self.privileged = privileged;
125 self
126 }
127 pub fn set_x11_forwarding(mut self, x11_forwarding: bool) -> Self
129 {
130 self.x11_forwarding = x11_forwarding;
131 self
132 }
133 pub fn set_network(mut self, network: Network) -> Self
135 {
136 self.network = network;
137 self
138 }
139 pub fn set_ipc(mut self, ipc: Ipc, size: Option<&str>) -> Self
141 {
142 self.ipc = ipc;
143 self.ipc_size = size.map(|x| x.to_string());
144 self
145 }
146 pub fn mount(mut self, host_dir: impl Into<String>, container_dir: impl Into<String>) -> Self
148 {
149 self.mounts.push((host_dir.into(), container_dir.into()));
150 self
151 }
152 pub fn set_username(mut self, username: impl Into<String>) -> Self
154 {
155 self.username = Some(username.into());
156 self
157 }
158 pub fn set_env_variable(mut self, key: impl Into<String>, variable: impl Into<String>) -> Self
160 {
161 self.env.push((key.into(), variable.into()));
162 self
163 }
164 pub fn copy_env_variable_from_host(mut self, key: impl Into<String>) -> Self
166 {
167 let key = key.into();
168 if let Ok(v) = std::env::var(&key)
169 {
170 self.env.push((key, v));
171 }
172 self
173 }
174 pub fn set_clean_up(mut self, clean_up: bool) -> Self
176 {
177 self.clean_up = clean_up;
178 self
179 }
180 pub fn set_name(mut self, name: impl Into<String>) -> Self
182 {
183 self.name = name.into();
184 self
185 }
186 pub fn generate_name(mut self, prefix: impl Into<String>) -> Self
188 {
189 self.name = generate_name(prefix.into());
190 self
191 }
192 pub fn map_port(mut self, host_port: u16, container_port: u16) -> Self
194 {
195 self.port_mappings.push((host_port, container_port));
196 self
197 }
198 pub fn work_directory(mut self, working_directory: impl Into<String>) -> Self
200 {
201 self.work_directory = Some(working_directory.into());
202 self
203 }
204 pub fn with_ok<T,E>(self, value: Result<T, E>, f: impl FnOnce(Self, T) -> Self) -> Self
207 {
208 if let Ok(v) = value
209 {
210 f(self, v)
211 } else {
212 self
213 }
214 }
215 pub fn with_some<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self
218 {
219 if let Some(v) = value
220 {
221 f(self, v)
222 } else {
223 self
224 }
225 }
226 pub fn cond(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self
229 {
230 if cond
231 {
232 f(self)
233 }
234 else
235 {
236 self
237 }
238 }
239 pub fn cond_or(
242 self,
243 cond: bool,
244 f: impl FnOnce(Self) -> Self,
245 g: impl FnOnce(Self) -> Self,
246 ) -> Self
247 {
248 if cond
249 {
250 f(self)
251 }
252 else
253 {
254 g(self)
255 }
256 }
257 pub fn unwrap_option<T>(self, value: &Option<T>, f: impl FnOnce(Self, &T) -> Self) -> Self
259 {
260 match value
261 {
262 Some(value) => f(self, value),
263 None => self,
264 }
265 }
266 pub fn unwrap_option_or<T>(
268 self,
269 value: &Option<T>,
270 f: impl FnOnce(Self, &T) -> Self,
271 g: impl FnOnce(Self) -> Self,
272 ) -> Self
273 {
274 match value
275 {
276 Some(value) => f(self, value),
277 None => g(self),
278 }
279 }
280
281 pub fn pull(&self) -> Result<()>
283 {
284 let mut cmd = Command::new("docker");
285 cmd.args(["pull", self.image.as_str()]);
286
287 let mut r = cmd.spawn()?;
289 r.wait()?;
290 Ok(())
291 }
292}
293
294pub struct Container
302{
303 config: Configurator,
304 process: Option<SharedChild>,
305}
306
307macro_rules! config_to_arg {
308 ($check:expr, $args:ident, $arg:expr) => {
309 if $check
310 {
311 $args.push($arg.into());
312 }
313 };
314}
315
316impl Container
317{
318 pub fn configure(image: impl Into<String>) -> Configurator
320 {
321 Configurator {
322 image: image.into(),
323 capture_stdio: false,
324 interactive: false,
325 tty: false,
326 daemon: false,
327 privileged: false,
328 x11_forwarding: false,
329 network: Network::Bridge,
330 mounts: Default::default(),
331 env: Default::default(),
332 username: None,
333 clean_up: true,
334 name: generate_name("unknown".to_string()),
335 command: Default::default(),
336 port_mappings: Default::default(),
337 work_directory: None,
338 ipc: Ipc::None,
339 ipc_size: None
340 }
341 }
342
343 fn create_args(&mut self) -> Result<Vec<OsString>>
344 {
345 let mut args = Vec::<OsString>::new();
346 args.push("run".into());
347 args.push("--name".into());
348 args.push(self.config.name.to_owned().into());
349
350 config_to_arg!(self.config.daemon, args, "-d");
351 config_to_arg!(self.config.tty, args, "-t");
352 config_to_arg!(self.config.interactive, args, "-i");
353 config_to_arg!(self.config.privileged, args, "--privileged");
354 config_to_arg!(self.config.clean_up, args, "--rm");
355
356 args.push("--network".into());
357 args.push(
358 match self.config.network
359 {
360 Network::Bridge => "bridge",
361 Network::Host => "host",
362 Network::None => "none",
363 }
364 .into(),
365 );
366
367 match self.config.ipc {
368 Ipc::None=> {},
369 Ipc::Host=> {
370 args.push("--ipc".into());
371 args.push("host".into());
372 if let Some(ipc_size) = &self.config.ipc_size
373 {
374 args.push("--shm-size".into());
375 args.push(ipc_size.into());
376 }
377 }
378 }
379
380 for (host_port, container_port) in self.config.port_mappings.iter()
381 {
382 args.push("-p".into());
383 args.push(format!("{}:{}", host_port, container_port).into());
384 }
385
386 if self.config.x11_forwarding
387 {
388 args.push("-e".into());
389 args.push(format!("DISPLAY={}", std::env::var("DISPLAY")?).into());
390 args.push("-v".into());
391 args.push(
392 format!(
393 "/home/{}/.Xauthority:/home/{}/.Xauthority",
394 whoami::username(),
395 self
396 .config
397 .username
398 .as_ref()
399 .ok_or(Error::MissingUsername)?
400 )
401 .into(),
402 );
403 args.push("-v".into());
404 args.push("/tmp/.X11-unix:/tmp/.X11-unix".into());
405 args.push("-h".into());
406 args.push(hostname::get()?)
407 }
408
409 for (host_dir, container_dir) in self.config.mounts.iter()
410 {
411 args.push("-v".into());
412 args.push(format!("{}:{}", host_dir, container_dir).into());
413 }
414
415 for (env_name, env_value) in self.config.env.iter()
417 {
418 args.push("-e".into());
419 args.push(format!("{}={}", env_name, env_value).into());
420 }
421
422 if let Some(working_directory) = self.config.work_directory.as_ref()
424 {
425 args.push("-w".into());
426 args.push(working_directory.into());
427 }
428
429 args.push(self.config.image.to_owned().into());
431
432 let mut command = self
434 .config
435 .command
436 .iter()
437 .map(|x| -> OsString { x.into() })
438 .collect();
439 args.append(&mut command);
440 Ok(args)
441 }
442
443 pub fn command(&mut self) -> Result<Vec<OsString>>
445 {
446 let mut args = self.create_args()?;
447 let mut r = Vec::<OsString>::new();
448 r.push("docker".into());
449 r.append(&mut args);
450 Ok(r)
451 }
452
453 pub fn start(&mut self) -> Result<()>
455 {
456 if self.is_running()?
457 {
458 return Err(Error::ContainerRunning);
459 }
460
461 let mut cmd = Command::new("docker");
463 cmd.args(self.create_args()?);
464
465 if self.config.capture_stdio
466 {
467 cmd.stdin(Stdio::piped());
468 cmd.stdout(Stdio::piped());
469 cmd.stderr(Stdio::piped());
470 }
471
472 self.process = Some(SharedChild::spawn(&mut cmd)?);
474 Ok(())
475 }
476 pub fn wait(&self) -> Result<ExitStatus>
478 {
479 if let Some(process) = &self.process
480 {
481 Ok(process.wait()?)
482 }
483 else
484 {
485 Err(Error::ContainerNotRunning)
486 }
487 }
488 pub fn stop(&self) -> Result<()>
490 {
491 if self.process.is_some()
492 {
493 Command::new("docker")
494 .args(["kill", self.config.name.to_owned().as_str()])
495 .output()?;
496 Ok(())
497 }
498 else
499 {
500 Err(Error::ContainerNotRunning)
501 }
502 }
503 pub fn is_running(&self) -> Result<bool>
505 {
506 if let Some(p) = &self.process
507 && p.try_wait()?.is_none()
508 {
509 return Ok(true);
510 }
511 Ok(false)
512 }
513 pub fn take_stdin(&self) -> Result<std::process::ChildStdin>
515 {
516 self
517 .process
518 .as_ref()
519 .ok_or(Error::ContainerNotRunning)?
520 .take_stdin()
521 .ok_or(Error::StdIONotPiped)
522 }
523 pub fn take_stdout(&self) -> Result<std::process::ChildStdout>
525 {
526 self
527 .process
528 .as_ref()
529 .ok_or(Error::ContainerNotRunning)?
530 .take_stdout()
531 .ok_or(Error::StdIONotPiped)
532 }
533 pub fn take_stderr(&self) -> Result<std::process::ChildStderr>
535 {
536 self
537 .process
538 .as_ref()
539 .ok_or(Error::ContainerNotRunning)?
540 .take_stderr()
541 .ok_or(Error::StdIONotPiped)
542 }
543}
544
545impl Drop for Container
546{
547 fn drop(&mut self)
548 {
549 if self
550 .is_running()
551 .expect("to successfully query for running")
552 {
553 self.stop().expect("to successfully stop");
554 self.wait().expect("to successfully wait");
555 }
556 }
557}
558
559#[cfg(test)]
560mod tests
561{
562 use std::io::Read;
563
564 use super::*;
565
566 #[test]
567 fn start_wait_stop_containers()
568 {
569 let mut container = Container::configure("alpine")
570 .set_command(["sleep", "1"])
571 .create();
572 container.start().expect("To start.");
574 assert!(container.is_running().unwrap());
575 container.stop().expect("To stop.");
576 let wait_status = container.wait().unwrap();
577 assert!(wait_status.success());
578
579 assert!(!container.is_running().unwrap());
580
581 container.start().expect("To start.");
583 assert!(container.is_running().unwrap());
584 let wait_status = container.wait().unwrap();
585 assert!(wait_status.success());
586 assert_eq!(wait_status.code().unwrap(), 0);
587 assert!(!container.is_running().unwrap());
588 }
589 #[test]
590 fn interactive_containers()
591 {
592 use std::io::Write;
593
594 let mut container = Container::configure("alpine")
595 .set_interactive(true)
596 .set_capture_stdio(true)
597 .create();
598 container.start().unwrap();
599 let mut stdin = container.take_stdin().unwrap();
600 stdin.write_all(b"echo Hello World!").unwrap();
601 drop(stdin);
602 let mut buf = vec![];
603 container
604 .take_stdout()
605 .unwrap()
606 .read_to_end(&mut buf)
607 .unwrap();
608 assert_eq!(buf, b"Hello World!\n");
609 }
610}