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 },
251
252 Named {
254 name: String,
256 guest: String,
258 create: Option<NamedVolumeCreate>,
262 options: MountOptions,
264 stat_virtualization: StatVirtualization,
266 host_permissions: HostPermissions,
268 },
269
270 Tmpfs {
272 guest: String,
274 size_mib: Option<u32>,
276 options: MountOptions,
278 },
279
280 DiskImage {
282 #[cfg_attr(feature = "ts", ts(type = "string"))]
284 host: PathBuf,
285 guest: String,
287 format: DiskImageFormat,
289 fstype: Option<String>,
291 options: MountOptions,
293 },
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
299pub enum Patch {
300 Text {
302 path: String,
304 content: String,
306 mode: Option<u32>,
308 replace: bool,
310 },
311
312 File {
314 path: String,
316 content: Vec<u8>,
318 mode: Option<u32>,
320 replace: bool,
322 },
323
324 CopyFile {
326 #[cfg_attr(feature = "ts", ts(type = "string"))]
328 src: PathBuf,
329 dst: String,
331 mode: Option<u32>,
333 replace: bool,
335 },
336
337 CopyDir {
339 #[cfg_attr(feature = "ts", ts(type = "string"))]
341 src: PathBuf,
342 dst: String,
344 replace: bool,
346 },
347
348 Symlink {
350 target: String,
352 link: String,
354 replace: bool,
356 },
357
358 Mkdir {
360 path: String,
362 mode: Option<u32>,
364 },
365
366 Remove {
368 path: String,
370 },
371
372 Append {
374 path: String,
376 content: String,
378 },
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
389#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
390#[serde(default)]
391pub struct NetworkSpec {
392 pub enabled: bool,
394
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub interface: Option<Value>,
398
399 pub ports: Vec<PublishedPortSpec>,
401
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub policy: Option<Value>,
405
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub dns: Option<Value>,
409
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub tls: Option<Value>,
413
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub secrets: Option<Value>,
417
418 pub max_connections: Option<usize>,
420
421 pub trust_host_cas: bool,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
428pub struct PublishedPortSpec {
429 pub host_port: u16,
431
432 pub guest_port: u16,
434
435 #[serde(default)]
437 pub protocol: PortProtocol,
438
439 pub host_bind: String,
441}
442
443#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
445#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
446pub enum PortProtocol {
447 #[default]
449 #[serde(rename = "tcp")]
450 Tcp,
451
452 #[serde(rename = "udp")]
454 Udp,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
463#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
464pub struct HandoffInit {
465 #[cfg_attr(feature = "ts", ts(type = "string"))]
467 pub cmd: PathBuf,
468
469 #[serde(default)]
471 pub args: Vec<String>,
472
473 #[serde(default)]
475 pub env: Vec<(String, String)>,
476}
477
478#[derive(Debug, Default, Clone, Serialize, Deserialize)]
484#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
485pub struct SandboxPolicy {
486 #[serde(default)]
495 pub ephemeral: bool,
496
497 pub max_duration_secs: Option<u64>,
499
500 pub idle_timeout_secs: Option<u64>,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
510#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
511pub enum SnapshotDestination {
512 Name(String),
514
515 Path(
517 #[cfg_attr(feature = "ts", ts(type = "string"))]
519 PathBuf,
520 ),
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
525#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
526pub struct SnapshotSpec {
527 pub source_sandbox: String,
529
530 pub destination: SnapshotDestination,
532
533 pub labels: Vec<(String, String)>,
535
536 pub force: bool,
538
539 pub record_integrity: bool,
541}
542
543#[derive(Debug, Default, Clone, Serialize, Deserialize)]
551#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
552#[serde(default)]
553pub struct SandboxSpec {
554 pub name: String,
556
557 pub image: RootfsSource,
559
560 pub resources: SandboxResources,
562
563 pub runtime: SandboxRuntimeOptions,
565
566 pub env: Vec<EnvVar>,
568
569 pub labels: BTreeMap<String, String>,
571
572 pub rlimits: Vec<Rlimit>,
574
575 pub mounts: Vec<VolumeMount>,
577
578 pub patches: Vec<Patch>,
580
581 pub network: NetworkSpec,
583
584 pub init: Option<HandoffInit>,
586
587 pub pull_policy: PullPolicy,
589
590 pub security_profile: SecurityProfile,
592
593 pub lifecycle: SandboxPolicy,
595}
596
597#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
599#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
600#[serde(default)]
601pub struct SandboxResources {
602 pub cpus: u8,
604
605 pub memory_mib: u32,
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize)]
611#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
612#[serde(default)]
613pub struct SandboxRuntimeOptions {
614 pub workdir: Option<String>,
616
617 pub shell: Option<String>,
619
620 pub scripts: BTreeMap<String, String>,
622
623 pub entrypoint: Option<Vec<String>>,
625
626 pub cmd: Option<Vec<String>>,
628
629 pub hostname: Option<String>,
631
632 pub user: Option<String>,
634
635 pub log_level: Option<SandboxLogLevel>,
637
638 pub metrics_sample_interval_ms: Option<u64>,
640
641 pub disable_metrics_sample: bool,
643}
644
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
648pub struct EnvVar {
649 pub key: String,
651
652 pub value: String,
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
658#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
659#[serde(rename_all = "lowercase")]
660pub enum SandboxLogLevel {
661 Error,
663
664 Warn,
666
667 Info,
669
670 Debug,
672
673 Trace,
675}
676
677#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
683#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
684pub enum RlimitResource {
685 Cpu,
687 Fsize,
689 Data,
691 Stack,
693 Core,
695 Rss,
697 Nproc,
699 Nofile,
701 Memlock,
703 As,
705 Locks,
707 Sigpending,
709 Msgqueue,
711 Nice,
713 Rtprio,
715 Rttime,
717}
718
719#[derive(Debug, Clone, Serialize, Deserialize)]
721#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
722pub struct Rlimit {
723 pub resource: RlimitResource,
725
726 pub soft: u64,
728
729 pub hard: u64,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
739#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
740#[serde(rename_all = "lowercase")]
741pub enum LogSource {
742 Stdout,
744
745 Stderr,
747
748 Output,
750
751 System,
753}
754
755impl DiskImageFormat {
760 pub fn as_str(&self) -> &'static str {
762 match self {
763 Self::Qcow2 => "qcow2",
764 Self::Raw => "raw",
765 Self::Vmdk => "vmdk",
766 }
767 }
768
769 pub fn from_extension(ext: &str) -> Option<Self> {
773 match ext {
774 "qcow2" => Some(Self::Qcow2),
775 "raw" => Some(Self::Raw),
776 "vmdk" => Some(Self::Vmdk),
777 _ => None,
778 }
779 }
780}
781
782impl OciRootfsSource {
783 pub fn new(reference: impl Into<String>) -> Self {
785 Self {
786 reference: reference.into(),
787 upper_size_mib: None,
788 }
789 }
790}
791
792impl RootfsSource {
793 pub fn oci(reference: impl Into<String>) -> Self {
795 Self::Oci(OciRootfsSource::new(reference))
796 }
797
798 pub fn oci_reference(&self) -> Option<&str> {
800 match self {
801 Self::Oci(oci) => Some(&oci.reference),
802 _ => None,
803 }
804 }
805
806 pub fn oci_upper_size_mib(&self) -> Option<u32> {
808 match self {
809 Self::Oci(oci) => oci.upper_size_mib,
810 _ => None,
811 }
812 }
813}
814
815impl EnvVar {
816 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
818 Self {
819 key: key.into(),
820 value: value.into(),
821 }
822 }
823
824 pub fn as_pair(&self) -> (&str, &str) {
826 (&self.key, &self.value)
827 }
828}
829
830impl VolumeKind {
831 pub fn as_str(self) -> &'static str {
833 match self {
834 Self::Directory => "dir",
835 Self::Disk => "disk",
836 }
837 }
838
839 pub fn from_db_value(value: &str) -> Self {
841 match value {
842 "disk" => Self::Disk,
843 _ => Self::Directory,
844 }
845 }
846}
847
848impl VolumeSpec {
849 pub fn new(name: impl Into<String>) -> Self {
851 Self {
852 name: name.into(),
853 kind: VolumeKind::Directory,
854 quota_mib: None,
855 capacity_mib: None,
856 labels: Vec::new(),
857 }
858 }
859}
860
861impl NamedVolumeCreate {
862 pub fn mode(&self) -> NamedVolumeMode {
864 self.mode
865 }
866
867 pub fn name(&self) -> &str {
869 &self.name
870 }
871
872 pub fn kind(&self) -> VolumeKind {
874 self.kind
875 }
876
877 pub fn quota_mib(&self) -> Option<u32> {
879 self.quota_mib
880 }
881
882 pub fn capacity_mib(&self) -> Option<u32> {
884 self.capacity_mib
885 }
886
887 pub fn labels(&self) -> &[(String, String)] {
889 &self.labels
890 }
891}
892
893impl VolumeMount {
894 pub fn guest(&self) -> &str {
896 match self {
897 Self::Bind { guest, .. }
898 | Self::Named { guest, .. }
899 | Self::Tmpfs { guest, .. }
900 | Self::DiskImage { guest, .. } => guest,
901 }
902 }
903
904 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
906 match self {
907 Self::Named { create, .. } => create.as_ref(),
908 _ => None,
909 }
910 }
911}
912
913impl RlimitResource {
914 pub fn as_str(&self) -> &'static str {
916 match self {
917 Self::Cpu => "cpu",
918 Self::Fsize => "fsize",
919 Self::Data => "data",
920 Self::Stack => "stack",
921 Self::Core => "core",
922 Self::Rss => "rss",
923 Self::Nproc => "nproc",
924 Self::Nofile => "nofile",
925 Self::Memlock => "memlock",
926 Self::As => "as",
927 Self::Locks => "locks",
928 Self::Sigpending => "sigpending",
929 Self::Msgqueue => "msgqueue",
930 Self::Nice => "nice",
931 Self::Rtprio => "rtprio",
932 Self::Rttime => "rttime",
933 }
934 }
935}
936
937impl LogSource {
938 pub fn effective(requested: &[Self]) -> Vec<Self> {
940 if requested.is_empty() {
941 vec![Self::Stdout, Self::Stderr, Self::Output]
942 } else {
943 let mut sources = requested.to_vec();
944 sources.sort_by_key(|src| match src {
945 Self::Stdout => 0,
946 Self::Stderr => 1,
947 Self::Output => 2,
948 Self::System => 3,
949 });
950 sources.dedup();
951 sources
952 }
953 }
954}
955
956impl SandboxLogLevel {
957 pub const fn as_str(self) -> &'static str {
959 match self {
960 Self::Error => "error",
961 Self::Warn => "warn",
962 Self::Info => "info",
963 Self::Debug => "debug",
964 Self::Trace => "trace",
965 }
966 }
967}
968
969impl std::fmt::Display for DiskImageFormat {
974 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
975 f.write_str(self.as_str())
976 }
977}
978
979impl FromStr for DiskImageFormat {
980 type Err = String;
981
982 fn from_str(s: &str) -> Result<Self, Self::Err> {
983 match s {
984 "qcow2" => Ok(Self::Qcow2),
985 "raw" => Ok(Self::Raw),
986 "vmdk" => Ok(Self::Vmdk),
987 _ => Err(format!("unknown disk image format: {s}")),
988 }
989 }
990}
991
992impl Default for RootfsSource {
993 fn default() -> Self {
994 Self::oci(String::new())
995 }
996}
997
998impl Default for SandboxResources {
999 fn default() -> Self {
1000 Self {
1001 cpus: DEFAULT_SANDBOX_CPUS,
1002 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1003 }
1004 }
1005}
1006
1007impl Default for SandboxRuntimeOptions {
1008 fn default() -> Self {
1009 Self {
1010 workdir: None,
1011 shell: None,
1012 scripts: BTreeMap::new(),
1013 entrypoint: None,
1014 cmd: None,
1015 hostname: None,
1016 user: None,
1017 log_level: None,
1018 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1019 disable_metrics_sample: false,
1020 }
1021 }
1022}
1023
1024impl Default for NetworkSpec {
1025 fn default() -> Self {
1026 Self {
1027 enabled: true,
1028 interface: None,
1029 ports: Vec::new(),
1030 policy: None,
1031 dns: None,
1032 tls: None,
1033 secrets: None,
1034 max_connections: None,
1035 trust_host_cas: false,
1036 }
1037 }
1038}
1039
1040impl Default for PublishedPortSpec {
1041 fn default() -> Self {
1042 Self {
1043 host_port: 0,
1044 guest_port: 0,
1045 protocol: PortProtocol::Tcp,
1046 host_bind: "127.0.0.1".into(),
1047 }
1048 }
1049}
1050
1051impl From<(String, String)> for EnvVar {
1052 fn from((key, value): (String, String)) -> Self {
1053 Self { key, value }
1054 }
1055}
1056
1057impl From<EnvVar> for (String, String) {
1058 fn from(var: EnvVar) -> Self {
1059 (var.key, var.value)
1060 }
1061}
1062
1063impl FromStr for SandboxLogLevel {
1064 type Err = String;
1065
1066 fn from_str(s: &str) -> Result<Self, Self::Err> {
1067 match s {
1068 "error" => Ok(Self::Error),
1069 "warn" => Ok(Self::Warn),
1070 "info" => Ok(Self::Info),
1071 "debug" => Ok(Self::Debug),
1072 "trace" => Ok(Self::Trace),
1073 _ => Err(format!("unknown sandbox log level: {s}")),
1074 }
1075 }
1076}
1077
1078impl Serialize for VolumeMount {
1079 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1080 use serde::ser::SerializeMap;
1081
1082 match self {
1083 Self::Bind {
1084 host,
1085 guest,
1086 options,
1087 stat_virtualization,
1088 host_permissions,
1089 } => {
1090 let mut map = serializer.serialize_map(Some(6))?;
1091 map.serialize_entry("type", "Bind")?;
1092 map.serialize_entry("host", host)?;
1093 map.serialize_entry("guest", guest)?;
1094 map.serialize_entry("options", options)?;
1095 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1096 map.serialize_entry("host_permissions", host_permissions)?;
1097 map.end()
1098 }
1099 Self::Named {
1100 name,
1101 guest,
1102 create: _,
1103 options,
1104 stat_virtualization,
1105 host_permissions,
1106 } => {
1107 let mut map = serializer.serialize_map(Some(6))?;
1108 map.serialize_entry("type", "Named")?;
1109 map.serialize_entry("name", name)?;
1110 map.serialize_entry("guest", guest)?;
1111 map.serialize_entry("options", options)?;
1112 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1113 map.serialize_entry("host_permissions", host_permissions)?;
1114 map.end()
1115 }
1116 Self::Tmpfs {
1117 guest,
1118 size_mib,
1119 options,
1120 } => {
1121 let mut map = serializer.serialize_map(Some(4))?;
1122 map.serialize_entry("type", "Tmpfs")?;
1123 map.serialize_entry("guest", guest)?;
1124 map.serialize_entry("size_mib", size_mib)?;
1125 map.serialize_entry("options", options)?;
1126 map.end()
1127 }
1128 Self::DiskImage {
1129 host,
1130 guest,
1131 format,
1132 fstype,
1133 options,
1134 } => {
1135 let mut map = serializer.serialize_map(Some(6))?;
1136 map.serialize_entry("type", "DiskImage")?;
1137 map.serialize_entry("host", host)?;
1138 map.serialize_entry("guest", guest)?;
1139 map.serialize_entry("format", format)?;
1140 map.serialize_entry("fstype", fstype)?;
1141 map.serialize_entry("options", options)?;
1142 map.end()
1143 }
1144 }
1145 }
1146}
1147
1148impl<'de> Deserialize<'de> for VolumeMount {
1149 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1150 fn default_strict() -> StatVirtualization {
1151 StatVirtualization::Strict
1152 }
1153
1154 fn default_private() -> HostPermissions {
1155 HostPermissions::Private
1156 }
1157
1158 #[derive(Deserialize)]
1159 #[serde(tag = "type")]
1160 enum VolumeMountHelper {
1161 Bind {
1162 host: PathBuf,
1163 guest: String,
1164 #[serde(default)]
1165 options: Option<MountOptions>,
1166 #[serde(default)]
1167 readonly: bool,
1168 #[serde(default = "default_strict")]
1169 stat_virtualization: StatVirtualization,
1170 #[serde(default = "default_private")]
1171 host_permissions: HostPermissions,
1172 },
1173 Named {
1174 name: String,
1175 guest: String,
1176 #[serde(default)]
1177 options: Option<MountOptions>,
1178 #[serde(default)]
1179 readonly: bool,
1180 #[serde(default = "default_strict")]
1181 stat_virtualization: StatVirtualization,
1182 #[serde(default = "default_private")]
1183 host_permissions: HostPermissions,
1184 },
1185 Tmpfs {
1186 guest: String,
1187 #[serde(default)]
1188 size_mib: Option<u32>,
1189 #[serde(default)]
1190 options: Option<MountOptions>,
1191 #[serde(default)]
1192 readonly: bool,
1193 },
1194 DiskImage {
1195 host: PathBuf,
1196 guest: String,
1197 format: DiskImageFormat,
1198 #[serde(default)]
1199 fstype: Option<String>,
1200 #[serde(default)]
1201 options: Option<MountOptions>,
1202 #[serde(default)]
1203 readonly: bool,
1204 },
1205 }
1206
1207 let helper = VolumeMountHelper::deserialize(deserializer)?;
1208 Ok(match helper {
1209 VolumeMountHelper::Bind {
1210 host,
1211 guest,
1212 options,
1213 readonly,
1214 stat_virtualization,
1215 host_permissions,
1216 } => Self::Bind {
1217 host,
1218 guest,
1219 options: decode_mount_options(options, readonly),
1220 stat_virtualization,
1221 host_permissions,
1222 },
1223 VolumeMountHelper::Named {
1224 name,
1225 guest,
1226 options,
1227 readonly,
1228 stat_virtualization,
1229 host_permissions,
1230 } => Self::Named {
1231 name,
1232 guest,
1233 create: None,
1234 options: decode_mount_options(options, readonly),
1235 stat_virtualization,
1236 host_permissions,
1237 },
1238 VolumeMountHelper::Tmpfs {
1239 guest,
1240 size_mib,
1241 options,
1242 readonly,
1243 } => Self::Tmpfs {
1244 guest,
1245 size_mib,
1246 options: decode_mount_options(options, readonly),
1247 },
1248 VolumeMountHelper::DiskImage {
1249 host,
1250 guest,
1251 format,
1252 fstype,
1253 options,
1254 readonly,
1255 } => Self::DiskImage {
1256 host,
1257 guest,
1258 format,
1259 fstype,
1260 options: decode_mount_options(options, readonly),
1261 },
1262 })
1263 }
1264}
1265
1266impl fmt::Debug for VolumeMount {
1267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1268 match self {
1269 Self::Bind {
1270 host,
1271 guest,
1272 options,
1273 stat_virtualization,
1274 host_permissions,
1275 } => f
1276 .debug_struct("Bind")
1277 .field("host", host)
1278 .field("guest", guest)
1279 .field("options", options)
1280 .field("stat_virtualization", stat_virtualization)
1281 .field("host_permissions", host_permissions)
1282 .finish(),
1283 Self::Named {
1284 name,
1285 guest,
1286 create,
1287 options,
1288 stat_virtualization,
1289 host_permissions,
1290 } => f
1291 .debug_struct("Named")
1292 .field("name", name)
1293 .field("guest", guest)
1294 .field("create", create)
1295 .field("options", options)
1296 .field("stat_virtualization", stat_virtualization)
1297 .field("host_permissions", host_permissions)
1298 .finish(),
1299 Self::Tmpfs {
1300 guest,
1301 size_mib,
1302 options,
1303 } => f
1304 .debug_struct("Tmpfs")
1305 .field("guest", guest)
1306 .field("size_mib", size_mib)
1307 .field("options", options)
1308 .finish(),
1309 Self::DiskImage {
1310 host,
1311 guest,
1312 format,
1313 fstype,
1314 options,
1315 } => f
1316 .debug_struct("DiskImage")
1317 .field("host", host)
1318 .field("guest", guest)
1319 .field("format", format)
1320 .field("fstype", fstype)
1321 .field("options", options)
1322 .finish(),
1323 }
1324 }
1325}
1326
1327impl TryFrom<&str> for RlimitResource {
1329 type Error = String;
1330
1331 fn try_from(s: &str) -> Result<Self, Self::Error> {
1332 match s.to_ascii_lowercase().as_str() {
1333 "cpu" => Ok(Self::Cpu),
1334 "fsize" => Ok(Self::Fsize),
1335 "data" => Ok(Self::Data),
1336 "stack" => Ok(Self::Stack),
1337 "core" => Ok(Self::Core),
1338 "rss" => Ok(Self::Rss),
1339 "nproc" => Ok(Self::Nproc),
1340 "nofile" => Ok(Self::Nofile),
1341 "memlock" => Ok(Self::Memlock),
1342 "as" => Ok(Self::As),
1343 "locks" => Ok(Self::Locks),
1344 "sigpending" => Ok(Self::Sigpending),
1345 "msgqueue" => Ok(Self::Msgqueue),
1346 "nice" => Ok(Self::Nice),
1347 "rtprio" => Ok(Self::Rtprio),
1348 "rttime" => Ok(Self::Rttime),
1349 _ => Err(format!("unknown rlimit resource: {s}")),
1350 }
1351 }
1352}
1353
1354fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
1359 options.unwrap_or(MountOptions {
1360 readonly,
1361 ..MountOptions::default()
1362 })
1363}
1364
1365#[cfg(test)]
1370mod tests {
1371 use super::*;
1372
1373 #[test]
1374 fn disk_image_format_from_extension() {
1375 assert_eq!(
1376 DiskImageFormat::from_extension("qcow2"),
1377 Some(DiskImageFormat::Qcow2)
1378 );
1379 assert_eq!(
1380 DiskImageFormat::from_extension("raw"),
1381 Some(DiskImageFormat::Raw)
1382 );
1383 assert_eq!(
1384 DiskImageFormat::from_extension("vmdk"),
1385 Some(DiskImageFormat::Vmdk)
1386 );
1387 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
1388 assert_eq!(DiskImageFormat::from_extension(""), None);
1389 }
1390
1391 #[test]
1392 fn disk_image_format_display_roundtrip() {
1393 for format in [
1394 DiskImageFormat::Qcow2,
1395 DiskImageFormat::Raw,
1396 DiskImageFormat::Vmdk,
1397 ] {
1398 let rendered = format.to_string();
1399 let parsed: DiskImageFormat = rendered.parse().unwrap();
1400 assert_eq!(parsed, format);
1401 }
1402 }
1403
1404 #[test]
1405 fn disk_image_format_from_str_unknown() {
1406 assert!("ext4".parse::<DiskImageFormat>().is_err());
1407 }
1408
1409 #[test]
1410 fn log_source_effective_uses_default_user_program_sources() {
1411 assert_eq!(
1412 LogSource::effective(&[]),
1413 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
1414 );
1415 }
1416
1417 #[test]
1418 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
1419 assert_eq!(
1420 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
1421 vec![LogSource::Stdout, LogSource::System]
1422 );
1423 }
1424
1425 #[test]
1426 fn rlimit_resource_parses_case_insensitively() {
1427 assert_eq!(
1428 RlimitResource::try_from("NOFILE").unwrap(),
1429 RlimitResource::Nofile
1430 );
1431 assert!(RlimitResource::try_from("bogus").is_err());
1432 }
1433
1434 #[test]
1435 fn sandbox_policy_serde_roundtrip() {
1436 let policy = SandboxPolicy {
1437 ephemeral: true,
1438 max_duration_secs: Some(3600),
1439 idle_timeout_secs: Some(120),
1440 };
1441
1442 let json = serde_json::to_string(&policy).unwrap();
1443 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
1444
1445 assert!(decoded.ephemeral);
1446 assert_eq!(decoded.max_duration_secs, Some(3600));
1447 assert_eq!(decoded.idle_timeout_secs, Some(120));
1448 }
1449
1450 #[test]
1451 fn sandbox_policy_defaults_to_persistent() {
1452 assert!(!SandboxPolicy::default().ephemeral);
1453 }
1454
1455 #[test]
1456 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
1457 let decoded: SandboxPolicy =
1460 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
1461 assert!(!decoded.ephemeral);
1462 assert_eq!(decoded.max_duration_secs, Some(60));
1463 }
1464
1465 #[test]
1466 fn sandbox_spec_default_uses_static_resource_defaults() {
1467 let spec = SandboxSpec::default();
1468
1469 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
1470 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
1471 assert_eq!(
1472 spec.runtime.metrics_sample_interval_ms,
1473 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
1474 );
1475 }
1476
1477 #[test]
1478 fn sandbox_log_level_roundtrips_lowercase_values() {
1479 for (input, expected) in [
1480 ("error", SandboxLogLevel::Error),
1481 ("warn", SandboxLogLevel::Warn),
1482 ("info", SandboxLogLevel::Info),
1483 ("debug", SandboxLogLevel::Debug),
1484 ("trace", SandboxLogLevel::Trace),
1485 ] {
1486 let parsed: SandboxLogLevel = input.parse().unwrap();
1487 assert_eq!(parsed, expected);
1488 assert_eq!(parsed.as_str(), input);
1489 }
1490 }
1491}