use crate::{
error::OciSpecError,
runtime::{Capabilities, Capability},
};
use derive_builder::Builder;
use getset::{CopyGetters, Getters, MutGetters, Setters};
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize};
use std::path::PathBuf;
use std::sync::OnceLock;
use strum_macros::{Display as StrumDisplay, EnumString};
#[derive(
Builder,
Clone,
CopyGetters,
Debug,
Deserialize,
Getters,
MutGetters,
Setters,
Eq,
PartialEq,
Serialize,
)]
#[serde(rename_all = "camelCase")]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(error = "OciSpecError")
)]
pub struct Process {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
terminal: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
console_size: Option<Box>,
#[getset(get_mut = "pub", get = "pub", set = "pub")]
user: User,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
args: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_mut = "pub", get = "pub", set = "pub")]
command_line: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_mut = "pub", get = "pub", set = "pub")]
env: Option<Vec<String>>,
#[getset(get = "pub", set = "pub")]
cwd: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
capabilities: Option<LinuxCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
#[cfg(any(target_os = "linux", target_os = "solaris"))]
rlimits: Option<Vec<PosixRlimit>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
no_new_privileges: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
apparmor_profile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
oom_score_adj: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
selinux_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
io_priority: Option<LinuxIOPriority>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
scheduler: Option<Scheduler>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
exec_cpu_affinity: Option<ExecCPUAffinity>,
}
impl Default for Process {
fn default() -> Self {
Process {
terminal: false.into(),
console_size: Default::default(),
user: Default::default(),
args: vec!["sh".to_string()].into(),
env: vec![
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(),
"TERM=xterm".into(),
]
.into(),
cwd: "/".into(),
no_new_privileges: true.into(),
apparmor_profile: Default::default(),
selinux_label: Default::default(),
scheduler: Default::default(),
capabilities: Some(Default::default()),
#[cfg(any(target_os = "linux", target_os = "solaris"))]
rlimits: vec![PosixRlimit {
typ: PosixRlimitType::RlimitNofile,
hard: 1024,
soft: 1024,
}]
.into(),
oom_score_adj: None,
command_line: None,
io_priority: Default::default(),
exec_cpu_affinity: Default::default(),
}
}
}
#[derive(
Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
)]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(error = "OciSpecError")
)]
#[getset(get_copy = "pub", set = "pub")]
pub struct Box {
#[serde(default)]
height: u64,
#[serde(default)]
width: u64,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[cfg(any(target_os = "linux", target_os = "solaris"))]
pub enum PosixRlimitType {
RlimitCpu,
RlimitFsize,
RlimitData,
RlimitStack,
RlimitCore,
#[cfg(target_os = "linux")]
RlimitRss,
#[cfg(target_os = "linux")]
RlimitNproc,
RlimitNofile,
#[cfg(target_os = "linux")]
RlimitMemlock,
RlimitAs,
#[cfg(target_os = "linux")]
RlimitLocks,
#[cfg(target_os = "linux")]
RlimitSigpending,
#[cfg(target_os = "linux")]
RlimitMsgqueue,
#[cfg(target_os = "linux")]
RlimitNice,
#[cfg(target_os = "linux")]
RlimitRtprio,
#[cfg(target_os = "linux")]
RlimitRttime,
}
#[cfg(any(target_os = "linux", target_os = "solaris"))]
impl Default for PosixRlimitType {
fn default() -> Self {
Self::RlimitCpu
}
}
#[derive(
Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
)]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(error = "OciSpecError")
)]
#[getset(get_copy = "pub", set = "pub")]
#[cfg(any(target_os = "linux", target_os = "solaris"))]
pub struct PosixRlimit {
#[serde(rename = "type")]
typ: PosixRlimitType,
#[serde(default)]
hard: u64,
#[serde(default)]
soft: u64,
}
#[derive(
Builder,
Clone,
CopyGetters,
Debug,
Default,
Deserialize,
Getters,
MutGetters,
Setters,
Eq,
PartialEq,
Serialize,
)]
#[serde(rename_all = "camelCase")]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(error = "OciSpecError")
)]
pub struct User {
#[serde(default)]
#[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
uid: u32,
#[serde(default)]
#[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
gid: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
umask: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_mut = "pub", get = "pub", set = "pub")]
additional_gids: Option<Vec<u32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[getset(get_mut = "pub", get = "pub", set = "pub")]
username: Option<String>,
}
#[derive(Builder, Clone, Debug, Deserialize, Getters, Setters, Eq, PartialEq, Serialize)]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(error = "OciSpecError")
)]
#[getset(get = "pub", set = "pub")]
pub struct LinuxCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
bounding: Option<Capabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
effective: Option<Capabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
inheritable: Option<Capabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
permitted: Option<Capabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
ambient: Option<Capabilities>,
}
impl Default for LinuxCapabilities {
fn default() -> Self {
let audit_write = Capability::AuditWrite;
let cap_kill = Capability::Kill;
let net_bind = Capability::NetBindService;
let default_vec = vec![audit_write, cap_kill, net_bind]
.into_iter()
.collect::<Capabilities>();
LinuxCapabilities {
bounding: default_vec.clone().into(),
effective: default_vec.clone().into(),
inheritable: default_vec.clone().into(),
permitted: default_vec.clone().into(),
ambient: default_vec.into(),
}
}
}
#[derive(
Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
)]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(error = "OciSpecError")
)]
#[getset(get_copy = "pub", set = "pub")]
pub struct LinuxIOPriority {
#[serde(default)]
class: IOPriorityClass,
#[serde(default)]
priority: i64,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum IOPriorityClass {
IoprioClassRt,
IoprioClassBe,
IoprioClassIdle,
}
impl Default for IOPriorityClass {
fn default() -> Self {
Self::IoprioClassBe
}
}
#[derive(Builder, Clone, Debug, Deserialize, Getters, Setters, Eq, PartialEq, Serialize)]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(error = "OciSpecError")
)]
#[getset(get = "pub", set = "pub")]
pub struct Scheduler {
policy: LinuxSchedulerPolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
nice: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
priority: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
flags: Option<Vec<LinuxSchedulerFlag>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
runtime: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
deadline: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
period: Option<u64>,
}
impl Default for Scheduler {
fn default() -> Self {
Self {
policy: LinuxSchedulerPolicy::default(),
nice: None,
priority: None,
flags: None,
runtime: None,
deadline: None,
period: None,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum LinuxSchedulerPolicy {
SchedOther,
SchedFifo,
SchedRr,
SchedBatch,
SchedIso,
SchedIdle,
SchedDeadline,
}
impl Default for LinuxSchedulerPolicy {
fn default() -> Self {
LinuxSchedulerPolicy::SchedOther
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum LinuxSchedulerFlag {
SchedResetOnFork,
SchedFlagReclaim,
SchedFlagDLOverrun,
SchedFlagKeepPolicy,
SchedFlagKeepParams,
SchedFlagUtilClampMin,
SchedFlagUtilClampMax,
}
impl Default for LinuxSchedulerFlag {
fn default() -> Self {
LinuxSchedulerFlag::SchedResetOnFork
}
}
#[derive(
Builder, Clone, Debug, Default, Deserialize, Getters, Setters, Eq, PartialEq, Serialize,
)]
#[builder(
default,
pattern = "owned",
setter(into, strip_option),
build_fn(validate = "Self::validate", error = "OciSpecError")
)]
#[getset(get = "pub", set = "pub")]
pub struct ExecCPUAffinity {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize"
)]
cpu_affinity_initial: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize"
)]
cpu_affinity_final: Option<String>,
}
impl ExecCPUAffinityBuilder {
fn validate(&self) -> Result<(), OciSpecError> {
if let Some(Some(ref s)) = self.cpu_affinity_initial {
validate_cpu_affinity(s).map_err(|e| OciSpecError::Other(e.to_string()))?;
}
if let Some(Some(ref s)) = self.cpu_affinity_final {
validate_cpu_affinity(s).map_err(|e| OciSpecError::Other(e.to_string()))?;
}
Ok(())
}
}
fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<String> = Option::deserialize(deserializer)?;
if let Some(ref s) = value {
validate_cpu_affinity(s).map_err(de::Error::custom)?;
}
Ok(value)
}
fn exec_cpu_affinity_regex() -> &'static Regex {
static EXEC_CPU_AFFINITY_REGEX: OnceLock<Regex> = OnceLock::new();
EXEC_CPU_AFFINITY_REGEX.get_or_init(|| {
Regex::new(r"^(\d+(-\d+)?)(,\d+(-\d+)?)*$")
.expect("Failed to create regex for execCPUAffinity")
})
}
fn validate_cpu_affinity(s: &str) -> Result<(), String> {
if !exec_cpu_affinity_regex().is_match(s) {
return Err(format!("Invalid execCPUAffinity format: {}", s));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn posix_rlimit_type_enum_to_string() {
let type_a = PosixRlimitType::RlimitCpu;
assert_eq!(type_a.to_string(), "RLIMIT_CPU");
let type_b = PosixRlimitType::RlimitData;
assert_eq!(type_b.to_string(), "RLIMIT_DATA");
let type_c = PosixRlimitType::RlimitNofile;
assert_eq!(type_c.to_string(), "RLIMIT_NOFILE");
}
#[test]
fn posix_rlimit_type_string_to_enum() {
let posix_rlimit_type_str = "RLIMIT_CPU";
let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitCpu);
let posix_rlimit_type_str = "RLIMIT_DATA";
let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitData);
let posix_rlimit_type_str = "RLIMIT_NOFILE";
let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitNofile);
let invalid_posix_rlimit_type_str = "x";
let unknown_rlimit = invalid_posix_rlimit_type_str.parse::<PosixRlimitType>();
assert!(unknown_rlimit.is_err());
}
#[test]
fn exec_cpu_affinity_valid_initial_final() {
let json = json!({"cpu_affinity_initial": "0-3,7", "cpu_affinity_final": "4-6,8"});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_ok());
let json = json!({"cpu_affinity_initial": "0-3", "cpu_affinity_final": "4-6"});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_ok());
let json = json!({"cpu_affinity_initial": "0", "cpu_affinity_final": "4"});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_ok());
}
#[test]
fn exec_cpu_affinity_invalid_initial() {
let json = json!({"cpu_affinity_initial": "0-3,,7", "cpu_affinity_final": "4-6,8"});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_err());
}
#[test]
fn exec_cpu_affinity_invalid_final() {
let json = json!({"cpu_affinity_initial": "0-3,7", "cpu_affinity_final": "4-6.,8"});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_err());
}
#[test]
fn exec_cpu_affinity_valid_final() {
let json = json!({"cpu_affinity_final": "0,1,2,3"});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_ok());
assert!(result.unwrap().cpu_affinity_initial.is_none());
}
#[test]
fn exec_cpu_affinity_valid_initial() {
let json = json!({"cpu_affinity_initial": "0-1,2-5"});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_ok());
assert!(result.unwrap().cpu_affinity_final.is_none());
}
#[test]
fn exec_cpu_affinity_empty() {
let json = json!({});
let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
assert!(result.is_ok());
let affinity = result.unwrap();
assert!(affinity.cpu_affinity_initial.is_none());
assert!(affinity.cpu_affinity_final.is_none());
}
#[test]
fn test_build_valid_input() {
let affinity = ExecCPUAffinityBuilder::default()
.cpu_affinity_initial("0-3,7,8,9,10".to_string())
.cpu_affinity_final("4-6,8".to_string())
.build();
assert!(affinity.is_ok());
let affinity = affinity.unwrap();
assert_eq!(
affinity.cpu_affinity_initial,
Some("0-3,7,8,9,10".to_string())
);
assert_eq!(affinity.cpu_affinity_final, Some("4-6,8".to_string()));
}
#[test]
fn test_build_invalid_initial() {
let affinity = ExecCPUAffinityBuilder::default()
.cpu_affinity_initial("0-3,i".to_string())
.cpu_affinity_final("4-6,8".to_string())
.build();
let err = affinity.unwrap_err();
assert_eq!(err.to_string(), "Invalid execCPUAffinity format: 0-3,i");
let affinity = ExecCPUAffinityBuilder::default()
.cpu_affinity_initial("-".to_string())
.cpu_affinity_final("4-6,8".to_string())
.build();
let err = affinity.unwrap_err();
assert_eq!(err.to_string(), "Invalid execCPUAffinity format: -");
}
#[test]
fn test_build_invalid_final() {
let affinity = ExecCPUAffinityBuilder::default()
.cpu_affinity_initial("0-3,7".to_string())
.cpu_affinity_final("0-l1".to_string())
.build();
let err = affinity.unwrap_err();
assert_eq!(err.to_string(), "Invalid execCPUAffinity format: 0-l1");
let affinity = ExecCPUAffinityBuilder::default()
.cpu_affinity_initial("0-3,7".to_string())
.cpu_affinity_final(",1,2".to_string())
.build();
let err = affinity.unwrap_err();
assert_eq!(err.to_string(), "Invalid execCPUAffinity format: ,1,2");
}
#[test]
fn test_build_empty() {
let affinity = ExecCPUAffinityBuilder::default().build();
let affinity = affinity.unwrap();
assert!(affinity.cpu_affinity_initial.is_none());
assert!(affinity.cpu_affinity_final.is_none());
}
}