1use std::collections::BTreeMap;
4use std::fmt;
5use std::path::PathBuf;
6use std::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11pub const DEFAULT_SANDBOX_CPUS: u8 = 1;
17
18pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
20
21pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
31pub enum DiskImageFormat {
32 Qcow2,
34 Raw,
36 Vmdk,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
43pub enum RootfsSource {
44 Bind(
46 #[cfg_attr(feature = "ts", ts(type = "string"))]
48 PathBuf,
49 ),
50
51 Oci(OciRootfsSource),
53
54 DiskImage {
56 #[cfg_attr(feature = "ts", ts(type = "string"))]
58 path: PathBuf,
59 format: DiskImageFormat,
61 fstype: Option<String>,
63 },
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
69pub struct OciRootfsSource {
70 pub reference: String,
72
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub upper_size_mib: Option<u32>,
76}
77
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
80#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
81pub enum PullPolicy {
82 #[default]
84 IfMissing,
85
86 Always,
88
89 Never,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
102#[serde(rename_all = "lowercase")]
103pub enum StatVirtualization {
104 Strict,
106 Relaxed,
108 Off,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
117#[serde(rename_all = "lowercase")]
118pub enum HostPermissions {
119 Private,
121 Mirror,
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
127#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
128#[serde(rename_all = "lowercase")]
129pub enum SecurityProfile {
130 #[default]
134 Default,
135
136 Restricted,
140}
141
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
144#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
145#[serde(default)]
146pub struct MountOptions {
147 pub readonly: bool,
151
152 pub noexec: bool,
156
157 pub nosuid: bool,
159
160 pub nodev: bool,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
167pub enum VolumeKind {
168 Directory,
170
171 Disk,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
178pub struct VolumeSpec {
179 pub name: String,
181
182 pub kind: VolumeKind,
184
185 pub quota_mib: Option<u32>,
187
188 pub capacity_mib: Option<u32>,
190
191 pub labels: Vec<(String, String)>,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
197#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
198pub enum NamedVolumeMode {
199 Existing,
201
202 Create,
204
205 EnsureExists,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
212pub struct NamedVolumeCreate {
213 pub mode: NamedVolumeMode,
215
216 pub name: String,
218
219 pub kind: VolumeKind,
221
222 pub quota_mib: Option<u32>,
224
225 pub capacity_mib: Option<u32>,
227
228 pub labels: Vec<(String, String)>,
230}
231
232#[derive(Clone)]
234#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
235#[cfg_attr(feature = "ts", ts(tag = "type"))]
236pub enum VolumeMount {
237 Bind {
239 #[cfg_attr(feature = "ts", ts(type = "string"))]
241 host: PathBuf,
242 guest: String,
244 options: MountOptions,
246 stat_virtualization: StatVirtualization,
248 host_permissions: HostPermissions,
250 quota_mib: Option<u32>,
256 },
257
258 Named {
260 name: String,
262 guest: String,
264 create: Option<NamedVolumeCreate>,
268 options: MountOptions,
270 stat_virtualization: StatVirtualization,
272 host_permissions: HostPermissions,
274 },
275
276 Tmpfs {
278 guest: String,
280 size_mib: Option<u32>,
282 options: MountOptions,
284 },
285
286 DiskImage {
288 #[cfg_attr(feature = "ts", ts(type = "string"))]
290 host: PathBuf,
291 guest: String,
293 format: DiskImageFormat,
295 fstype: Option<String>,
297 options: MountOptions,
299 },
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
305pub enum Patch {
306 Text {
308 path: String,
310 content: String,
312 mode: Option<u32>,
314 replace: bool,
316 },
317
318 File {
320 path: String,
322 content: Vec<u8>,
324 mode: Option<u32>,
326 replace: bool,
328 },
329
330 CopyFile {
332 #[cfg_attr(feature = "ts", ts(type = "string"))]
334 src: PathBuf,
335 dst: String,
337 mode: Option<u32>,
339 replace: bool,
341 },
342
343 CopyDir {
345 #[cfg_attr(feature = "ts", ts(type = "string"))]
347 src: PathBuf,
348 dst: String,
350 replace: bool,
352 },
353
354 Symlink {
356 target: String,
358 link: String,
360 replace: bool,
362 },
363
364 Mkdir {
366 path: String,
368 mode: Option<u32>,
370 },
371
372 Remove {
374 path: String,
376 },
377
378 Append {
380 path: String,
382 content: String,
384 },
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
395#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
396#[serde(default)]
397pub struct NetworkSpec {
398 pub enabled: bool,
400
401 #[serde(skip_serializing_if = "Option::is_none")]
403 pub interface: Option<Value>,
404
405 pub ports: Vec<PublishedPortSpec>,
407
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub policy: Option<Value>,
411
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub dns: Option<Value>,
415
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub tls: Option<Value>,
419
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub secrets: Option<Value>,
423
424 pub max_connections: Option<usize>,
426
427 pub trust_host_cas: bool,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
434pub struct PublishedPortSpec {
435 pub host_port: u16,
437
438 pub guest_port: u16,
440
441 #[serde(default)]
443 pub protocol: PortProtocol,
444
445 pub host_bind: String,
447}
448
449#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
451#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
452pub enum PortProtocol {
453 #[default]
455 #[serde(rename = "tcp")]
456 Tcp,
457
458 #[serde(rename = "udp")]
460 Udp,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
469#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
470pub struct HandoffInit {
471 #[cfg_attr(feature = "ts", ts(type = "string"))]
473 pub cmd: PathBuf,
474
475 #[serde(default)]
477 pub args: Vec<String>,
478
479 #[serde(default)]
481 pub env: Vec<(String, String)>,
482}
483
484#[derive(Debug, Default, Clone, Serialize, Deserialize)]
490#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
491pub struct SandboxPolicy {
492 #[serde(default)]
501 pub ephemeral: bool,
502
503 pub max_duration_secs: Option<u64>,
505
506 pub idle_timeout_secs: Option<u64>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
516#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
517pub enum SnapshotDestination {
518 Name(String),
520
521 Path(
523 #[cfg_attr(feature = "ts", ts(type = "string"))]
525 PathBuf,
526 ),
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
531#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
532pub struct SnapshotSpec {
533 pub source_sandbox: String,
535
536 pub destination: SnapshotDestination,
538
539 pub labels: Vec<(String, String)>,
541
542 pub force: bool,
544
545 pub record_integrity: bool,
547}
548
549#[derive(Debug, Default, Clone, Serialize, Deserialize)]
557#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
558#[serde(default)]
559pub struct SandboxSpec {
560 pub name: String,
562
563 pub image: RootfsSource,
565
566 pub resources: SandboxResources,
568
569 pub runtime: SandboxRuntimeOptions,
571
572 pub env: Vec<EnvVar>,
574
575 pub labels: BTreeMap<String, String>,
577
578 pub rlimits: Vec<Rlimit>,
580
581 pub mounts: Vec<VolumeMount>,
583
584 pub patches: Vec<Patch>,
586
587 pub network: NetworkSpec,
589
590 pub init: Option<HandoffInit>,
592
593 pub pull_policy: PullPolicy,
595
596 pub security_profile: SecurityProfile,
598
599 pub lifecycle: SandboxPolicy,
601}
602
603#[derive(Debug, Clone, Copy, Serialize)]
605#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
606pub struct SandboxResources {
607 pub cpus: u8,
609
610 pub memory_mib: u32,
612
613 pub max_cpus: u8,
615
616 pub max_memory_mib: u32,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
623#[serde(default)]
624pub struct SandboxRuntimeOptions {
625 pub workdir: Option<String>,
627
628 pub shell: Option<String>,
630
631 pub scripts: BTreeMap<String, String>,
633
634 pub entrypoint: Option<Vec<String>>,
636
637 pub cmd: Option<Vec<String>>,
639
640 pub hostname: Option<String>,
642
643 pub user: Option<String>,
645
646 pub log_level: Option<SandboxLogLevel>,
648
649 pub metrics_sample_interval_ms: Option<u64>,
651
652 pub disable_metrics_sample: bool,
654}
655
656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
658#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
659pub struct EnvVar {
660 pub key: String,
662
663 pub value: String,
665}
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
669#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
670#[serde(rename_all = "lowercase")]
671pub enum SandboxLogLevel {
672 Error,
674
675 Warn,
677
678 Info,
680
681 Debug,
683
684 Trace,
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
694#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
695pub enum RlimitResource {
696 Cpu,
698 Fsize,
700 Data,
702 Stack,
704 Core,
706 Rss,
708 Nproc,
710 Nofile,
712 Memlock,
714 As,
716 Locks,
718 Sigpending,
720 Msgqueue,
722 Nice,
724 Rtprio,
726 Rttime,
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize)]
732#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
733pub struct Rlimit {
734 pub resource: RlimitResource,
736
737 pub soft: u64,
739
740 pub hard: u64,
742}
743
744#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
750#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
751#[serde(rename_all = "lowercase")]
752pub enum LogSource {
753 Stdout,
755
756 Stderr,
758
759 Output,
761
762 System,
764}
765
766impl DiskImageFormat {
771 pub fn as_str(&self) -> &'static str {
773 match self {
774 Self::Qcow2 => "qcow2",
775 Self::Raw => "raw",
776 Self::Vmdk => "vmdk",
777 }
778 }
779
780 pub fn from_extension(ext: &str) -> Option<Self> {
784 match ext {
785 "qcow2" => Some(Self::Qcow2),
786 "raw" => Some(Self::Raw),
787 "vmdk" => Some(Self::Vmdk),
788 _ => None,
789 }
790 }
791}
792
793impl OciRootfsSource {
794 pub fn new(reference: impl Into<String>) -> Self {
796 Self {
797 reference: reference.into(),
798 upper_size_mib: None,
799 }
800 }
801}
802
803impl RootfsSource {
804 pub fn oci(reference: impl Into<String>) -> Self {
806 Self::Oci(OciRootfsSource::new(reference))
807 }
808
809 pub fn oci_reference(&self) -> Option<&str> {
811 match self {
812 Self::Oci(oci) => Some(&oci.reference),
813 _ => None,
814 }
815 }
816
817 pub fn oci_upper_size_mib(&self) -> Option<u32> {
819 match self {
820 Self::Oci(oci) => oci.upper_size_mib,
821 _ => None,
822 }
823 }
824}
825
826impl EnvVar {
827 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
829 Self {
830 key: key.into(),
831 value: value.into(),
832 }
833 }
834
835 pub fn as_pair(&self) -> (&str, &str) {
837 (&self.key, &self.value)
838 }
839}
840
841impl VolumeKind {
842 pub fn as_str(self) -> &'static str {
844 match self {
845 Self::Directory => "dir",
846 Self::Disk => "disk",
847 }
848 }
849
850 pub fn from_db_value(value: &str) -> Self {
852 match value {
853 "disk" => Self::Disk,
854 _ => Self::Directory,
855 }
856 }
857}
858
859impl VolumeSpec {
860 pub fn new(name: impl Into<String>) -> Self {
862 Self {
863 name: name.into(),
864 kind: VolumeKind::Directory,
865 quota_mib: None,
866 capacity_mib: None,
867 labels: Vec::new(),
868 }
869 }
870}
871
872impl NamedVolumeCreate {
873 pub fn mode(&self) -> NamedVolumeMode {
875 self.mode
876 }
877
878 pub fn name(&self) -> &str {
880 &self.name
881 }
882
883 pub fn kind(&self) -> VolumeKind {
885 self.kind
886 }
887
888 pub fn quota_mib(&self) -> Option<u32> {
890 self.quota_mib
891 }
892
893 pub fn capacity_mib(&self) -> Option<u32> {
895 self.capacity_mib
896 }
897
898 pub fn labels(&self) -> &[(String, String)] {
900 &self.labels
901 }
902}
903
904impl VolumeMount {
905 pub fn guest(&self) -> &str {
907 match self {
908 Self::Bind { guest, .. }
909 | Self::Named { guest, .. }
910 | Self::Tmpfs { guest, .. }
911 | Self::DiskImage { guest, .. } => guest,
912 }
913 }
914
915 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
917 match self {
918 Self::Named { create, .. } => create.as_ref(),
919 _ => None,
920 }
921 }
922}
923
924impl RlimitResource {
925 pub fn as_str(&self) -> &'static str {
927 match self {
928 Self::Cpu => "cpu",
929 Self::Fsize => "fsize",
930 Self::Data => "data",
931 Self::Stack => "stack",
932 Self::Core => "core",
933 Self::Rss => "rss",
934 Self::Nproc => "nproc",
935 Self::Nofile => "nofile",
936 Self::Memlock => "memlock",
937 Self::As => "as",
938 Self::Locks => "locks",
939 Self::Sigpending => "sigpending",
940 Self::Msgqueue => "msgqueue",
941 Self::Nice => "nice",
942 Self::Rtprio => "rtprio",
943 Self::Rttime => "rttime",
944 }
945 }
946}
947
948impl LogSource {
949 pub fn effective(requested: &[Self]) -> Vec<Self> {
951 if requested.is_empty() {
952 vec![Self::Stdout, Self::Stderr, Self::Output]
953 } else {
954 let mut sources = requested.to_vec();
955 sources.sort_by_key(|src| match src {
956 Self::Stdout => 0,
957 Self::Stderr => 1,
958 Self::Output => 2,
959 Self::System => 3,
960 });
961 sources.dedup();
962 sources
963 }
964 }
965}
966
967impl SandboxLogLevel {
968 pub const fn as_str(self) -> &'static str {
970 match self {
971 Self::Error => "error",
972 Self::Warn => "warn",
973 Self::Info => "info",
974 Self::Debug => "debug",
975 Self::Trace => "trace",
976 }
977 }
978}
979
980impl std::fmt::Display for DiskImageFormat {
985 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
986 f.write_str(self.as_str())
987 }
988}
989
990impl FromStr for DiskImageFormat {
991 type Err = String;
992
993 fn from_str(s: &str) -> Result<Self, Self::Err> {
994 match s {
995 "qcow2" => Ok(Self::Qcow2),
996 "raw" => Ok(Self::Raw),
997 "vmdk" => Ok(Self::Vmdk),
998 _ => Err(format!("unknown disk image format: {s}")),
999 }
1000 }
1001}
1002
1003impl Default for RootfsSource {
1004 fn default() -> Self {
1005 Self::oci(String::new())
1006 }
1007}
1008
1009impl Default for SandboxResources {
1010 fn default() -> Self {
1011 Self {
1012 cpus: DEFAULT_SANDBOX_CPUS,
1013 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1014 max_cpus: DEFAULT_SANDBOX_CPUS,
1015 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1016 }
1017 }
1018}
1019
1020impl<'de> Deserialize<'de> for SandboxResources {
1021 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1022 where
1023 D: serde::Deserializer<'de>,
1024 {
1025 #[derive(Deserialize)]
1026 struct RawResources {
1027 #[serde(default = "default_sandbox_cpus")]
1028 cpus: u8,
1029 #[serde(default = "default_sandbox_memory_mib")]
1030 memory_mib: u32,
1031 max_cpus: Option<u8>,
1032 max_memory_mib: Option<u32>,
1033 }
1034
1035 let raw = RawResources::deserialize(deserializer)?;
1036 Ok(Self {
1037 cpus: raw.cpus,
1038 memory_mib: raw.memory_mib,
1039 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1043 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1044 })
1045 }
1046}
1047
1048impl Default for SandboxRuntimeOptions {
1049 fn default() -> Self {
1050 Self {
1051 workdir: None,
1052 shell: None,
1053 scripts: BTreeMap::new(),
1054 entrypoint: None,
1055 cmd: None,
1056 hostname: None,
1057 user: None,
1058 log_level: None,
1059 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1060 disable_metrics_sample: false,
1061 }
1062 }
1063}
1064
1065impl Default for NetworkSpec {
1066 fn default() -> Self {
1067 Self {
1068 enabled: true,
1069 interface: None,
1070 ports: Vec::new(),
1071 policy: None,
1072 dns: None,
1073 tls: None,
1074 secrets: None,
1075 max_connections: None,
1076 trust_host_cas: false,
1077 }
1078 }
1079}
1080
1081impl Default for PublishedPortSpec {
1082 fn default() -> Self {
1083 Self {
1084 host_port: 0,
1085 guest_port: 0,
1086 protocol: PortProtocol::Tcp,
1087 host_bind: "127.0.0.1".into(),
1088 }
1089 }
1090}
1091
1092impl From<(String, String)> for EnvVar {
1093 fn from((key, value): (String, String)) -> Self {
1094 Self { key, value }
1095 }
1096}
1097
1098impl From<EnvVar> for (String, String) {
1099 fn from(var: EnvVar) -> Self {
1100 (var.key, var.value)
1101 }
1102}
1103
1104impl FromStr for SandboxLogLevel {
1105 type Err = String;
1106
1107 fn from_str(s: &str) -> Result<Self, Self::Err> {
1108 match s {
1109 "error" => Ok(Self::Error),
1110 "warn" => Ok(Self::Warn),
1111 "info" => Ok(Self::Info),
1112 "debug" => Ok(Self::Debug),
1113 "trace" => Ok(Self::Trace),
1114 _ => Err(format!("unknown sandbox log level: {s}")),
1115 }
1116 }
1117}
1118
1119impl Serialize for VolumeMount {
1120 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1121 use serde::ser::SerializeMap;
1122
1123 match self {
1124 Self::Bind {
1125 host,
1126 guest,
1127 options,
1128 stat_virtualization,
1129 host_permissions,
1130 quota_mib,
1131 } => {
1132 let mut map = serializer.serialize_map(Some(7))?;
1133 map.serialize_entry("type", "Bind")?;
1134 map.serialize_entry("host", host)?;
1135 map.serialize_entry("guest", guest)?;
1136 map.serialize_entry("options", options)?;
1137 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1138 map.serialize_entry("host_permissions", host_permissions)?;
1139 map.serialize_entry("quota_mib", quota_mib)?;
1140 map.end()
1141 }
1142 Self::Named {
1143 name,
1144 guest,
1145 create: _,
1146 options,
1147 stat_virtualization,
1148 host_permissions,
1149 } => {
1150 let mut map = serializer.serialize_map(Some(6))?;
1151 map.serialize_entry("type", "Named")?;
1152 map.serialize_entry("name", name)?;
1153 map.serialize_entry("guest", guest)?;
1154 map.serialize_entry("options", options)?;
1155 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1156 map.serialize_entry("host_permissions", host_permissions)?;
1157 map.end()
1158 }
1159 Self::Tmpfs {
1160 guest,
1161 size_mib,
1162 options,
1163 } => {
1164 let mut map = serializer.serialize_map(Some(4))?;
1165 map.serialize_entry("type", "Tmpfs")?;
1166 map.serialize_entry("guest", guest)?;
1167 map.serialize_entry("size_mib", size_mib)?;
1168 map.serialize_entry("options", options)?;
1169 map.end()
1170 }
1171 Self::DiskImage {
1172 host,
1173 guest,
1174 format,
1175 fstype,
1176 options,
1177 } => {
1178 let mut map = serializer.serialize_map(Some(6))?;
1179 map.serialize_entry("type", "DiskImage")?;
1180 map.serialize_entry("host", host)?;
1181 map.serialize_entry("guest", guest)?;
1182 map.serialize_entry("format", format)?;
1183 map.serialize_entry("fstype", fstype)?;
1184 map.serialize_entry("options", options)?;
1185 map.end()
1186 }
1187 }
1188 }
1189}
1190
1191impl<'de> Deserialize<'de> for VolumeMount {
1192 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1193 fn default_strict() -> StatVirtualization {
1194 StatVirtualization::Strict
1195 }
1196
1197 fn default_private() -> HostPermissions {
1198 HostPermissions::Private
1199 }
1200
1201 #[derive(Deserialize)]
1202 #[serde(tag = "type")]
1203 enum VolumeMountHelper {
1204 Bind {
1205 host: PathBuf,
1206 guest: String,
1207 #[serde(default)]
1208 options: Option<MountOptions>,
1209 #[serde(default)]
1210 readonly: bool,
1211 #[serde(default = "default_strict")]
1212 stat_virtualization: StatVirtualization,
1213 #[serde(default = "default_private")]
1214 host_permissions: HostPermissions,
1215 #[serde(default)]
1216 quota_mib: Option<u32>,
1217 },
1218 Named {
1219 name: String,
1220 guest: String,
1221 #[serde(default)]
1222 options: Option<MountOptions>,
1223 #[serde(default)]
1224 readonly: bool,
1225 #[serde(default = "default_strict")]
1226 stat_virtualization: StatVirtualization,
1227 #[serde(default = "default_private")]
1228 host_permissions: HostPermissions,
1229 },
1230 Tmpfs {
1231 guest: String,
1232 #[serde(default)]
1233 size_mib: Option<u32>,
1234 #[serde(default)]
1235 options: Option<MountOptions>,
1236 #[serde(default)]
1237 readonly: bool,
1238 },
1239 DiskImage {
1240 host: PathBuf,
1241 guest: String,
1242 format: DiskImageFormat,
1243 #[serde(default)]
1244 fstype: Option<String>,
1245 #[serde(default)]
1246 options: Option<MountOptions>,
1247 #[serde(default)]
1248 readonly: bool,
1249 },
1250 }
1251
1252 let helper = VolumeMountHelper::deserialize(deserializer)?;
1253 Ok(match helper {
1254 VolumeMountHelper::Bind {
1255 host,
1256 guest,
1257 options,
1258 readonly,
1259 stat_virtualization,
1260 host_permissions,
1261 quota_mib,
1262 } => Self::Bind {
1263 host,
1264 guest,
1265 options: decode_mount_options(options, readonly),
1266 stat_virtualization,
1267 host_permissions,
1268 quota_mib,
1269 },
1270 VolumeMountHelper::Named {
1271 name,
1272 guest,
1273 options,
1274 readonly,
1275 stat_virtualization,
1276 host_permissions,
1277 } => Self::Named {
1278 name,
1279 guest,
1280 create: None,
1281 options: decode_mount_options(options, readonly),
1282 stat_virtualization,
1283 host_permissions,
1284 },
1285 VolumeMountHelper::Tmpfs {
1286 guest,
1287 size_mib,
1288 options,
1289 readonly,
1290 } => Self::Tmpfs {
1291 guest,
1292 size_mib,
1293 options: decode_mount_options(options, readonly),
1294 },
1295 VolumeMountHelper::DiskImage {
1296 host,
1297 guest,
1298 format,
1299 fstype,
1300 options,
1301 readonly,
1302 } => Self::DiskImage {
1303 host,
1304 guest,
1305 format,
1306 fstype,
1307 options: decode_mount_options(options, readonly),
1308 },
1309 })
1310 }
1311}
1312
1313impl fmt::Debug for VolumeMount {
1314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1315 match self {
1316 Self::Bind {
1317 host,
1318 guest,
1319 options,
1320 stat_virtualization,
1321 host_permissions,
1322 quota_mib,
1323 } => f
1324 .debug_struct("Bind")
1325 .field("host", host)
1326 .field("guest", guest)
1327 .field("options", options)
1328 .field("stat_virtualization", stat_virtualization)
1329 .field("host_permissions", host_permissions)
1330 .field("quota_mib", quota_mib)
1331 .finish(),
1332 Self::Named {
1333 name,
1334 guest,
1335 create,
1336 options,
1337 stat_virtualization,
1338 host_permissions,
1339 } => f
1340 .debug_struct("Named")
1341 .field("name", name)
1342 .field("guest", guest)
1343 .field("create", create)
1344 .field("options", options)
1345 .field("stat_virtualization", stat_virtualization)
1346 .field("host_permissions", host_permissions)
1347 .finish(),
1348 Self::Tmpfs {
1349 guest,
1350 size_mib,
1351 options,
1352 } => f
1353 .debug_struct("Tmpfs")
1354 .field("guest", guest)
1355 .field("size_mib", size_mib)
1356 .field("options", options)
1357 .finish(),
1358 Self::DiskImage {
1359 host,
1360 guest,
1361 format,
1362 fstype,
1363 options,
1364 } => f
1365 .debug_struct("DiskImage")
1366 .field("host", host)
1367 .field("guest", guest)
1368 .field("format", format)
1369 .field("fstype", fstype)
1370 .field("options", options)
1371 .finish(),
1372 }
1373 }
1374}
1375
1376impl TryFrom<&str> for RlimitResource {
1378 type Error = String;
1379
1380 fn try_from(s: &str) -> Result<Self, Self::Error> {
1381 match s.to_ascii_lowercase().as_str() {
1382 "cpu" => Ok(Self::Cpu),
1383 "fsize" => Ok(Self::Fsize),
1384 "data" => Ok(Self::Data),
1385 "stack" => Ok(Self::Stack),
1386 "core" => Ok(Self::Core),
1387 "rss" => Ok(Self::Rss),
1388 "nproc" => Ok(Self::Nproc),
1389 "nofile" => Ok(Self::Nofile),
1390 "memlock" => Ok(Self::Memlock),
1391 "as" => Ok(Self::As),
1392 "locks" => Ok(Self::Locks),
1393 "sigpending" => Ok(Self::Sigpending),
1394 "msgqueue" => Ok(Self::Msgqueue),
1395 "nice" => Ok(Self::Nice),
1396 "rtprio" => Ok(Self::Rtprio),
1397 "rttime" => Ok(Self::Rttime),
1398 _ => Err(format!("unknown rlimit resource: {s}")),
1399 }
1400 }
1401}
1402
1403fn default_sandbox_cpus() -> u8 {
1408 DEFAULT_SANDBOX_CPUS
1409}
1410
1411fn default_sandbox_memory_mib() -> u32 {
1412 DEFAULT_SANDBOX_MEMORY_MIB
1413}
1414
1415fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
1416 options.unwrap_or(MountOptions {
1417 readonly,
1418 ..MountOptions::default()
1419 })
1420}
1421
1422#[cfg(test)]
1427mod tests {
1428 use super::*;
1429
1430 #[test]
1431 fn disk_image_format_from_extension() {
1432 assert_eq!(
1433 DiskImageFormat::from_extension("qcow2"),
1434 Some(DiskImageFormat::Qcow2)
1435 );
1436 assert_eq!(
1437 DiskImageFormat::from_extension("raw"),
1438 Some(DiskImageFormat::Raw)
1439 );
1440 assert_eq!(
1441 DiskImageFormat::from_extension("vmdk"),
1442 Some(DiskImageFormat::Vmdk)
1443 );
1444 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
1445 assert_eq!(DiskImageFormat::from_extension(""), None);
1446 }
1447
1448 #[test]
1449 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
1450 let resources: SandboxResources =
1451 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
1452
1453 assert_eq!(resources.cpus, 4);
1454 assert_eq!(resources.max_cpus, 4);
1455 assert_eq!(resources.memory_mib, 2048);
1456 assert_eq!(resources.max_memory_mib, 2048);
1457 }
1458
1459 #[test]
1460 fn disk_image_format_display_roundtrip() {
1461 for format in [
1462 DiskImageFormat::Qcow2,
1463 DiskImageFormat::Raw,
1464 DiskImageFormat::Vmdk,
1465 ] {
1466 let rendered = format.to_string();
1467 let parsed: DiskImageFormat = rendered.parse().unwrap();
1468 assert_eq!(parsed, format);
1469 }
1470 }
1471
1472 #[test]
1473 fn disk_image_format_from_str_unknown() {
1474 assert!("ext4".parse::<DiskImageFormat>().is_err());
1475 }
1476
1477 #[test]
1478 fn log_source_effective_uses_default_user_program_sources() {
1479 assert_eq!(
1480 LogSource::effective(&[]),
1481 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
1482 );
1483 }
1484
1485 #[test]
1486 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
1487 assert_eq!(
1488 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
1489 vec![LogSource::Stdout, LogSource::System]
1490 );
1491 }
1492
1493 #[test]
1494 fn rlimit_resource_parses_case_insensitively() {
1495 assert_eq!(
1496 RlimitResource::try_from("NOFILE").unwrap(),
1497 RlimitResource::Nofile
1498 );
1499 assert!(RlimitResource::try_from("bogus").is_err());
1500 }
1501
1502 #[test]
1503 fn sandbox_policy_serde_roundtrip() {
1504 let policy = SandboxPolicy {
1505 ephemeral: true,
1506 max_duration_secs: Some(3600),
1507 idle_timeout_secs: Some(120),
1508 };
1509
1510 let json = serde_json::to_string(&policy).unwrap();
1511 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
1512
1513 assert!(decoded.ephemeral);
1514 assert_eq!(decoded.max_duration_secs, Some(3600));
1515 assert_eq!(decoded.idle_timeout_secs, Some(120));
1516 }
1517
1518 #[test]
1519 fn sandbox_policy_defaults_to_persistent() {
1520 assert!(!SandboxPolicy::default().ephemeral);
1521 }
1522
1523 #[test]
1524 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
1525 let decoded: SandboxPolicy =
1528 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
1529 assert!(!decoded.ephemeral);
1530 assert_eq!(decoded.max_duration_secs, Some(60));
1531 }
1532
1533 #[test]
1534 fn sandbox_spec_default_uses_static_resource_defaults() {
1535 let spec = SandboxSpec::default();
1536
1537 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
1538 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
1539 assert_eq!(
1540 spec.runtime.metrics_sample_interval_ms,
1541 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
1542 );
1543 }
1544
1545 #[test]
1546 fn sandbox_log_level_roundtrips_lowercase_values() {
1547 for (input, expected) in [
1548 ("error", SandboxLogLevel::Error),
1549 ("warn", SandboxLogLevel::Warn),
1550 ("info", SandboxLogLevel::Info),
1551 ("debug", SandboxLogLevel::Debug),
1552 ("trace", SandboxLogLevel::Trace),
1553 ] {
1554 let parsed: SandboxLogLevel = input.parse().unwrap();
1555 assert_eq!(parsed, expected);
1556 assert_eq!(parsed.as_str(), input);
1557 }
1558 }
1559}