1use crate::{
2 error::OciSpecError,
3 runtime::{Capabilities, Capability},
4};
5use derive_builder::Builder;
6use getset::{CopyGetters, Getters, MutGetters, Setters};
7use regex::Regex;
8use serde::{de, Deserialize, Deserializer, Serialize};
9use std::path::PathBuf;
10use std::sync::OnceLock;
11use strum_macros::{Display as StrumDisplay, EnumString};
12
13#[derive(
14 Builder,
15 Clone,
16 CopyGetters,
17 Debug,
18 Deserialize,
19 Getters,
20 MutGetters,
21 Setters,
22 Eq,
23 PartialEq,
24 Serialize,
25)]
26#[serde(rename_all = "camelCase")]
27#[builder(
28 default,
29 pattern = "owned",
30 setter(into, strip_option),
31 build_fn(error = "OciSpecError")
32)]
33pub struct Process {
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 #[getset(get_copy = "pub", set = "pub")]
38 terminal: Option<bool>,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 #[getset(get_copy = "pub", set = "pub")]
43 console_size: Option<Box>,
45
46 #[getset(get_mut = "pub", get = "pub", set = "pub")]
47 user: User,
49
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 #[getset(get = "pub", set = "pub")]
52 args: Option<Vec<String>>,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 #[getset(get_mut = "pub", get = "pub", set = "pub")]
58 command_line: Option<String>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 #[getset(get_mut = "pub", get = "pub", set = "pub")]
64 env: Option<Vec<String>>,
66
67 #[getset(get = "pub", set = "pub")]
68 cwd: PathBuf,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 #[getset(get = "pub", set = "pub")]
74 capabilities: Option<LinuxCapabilities>,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 #[getset(get = "pub", set = "pub")]
79 rlimits: Option<Vec<PosixRlimit>>,
81
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 #[getset(get_copy = "pub", set = "pub")]
84 no_new_privileges: Option<bool>,
87
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 #[getset(get = "pub", set = "pub")]
90 apparmor_profile: Option<String>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
94 #[getset(get_copy = "pub", set = "pub")]
95 oom_score_adj: Option<i32>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 #[getset(get = "pub", set = "pub")]
100 selinux_label: Option<String>,
103
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 #[getset(get = "pub", set = "pub")]
106 io_priority: Option<LinuxIOPriority>,
108
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 #[getset(get = "pub", set = "pub")]
111 scheduler: Option<Scheduler>,
113
114 #[serde(
115 rename = "execCPUAffinity",
116 default,
117 skip_serializing_if = "Option::is_none"
118 )]
119 #[getset(get = "pub", set = "pub")]
120 exec_cpu_affinity: Option<ExecCPUAffinity>,
122}
123
124impl Default for Process {
126 fn default() -> Self {
127 Process {
128 terminal: false.into(),
130 console_size: Default::default(),
132 user: Default::default(),
134 args: vec!["sh".to_string()].into(),
136 env: vec![
138 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(),
139 "TERM=xterm".into(),
140 ]
141 .into(),
142 cwd: "/".into(),
144 no_new_privileges: true.into(),
146 apparmor_profile: Default::default(),
148 selinux_label: Default::default(),
150 scheduler: Default::default(),
152 capabilities: Some(Default::default()),
154 rlimits: vec![PosixRlimit {
157 typ: PosixRlimitType::RlimitNofile,
158 hard: 1024,
159 soft: 1024,
160 }]
161 .into(),
162 oom_score_adj: None,
163 command_line: None,
164 io_priority: Default::default(),
166 exec_cpu_affinity: Default::default(),
167 }
168 }
169}
170
171#[derive(
172 Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
173)]
174#[builder(
175 default,
176 pattern = "owned",
177 setter(into, strip_option),
178 build_fn(error = "OciSpecError")
179)]
180#[getset(get_copy = "pub", set = "pub")]
181pub struct Box {
184 #[serde(default)]
185 height: u64,
187
188 #[serde(default)]
189 width: u64,
191}
192
193#[derive(
194 Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString,
195)]
196#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
197#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
198pub enum PosixRlimitType {
200 #[default]
202 RlimitCpu,
203
204 RlimitFsize,
206
207 RlimitData,
210
211 RlimitStack,
213
214 RlimitCore,
216
217 RlimitRss,
220
221 RlimitNproc,
223
224 RlimitNofile,
227
228 RlimitMemlock,
230
231 RlimitAs,
233
234 RlimitLocks,
236
237 RlimitSigpending,
239
240 RlimitMsgqueue,
243
244 RlimitNice,
246
247 RlimitRtprio,
249
250 RlimitRttime,
254}
255
256#[derive(
257 Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
258)]
259#[builder(
260 default,
261 pattern = "owned",
262 setter(into, strip_option),
263 build_fn(error = "OciSpecError")
264)]
265#[getset(get_copy = "pub", set = "pub")]
266pub struct PosixRlimit {
268 #[serde(rename = "type")]
269 typ: PosixRlimitType,
271
272 #[serde(default)]
273 hard: u64,
275
276 #[serde(default)]
277 soft: u64,
279}
280
281#[derive(
282 Builder,
283 Clone,
284 CopyGetters,
285 Debug,
286 Default,
287 Deserialize,
288 Getters,
289 MutGetters,
290 Setters,
291 Eq,
292 PartialEq,
293 Serialize,
294)]
295#[serde(rename_all = "camelCase")]
296#[builder(
297 default,
298 pattern = "owned",
299 setter(into, strip_option),
300 build_fn(error = "OciSpecError")
301)]
302pub struct User {
304 #[serde(default)]
305 #[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
306 uid: u32,
308
309 #[serde(default)]
310 #[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
311 gid: u32,
313
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 #[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
316 umask: Option<u32>,
318
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 #[getset(get_mut = "pub", get = "pub", set = "pub")]
321 additional_gids: Option<Vec<u32>>,
324
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 #[getset(get_mut = "pub", get = "pub", set = "pub")]
327 username: Option<String>,
329}
330
331#[derive(Builder, Clone, Debug, Deserialize, Getters, Setters, Eq, PartialEq, Serialize)]
332#[builder(
333 default,
334 pattern = "owned",
335 setter(into, strip_option),
336 build_fn(error = "OciSpecError")
337)]
338#[getset(get = "pub", set = "pub")]
339pub struct LinuxCapabilities {
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 bounding: Option<Capabilities>,
345
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 effective: Option<Capabilities>,
349
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 inheritable: Option<Capabilities>,
353
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 permitted: Option<Capabilities>,
357
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 ambient: Option<Capabilities>,
361}
362
363impl Default for LinuxCapabilities {
368 fn default() -> Self {
369 let audit_write = Capability::AuditWrite;
370 let cap_kill = Capability::Kill;
371 let net_bind = Capability::NetBindService;
372 let default_vec = vec![audit_write, cap_kill, net_bind]
373 .into_iter()
374 .collect::<Capabilities>();
375 LinuxCapabilities {
376 bounding: default_vec.clone().into(),
377 effective: default_vec.clone().into(),
378 inheritable: default_vec.clone().into(),
379 permitted: default_vec.clone().into(),
380 ambient: default_vec.into(),
381 }
382 }
383}
384
385#[derive(
386 Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
387)]
388#[builder(
389 default,
390 pattern = "owned",
391 setter(into, strip_option),
392 build_fn(error = "OciSpecError")
393)]
394#[getset(get_copy = "pub", set = "pub")]
395pub struct LinuxIOPriority {
397 #[serde(default)]
398 class: IOPriorityClass,
400
401 #[serde(default)]
402 priority: i64,
404}
405
406#[derive(
407 Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString,
408)]
409#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
410#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
411pub enum IOPriorityClass {
413 IoprioClassRt,
422 #[default]
429 IoprioClassBe,
430 IoprioClassIdle,
434}
435
436#[derive(Builder, Clone, Debug, Deserialize, Getters, Setters, Eq, PartialEq, Serialize)]
437#[builder(
438 default,
439 pattern = "owned",
440 setter(into, strip_option),
441 build_fn(error = "OciSpecError")
442)]
443#[getset(get = "pub", set = "pub")]
444pub struct Scheduler {
447 policy: LinuxSchedulerPolicy,
449
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 nice: Option<i32>,
453
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 priority: Option<i32>,
457
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 flags: Option<Vec<LinuxSchedulerFlag>>,
461
462 #[serde(default, skip_serializing_if = "Option::is_none")]
464 runtime: Option<u64>,
467
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 deadline: Option<u64>,
471
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 period: Option<u64>,
475}
476
477impl Default for Scheduler {
479 fn default() -> Self {
480 Self {
481 policy: LinuxSchedulerPolicy::default(),
482 nice: None,
483 priority: None,
484 flags: None,
485 runtime: None,
486 deadline: None,
487 period: None,
488 }
489 }
490}
491
492#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
493#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
494#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
495pub enum LinuxSchedulerPolicy {
497 SchedOther,
499 SchedFifo,
501 SchedRr,
503 SchedBatch,
505 SchedIso,
507 SchedIdle,
509 SchedDeadline,
511}
512
513impl Default for LinuxSchedulerPolicy {
515 fn default() -> Self {
516 LinuxSchedulerPolicy::SchedOther
517 }
518}
519
520#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
521#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
522#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
523pub enum LinuxSchedulerFlag {
525 SchedResetOnFork,
527 SchedFlagReclaim,
529 SchedFlagDLOverrun,
531 SchedFlagKeepPolicy,
533 SchedFlagKeepParams,
535 SchedFlagUtilClampMin,
537 SchedFlagUtilClampMax,
539}
540
541impl Default for LinuxSchedulerFlag {
543 fn default() -> Self {
544 LinuxSchedulerFlag::SchedResetOnFork
545 }
546}
547
548#[derive(
549 Builder, Clone, Debug, Default, Deserialize, Getters, Setters, Eq, PartialEq, Serialize,
550)]
551#[builder(
552 default,
553 pattern = "owned",
554 setter(into, strip_option),
555 build_fn(validate = "Self::validate", error = "OciSpecError")
556)]
557#[getset(get = "pub", set = "pub")]
558pub struct ExecCPUAffinity {
561 #[serde(
562 default,
563 skip_serializing_if = "Option::is_none",
564 deserialize_with = "deserialize"
565 )]
566 initial: Option<String>,
571
572 #[serde(
573 default,
574 rename = "final",
575 skip_serializing_if = "Option::is_none",
576 deserialize_with = "deserialize"
577 )]
578 cpu_affinity_final: Option<String>,
583}
584
585impl ExecCPUAffinityBuilder {
586 fn validate(&self) -> Result<(), OciSpecError> {
587 if let Some(Some(ref s)) = self.initial {
588 validate_cpu_affinity(s).map_err(|e| OciSpecError::Other(e.to_string()))?;
589 }
590
591 if let Some(Some(ref s)) = self.cpu_affinity_final {
592 validate_cpu_affinity(s).map_err(|e| OciSpecError::Other(e.to_string()))?;
593 }
594
595 Ok(())
596 }
597}
598
599fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
600where
601 D: Deserializer<'de>,
602{
603 let value: Option<String> = Option::deserialize(deserializer)?;
604
605 if let Some(ref s) = value {
606 validate_cpu_affinity(s).map_err(de::Error::custom)?;
607 }
608
609 Ok(value)
610}
611
612fn exec_cpu_affinity_regex() -> &'static Regex {
613 static EXEC_CPU_AFFINITY_REGEX: OnceLock<Regex> = OnceLock::new();
614 EXEC_CPU_AFFINITY_REGEX.get_or_init(|| {
615 Regex::new(r"^(\d+(-\d+)?)(,\d+(-\d+)?)*$")
616 .expect("Failed to create regex for execCPUAffinity")
617 })
618}
619
620fn validate_cpu_affinity(s: &str) -> Result<(), String> {
621 if !exec_cpu_affinity_regex().is_match(s) {
622 return Err(format!("Invalid execCPUAffinity format: {s}"));
623 }
624
625 Ok(())
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use serde_json::json;
632
633 #[test]
635 fn posix_rlimit_type_enum_to_string() {
636 let type_a = PosixRlimitType::RlimitCpu;
637 assert_eq!(type_a.to_string(), "RLIMIT_CPU");
638
639 let type_b = PosixRlimitType::RlimitData;
640 assert_eq!(type_b.to_string(), "RLIMIT_DATA");
641
642 let type_c = PosixRlimitType::RlimitNofile;
643 assert_eq!(type_c.to_string(), "RLIMIT_NOFILE");
644 }
645
646 #[test]
647 fn posix_rlimit_type_string_to_enum() {
648 let posix_rlimit_type_str = "RLIMIT_CPU";
649 let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
650 assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitCpu);
651
652 let posix_rlimit_type_str = "RLIMIT_DATA";
653 let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
654 assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitData);
655
656 let posix_rlimit_type_str = "RLIMIT_NOFILE";
657 let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
658 assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitNofile);
659
660 let invalid_posix_rlimit_type_str = "x";
661 let unknown_rlimit = invalid_posix_rlimit_type_str.parse::<PosixRlimitType>();
662 assert!(unknown_rlimit.is_err());
663 }
664
665 #[test]
666 fn exec_cpu_affinity_valid_initial_final() {
667 let json = json!({"initial": "0-3,7", "final": "4-6,8"});
668 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
669 assert!(result.is_ok());
670
671 let json = json!({"initial": "0-3", "final": "4-6"});
672 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
673 assert!(result.is_ok());
674
675 let json = json!({"initial": "0", "final": "4"});
676 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
677 assert!(result.is_ok());
678 }
679
680 #[test]
681 fn exec_cpu_affinity_invalid_initial() {
682 let json = json!({"initial": "0-3,,7", "final": "4-6,8"});
683 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
684 assert!(result.is_err());
685 }
686
687 #[test]
688 fn exec_cpu_affinity_invalid_final() {
689 let json = json!({"initial": "0-3,7", "final": "4-6.,8"});
690 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
691 assert!(result.is_err());
692 }
693
694 #[test]
695 fn exec_cpu_affinity_valid_final() {
696 let json = json!({"final": "0,1,2,3"});
697 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
698 assert!(result.is_ok());
699 assert!(result.unwrap().initial.is_none());
700 }
701
702 #[test]
703 fn exec_cpu_affinity_valid_initial() {
704 let json = json!({"initial": "0-1,2-5"});
705 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
706 assert!(result.is_ok());
707 assert!(result.unwrap().cpu_affinity_final.is_none());
708 }
709
710 #[test]
711 fn exec_cpu_affinity_empty() {
712 let json = json!({});
713 let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
714 assert!(result.is_ok());
715 let affinity = result.unwrap();
716 assert!(affinity.initial.is_none());
717 assert!(affinity.cpu_affinity_final.is_none());
718 }
719
720 #[test]
721 fn test_build_valid_input() {
722 let affinity = ExecCPUAffinityBuilder::default()
723 .initial("0-3,7,8,9,10".to_string())
724 .cpu_affinity_final("4-6,8".to_string())
725 .build();
726 assert!(affinity.is_ok());
727 let affinity = affinity.unwrap();
728 assert_eq!(affinity.initial, Some("0-3,7,8,9,10".to_string()));
729 assert_eq!(affinity.cpu_affinity_final, Some("4-6,8".to_string()));
730 }
731
732 #[test]
733 fn test_build_invalid_initial() {
734 let affinity = ExecCPUAffinityBuilder::default()
735 .initial("0-3,i".to_string())
736 .cpu_affinity_final("4-6,8".to_string())
737 .build();
738 let err = affinity.unwrap_err();
739 assert_eq!(err.to_string(), "Invalid execCPUAffinity format: 0-3,i");
740
741 let affinity = ExecCPUAffinityBuilder::default()
742 .initial("-".to_string())
743 .cpu_affinity_final("4-6,8".to_string())
744 .build();
745 let err = affinity.unwrap_err();
746 assert_eq!(err.to_string(), "Invalid execCPUAffinity format: -");
747 }
748
749 #[test]
750 fn test_build_invalid_final() {
751 let affinity = ExecCPUAffinityBuilder::default()
752 .initial("0-3,7".to_string())
753 .cpu_affinity_final("0-l1".to_string())
754 .build();
755 let err = affinity.unwrap_err();
756 assert_eq!(err.to_string(), "Invalid execCPUAffinity format: 0-l1");
757
758 let affinity = ExecCPUAffinityBuilder::default()
759 .initial("0-3,7".to_string())
760 .cpu_affinity_final(",1,2".to_string())
761 .build();
762 let err = affinity.unwrap_err();
763 assert_eq!(err.to_string(), "Invalid execCPUAffinity format: ,1,2");
764 }
765
766 #[test]
767 fn test_build_empty() {
768 let affinity = ExecCPUAffinityBuilder::default().build();
769 let affinity = affinity.unwrap();
770 assert!(affinity.initial.is_none());
771 assert!(affinity.cpu_affinity_final.is_none());
772 }
773}