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, Deserialize)]
605#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
606#[serde(default)]
607pub struct SandboxResources {
608 pub cpus: u8,
610
611 pub memory_mib: u32,
613}
614
615#[derive(Debug, Clone, Serialize, Deserialize)]
617#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
618#[serde(default)]
619pub struct SandboxRuntimeOptions {
620 pub workdir: Option<String>,
622
623 pub shell: Option<String>,
625
626 pub scripts: BTreeMap<String, String>,
628
629 pub entrypoint: Option<Vec<String>>,
631
632 pub cmd: Option<Vec<String>>,
634
635 pub hostname: Option<String>,
637
638 pub user: Option<String>,
640
641 pub log_level: Option<SandboxLogLevel>,
643
644 pub metrics_sample_interval_ms: Option<u64>,
646
647 pub disable_metrics_sample: bool,
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
653#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
654pub struct EnvVar {
655 pub key: String,
657
658 pub value: String,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
664#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
665#[serde(rename_all = "lowercase")]
666pub enum SandboxLogLevel {
667 Error,
669
670 Warn,
672
673 Info,
675
676 Debug,
678
679 Trace,
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
689#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
690pub enum RlimitResource {
691 Cpu,
693 Fsize,
695 Data,
697 Stack,
699 Core,
701 Rss,
703 Nproc,
705 Nofile,
707 Memlock,
709 As,
711 Locks,
713 Sigpending,
715 Msgqueue,
717 Nice,
719 Rtprio,
721 Rttime,
723}
724
725#[derive(Debug, Clone, Serialize, Deserialize)]
727#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
728pub struct Rlimit {
729 pub resource: RlimitResource,
731
732 pub soft: u64,
734
735 pub hard: u64,
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
745#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
746#[serde(rename_all = "lowercase")]
747pub enum LogSource {
748 Stdout,
750
751 Stderr,
753
754 Output,
756
757 System,
759}
760
761impl DiskImageFormat {
766 pub fn as_str(&self) -> &'static str {
768 match self {
769 Self::Qcow2 => "qcow2",
770 Self::Raw => "raw",
771 Self::Vmdk => "vmdk",
772 }
773 }
774
775 pub fn from_extension(ext: &str) -> Option<Self> {
779 match ext {
780 "qcow2" => Some(Self::Qcow2),
781 "raw" => Some(Self::Raw),
782 "vmdk" => Some(Self::Vmdk),
783 _ => None,
784 }
785 }
786}
787
788impl OciRootfsSource {
789 pub fn new(reference: impl Into<String>) -> Self {
791 Self {
792 reference: reference.into(),
793 upper_size_mib: None,
794 }
795 }
796}
797
798impl RootfsSource {
799 pub fn oci(reference: impl Into<String>) -> Self {
801 Self::Oci(OciRootfsSource::new(reference))
802 }
803
804 pub fn oci_reference(&self) -> Option<&str> {
806 match self {
807 Self::Oci(oci) => Some(&oci.reference),
808 _ => None,
809 }
810 }
811
812 pub fn oci_upper_size_mib(&self) -> Option<u32> {
814 match self {
815 Self::Oci(oci) => oci.upper_size_mib,
816 _ => None,
817 }
818 }
819}
820
821impl EnvVar {
822 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
824 Self {
825 key: key.into(),
826 value: value.into(),
827 }
828 }
829
830 pub fn as_pair(&self) -> (&str, &str) {
832 (&self.key, &self.value)
833 }
834}
835
836impl VolumeKind {
837 pub fn as_str(self) -> &'static str {
839 match self {
840 Self::Directory => "dir",
841 Self::Disk => "disk",
842 }
843 }
844
845 pub fn from_db_value(value: &str) -> Self {
847 match value {
848 "disk" => Self::Disk,
849 _ => Self::Directory,
850 }
851 }
852}
853
854impl VolumeSpec {
855 pub fn new(name: impl Into<String>) -> Self {
857 Self {
858 name: name.into(),
859 kind: VolumeKind::Directory,
860 quota_mib: None,
861 capacity_mib: None,
862 labels: Vec::new(),
863 }
864 }
865}
866
867impl NamedVolumeCreate {
868 pub fn mode(&self) -> NamedVolumeMode {
870 self.mode
871 }
872
873 pub fn name(&self) -> &str {
875 &self.name
876 }
877
878 pub fn kind(&self) -> VolumeKind {
880 self.kind
881 }
882
883 pub fn quota_mib(&self) -> Option<u32> {
885 self.quota_mib
886 }
887
888 pub fn capacity_mib(&self) -> Option<u32> {
890 self.capacity_mib
891 }
892
893 pub fn labels(&self) -> &[(String, String)] {
895 &self.labels
896 }
897}
898
899impl VolumeMount {
900 pub fn guest(&self) -> &str {
902 match self {
903 Self::Bind { guest, .. }
904 | Self::Named { guest, .. }
905 | Self::Tmpfs { guest, .. }
906 | Self::DiskImage { guest, .. } => guest,
907 }
908 }
909
910 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
912 match self {
913 Self::Named { create, .. } => create.as_ref(),
914 _ => None,
915 }
916 }
917}
918
919impl RlimitResource {
920 pub fn as_str(&self) -> &'static str {
922 match self {
923 Self::Cpu => "cpu",
924 Self::Fsize => "fsize",
925 Self::Data => "data",
926 Self::Stack => "stack",
927 Self::Core => "core",
928 Self::Rss => "rss",
929 Self::Nproc => "nproc",
930 Self::Nofile => "nofile",
931 Self::Memlock => "memlock",
932 Self::As => "as",
933 Self::Locks => "locks",
934 Self::Sigpending => "sigpending",
935 Self::Msgqueue => "msgqueue",
936 Self::Nice => "nice",
937 Self::Rtprio => "rtprio",
938 Self::Rttime => "rttime",
939 }
940 }
941}
942
943impl LogSource {
944 pub fn effective(requested: &[Self]) -> Vec<Self> {
946 if requested.is_empty() {
947 vec![Self::Stdout, Self::Stderr, Self::Output]
948 } else {
949 let mut sources = requested.to_vec();
950 sources.sort_by_key(|src| match src {
951 Self::Stdout => 0,
952 Self::Stderr => 1,
953 Self::Output => 2,
954 Self::System => 3,
955 });
956 sources.dedup();
957 sources
958 }
959 }
960}
961
962impl SandboxLogLevel {
963 pub const fn as_str(self) -> &'static str {
965 match self {
966 Self::Error => "error",
967 Self::Warn => "warn",
968 Self::Info => "info",
969 Self::Debug => "debug",
970 Self::Trace => "trace",
971 }
972 }
973}
974
975impl std::fmt::Display for DiskImageFormat {
980 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
981 f.write_str(self.as_str())
982 }
983}
984
985impl FromStr for DiskImageFormat {
986 type Err = String;
987
988 fn from_str(s: &str) -> Result<Self, Self::Err> {
989 match s {
990 "qcow2" => Ok(Self::Qcow2),
991 "raw" => Ok(Self::Raw),
992 "vmdk" => Ok(Self::Vmdk),
993 _ => Err(format!("unknown disk image format: {s}")),
994 }
995 }
996}
997
998impl Default for RootfsSource {
999 fn default() -> Self {
1000 Self::oci(String::new())
1001 }
1002}
1003
1004impl Default for SandboxResources {
1005 fn default() -> Self {
1006 Self {
1007 cpus: DEFAULT_SANDBOX_CPUS,
1008 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1009 }
1010 }
1011}
1012
1013impl Default for SandboxRuntimeOptions {
1014 fn default() -> Self {
1015 Self {
1016 workdir: None,
1017 shell: None,
1018 scripts: BTreeMap::new(),
1019 entrypoint: None,
1020 cmd: None,
1021 hostname: None,
1022 user: None,
1023 log_level: None,
1024 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1025 disable_metrics_sample: false,
1026 }
1027 }
1028}
1029
1030impl Default for NetworkSpec {
1031 fn default() -> Self {
1032 Self {
1033 enabled: true,
1034 interface: None,
1035 ports: Vec::new(),
1036 policy: None,
1037 dns: None,
1038 tls: None,
1039 secrets: None,
1040 max_connections: None,
1041 trust_host_cas: false,
1042 }
1043 }
1044}
1045
1046impl Default for PublishedPortSpec {
1047 fn default() -> Self {
1048 Self {
1049 host_port: 0,
1050 guest_port: 0,
1051 protocol: PortProtocol::Tcp,
1052 host_bind: "127.0.0.1".into(),
1053 }
1054 }
1055}
1056
1057impl From<(String, String)> for EnvVar {
1058 fn from((key, value): (String, String)) -> Self {
1059 Self { key, value }
1060 }
1061}
1062
1063impl From<EnvVar> for (String, String) {
1064 fn from(var: EnvVar) -> Self {
1065 (var.key, var.value)
1066 }
1067}
1068
1069impl FromStr for SandboxLogLevel {
1070 type Err = String;
1071
1072 fn from_str(s: &str) -> Result<Self, Self::Err> {
1073 match s {
1074 "error" => Ok(Self::Error),
1075 "warn" => Ok(Self::Warn),
1076 "info" => Ok(Self::Info),
1077 "debug" => Ok(Self::Debug),
1078 "trace" => Ok(Self::Trace),
1079 _ => Err(format!("unknown sandbox log level: {s}")),
1080 }
1081 }
1082}
1083
1084impl Serialize for VolumeMount {
1085 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1086 use serde::ser::SerializeMap;
1087
1088 match self {
1089 Self::Bind {
1090 host,
1091 guest,
1092 options,
1093 stat_virtualization,
1094 host_permissions,
1095 quota_mib,
1096 } => {
1097 let mut map = serializer.serialize_map(Some(7))?;
1098 map.serialize_entry("type", "Bind")?;
1099 map.serialize_entry("host", host)?;
1100 map.serialize_entry("guest", guest)?;
1101 map.serialize_entry("options", options)?;
1102 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1103 map.serialize_entry("host_permissions", host_permissions)?;
1104 map.serialize_entry("quota_mib", quota_mib)?;
1105 map.end()
1106 }
1107 Self::Named {
1108 name,
1109 guest,
1110 create: _,
1111 options,
1112 stat_virtualization,
1113 host_permissions,
1114 } => {
1115 let mut map = serializer.serialize_map(Some(6))?;
1116 map.serialize_entry("type", "Named")?;
1117 map.serialize_entry("name", name)?;
1118 map.serialize_entry("guest", guest)?;
1119 map.serialize_entry("options", options)?;
1120 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1121 map.serialize_entry("host_permissions", host_permissions)?;
1122 map.end()
1123 }
1124 Self::Tmpfs {
1125 guest,
1126 size_mib,
1127 options,
1128 } => {
1129 let mut map = serializer.serialize_map(Some(4))?;
1130 map.serialize_entry("type", "Tmpfs")?;
1131 map.serialize_entry("guest", guest)?;
1132 map.serialize_entry("size_mib", size_mib)?;
1133 map.serialize_entry("options", options)?;
1134 map.end()
1135 }
1136 Self::DiskImage {
1137 host,
1138 guest,
1139 format,
1140 fstype,
1141 options,
1142 } => {
1143 let mut map = serializer.serialize_map(Some(6))?;
1144 map.serialize_entry("type", "DiskImage")?;
1145 map.serialize_entry("host", host)?;
1146 map.serialize_entry("guest", guest)?;
1147 map.serialize_entry("format", format)?;
1148 map.serialize_entry("fstype", fstype)?;
1149 map.serialize_entry("options", options)?;
1150 map.end()
1151 }
1152 }
1153 }
1154}
1155
1156impl<'de> Deserialize<'de> for VolumeMount {
1157 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1158 fn default_strict() -> StatVirtualization {
1159 StatVirtualization::Strict
1160 }
1161
1162 fn default_private() -> HostPermissions {
1163 HostPermissions::Private
1164 }
1165
1166 #[derive(Deserialize)]
1167 #[serde(tag = "type")]
1168 enum VolumeMountHelper {
1169 Bind {
1170 host: PathBuf,
1171 guest: String,
1172 #[serde(default)]
1173 options: Option<MountOptions>,
1174 #[serde(default)]
1175 readonly: bool,
1176 #[serde(default = "default_strict")]
1177 stat_virtualization: StatVirtualization,
1178 #[serde(default = "default_private")]
1179 host_permissions: HostPermissions,
1180 #[serde(default)]
1181 quota_mib: Option<u32>,
1182 },
1183 Named {
1184 name: String,
1185 guest: String,
1186 #[serde(default)]
1187 options: Option<MountOptions>,
1188 #[serde(default)]
1189 readonly: bool,
1190 #[serde(default = "default_strict")]
1191 stat_virtualization: StatVirtualization,
1192 #[serde(default = "default_private")]
1193 host_permissions: HostPermissions,
1194 },
1195 Tmpfs {
1196 guest: String,
1197 #[serde(default)]
1198 size_mib: Option<u32>,
1199 #[serde(default)]
1200 options: Option<MountOptions>,
1201 #[serde(default)]
1202 readonly: bool,
1203 },
1204 DiskImage {
1205 host: PathBuf,
1206 guest: String,
1207 format: DiskImageFormat,
1208 #[serde(default)]
1209 fstype: Option<String>,
1210 #[serde(default)]
1211 options: Option<MountOptions>,
1212 #[serde(default)]
1213 readonly: bool,
1214 },
1215 }
1216
1217 let helper = VolumeMountHelper::deserialize(deserializer)?;
1218 Ok(match helper {
1219 VolumeMountHelper::Bind {
1220 host,
1221 guest,
1222 options,
1223 readonly,
1224 stat_virtualization,
1225 host_permissions,
1226 quota_mib,
1227 } => Self::Bind {
1228 host,
1229 guest,
1230 options: decode_mount_options(options, readonly),
1231 stat_virtualization,
1232 host_permissions,
1233 quota_mib,
1234 },
1235 VolumeMountHelper::Named {
1236 name,
1237 guest,
1238 options,
1239 readonly,
1240 stat_virtualization,
1241 host_permissions,
1242 } => Self::Named {
1243 name,
1244 guest,
1245 create: None,
1246 options: decode_mount_options(options, readonly),
1247 stat_virtualization,
1248 host_permissions,
1249 },
1250 VolumeMountHelper::Tmpfs {
1251 guest,
1252 size_mib,
1253 options,
1254 readonly,
1255 } => Self::Tmpfs {
1256 guest,
1257 size_mib,
1258 options: decode_mount_options(options, readonly),
1259 },
1260 VolumeMountHelper::DiskImage {
1261 host,
1262 guest,
1263 format,
1264 fstype,
1265 options,
1266 readonly,
1267 } => Self::DiskImage {
1268 host,
1269 guest,
1270 format,
1271 fstype,
1272 options: decode_mount_options(options, readonly),
1273 },
1274 })
1275 }
1276}
1277
1278impl fmt::Debug for VolumeMount {
1279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1280 match self {
1281 Self::Bind {
1282 host,
1283 guest,
1284 options,
1285 stat_virtualization,
1286 host_permissions,
1287 quota_mib,
1288 } => f
1289 .debug_struct("Bind")
1290 .field("host", host)
1291 .field("guest", guest)
1292 .field("options", options)
1293 .field("stat_virtualization", stat_virtualization)
1294 .field("host_permissions", host_permissions)
1295 .field("quota_mib", quota_mib)
1296 .finish(),
1297 Self::Named {
1298 name,
1299 guest,
1300 create,
1301 options,
1302 stat_virtualization,
1303 host_permissions,
1304 } => f
1305 .debug_struct("Named")
1306 .field("name", name)
1307 .field("guest", guest)
1308 .field("create", create)
1309 .field("options", options)
1310 .field("stat_virtualization", stat_virtualization)
1311 .field("host_permissions", host_permissions)
1312 .finish(),
1313 Self::Tmpfs {
1314 guest,
1315 size_mib,
1316 options,
1317 } => f
1318 .debug_struct("Tmpfs")
1319 .field("guest", guest)
1320 .field("size_mib", size_mib)
1321 .field("options", options)
1322 .finish(),
1323 Self::DiskImage {
1324 host,
1325 guest,
1326 format,
1327 fstype,
1328 options,
1329 } => f
1330 .debug_struct("DiskImage")
1331 .field("host", host)
1332 .field("guest", guest)
1333 .field("format", format)
1334 .field("fstype", fstype)
1335 .field("options", options)
1336 .finish(),
1337 }
1338 }
1339}
1340
1341impl TryFrom<&str> for RlimitResource {
1343 type Error = String;
1344
1345 fn try_from(s: &str) -> Result<Self, Self::Error> {
1346 match s.to_ascii_lowercase().as_str() {
1347 "cpu" => Ok(Self::Cpu),
1348 "fsize" => Ok(Self::Fsize),
1349 "data" => Ok(Self::Data),
1350 "stack" => Ok(Self::Stack),
1351 "core" => Ok(Self::Core),
1352 "rss" => Ok(Self::Rss),
1353 "nproc" => Ok(Self::Nproc),
1354 "nofile" => Ok(Self::Nofile),
1355 "memlock" => Ok(Self::Memlock),
1356 "as" => Ok(Self::As),
1357 "locks" => Ok(Self::Locks),
1358 "sigpending" => Ok(Self::Sigpending),
1359 "msgqueue" => Ok(Self::Msgqueue),
1360 "nice" => Ok(Self::Nice),
1361 "rtprio" => Ok(Self::Rtprio),
1362 "rttime" => Ok(Self::Rttime),
1363 _ => Err(format!("unknown rlimit resource: {s}")),
1364 }
1365 }
1366}
1367
1368fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
1373 options.unwrap_or(MountOptions {
1374 readonly,
1375 ..MountOptions::default()
1376 })
1377}
1378
1379#[cfg(test)]
1384mod tests {
1385 use super::*;
1386
1387 #[test]
1388 fn disk_image_format_from_extension() {
1389 assert_eq!(
1390 DiskImageFormat::from_extension("qcow2"),
1391 Some(DiskImageFormat::Qcow2)
1392 );
1393 assert_eq!(
1394 DiskImageFormat::from_extension("raw"),
1395 Some(DiskImageFormat::Raw)
1396 );
1397 assert_eq!(
1398 DiskImageFormat::from_extension("vmdk"),
1399 Some(DiskImageFormat::Vmdk)
1400 );
1401 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
1402 assert_eq!(DiskImageFormat::from_extension(""), None);
1403 }
1404
1405 #[test]
1406 fn disk_image_format_display_roundtrip() {
1407 for format in [
1408 DiskImageFormat::Qcow2,
1409 DiskImageFormat::Raw,
1410 DiskImageFormat::Vmdk,
1411 ] {
1412 let rendered = format.to_string();
1413 let parsed: DiskImageFormat = rendered.parse().unwrap();
1414 assert_eq!(parsed, format);
1415 }
1416 }
1417
1418 #[test]
1419 fn disk_image_format_from_str_unknown() {
1420 assert!("ext4".parse::<DiskImageFormat>().is_err());
1421 }
1422
1423 #[test]
1424 fn log_source_effective_uses_default_user_program_sources() {
1425 assert_eq!(
1426 LogSource::effective(&[]),
1427 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
1428 );
1429 }
1430
1431 #[test]
1432 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
1433 assert_eq!(
1434 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
1435 vec![LogSource::Stdout, LogSource::System]
1436 );
1437 }
1438
1439 #[test]
1440 fn rlimit_resource_parses_case_insensitively() {
1441 assert_eq!(
1442 RlimitResource::try_from("NOFILE").unwrap(),
1443 RlimitResource::Nofile
1444 );
1445 assert!(RlimitResource::try_from("bogus").is_err());
1446 }
1447
1448 #[test]
1449 fn sandbox_policy_serde_roundtrip() {
1450 let policy = SandboxPolicy {
1451 ephemeral: true,
1452 max_duration_secs: Some(3600),
1453 idle_timeout_secs: Some(120),
1454 };
1455
1456 let json = serde_json::to_string(&policy).unwrap();
1457 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
1458
1459 assert!(decoded.ephemeral);
1460 assert_eq!(decoded.max_duration_secs, Some(3600));
1461 assert_eq!(decoded.idle_timeout_secs, Some(120));
1462 }
1463
1464 #[test]
1465 fn sandbox_policy_defaults_to_persistent() {
1466 assert!(!SandboxPolicy::default().ephemeral);
1467 }
1468
1469 #[test]
1470 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
1471 let decoded: SandboxPolicy =
1474 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
1475 assert!(!decoded.ephemeral);
1476 assert_eq!(decoded.max_duration_secs, Some(60));
1477 }
1478
1479 #[test]
1480 fn sandbox_spec_default_uses_static_resource_defaults() {
1481 let spec = SandboxSpec::default();
1482
1483 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
1484 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
1485 assert_eq!(
1486 spec.runtime.metrics_sample_interval_ms,
1487 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
1488 );
1489 }
1490
1491 #[test]
1492 fn sandbox_log_level_roundtrips_lowercase_values() {
1493 for (input, expected) in [
1494 ("error", SandboxLogLevel::Error),
1495 ("warn", SandboxLogLevel::Warn),
1496 ("info", SandboxLogLevel::Info),
1497 ("debug", SandboxLogLevel::Debug),
1498 ("trace", SandboxLogLevel::Trace),
1499 ] {
1500 let parsed: SandboxLogLevel = input.parse().unwrap();
1501 assert_eq!(parsed, expected);
1502 assert_eq!(parsed.as_str(), input);
1503 }
1504 }
1505}