use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::error::{Error, ErrorCode};
const DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
const HOST_NAME_MAX: usize = 64;
#[derive(Debug, Clone)]
pub struct RunRequest {
pub rootfs: PathBuf,
pub argv: Vec<String>,
pub env: Vec<(String, String)>,
pub cwd: String,
pub hostname: String,
pub state_root: Option<PathBuf>,
pub isolate_network: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct PreparedRun {
pub rootfs: PathBuf,
pub argv: Vec<String>,
pub env: Vec<String>,
pub cwd: String,
pub hostname: String,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub state_root: Option<PathBuf>,
pub isolate_network: bool,
}
impl RunRequest {
pub fn new(
rootfs: impl Into<PathBuf>,
argv: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
rootfs: rootfs.into(),
argv: argv.into_iter().map(Into::into).collect(),
env: Vec::new(),
cwd: "/".to_string(),
hostname: "runner".to_string(),
state_root: None,
isolate_network: false,
}
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
self.cwd = cwd.into();
self
}
pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
self.hostname = hostname.into();
self
}
pub fn state_root(mut self, state_root: impl Into<PathBuf>) -> Self {
self.state_root = Some(state_root.into());
self
}
pub fn isolate_network(mut self, isolate: bool) -> Self {
self.isolate_network = isolate;
self
}
pub(crate) fn prepare(&self) -> Result<PreparedRun, Error> {
if self.argv.is_empty() || self.argv.iter().any(|arg| arg.is_empty()) {
return Err(Error::new(
ErrorCode::InvalidArgument,
"command is empty",
"",
));
}
if self.argv.iter().any(|arg| arg.contains('\0')) {
return Err(Error::new(
ErrorCode::InvalidArgument,
"command contains a NUL byte",
"",
));
}
if self.cwd.is_empty() || !self.cwd.starts_with('/') {
return Err(Error::new(
ErrorCode::InvalidArgument,
"cwd must be an absolute path",
"",
));
}
if self.hostname.is_empty() || self.hostname.len() > HOST_NAME_MAX {
return Err(Error::new(
ErrorCode::InvalidArgument,
"hostname must be 1 to 64 bytes",
"",
));
}
if self.hostname.contains('\0') {
return Err(Error::new(
ErrorCode::InvalidArgument,
"hostname contains a NUL byte",
"",
));
}
let rootfs = canonicalize_dir(&self.rootfs)?;
let state_root = match &self.state_root {
None => None,
Some(path) => {
std::fs::create_dir_all(path).map_err(|err| {
Error::new(
ErrorCode::InvalidArgument,
format!("state directory {}: {err}", path.display()),
"",
)
})?;
Some(canonicalize_dir(path)?)
}
};
Ok(PreparedRun {
rootfs,
argv: self.argv.clone(),
env: merge_env(&self.env)?,
cwd: self.cwd.clone(),
hostname: self.hostname.clone(),
state_root,
isolate_network: self.isolate_network,
})
}
}
fn canonicalize_dir(path: &Path) -> Result<PathBuf, Error> {
let canon = path.canonicalize().map_err(|err| {
let code = if err.kind() == std::io::ErrorKind::NotFound {
ErrorCode::NotFound
} else {
ErrorCode::InvalidArgument
};
Error::new(code, format!("{}: {err}", path.display()), "")
})?;
if !canon.is_dir() {
return Err(Error::new(
ErrorCode::InvalidArgument,
format!("{} is not a directory", canon.display()),
"",
));
}
Ok(canon)
}
fn merge_env(pairs: &[(String, String)]) -> Result<Vec<String>, Error> {
let mut map = BTreeMap::new();
map.insert("PATH".to_string(), DEFAULT_PATH.to_string());
map.insert("HOME".to_string(), "/root".to_string());
for (key, value) in pairs {
if key.is_empty() || key.contains('=') || key.contains('\0') || value.contains('\0') {
return Err(Error::new(
ErrorCode::InvalidArgument,
format!("environment entry {key} is not a usable KEY=VALUE"),
"",
));
}
map.insert(key.clone(), value.clone());
}
Ok(map
.into_iter()
.map(|(key, value)| format!("{key}={value}"))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_an_empty_command_and_a_relative_cwd() {
let err = RunRequest::new("/no/such", Vec::<String>::new())
.prepare()
.unwrap_err();
assert_eq!(err.code(), ErrorCode::InvalidArgument);
let dir = std::env::temp_dir().join(format!(
"ror-req-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let err = RunRequest::new(&dir, ["/bin/true"])
.cwd("relative")
.prepare()
.unwrap_err();
assert_eq!(err.code(), ErrorCode::InvalidArgument);
let err = RunRequest::new(&dir, ["/bin/true"])
.env("=bad", "x")
.prepare()
.unwrap_err();
assert_eq!(err.code(), ErrorCode::InvalidArgument);
let prepared = RunRequest::new(&dir, ["/bin/true"])
.env("FOO", "bar")
.prepare()
.unwrap();
assert!(prepared.env.iter().any(|item| item == "FOO=bar"));
assert!(prepared.env.iter().any(|item| item.starts_with("PATH=")));
assert_eq!(prepared.hostname, "runner");
assert!(!prepared.isolate_network);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn missing_rootfs_is_not_found() {
let err = RunRequest::new("/no/such/oci-runner-rootfs", ["/bin/true"])
.prepare()
.unwrap_err();
assert_eq!(err.code(), ErrorCode::NotFound);
}
}