1#![deny(missing_docs)]
2
3pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum BaseCommand {
22 Docker,
24
25 SudoDocker,
27
28 Podman,
30}
31
32fn 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
44fn 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#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct Launcher {
65 base_command: Command,
66}
67
68impl Launcher {
69 pub fn new(base_command: Command) -> Self {
72 Self { base_command }
73 }
74
75 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 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 pub fn is_docker(&self) -> bool {
111 self.is_program("docker")
112 }
113
114 pub fn is_podman(&self) -> bool {
118 self.is_program("podman")
119 }
120
121 pub fn base_command(&self) -> &Command {
123 &self.base_command
124 }
125
126 pub fn build(&self, opt: BuildOpt) -> Command {
128 let mut cmd = self.base_command.clone();
129 cmd.add_arg("build");
130
131 for (key, value) in opt.build_args {
133 cmd.add_arg_pair("--build-arg", format!("{}={}", key, value));
134 }
135
136 if let Some(dockerfile) = &opt.dockerfile {
138 cmd.add_arg_pair("--file", dockerfile);
139 }
140
141 if let Some(iidfile) = &opt.iidfile {
143 cmd.add_arg_pair("--iidfile", iidfile);
144 }
145
146 if opt.no_cache {
148 cmd.add_arg("--no-cache");
149 }
150
151 if opt.pull {
153 cmd.add_arg("--pull");
154 }
155
156 if opt.quiet {
158 cmd.add_arg("--quiet");
159 }
160
161 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 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 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 pub fn run(&self, opt: RunOpt) -> Command {
190 let mut cmd = self.base_command.clone();
191 cmd.add_arg("run");
192
193 if opt.detach {
195 cmd.add_arg("--detach");
196 }
197
198 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 if opt.init {
209 cmd.add_arg("--init");
210 }
211
212 if opt.interactive {
214 cmd.add_arg("--interactive");
215 }
216
217 if let Some(name) = &opt.name {
219 cmd.add_arg_pair("--name", name);
220 }
221
222 if let Some(network) = &opt.network {
224 cmd.add_arg_pair("--network", network);
225 }
226
227 for publish in &opt.publish {
229 cmd.add_arg_pair("--publish", publish.arg());
230 }
231
232 if opt.read_only {
234 cmd.add_arg("--read-only");
235 }
236
237 if opt.remove {
239 cmd.add_arg("--rm");
240 }
241
242 if opt.tty {
244 cmd.add_arg("--tty");
245 }
246
247 if let Some(user) = &opt.user {
249 cmd.add_arg_pair("--user", user.arg());
250 }
251
252 for vol in &opt.volumes {
254 cmd.add_arg_pair("--volume", vol.arg());
255 }
256
257 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 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#[derive(Clone, Debug, Default, Eq, PartialEq)]
299pub struct BuildOpt {
300 pub build_args: Vec<(String, String)>,
302
303 pub context: PathBuf,
306
307 pub dockerfile: Option<PathBuf>,
311
312 pub iidfile: Option<PathBuf>,
314
315 pub no_cache: bool,
317
318 pub pull: bool,
320
321 pub quiet: bool,
323
324 pub tag: Option<String>,
326}
327
328#[derive(Clone, Debug, Default, Eq, PartialEq)]
330pub struct CreateNetworkOpt {
331 pub name: String,
333}
334
335#[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
380pub struct PublishPorts {
381 pub container: PortRange,
383
384 pub host: Option<PortRange>,
386
387 pub ip: Option<String>,
390}
391
392impl PublishPorts {
393 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#[derive(Clone, Debug, Eq, PartialEq)]
412pub enum NameOrId {
413 Name(String),
415
416 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#[derive(Clone, Debug, Eq, PartialEq)]
443pub struct UserAndGroup {
444 pub user: NameOrId,
446
447 pub group: Option<NameOrId>,
449}
450
451impl UserAndGroup {
452 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 pub fn root() -> Self {
462 Self {
463 user: 0.into(),
464 group: Some(0.into()),
465 }
466 }
467
468 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#[derive(Clone, Debug, Default, Eq, PartialEq)]
482pub struct Volume {
483 pub src: PathBuf,
486
487 pub dst: PathBuf,
490
491 pub read_write: bool,
493
494 pub options: Vec<String>,
496}
497
498impl Volume {
499 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#[derive(Clone, Debug, Default, Eq, PartialEq)]
520pub struct RunOpt {
521 pub image: String,
523
524 pub env: Vec<(OsString, OsString)>,
526
527 pub detach: bool,
530
531 pub init: bool,
534
535 pub interactive: bool,
537
538 pub name: Option<String>,
540
541 pub network: Option<String>,
543
544 pub user: Option<UserAndGroup>,
546
547 pub publish: Vec<PublishPorts>,
549
550 pub read_only: bool,
552
553 pub remove: bool,
556
557 pub tty: bool,
559
560 pub volumes: Vec<Volume>,
562
563 pub command: Option<PathBuf>,
565
566 pub args: Vec<OsString>,
568}
569
570#[derive(Clone, Debug, Default, Eq, PartialEq)]
572pub struct StopOpt {
573 pub containers: Vec<String>,
575
576 pub time: Option<u32>,
579}