use nix::unistd::User;
use std::env;
use crate::{ApparmorMode, AttachOptions, attach, exec};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
fn parse_container_types(s: &str) -> Result<Vec<Box<dyn container_pid::Container>>, String> {
let mut valid_types = Vec::new();
let mut unknown_names = Vec::new();
for token in s.split(',') {
let trimmed = token.trim();
if let Some(container_type) = crate::lookup_container_type(trimmed) {
valid_types.push(container_type);
} else {
unknown_names.push(trimmed.to_string());
}
}
if !unknown_names.is_empty() {
return Err(format!(
"unknown container type(s): {}",
unknown_names.join(", ")
));
}
Ok(valid_types)
}
fn parse_apparmor_mode(s: &str) -> Result<ApparmorMode, String> {
match s.to_lowercase().as_str() {
"auto" => Ok(ApparmorMode::Auto),
"off" => Ok(ApparmorMode::Off),
_ => Err(format!(
"invalid apparmor mode '{}', expected 'auto' or 'off'",
s
)),
}
}
fn print_attach_help() {
eprintln!("cntr-attach {}", VERSION);
eprintln!("by {}", AUTHORS);
eprintln!();
eprintln!("USAGE:");
eprintln!(" cntr attach [OPTIONS] <CONTAINER_ID> [-- <COMMAND>...]");
eprintln!();
eprintln!("ARGS:");
eprintln!(" <CONTAINER_ID> Container ID, name, or process ID");
eprintln!();
eprintln!("OPTIONS:");
eprintln!(" -t, --type <TYPES> Container types to try (comma-separated)");
eprintln!(
" [possible: process_id,podman,docker,nspawn,lxc,lxd,containerd,command,kubernetes]"
);
eprintln!(" [default: all but command]");
eprintln!(" --effective-user <USER> Effective username for new files on host");
eprintln!(" --apparmor <MODE> AppArmor profile mode");
eprintln!(" [possible: auto, off]");
eprintln!(" [default: auto]");
eprintln!(" -h, --help Print help");
eprintln!(" -V, --version Print version");
eprintln!();
eprintln!("COMMAND:");
eprintln!(" Command and arguments to execute [default: $SHELL]");
eprintln!(" Use '--' to separate command from options");
}
fn print_exec_help() {
eprintln!("cntr-exec {}", VERSION);
eprintln!("by {}", AUTHORS);
eprintln!();
eprintln!("USAGE:");
eprintln!(" cntr exec [OPTIONS] <CONTAINER_ID> [-- <COMMAND>...]");
eprintln!();
eprintln!("ARGS:");
eprintln!(" <CONTAINER_ID> Container ID, name, or process ID (required)");
eprintln!();
eprintln!("OPTIONS:");
eprintln!(" -t, --type <TYPES> Container types to try (comma-separated)");
eprintln!(
" [possible: process_id,podman,docker,nspawn,lxc,lxd,containerd,command,kubernetes]"
);
eprintln!(" [default: all but command]");
eprintln!(" --apparmor <MODE> AppArmor profile mode");
eprintln!(" [possible: auto, off]");
eprintln!(" [default: auto]");
eprintln!(" -h, --help Print help");
eprintln!(" -V, --version Print version");
eprintln!();
eprintln!("COMMAND:");
eprintln!(" Command and arguments to execute [default: /bin/sh]");
eprintln!(" Use '--' to separate command from options");
}
fn print_help() {
eprintln!("cntr {}", VERSION);
eprintln!("by {}", AUTHORS);
eprintln!();
eprintln!("Enter or execute in container");
eprintln!();
eprintln!("USAGE:");
eprintln!(" cntr <SUBCOMMAND>");
eprintln!();
eprintln!("SUBCOMMANDS:");
eprintln!(" attach Enter container with mount overlay");
eprintln!(" exec Execute command in container");
eprintln!(" help Print help");
eprintln!(" version Print version");
}
fn parse_attach_args<I>(mut args: I) -> Result<std::process::ExitCode, Box<dyn std::error::Error>>
where
I: Iterator<Item = String>,
{
let mut container_id: Option<String> = None;
let mut container_types: Vec<Box<dyn container_pid::Container>> = vec![];
let mut effective_user: Option<User> = None;
let mut apparmor_mode = ApparmorMode::Auto;
let mut command_parts: Vec<String> = vec![];
let mut in_command = false;
while let Some(arg) = args.next() {
if in_command {
command_parts.push(arg);
continue;
}
match arg.as_str() {
"-h" | "--help" => {
print_attach_help();
return Ok(std::process::ExitCode::SUCCESS);
}
"-V" | "--version" => {
eprintln!("cntr {}", VERSION);
return Ok(std::process::ExitCode::SUCCESS);
}
"-t" | "--type" => {
let types_str = args.next().ok_or("--type requires an argument")?;
container_types = parse_container_types(&types_str)
.map_err(|e| format!("invalid --type argument '{}': {}", types_str, e))?;
}
"--effective-user" => {
let username = args.next().ok_or("--effective-user requires an argument")?;
match User::from_name(&username) {
Ok(Some(user)) => effective_user = Some(user),
Ok(None) => return Err(format!("user '{}' not found", username).into()),
Err(e) => {
return Err(format!("failed to lookup user '{}': {}", username, e).into());
}
}
}
"--apparmor" => {
let mode_str = args.next().ok_or("--apparmor requires an argument")?;
apparmor_mode = parse_apparmor_mode(&mode_str).map_err(|e| e.to_string())?;
}
"--" => {
in_command = true;
}
_ if arg.starts_with('-') => {
return Err(format!("unknown option: {}", arg).into());
}
_ => {
if container_id.is_none() {
container_id = Some(arg);
} else {
command_parts.push(arg);
in_command = true;
}
}
}
}
let container_name = container_id.ok_or("missing required argument: <CONTAINER_ID>")?;
let (command, arguments) = if command_parts.is_empty() {
(None, vec![])
} else {
let mut parts = command_parts;
let cmd = parts.remove(0);
(Some(cmd), parts)
};
let options = AttachOptions {
command,
arguments,
container_name: container_name.clone(),
container_types,
effective_user,
apparmor_mode,
};
attach(&options)
.map_err(|e| format!("failed to attach to container '{}': {}", container_name, e))?;
Ok(std::process::ExitCode::SUCCESS)
}
fn parse_exec_args<I>(mut args: I) -> Result<std::process::ExitCode, Box<dyn std::error::Error>>
where
I: Iterator<Item = String>,
{
let mut container_id: Option<String> = None;
let mut container_types: Vec<Box<dyn container_pid::Container>> = vec![];
let mut apparmor_mode = ApparmorMode::Auto;
let mut command_parts: Vec<String> = vec![];
let mut in_command = false;
while let Some(arg) = args.next() {
if in_command {
command_parts.push(arg);
continue;
}
match arg.as_str() {
"-h" | "--help" => {
print_exec_help();
return Ok(std::process::ExitCode::SUCCESS);
}
"-V" | "--version" => {
eprintln!("cntr {}", VERSION);
return Ok(std::process::ExitCode::SUCCESS);
}
"-t" | "--type" => {
let types_str = args.next().ok_or("--type requires an argument")?;
container_types = parse_container_types(&types_str)
.map_err(|e| format!("invalid --type argument '{}': {}", types_str, e))?;
}
"--apparmor" => {
let mode_str = args.next().ok_or("--apparmor requires an argument")?;
apparmor_mode = parse_apparmor_mode(&mode_str).map_err(|e| e.to_string())?;
}
"--" => {
in_command = true;
}
_ if arg.starts_with('-') => {
return Err(format!("unknown option: {}", arg).into());
}
_ => {
if container_id.is_none() {
container_id = Some(arg);
} else {
command_parts.push(arg);
in_command = true;
}
}
}
}
let (command, arguments) = if command_parts.is_empty() {
(None, vec![])
} else {
let mut parts = command_parts;
let cmd = parts.remove(0);
(Some(cmd), parts)
};
let container_name = container_id.ok_or("container ID is required for exec")?;
let options = exec::ExecOptions {
command,
arguments,
container_name: container_name.clone(),
container_types,
apparmor_mode,
};
exec::exec(&options)
.map_err(|e| format!("failed to exec into container '{}': {}", container_name, e))?;
Ok(std::process::ExitCode::SUCCESS)
}
fn maybe_set_dumpable() {
if env::var("CNTR_ALLOW_SETCAP").as_deref() == Ok("1") {
use crate::syscalls::prctl::prctl;
if let Err(e) = prctl(4, 1, 0, 0, 0) {
log::warn!("failed to set PR_SET_DUMPABLE: {}", e);
}
}
}
pub fn run_with_args<I, T>(args: I) -> Result<std::process::ExitCode, Box<dyn std::error::Error>>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
maybe_set_dumpable();
let args: Vec<String> = args
.into_iter()
.map(|s| {
let os_string: std::ffi::OsString = s.into();
os_string.into_string().map_err(|invalid| {
format!(
"argument contains invalid UTF-8: {}",
invalid.to_string_lossy()
)
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut args_iter = args.into_iter();
let _prog = args_iter.next();
let subcommand = match args_iter.next() {
Some(cmd) => cmd,
None => {
print_help();
return Err("no subcommand provided".into());
}
};
match subcommand.as_str() {
"attach" => parse_attach_args(args_iter),
"exec" => parse_exec_args(args_iter),
"help" | "-h" | "--help" => {
print_help();
Ok(std::process::ExitCode::SUCCESS)
}
"version" | "-V" | "--version" => {
eprintln!("cntr {}", VERSION);
Ok(std::process::ExitCode::SUCCESS)
}
_ => Err(format!("unknown subcommand: {}", subcommand).into()),
}
}