docker_command/
lib.rs

1#![deny(missing_docs)]
2
3//! Create [`Command`]s for running Docker or Docker-compatible clients.
4//!
5//! Rather than speaking directly to the Docker daemon, this library
6//! produces commands that can be run in a subprocess to invoke the
7//! Docker client (or a compatible client such as Podman).
8//!
9//! [`Command`]: https://docs.rs/command-run/latest/command_run/struct.Command.html
10
11pub use command_run;
12
13use command_run::Command;
14use std::ffi::{OsStr, OsString};
15use std::ops::RangeInclusive;
16use std::path::PathBuf;
17use std::{env, fmt};
18
19/// Preset base commands that a [`Launcher`] can be constructed from.
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum BaseCommand {
22    /// Docker without sudo.
23    Docker,
24
25    /// Docker with sudo.
26    SudoDocker,
27
28    /// Podman.
29    Podman,
30}
31
32// TODO: is there a good existing crate for this? I found a few on
33// crates.io that didn't look quite right.
34fn is_exe_in_path(exe_name: &OsStr) -> bool {
35    let paths = if let Some(paths) = env::var_os("PATH") {
36        paths
37    } else {
38        return false;
39    };
40
41    env::split_paths(&paths).any(|path| path.join(exe_name).exists())
42}
43
44// TODO: consider using nix or some other crate.
45fn is_user_in_group(target_group: &str) -> bool {
46    let mut cmd = Command::new("groups");
47    cmd.log_command = false;
48    cmd.capture = true;
49    cmd.log_output_on_error = true;
50    let output = if let Ok(output) = cmd.run() {
51        output
52    } else {
53        return false;
54    };
55    let stdout = output.stdout_string_lossy();
56    stdout.split_whitespace().any(|group| group == target_group)
57}
58
59/// Base container command used for building and running containers.
60///
61/// This allows variations such as "docker", "sudo docker", and
62/// "podman".
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct Launcher {
65    base_command: Command,
66}
67
68impl Launcher {
69    /// Create a new `Launcher` with the specified base [`Command`]. The
70    /// base command is used to create all the other commands.
71    pub fn new(base_command: Command) -> Self {
72        Self { base_command }
73    }
74
75    /// Automatically choose a base command.
76    ///
77    /// * Chooses `podman` if is in the `$PATH`.
78    /// * Otherwise chooses `docker` if it is in the `$PATH`.
79    ///   * If the current user is not in a `docker` group, `sudo` is added.
80    ///
81    /// If neither command is in the `$PATH`, returns `None`.
82    pub fn auto() -> Option<Self> {
83        let docker = OsStr::new("docker");
84        let podman = OsStr::new("podman");
85        if is_exe_in_path(podman) {
86            Some(BaseCommand::Podman.into())
87        } else if is_exe_in_path(docker) {
88            Some(if is_user_in_group("docker") {
89                BaseCommand::Docker.into()
90            } else {
91                BaseCommand::SudoDocker.into()
92            })
93        } else {
94            None
95        }
96    }
97
98    /// Whether the base command appears to be the given `program`. This
99    /// checks if base command's program or any arguments match the
100    /// input `program`.
101    fn is_program(&self, program: &str) -> bool {
102        let program = OsStr::new(program);
103        self.base_command.program == program
104            || self.base_command.args.contains(&program.into())
105    }
106
107    /// Whether the base command appears to be docker. This checks if
108    /// the program or any of the arguments in the base command match
109    /// "docker".
110    pub fn is_docker(&self) -> bool {
111        self.is_program("docker")
112    }
113
114    /// Whether the base command appears to be podman. This checks if
115    /// the program or any of the arguments in the base command match
116    /// "podman".
117    pub fn is_podman(&self) -> bool {
118        self.is_program("podman")
119    }
120
121    /// Get the base [`Command`].
122    pub fn base_command(&self) -> &Command {
123        &self.base_command
124    }
125
126    /// Create a [`Command`] for building a container.
127    pub fn build(&self, opt: BuildOpt) -> Command {
128        let mut cmd = self.base_command.clone();
129        cmd.add_arg("build");
130
131        // --build-arg
132        for (key, value) in opt.build_args {
133            cmd.add_arg_pair("--build-arg", format!("{}={}", key, value));
134        }
135
136        // --file
137        if let Some(dockerfile) = &opt.dockerfile {
138            cmd.add_arg_pair("--file", dockerfile);
139        }
140
141        // --iidfile
142        if let Some(iidfile) = &opt.iidfile {
143            cmd.add_arg_pair("--iidfile", iidfile);
144        }
145
146        // --no-cache
147        if opt.no_cache {
148            cmd.add_arg("--no-cache");
149        }
150
151        // --pull
152        if opt.pull {
153            cmd.add_arg("--pull");
154        }
155
156        // --quiet
157        if opt.quiet {
158            cmd.add_arg("--quiet");
159        }
160
161        // --tag
162        if let Some(tag) = &opt.tag {
163            cmd.add_arg_pair("--tag", tag);
164        }
165
166        cmd.add_arg(opt.context);
167        cmd
168    }
169
170    /// Create a [`Command`] for creating a network.
171    pub fn create_network(&self, opt: CreateNetworkOpt) -> Command {
172        let mut cmd = self.base_command.clone();
173        cmd.add_arg_pair("network", "create");
174        cmd.add_arg(opt.name);
175
176        cmd
177    }
178
179    /// Create a [`Command`] for removing a network.
180    pub fn remove_network(&self, name: &str) -> Command {
181        let mut cmd = self.base_command.clone();
182        cmd.add_arg_pair("network", "rm");
183        cmd.add_arg(name);
184
185        cmd
186    }
187
188    /// Create a [`Command`] for running a container.
189    pub fn run(&self, opt: RunOpt) -> Command {
190        let mut cmd = self.base_command.clone();
191        cmd.add_arg("run");
192
193        // --detach
194        if opt.detach {
195            cmd.add_arg("--detach");
196        }
197
198        // --env
199        for (key, value) in &opt.env {
200            let mut arg = OsString::new();
201            arg.push(key);
202            arg.push("=");
203            arg.push(value);
204            cmd.add_arg_pair("--env", arg);
205        }
206
207        // --init
208        if opt.init {
209            cmd.add_arg("--init");
210        }
211
212        // --interactive
213        if opt.interactive {
214            cmd.add_arg("--interactive");
215        }
216
217        // --name
218        if let Some(name) = &opt.name {
219            cmd.add_arg_pair("--name", name);
220        }
221
222        // --network
223        if let Some(network) = &opt.network {
224            cmd.add_arg_pair("--network", network);
225        }
226
227        // --publish
228        for publish in &opt.publish {
229            cmd.add_arg_pair("--publish", publish.arg());
230        }
231
232        // --read-only
233        if opt.read_only {
234            cmd.add_arg("--read-only");
235        }
236
237        // --rm
238        if opt.remove {
239            cmd.add_arg("--rm");
240        }
241
242        // --tty
243        if opt.tty {
244            cmd.add_arg("--tty");
245        }
246
247        // --user
248        if let Some(user) = &opt.user {
249            cmd.add_arg_pair("--user", user.arg());
250        }
251
252        // --volume
253        for vol in &opt.volumes {
254            cmd.add_arg_pair("--volume", vol.arg());
255        }
256
257        // Add image and command+args
258        cmd.add_arg(opt.image);
259        if let Some(command) = &opt.command {
260            cmd.add_arg(command);
261        }
262        cmd.add_args(&opt.args);
263        cmd
264    }
265
266    /// Create a [`Command`] for stopping containers.
267    pub fn stop(&self, opt: StopOpt) -> Command {
268        let mut cmd = self.base_command.clone();
269        cmd.add_arg("stop");
270
271        if let Some(time) = opt.time {
272            cmd.add_arg_pair("--time", &time.to_string());
273        }
274
275        cmd.add_args(&opt.containers);
276
277        cmd
278    }
279}
280
281impl From<BaseCommand> for Launcher {
282    fn from(bc: BaseCommand) -> Launcher {
283        let docker = "docker";
284        let podman = "podman";
285        Self {
286            base_command: match bc {
287                BaseCommand::Docker => Command::new(docker),
288                BaseCommand::SudoDocker => {
289                    Command::with_args("sudo", &[docker])
290                }
291                BaseCommand::Podman => Command::new(podman),
292            },
293        }
294    }
295}
296
297/// Options for building a container.
298#[derive(Clone, Debug, Default, Eq, PartialEq)]
299pub struct BuildOpt {
300    /// Build-time variables.
301    pub build_args: Vec<(String, String)>,
302
303    /// Root directory containing files that can be pulled into the
304    /// container.
305    pub context: PathBuf,
306
307    /// Dockerfile to build. This must be somewhere in the `context`
308    /// directory. If not set (the default) then
309    /// `<context>/Dockerfile` is used.
310    pub dockerfile: Option<PathBuf>,
311
312    /// If set, the image ID will be written to this path.
313    pub iidfile: Option<PathBuf>,
314
315    /// Do not use cache when building the image.
316    pub no_cache: bool,
317
318    /// Always attempt to pull a newer version of the image.
319    pub pull: bool,
320
321    /// Suppress the build output and print image ID on success.
322    pub quiet: bool,
323
324    /// If set, the image will be tagged with this name.
325    pub tag: Option<String>,
326}
327
328/// Options for creating a network.
329#[derive(Clone, Debug, Default, Eq, PartialEq)]
330pub struct CreateNetworkOpt {
331    /// Network name.
332    pub name: String,
333}
334
335/// Port or range of ports.
336///
337/// # Examples
338///
339/// Specify a single port:
340///
341/// ```
342/// use docker_command::PortRange;
343/// let port = PortRange::from(123);
344/// assert_eq!(port, PortRange(123..=123));
345/// ```
346///
347/// Specify a port range:
348///
349/// ```
350/// use docker_command::PortRange;
351/// PortRange(123..=567);
352/// ```
353#[derive(Clone, Debug, Eq, PartialEq)]
354pub struct PortRange(pub RangeInclusive<u16>);
355
356impl Default for PortRange {
357    fn default() -> Self {
358        Self(0..=0)
359    }
360}
361
362impl fmt::Display for PortRange {
363    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
364        if self.0.start() == self.0.end() {
365            write!(f, "{}", self.0.start())
366        } else {
367            write!(f, "{}-{}", self.0.start(), self.0.end())
368        }
369    }
370}
371
372impl From<u16> for PortRange {
373    fn from(port: u16) -> Self {
374        Self(port..=port)
375    }
376}
377
378/// Options for publishing ports from a container to the host.
379#[derive(Clone, Debug, Default, Eq, PartialEq)]
380pub struct PublishPorts {
381    /// Port or port range in the container to publish.
382    pub container: PortRange,
383
384    /// Port or port range on the host.
385    pub host: Option<PortRange>,
386
387    /// Host IP. If set to `0.0.0.0` or `None`, the port will be bound
388    /// to all IPs on the host.
389    pub ip: Option<String>,
390}
391
392impl PublishPorts {
393    /// Format as an argument.
394    pub fn arg(&self) -> String {
395        match (&self.ip, &self.host) {
396            (Some(ip), Some(host_ports)) => {
397                format!("{}:{}:{}", ip, host_ports, self.container)
398            }
399            (Some(ip), None) => {
400                format!("{}::{}", ip, self.container)
401            }
402            (None, Some(host_ports)) => {
403                format!("{}:{}", host_ports, self.container)
404            }
405            (None, None) => format!("{}", self.container),
406        }
407    }
408}
409
410/// Name or numeric ID for a user or group.
411#[derive(Clone, Debug, Eq, PartialEq)]
412pub enum NameOrId {
413    /// Name or the user or group.
414    Name(String),
415
416    /// Numeric ID of the user or group.
417    Id(u32),
418}
419
420impl fmt::Display for NameOrId {
421    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
422        match self {
423            Self::Name(name) => write!(f, "{}", name),
424            Self::Id(id) => write!(f, "{}", id),
425        }
426    }
427}
428
429impl From<String> for NameOrId {
430    fn from(name: String) -> Self {
431        Self::Name(name)
432    }
433}
434
435impl From<u32> for NameOrId {
436    fn from(id: u32) -> Self {
437        Self::Id(id)
438    }
439}
440
441/// User and (optionally) group.
442#[derive(Clone, Debug, Eq, PartialEq)]
443pub struct UserAndGroup {
444    /// User or UID.
445    pub user: NameOrId,
446
447    /// Group or GID.
448    pub group: Option<NameOrId>,
449}
450
451impl UserAndGroup {
452    /// Get a `UserAndGroup` with the current UID and GID set.
453    pub fn current() -> Self {
454        Self {
455            user: users::get_current_uid().into(),
456            group: Some(users::get_current_gid().into()),
457        }
458    }
459
460    /// Get a `UserAndGroup` with UID and GID set to zero.
461    pub fn root() -> Self {
462        Self {
463            user: 0.into(),
464            group: Some(0.into()),
465        }
466    }
467
468    /// Format as an argument. If `group` is set, the format is
469    /// `<user>:<group>`, otherwise just `<user>`.
470    pub fn arg(&self) -> String {
471        let mut out = self.user.to_string();
472        if let Some(group) = &self.group {
473            out.push(':');
474            out.push_str(&group.to_string());
475        }
476        out
477    }
478}
479
480/// Volume specification used when running a container.
481#[derive(Clone, Debug, Default, Eq, PartialEq)]
482pub struct Volume {
483    /// Either a path on the host (if an absolute path) or the name of
484    /// a volume.
485    pub src: PathBuf,
486
487    /// Absolute path in the container where the volume will be
488    /// mounted.
489    pub dst: PathBuf,
490
491    /// If true, mount the volume read-write. Defaults to `false`.
492    pub read_write: bool,
493
494    /// Additional options to set on the volume.
495    pub options: Vec<String>,
496}
497
498impl Volume {
499    /// Format as an argument.
500    pub fn arg(&self) -> OsString {
501        let mut out = OsString::new();
502        out.push(&self.src);
503        out.push(":");
504        out.push(&self.dst);
505        if self.read_write {
506            out.push(":rw");
507        } else {
508            out.push(":ro");
509        }
510        for opt in &self.options {
511            out.push(",");
512            out.push(opt);
513        }
514        out
515    }
516}
517
518/// Options for running a container.
519#[derive(Clone, Debug, Default, Eq, PartialEq)]
520pub struct RunOpt {
521    /// Container image to run.
522    pub image: String,
523
524    /// Set environment variables.
525    pub env: Vec<(OsString, OsString)>,
526
527    /// If true, run the container in the background and print
528    /// container ID. Defaults to `false`.
529    pub detach: bool,
530
531    /// Run an init inside the container that forwards signals and
532    /// reaps processes.
533    pub init: bool,
534
535    /// Keep stdin open even if not attached.
536    pub interactive: bool,
537
538    /// Optional name to give the container.
539    pub name: Option<String>,
540
541    /// Connect a container to a network.
542    pub network: Option<String>,
543
544    /// User (and optionally) group to use inside the container.
545    pub user: Option<UserAndGroup>,
546
547    /// Publish ports from the container to the host.
548    pub publish: Vec<PublishPorts>,
549
550    /// Mount the container's root filesystem as read only.
551    pub read_only: bool,
552
553    /// If true, automatically remove the container when it
554    /// exits. Defaults to `false`.
555    pub remove: bool,
556
557    /// Allocate a psuedo-TTY.
558    pub tty: bool,
559
560    /// Volumes to mount in the container.
561    pub volumes: Vec<Volume>,
562
563    /// Optional command to run.
564    pub command: Option<PathBuf>,
565
566    /// Optional arguments to pass to the command.
567    pub args: Vec<OsString>,
568}
569
570/// Options for stopping a container.
571#[derive(Clone, Debug, Default, Eq, PartialEq)]
572pub struct StopOpt {
573    /// Containers to stop, specified as names or IDs.
574    pub containers: Vec<String>,
575
576    /// Seconds to wait for stop before killing the container. If None,
577    /// defaults to 10 seconds.
578    pub time: Option<u32>,
579}