docker_ctl/
container.rs

1//! Control a container
2
3use std::{
4  ffi::OsString,
5  process::{Command, ExitStatus, Stdio},
6};
7
8use shared_child::SharedChild;
9
10use crate::{Error, Result};
11
12/// Network mode, refer to the [docker documentation](https://docs.docker.com/engine/network/)
13/// for an explanation on the different modes.
14pub enum Network
15{
16  /// The default network driver.
17  Bridge,
18  /// Remove network isolation between the container and the Docker host.
19  Host,
20  /// Completely isolate a container from the host and other containers.
21  None,
22}
23
24/// Setup IPC via shared memory between container and host.
25pub enum Ipc
26{
27  /// No IPC
28  None,
29  /// IPC with Host
30  Host
31}
32
33//   ____             __ _                       _
34//  / ___|___  _ __  / _(_) __ _ _   _ _ __ __ _| |_ ___  _ __
35// | |   / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __/ _ \| '__|
36// | |__| (_) | | | |  _| | (_| | |_| | | | (_| | || (_) | |
37//  \____\___/|_| |_|_| |_|\__, |\__,_|_|  \__,_|\__\___/|_|
38//                         |___/
39
40fn 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
54/// Configuration of the container
55pub 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  /// Create the container
80  pub fn create(self) -> Container
81  {
82    Container {
83      config: self,
84      process: None,
85    }
86  }
87  /// Set the command for the docker
88  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  /// Capture stdin/err/out
97  pub fn set_capture_stdio(mut self, capture: bool) -> Self
98  {
99    self.capture_stdio = capture;
100    self
101  }
102  /// Set the docker in interactive mode.
103  pub fn set_interactive(mut self, interactive: bool) -> Self
104  {
105    self.interactive = interactive;
106    self
107  }
108  /// Set the docker in TTY mode.
109  pub fn set_tty(mut self, tty: bool) -> Self
110  {
111    self.tty = tty;
112    self
113  }
114  /// Enable/disable privileged (see [docker documentation](https://docs.docker.com/engine/containers/run/#runtime-privilege-and-linux-capabilities)).
115  #[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  /// Enable/disable privileged (see [docker documentation](https://docs.docker.com/engine/containers/run/#runtime-privilege-and-linux-capabilities)).
122  pub fn set_privileged(mut self, privileged: bool) -> Self
123  {
124    self.privileged = privileged;
125    self
126  }
127  /// Forward X11 to the container
128  pub fn set_x11_forwarding(mut self, x11_forwarding: bool) -> Self
129  {
130    self.x11_forwarding = x11_forwarding;
131    self
132  }
133  /// Set the network mode
134  pub fn set_network(mut self, network: Network) -> Self
135  {
136    self.network = network;
137    self
138  }
139  /// Set the IPC mode. Size is optional, for instance "1g"
140  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  /// Mount a drive from the host to the container
147  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  /// Set the username in the container, this is required for X11 forwarding
153  pub fn set_username(mut self, username: impl Into<String>) -> Self
154  {
155    self.username = Some(username.into());
156    self
157  }
158  /// Set an environment variable
159  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  /// Copy a variable from host environment. Silently fails if the variable is not available in host.
165  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  /// Set if the container should be cleaned up after execution (set --rm)
175  pub fn set_clean_up(mut self, clean_up: bool) -> Self
176  {
177    self.clean_up = clean_up;
178    self
179  }
180  /// set the name of the container
181  pub fn set_name(mut self, name: impl Into<String>) -> Self
182  {
183    self.name = name.into();
184    self
185  }
186  /// generate a name with the given prefix
187  pub fn generate_name(mut self, prefix: impl Into<String>) -> Self
188  {
189    self.name = generate_name(prefix.into());
190    self
191  }
192  /// Map a host port to a container port
193  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  /// Set the work directory in the container
199  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  /// Convenience function for configuring the container with a value that
205  /// comes from a result.
206  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  /// Convenience function for configuring the container with a value that
216  /// comes from an option.
217  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  /// Convenience function for configuring the container with a condition.
227  /// If `cond` is true, the closure `f` is executed.
228  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  /// Convenience function for configuring the container with cond.
240  /// If `cond` is true, the closure `f` is executed, otherwise `g` is executed.
241  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  /// If the option is set call the function `f`. Otherwise do nothing and return self.
258  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  /// If the option is set call the function `f`. Otherwise do nothing and return self.
267  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  /// Pull the image.
282  pub fn pull(&self) -> Result<()>
283  {
284    let mut cmd = Command::new("docker");
285    cmd.args(["pull", self.image.as_str()]);
286
287    // Spawn
288    let mut r = cmd.spawn()?;
289    r.wait()?;
290    Ok(())
291  }
292}
293
294//   ____            _        _
295//  / ___|___  _ __ | |_ __ _(_)_ __   ___ _ __
296// | |   / _ \| '_ \| __/ _` | | '_ \ / _ \ '__|
297// | |__| (_) | | | | || (_| | | | | |  __/ |
298//  \____\___/|_| |_|\__\__,_|_|_| |_|\___|_|
299
300/// Handle to a container
301pub 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  /// Call this function to configure a new container
319  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    // Add environment
416    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    // Add the working directory
423    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    // Set the image
430    args.push(self.config.image.to_owned().into());
431
432    // Set the command arguments
433    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  /// Command line used to start the container
444  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  /// Call this function to start the container.
454  pub fn start(&mut self) -> Result<()>
455  {
456    if self.is_running()?
457    {
458      return Err(Error::ContainerRunning);
459    }
460
461    // Prepare the process
462    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    // Spawn
473    self.process = Some(SharedChild::spawn(&mut cmd)?);
474    Ok(())
475  }
476  /// Call this function to wait on the container
477  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  /// Call this function to stop the container
489  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  /// Check if running
504  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  /// Get stdin
514  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  /// Get stdout
524  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  /// Get stderr
534  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    // Try to start and stop container
573    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    // Run container and wait
582    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}