use std::ffi::CString;
use std::path::{Path, PathBuf};
use nix::unistd;
use oci_spec::runtime::Spec;
use super::{Executor, ExecutorError, ExecutorValidationError};
#[derive(Clone)]
pub struct DefaultExecutor {}
impl Executor for DefaultExecutor {
fn exec(&self, spec: &Spec) -> Result<(), ExecutorError> {
tracing::debug!("executing workload with default handler");
let args = spec
.process()
.as_ref()
.and_then(|p| p.args().as_ref())
.ok_or_else(|| {
tracing::error!("no arguments provided to execute");
ExecutorError::InvalidArg
})?;
let executable = args[0].as_str();
let cstring_path = CString::new(executable.as_bytes()).map_err(|err| {
tracing::error!("failed to convert path {executable:?} to cstring: {}", err,);
ExecutorError::InvalidArg
})?;
let a: Vec<CString> = args
.iter()
.map(|s| CString::new(s.as_bytes()).unwrap_or_default())
.collect();
unistd::execvp(&cstring_path, &a).map_err(|err| {
tracing::error!(?err, filename = ?cstring_path, args = ?a, "failed to execvp");
ExecutorError::Execution(
format!(
"error '{}' executing '{:?}' with args '{:?}'",
err, cstring_path, a
)
.into(),
)
})?;
unreachable!();
}
fn validate(&self, spec: &Spec) -> Result<(), ExecutorValidationError> {
let proc = spec
.process()
.as_ref()
.ok_or(ExecutorValidationError::ArgValidationError(
"spec did not contain process".into(),
))?;
if let Some(args) = proc.args() {
let envs: Vec<String> = proc.env().as_ref().unwrap_or(&vec![]).clone();
let path_vars: Vec<&String> = envs.iter().filter(|&e| e.starts_with("PATH=")).collect();
if path_vars.is_empty() {
tracing::error!("PATH environment variable is not set");
Err(ExecutorValidationError::ArgValidationError(
"PATH environment variable is not set".into(),
))?;
}
let path_var = path_vars[0].trim_start_matches("PATH=");
match get_executable_path(&args[0], path_var) {
None => {
tracing::error!(
executable = ?args[0],
"executable for container process not found in PATH",
);
Err(ExecutorValidationError::ArgValidationError(format!(
"executable '{}' not found in $PATH",
args[0]
)))?;
}
Some(path) => match is_executable(&path) {
Ok(true) => {
tracing::debug!(executable = ?path, "found executable in executor");
}
Ok(false) => {
tracing::error!(
executable = ?path,
"executable does not have the correct permission set",
);
Err(ExecutorValidationError::ArgValidationError(format!(
"executable '{}' at path '{:?}' does not have correct permissions",
args[0], path
)))?;
}
Err(err) => {
tracing::error!(
executable = ?path,
?err,
"failed to check permissions for executable",
);
Err(ExecutorValidationError::ArgValidationError(format!(
"failed to check permissions for executable '{}' at path '{:?}' : {}",
args[0], path, err
)))?;
}
},
}
}
Ok(())
}
}
pub fn get_executor() -> Box<dyn Executor> {
Box::new(DefaultExecutor {})
}
fn get_executable_path(name: &str, path_var: &str) -> Option<PathBuf> {
if name.contains('/') && PathBuf::from(name).exists() {
return Some(PathBuf::from(name));
}
for path in path_var.split(':') {
let potential_path = PathBuf::from(path).join(name);
if potential_path.exists() {
return Some(potential_path);
}
}
None
}
fn is_executable(path: &Path) -> std::result::Result<bool, std::io::Error> {
use std::os::unix::fs::PermissionsExt;
let metadata = path.metadata()?;
let permissions = metadata.permissions();
Ok(metadata.is_file() && permissions.mode() & 0o001 != 0)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::env;
use serial_test::serial;
use super::*;
#[test]
fn test_get_executable_path() {
let non_existing_abs_path = "/some/non/existent/absolute/path";
let existing_abs_path = "/usr/bin/sh";
let existing_binary = "sh";
let non_existing_binary = "non-existent";
let path_value = "/usr/bin:/bin";
assert_eq!(
get_executable_path(existing_abs_path, path_value),
Some(PathBuf::from(existing_abs_path))
);
assert_eq!(get_executable_path(non_existing_abs_path, path_value), None);
assert_eq!(
get_executable_path(existing_binary, path_value),
Some(PathBuf::from("/usr/bin/sh"))
);
assert_eq!(get_executable_path(non_existing_binary, path_value), None);
}
#[test]
fn test_is_executable() {
let tmp = tempfile::tempdir().expect("create temp directory for test");
let executable_path = PathBuf::from("/bin/sh");
let directory_path = tmp.path();
let non_executable_path = directory_path.join("non_executable_file");
let non_existent_path = PathBuf::from("/some/non/existent/path");
std::fs::File::create(non_executable_path.as_path()).unwrap();
assert!(is_executable(&non_existent_path).is_err());
assert!(is_executable(&executable_path).unwrap());
assert!(!is_executable(&non_executable_path).unwrap());
assert!(!is_executable(directory_path).unwrap());
}
#[test]
#[serial]
fn test_executor_set_envs() {
let original_envs: HashMap<String, String> = env::vars().collect();
{
let executor = get_executor();
let envs = HashMap::from([
("FOO".to_owned(), "hoge".to_owned()),
("BAR".to_owned(), "fuga".to_owned()),
("BAZ".to_owned(), "piyo".to_owned()),
]);
assert!(executor.setup_envs(envs).is_ok());
let current_envs = std::env::vars().collect::<HashMap<String, String>>();
assert_eq!(current_envs.get("FOO").unwrap(), "hoge");
assert_eq!(current_envs.get("BAR").unwrap(), "fuga");
assert_eq!(current_envs.get("BAZ").unwrap(), "piyo");
assert_eq!(current_envs.len(), 3);
}
original_envs.iter().for_each(|(key, value)| {
unsafe { env::set_var(key, value) };
});
}
}