#![deny(missing_docs)]
pub use command_run;
use command_run::Command;
use std::ffi::{OsStr, OsString};
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::{env, fmt};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BaseCommand {
Docker,
SudoDocker,
Podman,
}
fn is_exe_in_path(exe_name: &OsStr) -> bool {
let paths = if let Some(paths) = env::var_os("PATH") {
paths
} else {
return false;
};
env::split_paths(&paths).any(|path| path.join(exe_name).exists())
}
fn is_user_in_group(target_group: &str) -> bool {
let mut cmd = Command::new("groups");
cmd.log_command = false;
cmd.capture = true;
cmd.log_output_on_error = true;
let output = if let Ok(output) = cmd.run() {
output
} else {
return false;
};
let stdout = output.stdout_string_lossy();
stdout.split_whitespace().any(|group| group == target_group)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Launcher {
base_command: Command,
}
impl Launcher {
pub fn new(base_command: Command) -> Self {
Self { base_command }
}
pub fn auto() -> Option<Self> {
let docker = OsStr::new("docker");
let podman = OsStr::new("podman");
if is_exe_in_path(podman) {
Some(BaseCommand::Podman.into())
} else if is_exe_in_path(docker) {
Some(if is_user_in_group("docker") {
BaseCommand::Docker.into()
} else {
BaseCommand::SudoDocker.into()
})
} else {
None
}
}
fn is_program(&self, program: &str) -> bool {
let program = OsStr::new(program);
self.base_command.program == program
|| self.base_command.args.contains(&program.into())
}
pub fn is_docker(&self) -> bool {
self.is_program("docker")
}
pub fn is_podman(&self) -> bool {
self.is_program("podman")
}
pub fn base_command(&self) -> &Command {
&self.base_command
}
pub fn build(&self, opt: BuildOpt) -> Command {
let mut cmd = self.base_command.clone();
cmd.add_arg("build");
for (key, value) in opt.build_args {
cmd.add_arg_pair("--build-arg", format!("{}={}", key, value));
}
if let Some(dockerfile) = &opt.dockerfile {
cmd.add_arg_pair("--file", dockerfile);
}
if let Some(iidfile) = &opt.iidfile {
cmd.add_arg_pair("--iidfile", iidfile);
}
if opt.no_cache {
cmd.add_arg("--no-cache");
}
if opt.pull {
cmd.add_arg("--pull");
}
if opt.quiet {
cmd.add_arg("--quiet");
}
if let Some(tag) = &opt.tag {
cmd.add_arg_pair("--tag", tag);
}
cmd.add_arg(opt.context);
cmd
}
pub fn create_network(&self, opt: CreateNetworkOpt) -> Command {
let mut cmd = self.base_command.clone();
cmd.add_arg_pair("network", "create");
cmd.add_arg(opt.name);
cmd
}
pub fn remove_network(&self, name: &str) -> Command {
let mut cmd = self.base_command.clone();
cmd.add_arg_pair("network", "rm");
cmd.add_arg(name);
cmd
}
pub fn run(&self, opt: RunOpt) -> Command {
let mut cmd = self.base_command.clone();
cmd.add_arg("run");
if opt.detach {
cmd.add_arg("--detach");
}
for (key, value) in &opt.env {
let mut arg = OsString::new();
arg.push(key);
arg.push("=");
arg.push(value);
cmd.add_arg_pair("--env", arg);
}
if opt.init {
cmd.add_arg("--init");
}
if opt.interactive {
cmd.add_arg("--interactive");
}
if let Some(name) = &opt.name {
cmd.add_arg_pair("--name", name);
}
if let Some(network) = &opt.network {
cmd.add_arg_pair("--network", network);
}
for publish in &opt.publish {
cmd.add_arg_pair("--publish", publish.arg());
}
if opt.read_only {
cmd.add_arg("--read-only");
}
if opt.remove {
cmd.add_arg("--rm");
}
if opt.tty {
cmd.add_arg("--tty");
}
if let Some(user) = &opt.user {
cmd.add_arg_pair("--user", user.arg());
}
for vol in &opt.volumes {
cmd.add_arg_pair("--volume", vol.arg());
}
cmd.add_arg(opt.image);
if let Some(command) = &opt.command {
cmd.add_arg(command);
}
cmd.add_args(&opt.args);
cmd
}
pub fn stop(&self, opt: StopOpt) -> Command {
let mut cmd = self.base_command.clone();
cmd.add_arg("stop");
if let Some(time) = opt.time {
cmd.add_arg_pair("--time", &time.to_string());
}
cmd.add_args(&opt.containers);
cmd
}
}
impl From<BaseCommand> for Launcher {
fn from(bc: BaseCommand) -> Launcher {
let docker = "docker";
let podman = "podman";
Self {
base_command: match bc {
BaseCommand::Docker => Command::new(docker),
BaseCommand::SudoDocker => {
Command::with_args("sudo", &[docker])
}
BaseCommand::Podman => Command::new(podman),
},
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct BuildOpt {
pub build_args: Vec<(String, String)>,
pub context: PathBuf,
pub dockerfile: Option<PathBuf>,
pub iidfile: Option<PathBuf>,
pub no_cache: bool,
pub pull: bool,
pub quiet: bool,
pub tag: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CreateNetworkOpt {
pub name: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PortRange(pub RangeInclusive<u16>);
impl Default for PortRange {
fn default() -> Self {
Self(0..=0)
}
}
impl fmt::Display for PortRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.0.start() == self.0.end() {
write!(f, "{}", self.0.start())
} else {
write!(f, "{}-{}", self.0.start(), self.0.end())
}
}
}
impl From<u16> for PortRange {
fn from(port: u16) -> Self {
Self(port..=port)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PublishPorts {
pub container: PortRange,
pub host: Option<PortRange>,
pub ip: Option<String>,
}
impl PublishPorts {
pub fn arg(&self) -> String {
match (&self.ip, &self.host) {
(Some(ip), Some(host_ports)) => {
format!("{}:{}:{}", ip, host_ports, self.container)
}
(Some(ip), None) => {
format!("{}::{}", ip, self.container)
}
(None, Some(host_ports)) => {
format!("{}:{}", host_ports, self.container)
}
(None, None) => format!("{}", self.container),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum NameOrId {
Name(String),
Id(u32),
}
impl fmt::Display for NameOrId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Name(name) => write!(f, "{}", name),
Self::Id(id) => write!(f, "{}", id),
}
}
}
impl From<String> for NameOrId {
fn from(name: String) -> Self {
Self::Name(name)
}
}
impl From<u32> for NameOrId {
fn from(id: u32) -> Self {
Self::Id(id)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UserAndGroup {
pub user: NameOrId,
pub group: Option<NameOrId>,
}
impl UserAndGroup {
pub fn current() -> Self {
Self {
user: users::get_current_uid().into(),
group: Some(users::get_current_gid().into()),
}
}
pub fn root() -> Self {
Self {
user: 0.into(),
group: Some(0.into()),
}
}
pub fn arg(&self) -> String {
let mut out = self.user.to_string();
if let Some(group) = &self.group {
out.push(':');
out.push_str(&group.to_string());
}
out
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Volume {
pub src: PathBuf,
pub dst: PathBuf,
pub read_write: bool,
pub options: Vec<String>,
}
impl Volume {
pub fn arg(&self) -> OsString {
let mut out = OsString::new();
out.push(&self.src);
out.push(":");
out.push(&self.dst);
if self.read_write {
out.push(":rw");
} else {
out.push(":ro");
}
for opt in &self.options {
out.push(",");
out.push(opt);
}
out
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RunOpt {
pub image: String,
pub env: Vec<(OsString, OsString)>,
pub detach: bool,
pub init: bool,
pub interactive: bool,
pub name: Option<String>,
pub network: Option<String>,
pub user: Option<UserAndGroup>,
pub publish: Vec<PublishPorts>,
pub read_only: bool,
pub remove: bool,
pub tty: bool,
pub volumes: Vec<Volume>,
pub command: Option<PathBuf>,
pub args: Vec<OsString>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct StopOpt {
pub containers: Vec<String>,
pub time: Option<u32>,
}