1use std::collections::BTreeMap;
4use std::fmt;
5use std::net::{Ipv4Addr, Ipv6Addr};
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
10use serde::{Deserialize, Serialize};
11use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath};
12use zeroize::Zeroizing;
13
14use crate::modify::SecretSource;
15use crate::{TypesError, TypesResult};
16
17pub const DEFAULT_SANDBOX_CPUS: u8 = 1;
23
24pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
26
27pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
37#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
38pub enum DiskImageFormat {
39 Qcow2,
41 Raw,
43 Vmdk,
45}
46
47#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
50#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
51#[serde(rename_all = "kebab-case")]
52pub enum FlatClone {
53 #[default]
55 Auto,
56
57 Copy,
59
60 Reflink,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
67pub enum RootfsSource {
68 Bind {
70 #[cfg_attr(feature = "ts", ts(type = "string"))]
72 path: PathBuf,
73 #[serde(default)]
80 follow_root_symlinks: bool,
81 },
82
83 Oci(OciRootfsSource),
85
86 DiskImage {
88 #[cfg_attr(feature = "ts", ts(type = "string"))]
90 path: PathBuf,
91 format: DiskImageFormat,
93 fstype: Option<String>,
95 },
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
101#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
102pub struct OciRootfsSource {
103 pub reference: String,
105
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub root_disk: Option<RootDisk>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
118#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
119#[serde(tag = "kind", rename_all = "kebab-case")]
120pub enum RootDisk {
121 Managed {
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 size_mib: Option<u32>,
127 },
128
129 Tmpfs {
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 size_mib: Option<u32>,
135 },
136
137 DiskImage {
140 #[cfg_attr(feature = "ts", ts(type = "string"))]
142 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
143 path: PathBuf,
144 format: DiskImageFormat,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 fstype: Option<String>,
149 },
150
151 Flat {
156 #[serde(default, skip_serializing_if = "Option::is_none")]
159 size_mib: Option<u32>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 fstype: Option<String>,
163 #[serde(default, skip_serializing_if = "FlatClone::is_auto")]
165 clone: FlatClone,
166 },
167}
168
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
171#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
172#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
173pub enum PullPolicy {
174 #[default]
176 IfMissing,
177
178 Always,
180
181 Never,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
194#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
195#[serde(rename_all = "lowercase")]
196pub enum StatVirtualization {
197 Strict,
199 Relaxed,
201 Off,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
209#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
210#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
211#[serde(rename_all = "lowercase")]
212pub enum HostPermissions {
213 Private,
215 Mirror,
217}
218
219#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
221#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
222#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
223#[serde(rename_all = "lowercase")]
224pub enum SecurityProfile {
225 #[default]
229 Default,
230
231 Restricted,
235}
236
237#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
243#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
244#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
245#[serde(rename_all = "snake_case")]
246pub enum DeploymentProfile {
247 #[default]
249 SingleTenant,
250
251 MultiTenant,
253}
254
255#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
257#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
258#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
259#[serde(default)]
260pub struct MountOptions {
261 pub readonly: bool,
265
266 pub noexec: bool,
270
271 pub nosuid: bool,
273
274 pub nodev: bool,
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
280#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
281#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
282pub enum VolumeKind {
283 Directory,
285
286 Disk,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
293#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
294pub struct VolumeSpec {
295 pub name: String,
297
298 pub kind: VolumeKind,
300
301 pub quota_mib: Option<u32>,
303
304 pub capacity_mib: Option<u32>,
306
307 pub labels: Vec<(String, String)>,
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
313#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
314#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
315pub enum NamedVolumeMode {
316 Existing,
318
319 Create,
321
322 EnsureExists,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
329#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
330pub struct NamedVolumeCreate {
331 pub mode: NamedVolumeMode,
333
334 pub name: String,
336
337 pub kind: VolumeKind,
339
340 pub quota_mib: Option<u32>,
342
343 pub capacity_mib: Option<u32>,
345
346 pub labels: Vec<(String, String)>,
348}
349
350#[derive(Clone)]
352#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
353#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
354#[cfg_attr(feature = "ts", ts(tag = "type"))]
355pub enum VolumeMount {
356 Bind {
358 #[cfg_attr(feature = "ts", ts(type = "string"))]
360 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
361 host: PathBuf,
362 guest: String,
364 options: MountOptions,
366 stat_virtualization: StatVirtualization,
368 host_permissions: HostPermissions,
370 follow_root_symlinks: bool,
377 quota_mib: Option<u32>,
383 },
384
385 Named {
387 name: String,
389 guest: String,
391 create: Option<NamedVolumeCreate>,
395 options: MountOptions,
397 stat_virtualization: StatVirtualization,
399 host_permissions: HostPermissions,
401 follow_root_symlinks: bool,
406 },
407
408 Tmpfs {
410 guest: String,
412 size_mib: Option<u32>,
414 options: MountOptions,
416 },
417
418 DiskImage {
420 #[cfg_attr(feature = "ts", ts(type = "string"))]
422 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
423 host: PathBuf,
424 guest: String,
426 format: DiskImageFormat,
428 fstype: Option<String>,
430 options: MountOptions,
432 },
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
438#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
439pub enum Patch {
440 Text {
442 path: String,
444 content: String,
446 mode: Option<u32>,
448 replace: bool,
450 },
451
452 File {
454 path: String,
456 content: Vec<u8>,
458 mode: Option<u32>,
460 replace: bool,
462 },
463
464 CopyFile {
466 #[cfg_attr(feature = "ts", ts(type = "string"))]
468 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
469 src: PathBuf,
470 dst: String,
472 mode: Option<u32>,
474 replace: bool,
476 },
477
478 CopyDir {
480 #[cfg_attr(feature = "ts", ts(type = "string"))]
482 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
483 src: PathBuf,
484 dst: String,
486 replace: bool,
488 },
489
490 Symlink {
492 target: String,
494 link: String,
496 replace: bool,
498 },
499
500 Mkdir {
502 path: String,
504 mode: Option<u32>,
506 },
507
508 Remove {
510 path: String,
512 },
513
514 Append {
516 path: String,
518 content: String,
520 },
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
531#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
532#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
533#[serde(default)]
534pub struct NetworkSpec {
535 pub enabled: bool,
537
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub interface: Option<InterfaceOverrides>,
541
542 pub ports: Vec<PublishedPortSpec>,
544
545 #[serde(skip_serializing_if = "Option::is_none")]
547 pub policy: Option<NetworkPolicy>,
548
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub dns: Option<DnsConfig>,
552
553 #[serde(skip_serializing_if = "Option::is_none")]
555 pub tls: Option<TlsConfig>,
556
557 #[serde(skip_serializing_if = "Option::is_none")]
559 pub secrets: Option<SecretsConfig>,
560
561 pub max_connections: Option<usize>,
563
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub rate_limiter: Option<NetworkRateLimiterConfig>,
567
568 pub trust_host_cas: bool,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
575#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
576pub struct PublishedPortSpec {
577 pub host_port: u16,
579
580 pub guest_port: u16,
582
583 #[serde(default)]
585 pub protocol: PortProtocol,
586
587 pub host_bind: String,
589}
590
591#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
593#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
594#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
595pub enum PortProtocol {
596 #[default]
598 #[serde(rename = "tcp")]
599 Tcp,
600
601 #[serde(rename = "udp")]
603 Udp,
604}
605
606#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
612#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
613#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
614#[serde(default)]
615pub struct VsockSpec {
616 pub routes: Vec<VsockRouteSpec>,
618}
619
620impl VsockSpec {
621 pub fn is_empty(&self) -> bool {
623 self.routes.is_empty()
624 }
625}
626
627#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
629#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
630#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
631pub struct VsockRouteSpec {
632 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
634 pub host_socket: PathBuf,
635
636 pub port: u32,
638
639 #[serde(default)]
641 pub socket_type: VsockSocketType,
642}
643
644#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
646#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
647#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
648#[serde(rename_all = "snake_case")]
649pub enum VsockSocketType {
650 #[default]
652 Stream,
653
654 Dgram,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
664#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
665#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
666pub struct HandoffInit {
667 pub cmd: String,
671
672 #[serde(default)]
674 pub args: Vec<String>,
675
676 #[serde(default)]
678 pub env: Vec<(String, String)>,
679}
680
681#[derive(Debug, Default, Clone, Serialize, Deserialize)]
687#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
688#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
689pub struct SandboxPolicy {
690 #[serde(default)]
699 pub ephemeral: bool,
700
701 pub max_duration_secs: Option<u64>,
703
704 pub idle_timeout_secs: Option<u64>,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize)]
719#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
720pub struct SnapshotSpec {
721 pub name: String,
723
724 #[serde(default)]
727 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
728 pub dest_dir: Option<PathBuf>,
729
730 pub source_sandbox: String,
732
733 pub labels: Vec<(String, String)>,
735
736 pub force: bool,
738
739 pub record_integrity: bool,
741
742 #[serde(default)]
748 pub resumable: bool,
749}
750
751#[derive(Debug, Default, Clone, Serialize, Deserialize)]
759#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
760#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
761#[serde(default)]
762pub struct SandboxSpec {
763 pub name: String,
765
766 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
768 pub image: RootfsSource,
769
770 pub resources: SandboxResources,
772
773 pub runtime: SandboxRuntimeOptions,
775
776 pub env: Vec<EnvVar>,
778
779 pub labels: BTreeMap<String, String>,
781
782 pub rlimits: Vec<Rlimit>,
784
785 pub mounts: Vec<VolumeMount>,
787
788 pub patches: Vec<Patch>,
790
791 pub network: NetworkSpec,
793
794 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
796 pub vsock: VsockSpec,
797
798 pub init: Option<HandoffInit>,
800
801 pub pull_policy: PullPolicy,
803
804 pub security_profile: SecurityProfile,
806
807 pub deployment_profile: DeploymentProfile,
813
814 pub lifecycle: SandboxPolicy,
816}
817
818#[derive(Debug, Clone, Serialize)]
820#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
821#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
822pub struct SandboxResources {
823 pub cpus: u8,
825
826 pub memory_mib: u32,
828
829 pub max_cpus: u8,
831
832 pub max_memory_mib: u32,
834
835 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
837 pub cpu_placement: CpuPlacement,
838
839 #[serde(default, skip_serializing_if = "Option::is_none")]
841 pub placement_profile: Option<String>,
842
843 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
845 pub thp: TransparentHugePagePolicy,
846}
847
848#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
850#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
851#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
852#[serde(rename_all = "lowercase")]
853pub enum CpuPlacement {
854 #[default]
856 Inherit,
857
858 Auto,
860
861 Spread,
863
864 Compact,
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
870#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
871#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
872#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
873pub enum NumaPlacement {
874 PreferSingle,
876 StrictSingle,
878 Inherit,
880}
881
882#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
884#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
885#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
886#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
887pub enum MemoryPlacement {
888 FollowCpu,
890 Inherit,
892}
893
894#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
896#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
897#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
898#[serde(deny_unknown_fields)]
899pub struct PlacementProfile {
900 pub numa: NumaPlacement,
902 pub memory: MemoryPlacement,
904}
905
906#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
908#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
909#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
910#[serde(rename_all = "lowercase")]
911pub enum TransparentHugePagePolicy {
912 Always,
914
915 #[default]
917 Madvise,
918
919 Never,
921}
922
923#[derive(Debug, Clone, Serialize, Deserialize)]
925#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
926#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
927#[serde(default)]
928pub struct SandboxRuntimeOptions {
929 pub workdir: Option<String>,
931
932 pub shell: Option<String>,
934
935 pub scripts: BTreeMap<String, String>,
937
938 pub entrypoint: Option<Vec<String>>,
940
941 pub cmd: Option<Vec<String>>,
943
944 pub hostname: Option<String>,
946
947 pub user: Option<String>,
949
950 pub log_level: Option<SandboxLogLevel>,
952
953 pub metrics_sample_interval_ms: Option<u64>,
955
956 pub disable_metrics_sample: bool,
958}
959
960#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
962#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
963#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
964pub struct EnvVar {
965 pub key: String,
967
968 pub value: String,
970}
971
972#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
974#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
975#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
976#[serde(rename_all = "lowercase")]
977pub enum SandboxLogLevel {
978 Error,
980
981 Warn,
983
984 Info,
986
987 Debug,
989
990 Trace,
992}
993
994#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1000#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1001#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1002pub enum RlimitResource {
1003 Cpu,
1005 Fsize,
1007 Data,
1009 Stack,
1011 Core,
1013 Rss,
1015 Nproc,
1017 Nofile,
1019 Memlock,
1021 As,
1023 Locks,
1025 Sigpending,
1027 Msgqueue,
1029 Nice,
1031 Rtprio,
1033 Rttime,
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1039#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1040#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1041pub struct Rlimit {
1042 pub resource: RlimitResource,
1044
1045 pub soft: u64,
1047
1048 pub hard: u64,
1050}
1051
1052#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1058#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1059#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1060#[serde(rename_all = "lowercase")]
1061pub enum LogSource {
1062 Stdout,
1064
1065 Stderr,
1067
1068 Output,
1070
1071 System,
1073}
1074
1075impl DiskImageFormat {
1080 pub fn as_str(&self) -> &'static str {
1082 match self {
1083 Self::Qcow2 => "qcow2",
1084 Self::Raw => "raw",
1085 Self::Vmdk => "vmdk",
1086 }
1087 }
1088
1089 pub fn from_extension(ext: &str) -> Option<Self> {
1093 match ext {
1094 "qcow2" => Some(Self::Qcow2),
1095 "raw" => Some(Self::Raw),
1096 "vmdk" => Some(Self::Vmdk),
1097 _ => None,
1098 }
1099 }
1100}
1101
1102impl OciRootfsSource {
1103 pub fn new(reference: impl Into<String>) -> Self {
1105 Self {
1106 reference: reference.into(),
1107 root_disk: None,
1108 }
1109 }
1110}
1111
1112impl TransparentHugePagePolicy {
1113 pub fn is_madvise(&self) -> bool {
1115 matches!(self, Self::Madvise)
1116 }
1117
1118 pub fn as_str(self) -> &'static str {
1120 match self {
1121 Self::Always => "always",
1122 Self::Madvise => "madvise",
1123 Self::Never => "never",
1124 }
1125 }
1126}
1127
1128impl RootDisk {
1129 pub fn managed(size_mib: u32) -> Self {
1131 Self::Managed {
1132 size_mib: Some(size_mib),
1133 }
1134 }
1135
1136 pub fn tmpfs(size_mib: u32) -> Self {
1138 Self::Tmpfs {
1139 size_mib: Some(size_mib),
1140 }
1141 }
1142
1143 pub fn flat(size_mib: u32) -> Self {
1145 Self::Flat {
1146 size_mib: Some(size_mib),
1147 fstype: None,
1148 clone: FlatClone::Auto,
1149 }
1150 }
1151
1152 pub fn size_mib(&self) -> Option<u32> {
1154 match self {
1155 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1156 *size_mib
1157 }
1158 Self::DiskImage { .. } => None,
1159 }
1160 }
1161
1162 pub fn kind_str(&self) -> &'static str {
1164 match self {
1165 Self::Managed { .. } => "managed",
1166 Self::Tmpfs { .. } => "tmpfs",
1167 Self::DiskImage { .. } => "disk-image",
1168 Self::Flat { .. } => "flat",
1169 }
1170 }
1171
1172 pub fn is_managed(&self) -> bool {
1174 matches!(self, Self::Managed { .. })
1175 }
1176}
1177
1178impl FlatClone {
1179 pub const fn as_str(self) -> &'static str {
1181 match self {
1182 Self::Auto => "auto",
1183 Self::Copy => "copy",
1184 Self::Reflink => "reflink",
1185 }
1186 }
1187
1188 pub const fn is_auto(&self) -> bool {
1190 matches!(self, Self::Auto)
1191 }
1192}
1193
1194impl RootfsSource {
1195 pub fn oci(reference: impl Into<String>) -> Self {
1197 Self::Oci(OciRootfsSource::new(reference))
1198 }
1199
1200 pub fn oci_reference(&self) -> Option<&str> {
1202 match self {
1203 Self::Oci(oci) => Some(&oci.reference),
1204 _ => None,
1205 }
1206 }
1207
1208 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1210 match self {
1211 Self::Oci(oci) => oci.root_disk.as_ref(),
1212 _ => None,
1213 }
1214 }
1215
1216 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1219 match self {
1220 Self::Oci(oci) => match &oci.root_disk {
1221 Some(RootDisk::Managed { size_mib }) => *size_mib,
1222 Some(_) => None,
1223 None => None,
1224 },
1225 _ => None,
1226 }
1227 }
1228}
1229
1230impl EnvVar {
1231 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1233 Self {
1234 key: key.into(),
1235 value: value.into(),
1236 }
1237 }
1238
1239 pub fn as_pair(&self) -> (&str, &str) {
1241 (&self.key, &self.value)
1242 }
1243}
1244
1245impl VolumeKind {
1246 pub fn as_str(self) -> &'static str {
1248 match self {
1249 Self::Directory => "dir",
1250 Self::Disk => "disk",
1251 }
1252 }
1253
1254 pub fn from_db_value(value: &str) -> Self {
1256 match value {
1257 "disk" => Self::Disk,
1258 _ => Self::Directory,
1259 }
1260 }
1261}
1262
1263impl VolumeSpec {
1264 pub fn new(name: impl Into<String>) -> Self {
1266 Self {
1267 name: name.into(),
1268 kind: VolumeKind::Directory,
1269 quota_mib: None,
1270 capacity_mib: None,
1271 labels: Vec::new(),
1272 }
1273 }
1274}
1275
1276impl NamedVolumeCreate {
1277 pub fn mode(&self) -> NamedVolumeMode {
1279 self.mode
1280 }
1281
1282 pub fn name(&self) -> &str {
1284 &self.name
1285 }
1286
1287 pub fn kind(&self) -> VolumeKind {
1289 self.kind
1290 }
1291
1292 pub fn quota_mib(&self) -> Option<u32> {
1294 self.quota_mib
1295 }
1296
1297 pub fn capacity_mib(&self) -> Option<u32> {
1299 self.capacity_mib
1300 }
1301
1302 pub fn labels(&self) -> &[(String, String)] {
1304 &self.labels
1305 }
1306}
1307
1308impl VolumeMount {
1309 pub fn guest(&self) -> &str {
1311 match self {
1312 Self::Bind { guest, .. }
1313 | Self::Named { guest, .. }
1314 | Self::Tmpfs { guest, .. }
1315 | Self::DiskImage { guest, .. } => guest,
1316 }
1317 }
1318
1319 fn guest_mut(&mut self) -> &mut String {
1320 match self {
1321 Self::Bind { guest, .. }
1322 | Self::Named { guest, .. }
1323 | Self::Tmpfs { guest, .. }
1324 | Self::DiskImage { guest, .. } => guest,
1325 }
1326 }
1327
1328 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1330 match self {
1331 Self::Named { create, .. } => create.as_ref(),
1332 _ => None,
1333 }
1334 }
1335}
1336
1337pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1348 for mount in mounts.iter_mut() {
1349 let canonical = canonical_guest_mount_path(mount.guest())?;
1350 *mount.guest_mut() = canonical;
1351 }
1352
1353 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1354
1355 for pair in mounts.windows(2) {
1356 if pair[0].guest() == pair[1].guest() {
1357 return Err(TypesError::invalid_config(format!(
1358 "multiple volumes cannot mount the same guest path: {}",
1359 pair[0].guest()
1360 )));
1361 }
1362 }
1363
1364 Ok(())
1365}
1366
1367fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1368 let path = Utf8UnixPath::new(guest);
1369
1370 if !path.is_valid() {
1371 return Err(TypesError::invalid_config(format!(
1372 "guest mount path must be a valid Unix path: {guest}"
1373 )));
1374 }
1375 if !path.is_absolute() {
1376 return Err(TypesError::invalid_config(format!(
1377 "guest mount path must be absolute: {guest}"
1378 )));
1379 }
1380 if path
1381 .components()
1382 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1383 {
1384 return Err(TypesError::invalid_config(format!(
1385 "guest mount path must not contain '..': {guest}"
1386 )));
1387 }
1388 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1389 return Err(TypesError::invalid_config(format!(
1390 "guest mount path must not contain ':', ';', or ',': {guest}"
1391 )));
1392 }
1393
1394 let canonical = path.normalize().to_string();
1395 if canonical == "/" {
1396 return Err(TypesError::invalid_config(
1397 "cannot mount a volume at guest root /",
1398 ));
1399 }
1400
1401 Ok(canonical)
1402}
1403
1404fn guest_mount_order_key(guest: &str) -> (usize, String) {
1405 let path = Utf8UnixPath::new(guest);
1406 let depth = path.components().filter(Utf8Component::is_normal).count();
1407 (depth, guest.to_owned())
1408}
1409
1410impl RlimitResource {
1411 pub fn as_str(&self) -> &'static str {
1413 match self {
1414 Self::Cpu => "cpu",
1415 Self::Fsize => "fsize",
1416 Self::Data => "data",
1417 Self::Stack => "stack",
1418 Self::Core => "core",
1419 Self::Rss => "rss",
1420 Self::Nproc => "nproc",
1421 Self::Nofile => "nofile",
1422 Self::Memlock => "memlock",
1423 Self::As => "as",
1424 Self::Locks => "locks",
1425 Self::Sigpending => "sigpending",
1426 Self::Msgqueue => "msgqueue",
1427 Self::Nice => "nice",
1428 Self::Rtprio => "rtprio",
1429 Self::Rttime => "rttime",
1430 }
1431 }
1432}
1433
1434impl LogSource {
1435 pub fn effective(requested: &[Self]) -> Vec<Self> {
1437 if requested.is_empty() {
1438 vec![Self::Stdout, Self::Stderr, Self::Output]
1439 } else {
1440 let mut sources = requested.to_vec();
1441 sources.sort_by_key(|src| match src {
1442 Self::Stdout => 0,
1443 Self::Stderr => 1,
1444 Self::Output => 2,
1445 Self::System => 3,
1446 });
1447 sources.dedup();
1448 sources
1449 }
1450 }
1451}
1452
1453impl SandboxLogLevel {
1454 pub const fn as_str(self) -> &'static str {
1456 match self {
1457 Self::Error => "error",
1458 Self::Warn => "warn",
1459 Self::Info => "info",
1460 Self::Debug => "debug",
1461 Self::Trace => "trace",
1462 }
1463 }
1464}
1465
1466impl std::fmt::Display for DiskImageFormat {
1471 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1472 f.write_str(self.as_str())
1473 }
1474}
1475
1476impl FromStr for DiskImageFormat {
1477 type Err = String;
1478
1479 fn from_str(s: &str) -> Result<Self, Self::Err> {
1480 match s {
1481 "qcow2" => Ok(Self::Qcow2),
1482 "raw" => Ok(Self::Raw),
1483 "vmdk" => Ok(Self::Vmdk),
1484 _ => Err(format!("unknown disk image format: {s}")),
1485 }
1486 }
1487}
1488
1489impl fmt::Display for TransparentHugePagePolicy {
1490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1491 f.write_str(self.as_str())
1492 }
1493}
1494
1495impl FromStr for TransparentHugePagePolicy {
1496 type Err = String;
1497
1498 fn from_str(value: &str) -> Result<Self, Self::Err> {
1499 match value {
1500 "always" => Ok(Self::Always),
1501 "madvise" => Ok(Self::Madvise),
1502 "never" => Ok(Self::Never),
1503 _ => Err(format!(
1504 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1505 )),
1506 }
1507 }
1508}
1509
1510impl Default for RootfsSource {
1511 fn default() -> Self {
1512 Self::oci(String::new())
1513 }
1514}
1515
1516impl Default for SandboxResources {
1517 fn default() -> Self {
1518 Self {
1519 cpus: DEFAULT_SANDBOX_CPUS,
1520 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1521 max_cpus: DEFAULT_SANDBOX_CPUS,
1522 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1523 cpu_placement: CpuPlacement::Inherit,
1524 placement_profile: None,
1525 thp: TransparentHugePagePolicy::Madvise,
1526 }
1527 }
1528}
1529
1530impl<'de> Deserialize<'de> for SandboxResources {
1531 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1532 where
1533 D: serde::Deserializer<'de>,
1534 {
1535 #[derive(Deserialize)]
1536 struct RawResources {
1537 #[serde(default = "default_sandbox_cpus")]
1538 cpus: u8,
1539 #[serde(default = "default_sandbox_memory_mib")]
1540 memory_mib: u32,
1541 max_cpus: Option<u8>,
1542 max_memory_mib: Option<u32>,
1543 #[serde(default)]
1544 cpu_placement: CpuPlacement,
1545 #[serde(default)]
1546 placement_profile: Option<String>,
1547 #[serde(default)]
1548 thp: TransparentHugePagePolicy,
1549 }
1550
1551 let raw = RawResources::deserialize(deserializer)?;
1552 Ok(Self {
1553 cpus: raw.cpus,
1554 memory_mib: raw.memory_mib,
1555 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1559 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1560 cpu_placement: raw.cpu_placement,
1561 placement_profile: raw.placement_profile,
1562 thp: raw.thp,
1563 })
1564 }
1565}
1566
1567impl CpuPlacement {
1568 pub const fn is_inherit(&self) -> bool {
1570 matches!(self, Self::Inherit)
1571 }
1572}
1573
1574impl std::fmt::Display for CpuPlacement {
1575 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1576 f.write_str(match self {
1577 Self::Inherit => "inherit",
1578 Self::Auto => "auto",
1579 Self::Spread => "spread",
1580 Self::Compact => "compact",
1581 })
1582 }
1583}
1584
1585impl FromStr for CpuPlacement {
1586 type Err = String;
1587
1588 fn from_str(value: &str) -> Result<Self, Self::Err> {
1589 match value {
1590 "inherit" => Ok(Self::Inherit),
1591 "auto" => Ok(Self::Auto),
1592 "spread" => Ok(Self::Spread),
1593 "compact" => Ok(Self::Compact),
1594 _ => Err(format!(
1595 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1596 )),
1597 }
1598 }
1599}
1600
1601impl Default for SandboxRuntimeOptions {
1602 fn default() -> Self {
1603 Self {
1604 workdir: None,
1605 shell: None,
1606 scripts: BTreeMap::new(),
1607 entrypoint: None,
1608 cmd: None,
1609 hostname: None,
1610 user: None,
1611 log_level: None,
1612 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1613 disable_metrics_sample: false,
1614 }
1615 }
1616}
1617
1618impl Default for NetworkSpec {
1619 fn default() -> Self {
1620 Self {
1621 enabled: true,
1622 interface: None,
1623 ports: Vec::new(),
1624 policy: None,
1625 dns: None,
1626 tls: None,
1627 secrets: None,
1628 max_connections: None,
1629 rate_limiter: None,
1630 trust_host_cas: false,
1631 }
1632 }
1633}
1634
1635impl Default for PublishedPortSpec {
1636 fn default() -> Self {
1637 Self {
1638 host_port: 0,
1639 guest_port: 0,
1640 protocol: PortProtocol::Tcp,
1641 host_bind: "127.0.0.1".into(),
1642 }
1643 }
1644}
1645
1646impl From<(String, String)> for EnvVar {
1647 fn from((key, value): (String, String)) -> Self {
1648 Self { key, value }
1649 }
1650}
1651
1652impl From<EnvVar> for (String, String) {
1653 fn from(var: EnvVar) -> Self {
1654 (var.key, var.value)
1655 }
1656}
1657
1658impl FromStr for SandboxLogLevel {
1659 type Err = String;
1660
1661 fn from_str(s: &str) -> Result<Self, Self::Err> {
1662 match s {
1663 "error" => Ok(Self::Error),
1664 "warn" => Ok(Self::Warn),
1665 "info" => Ok(Self::Info),
1666 "debug" => Ok(Self::Debug),
1667 "trace" => Ok(Self::Trace),
1668 _ => Err(format!("unknown sandbox log level: {s}")),
1669 }
1670 }
1671}
1672
1673impl Serialize for VolumeMount {
1674 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1675 use serde::ser::SerializeMap;
1676
1677 match self {
1678 Self::Bind {
1679 host,
1680 guest,
1681 options,
1682 stat_virtualization,
1683 host_permissions,
1684 follow_root_symlinks,
1685 quota_mib,
1686 } => {
1687 let mut map = serializer.serialize_map(Some(8))?;
1688 map.serialize_entry("type", "Bind")?;
1689 map.serialize_entry("host", host)?;
1690 map.serialize_entry("guest", guest)?;
1691 map.serialize_entry("options", options)?;
1692 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1693 map.serialize_entry("host_permissions", host_permissions)?;
1694 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1695 map.serialize_entry("quota_mib", quota_mib)?;
1696 map.end()
1697 }
1698 Self::Named {
1699 name,
1700 guest,
1701 create: _,
1702 options,
1703 stat_virtualization,
1704 host_permissions,
1705 follow_root_symlinks,
1706 } => {
1707 let mut map = serializer.serialize_map(Some(7))?;
1708 map.serialize_entry("type", "Named")?;
1709 map.serialize_entry("name", name)?;
1710 map.serialize_entry("guest", guest)?;
1711 map.serialize_entry("options", options)?;
1712 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1713 map.serialize_entry("host_permissions", host_permissions)?;
1714 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1715 map.end()
1716 }
1717 Self::Tmpfs {
1718 guest,
1719 size_mib,
1720 options,
1721 } => {
1722 let mut map = serializer.serialize_map(Some(4))?;
1723 map.serialize_entry("type", "Tmpfs")?;
1724 map.serialize_entry("guest", guest)?;
1725 map.serialize_entry("size_mib", size_mib)?;
1726 map.serialize_entry("options", options)?;
1727 map.end()
1728 }
1729 Self::DiskImage {
1730 host,
1731 guest,
1732 format,
1733 fstype,
1734 options,
1735 } => {
1736 let mut map = serializer.serialize_map(Some(6))?;
1737 map.serialize_entry("type", "DiskImage")?;
1738 map.serialize_entry("host", host)?;
1739 map.serialize_entry("guest", guest)?;
1740 map.serialize_entry("format", format)?;
1741 map.serialize_entry("fstype", fstype)?;
1742 map.serialize_entry("options", options)?;
1743 map.end()
1744 }
1745 }
1746 }
1747}
1748
1749impl<'de> Deserialize<'de> for VolumeMount {
1750 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1751 fn default_strict() -> StatVirtualization {
1752 StatVirtualization::Strict
1753 }
1754
1755 fn default_private() -> HostPermissions {
1756 HostPermissions::Private
1757 }
1758
1759 #[derive(Deserialize)]
1760 #[serde(tag = "type")]
1761 enum VolumeMountHelper {
1762 Bind {
1763 host: PathBuf,
1764 guest: String,
1765 #[serde(default)]
1766 options: Option<MountOptions>,
1767 #[serde(default)]
1768 readonly: bool,
1769 #[serde(default = "default_strict")]
1770 stat_virtualization: StatVirtualization,
1771 #[serde(default = "default_private")]
1772 host_permissions: HostPermissions,
1773 #[serde(default)]
1774 follow_root_symlinks: bool,
1775 #[serde(default)]
1776 quota_mib: Option<u32>,
1777 },
1778 Named {
1779 name: String,
1780 guest: String,
1781 #[serde(default)]
1782 options: Option<MountOptions>,
1783 #[serde(default)]
1784 readonly: bool,
1785 #[serde(default = "default_strict")]
1786 stat_virtualization: StatVirtualization,
1787 #[serde(default = "default_private")]
1788 host_permissions: HostPermissions,
1789 #[serde(default)]
1790 follow_root_symlinks: bool,
1791 },
1792 Tmpfs {
1793 guest: String,
1794 #[serde(default)]
1795 size_mib: Option<u32>,
1796 #[serde(default)]
1797 options: Option<MountOptions>,
1798 #[serde(default)]
1799 readonly: bool,
1800 },
1801 DiskImage {
1802 host: PathBuf,
1803 guest: String,
1804 format: DiskImageFormat,
1805 #[serde(default)]
1806 fstype: Option<String>,
1807 #[serde(default)]
1808 options: Option<MountOptions>,
1809 #[serde(default)]
1810 readonly: bool,
1811 },
1812 }
1813
1814 let helper = VolumeMountHelper::deserialize(deserializer)?;
1815 Ok(match helper {
1816 VolumeMountHelper::Bind {
1817 host,
1818 guest,
1819 options,
1820 readonly,
1821 stat_virtualization,
1822 host_permissions,
1823 follow_root_symlinks,
1824 quota_mib,
1825 } => Self::Bind {
1826 host,
1827 guest,
1828 options: decode_mount_options(options, readonly),
1829 stat_virtualization,
1830 host_permissions,
1831 follow_root_symlinks,
1832 quota_mib,
1833 },
1834 VolumeMountHelper::Named {
1835 name,
1836 guest,
1837 options,
1838 readonly,
1839 stat_virtualization,
1840 host_permissions,
1841 follow_root_symlinks,
1842 } => Self::Named {
1843 name,
1844 guest,
1845 create: None,
1846 options: decode_mount_options(options, readonly),
1847 stat_virtualization,
1848 host_permissions,
1849 follow_root_symlinks,
1850 },
1851 VolumeMountHelper::Tmpfs {
1852 guest,
1853 size_mib,
1854 options,
1855 readonly,
1856 } => Self::Tmpfs {
1857 guest,
1858 size_mib,
1859 options: decode_mount_options(options, readonly),
1860 },
1861 VolumeMountHelper::DiskImage {
1862 host,
1863 guest,
1864 format,
1865 fstype,
1866 options,
1867 readonly,
1868 } => Self::DiskImage {
1869 host,
1870 guest,
1871 format,
1872 fstype,
1873 options: decode_mount_options(options, readonly),
1874 },
1875 })
1876 }
1877}
1878
1879impl fmt::Debug for VolumeMount {
1880 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1881 match self {
1882 Self::Bind {
1883 host,
1884 guest,
1885 options,
1886 stat_virtualization,
1887 host_permissions,
1888 follow_root_symlinks,
1889 quota_mib,
1890 } => f
1891 .debug_struct("Bind")
1892 .field("host", host)
1893 .field("guest", guest)
1894 .field("options", options)
1895 .field("stat_virtualization", stat_virtualization)
1896 .field("host_permissions", host_permissions)
1897 .field("follow_root_symlinks", follow_root_symlinks)
1898 .field("quota_mib", quota_mib)
1899 .finish(),
1900 Self::Named {
1901 name,
1902 guest,
1903 create,
1904 options,
1905 stat_virtualization,
1906 host_permissions,
1907 follow_root_symlinks,
1908 } => f
1909 .debug_struct("Named")
1910 .field("name", name)
1911 .field("guest", guest)
1912 .field("create", create)
1913 .field("options", options)
1914 .field("stat_virtualization", stat_virtualization)
1915 .field("host_permissions", host_permissions)
1916 .field("follow_root_symlinks", follow_root_symlinks)
1917 .finish(),
1918 Self::Tmpfs {
1919 guest,
1920 size_mib,
1921 options,
1922 } => f
1923 .debug_struct("Tmpfs")
1924 .field("guest", guest)
1925 .field("size_mib", size_mib)
1926 .field("options", options)
1927 .finish(),
1928 Self::DiskImage {
1929 host,
1930 guest,
1931 format,
1932 fstype,
1933 options,
1934 } => f
1935 .debug_struct("DiskImage")
1936 .field("host", host)
1937 .field("guest", guest)
1938 .field("format", format)
1939 .field("fstype", fstype)
1940 .field("options", options)
1941 .finish(),
1942 }
1943 }
1944}
1945
1946impl TryFrom<&str> for RlimitResource {
1948 type Error = String;
1949
1950 fn try_from(s: &str) -> Result<Self, Self::Error> {
1951 match s.to_ascii_lowercase().as_str() {
1952 "cpu" => Ok(Self::Cpu),
1953 "fsize" => Ok(Self::Fsize),
1954 "data" => Ok(Self::Data),
1955 "stack" => Ok(Self::Stack),
1956 "core" => Ok(Self::Core),
1957 "rss" => Ok(Self::Rss),
1958 "nproc" => Ok(Self::Nproc),
1959 "nofile" => Ok(Self::Nofile),
1960 "memlock" => Ok(Self::Memlock),
1961 "as" => Ok(Self::As),
1962 "locks" => Ok(Self::Locks),
1963 "sigpending" => Ok(Self::Sigpending),
1964 "msgqueue" => Ok(Self::Msgqueue),
1965 "nice" => Ok(Self::Nice),
1966 "rtprio" => Ok(Self::Rtprio),
1967 "rttime" => Ok(Self::Rttime),
1968 _ => Err(format!("unknown rlimit resource: {s}")),
1969 }
1970 }
1971}
1972
1973fn default_sandbox_cpus() -> u8 {
1978 DEFAULT_SANDBOX_CPUS
1979}
1980
1981fn default_sandbox_memory_mib() -> u32 {
1982 DEFAULT_SANDBOX_MEMORY_MIB
1983}
1984
1985fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
1986 options.unwrap_or(MountOptions {
1987 readonly,
1988 ..MountOptions::default()
1989 })
1990}
1991
1992pub(crate) fn default_strict() -> StatVirtualization {
1994 StatVirtualization::Strict
1995}
1996
1997pub(crate) fn default_private() -> HostPermissions {
1999 HostPermissions::Private
2000}
2001
2002pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2004
2005#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2012#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2013#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2014pub struct SecretsConfig {
2015 #[serde(default)]
2017 pub secrets: Vec<SecretEntry>,
2018
2019 #[serde(default)]
2021 pub on_violation: ViolationAction,
2022}
2023
2024#[derive(Clone, Serialize, Deserialize)]
2029#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2030#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2031pub struct SecretEntry {
2032 pub env_var: String,
2038
2039 #[serde(default = "empty_secret_value")]
2048 #[cfg_attr(feature = "ts", ts(type = "string"))]
2049 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2050 pub value: Zeroizing<String>,
2051
2052 #[serde(default, skip_serializing_if = "Option::is_none")]
2056 pub source: Option<SecretSource>,
2057
2058 pub placeholder: String,
2063
2064 #[serde(default)]
2066 pub allowed_hosts: Vec<HostPattern>,
2067
2068 #[serde(default)]
2070 pub injection: SecretInjection,
2071
2072 #[serde(default, skip_serializing_if = "Option::is_none")]
2074 pub on_violation: Option<ViolationAction>,
2075
2076 #[serde(default = "default_true")]
2081 pub require_tls_identity: bool,
2082}
2083
2084#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2086#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2087#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2088#[serde(rename_all = "kebab-case")]
2089pub enum HostPattern {
2090 #[serde(alias = "Exact")]
2092 Exact(String),
2093 #[serde(alias = "Wildcard")]
2095 Wildcard(String),
2096 #[serde(alias = "Any")]
2098 Any,
2099}
2100
2101#[derive(Debug, Clone, Serialize, Deserialize)]
2103#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2104#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2105pub struct SecretInjection {
2106 #[serde(default = "default_true")]
2108 pub headers: bool,
2109
2110 #[serde(default = "default_true")]
2112 pub basic_auth: bool,
2113
2114 #[serde(default)]
2116 pub query_params: bool,
2117
2118 #[serde(default)]
2126 pub body: bool,
2127}
2128
2129#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2131#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2132#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2133#[serde(rename_all = "kebab-case")]
2134pub enum ViolationAction {
2135 #[serde(alias = "Block")]
2137 Block,
2138 #[default]
2140 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2141 BlockAndLog,
2142 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2144 BlockAndTerminate,
2145 #[serde(alias = "Passthrough")]
2147 Passthrough(Vec<HostPattern>),
2148}
2149
2150#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2152pub enum SecretConfigError {
2153 #[error("secret #{secret_index}: env_var must not be empty")]
2155 EmptyEnvVar {
2156 secret_index: usize,
2158 },
2159
2160 #[error("secret #{secret_index}: env_var must not contain `=`")]
2162 EnvVarContainsEquals {
2163 secret_index: usize,
2165 },
2166
2167 #[error("secret #{secret_index}: env_var must not contain NUL")]
2169 EnvVarContainsNul {
2170 secret_index: usize,
2172 },
2173
2174 #[error("secret #{secret_index}: at least one allowed host is required")]
2176 MissingAllowedHosts {
2177 secret_index: usize,
2179 },
2180
2181 #[error("secret #{secret_index}: placeholder must not be empty")]
2183 EmptyPlaceholder {
2184 secret_index: usize,
2186 },
2187
2188 #[error(
2190 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2191 )]
2192 PlaceholderTooLong {
2193 secret_index: usize,
2195 actual_bytes: usize,
2197 max_bytes: usize,
2199 },
2200
2201 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2203 PlaceholderContainsNul {
2204 secret_index: usize,
2206 },
2207
2208 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2210 PlaceholderContainsLineBreak {
2211 secret_index: usize,
2213 },
2214}
2215
2216impl SecretsConfig {
2217 pub fn validate(&self) -> Result<(), SecretConfigError> {
2219 for (index, secret) in self.secrets.iter().enumerate() {
2220 secret.validate(index)?;
2221 }
2222 Ok(())
2223 }
2224}
2225
2226impl SecretEntry {
2227 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2229 validate_env_var(&self.env_var, secret_index)?;
2230
2231 if self.allowed_hosts.is_empty() {
2232 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2233 }
2234
2235 validate_placeholder(&self.placeholder, secret_index)
2236 }
2237}
2238
2239impl fmt::Debug for SecretEntry {
2241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2242 f.debug_struct("SecretEntry")
2243 .field("env_var", &self.env_var)
2244 .field("value", &"[REDACTED]")
2245 .field("source", &self.source)
2246 .field("placeholder", &self.placeholder)
2247 .field("allowed_hosts", &self.allowed_hosts)
2248 .field("injection", &self.injection)
2249 .field("on_violation", &self.on_violation)
2250 .field("require_tls_identity", &self.require_tls_identity)
2251 .finish()
2252 }
2253}
2254
2255impl HostPattern {
2256 pub fn parse(host: &str) -> Self {
2259 if host == "*" {
2260 HostPattern::Any
2261 } else if host.starts_with("*.") {
2262 HostPattern::Wildcard(host.to_string())
2263 } else {
2264 HostPattern::Exact(host.to_string())
2265 }
2266 }
2267
2268 pub fn matches(&self, hostname: &str) -> bool {
2273 match self {
2274 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2275 HostPattern::Wildcard(pattern) => {
2276 if let Some(suffix) = pattern.strip_prefix("*.") {
2277 hostname.eq_ignore_ascii_case(suffix)
2278 || (hostname.len() > suffix.len() + 1
2279 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2280 && hostname[hostname.len() - suffix.len()..]
2281 .eq_ignore_ascii_case(suffix))
2282 } else {
2283 hostname.eq_ignore_ascii_case(pattern)
2284 }
2285 }
2286 HostPattern::Any => true,
2287 }
2288 }
2289}
2290
2291impl Default for SecretInjection {
2292 fn default() -> Self {
2293 Self {
2294 headers: true,
2295 basic_auth: true,
2296 query_params: false,
2297 body: false,
2298 }
2299 }
2300}
2301
2302fn default_true() -> bool {
2303 true
2304}
2305
2306fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2307 if env_var.is_empty() {
2308 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2309 }
2310 if env_var.contains('=') {
2311 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2312 }
2313 if env_var.contains('\0') {
2314 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2315 }
2316 Ok(())
2317}
2318
2319fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2320 if placeholder.is_empty() {
2321 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2322 }
2323
2324 let actual_bytes = placeholder.len();
2325 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2326 return Err(SecretConfigError::PlaceholderTooLong {
2327 secret_index,
2328 actual_bytes,
2329 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2330 });
2331 }
2332
2333 if placeholder.contains('\0') {
2334 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2335 }
2336 if placeholder.contains('\r') || placeholder.contains('\n') {
2337 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2338 }
2339
2340 Ok(())
2341}
2342
2343#[derive(Debug, Clone, Serialize, Deserialize)]
2353#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2354#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2355pub struct TlsConfig {
2356 #[serde(default)]
2358 pub enabled: bool,
2359
2360 #[serde(default = "default_intercepted_ports")]
2362 pub intercepted_ports: Vec<u16>,
2363
2364 #[serde(default)]
2366 pub bypass: Vec<String>,
2367
2368 #[serde(default = "default_true")]
2370 pub verify_upstream: bool,
2371
2372 #[serde(default = "default_true")]
2375 pub block_quic_on_intercept: bool,
2376
2377 #[serde(default)]
2379 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2380 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2381 pub upstream_ca_cert: Vec<PathBuf>,
2382
2383 #[serde(default, alias = "scoped_upstream_ca_certs")]
2385 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2386
2387 #[serde(default)]
2389 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2390
2391 #[serde(default, alias = "ca")]
2394 pub intercept_ca: InterceptCaConfig,
2395
2396 #[serde(default)]
2398 pub cache: CertCacheConfig,
2399}
2400
2401#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2403#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2404#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2405pub struct InterceptCaConfig {
2406 #[serde(default)]
2409 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2410 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2411 pub cert_path: Option<PathBuf>,
2412
2413 #[serde(default)]
2416 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2417 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2418 pub key_path: Option<PathBuf>,
2419}
2420
2421#[derive(Debug, Clone, Serialize, Deserialize)]
2423#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2424#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2425pub struct CertCacheConfig {
2426 #[serde(default = "default_cache_capacity")]
2428 pub capacity: usize,
2429
2430 #[serde(default = "default_cert_validity_hours")]
2432 pub validity_hours: u64,
2433}
2434
2435#[derive(Debug, Clone, Serialize, Deserialize)]
2437#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2438#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2439pub struct ScopedUpstreamCaCert {
2440 pub pattern: String,
2442
2443 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2445 #[cfg_attr(feature = "ts", ts(type = "string"))]
2446 pub path: PathBuf,
2447}
2448
2449#[derive(Debug, Clone, Serialize, Deserialize)]
2451#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2452#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2453pub struct ScopedVerifyUpstream {
2454 pub pattern: String,
2456
2457 pub verify: bool,
2459}
2460
2461impl Default for TlsConfig {
2462 fn default() -> Self {
2463 Self {
2464 enabled: false,
2465 intercepted_ports: default_intercepted_ports(),
2466 bypass: Vec::new(),
2467 verify_upstream: true,
2468 block_quic_on_intercept: true,
2469 upstream_ca_cert: Vec::new(),
2470 scoped_upstream_ca_cert: Vec::new(),
2471 scoped_verify_upstream: Vec::new(),
2472 intercept_ca: InterceptCaConfig::default(),
2473 cache: CertCacheConfig::default(),
2474 }
2475 }
2476}
2477
2478impl Default for CertCacheConfig {
2479 fn default() -> Self {
2480 Self {
2481 capacity: default_cache_capacity(),
2482 validity_hours: default_cert_validity_hours(),
2483 }
2484 }
2485}
2486
2487fn default_intercepted_ports() -> Vec<u16> {
2488 vec![443]
2489}
2490
2491fn default_cache_capacity() -> usize {
2492 1000
2493}
2494
2495fn default_cert_validity_hours() -> u64 {
2496 24
2497}
2498
2499#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2505#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2506#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2507#[serde(rename_all = "snake_case")]
2508pub enum Action {
2509 Allow,
2511 Deny,
2513}
2514
2515#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2517#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2518#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2519#[serde(rename_all = "snake_case")]
2520pub enum Direction {
2521 Egress,
2523 Ingress,
2525 Any,
2527}
2528
2529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2531#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2532#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2533#[serde(rename_all = "snake_case")]
2534pub enum Protocol {
2535 Tcp,
2537 Udp,
2539 Icmpv4,
2541 Icmpv6,
2543}
2544
2545#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2547#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2548#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2549#[serde(rename_all = "snake_case")]
2550pub enum DestinationGroup {
2551 Public,
2553 Loopback,
2555 Private,
2557 LinkLocal,
2559 Metadata,
2561 Multicast,
2563 Host,
2565}
2566
2567#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2574#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2575#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2576#[serde(rename_all = "snake_case")]
2577pub enum Destination {
2578 Any,
2580 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2582 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2583 Domain(String),
2585 DomainSuffix(String),
2587 Group(DestinationGroup),
2589}
2590
2591#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2593#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2594#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2595pub struct PortRange {
2596 pub start: u16,
2598 pub end: u16,
2600}
2601
2602#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2605#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2606#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2607pub struct Rule {
2608 pub direction: Direction,
2610 pub destination: Destination,
2612 #[serde(default)]
2614 pub protocols: Vec<Protocol>,
2615 #[serde(default)]
2617 pub ports: Vec<PortRange>,
2618 pub action: Action,
2620}
2621
2622#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2625#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2626#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2627pub struct NetworkPolicy {
2628 #[serde(default = "action_deny")]
2630 pub default_egress: Action,
2631 #[serde(default = "action_deny")]
2633 pub default_ingress: Action,
2634 #[serde(default)]
2636 pub rules: Vec<Rule>,
2637}
2638
2639fn action_deny() -> Action {
2642 Action::Deny
2643}
2644
2645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2651#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2652#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2653#[serde(default)]
2654pub struct DnsConfig {
2655 pub rebind_protection: bool,
2657 pub nameservers: Vec<String>,
2660 pub query_timeout_ms: u64,
2662}
2663
2664impl Default for DnsConfig {
2665 fn default() -> Self {
2666 Self {
2667 rebind_protection: true,
2668 nameservers: Vec::new(),
2669 query_timeout_ms: 5000,
2670 }
2671 }
2672}
2673
2674#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2678#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2679#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2680#[serde(default)]
2681pub struct InterfaceOverrides {
2682 #[serde(skip_serializing_if = "Option::is_none")]
2684 pub mac: Option<[u8; 6]>,
2685 #[serde(skip_serializing_if = "Option::is_none")]
2687 pub mtu: Option<u16>,
2688 #[serde(skip_serializing_if = "Option::is_none")]
2690 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2691 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2692 pub ipv4_address: Option<Ipv4Addr>,
2693 #[serde(skip_serializing_if = "Option::is_none")]
2695 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2696 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2697 pub ipv4_pool: Option<Ipv4Network>,
2698 #[serde(skip_serializing_if = "Option::is_none")]
2700 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2701 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2702 pub ipv6_address: Option<Ipv6Addr>,
2703 #[serde(skip_serializing_if = "Option::is_none")]
2705 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2706 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2707 pub ipv6_pool: Option<Ipv6Network>,
2708}
2709
2710fn empty_secret_value() -> Zeroizing<String> {
2711 Zeroizing::new(String::new())
2712}
2713
2714#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2720pub enum NetworkRateLimitDirection {
2721 Egress,
2723 Ingress,
2725}
2726
2727#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2729#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2730#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2731#[serde(default)]
2732pub struct NetworkRateLimiterConfig {
2733 #[serde(skip_serializing_if = "Option::is_none")]
2735 pub egress: Option<RateLimiterConfig>,
2736
2737 #[serde(skip_serializing_if = "Option::is_none")]
2739 pub ingress: Option<RateLimiterConfig>,
2740}
2741
2742#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2748#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2749#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2750#[serde(default)]
2751pub struct RateLimiterConfig {
2752 #[serde(skip_serializing_if = "Option::is_none")]
2754 pub bandwidth: Option<TokenBucketConfig>,
2755
2756 #[serde(skip_serializing_if = "Option::is_none")]
2758 pub ops: Option<TokenBucketConfig>,
2759}
2760
2761#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2767#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2768#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2769pub struct TokenBucketConfig {
2770 pub size: u64,
2772
2773 pub refill_time_ms: u64,
2776
2777 #[serde(default)]
2779 pub one_time_burst: u64,
2780}
2781
2782#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2784pub enum RateLimitConfigError {
2785 #[error("rate limiter must configure at least one of bandwidth or ops")]
2787 EmptyLimiter,
2788
2789 #[error("{bucket} bucket: size must be greater than zero")]
2791 ZeroSize {
2792 bucket: &'static str,
2794 },
2795
2796 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
2798 ZeroRefillTime {
2799 bucket: &'static str,
2801 },
2802}
2803
2804impl RateLimiterConfig {
2805 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
2807 if self.bandwidth.is_none() && self.ops.is_none() {
2808 return Err(RateLimitConfigError::EmptyLimiter);
2809 }
2810 if let Some(bandwidth) = &self.bandwidth {
2811 bandwidth.validate("bandwidth")?;
2812 }
2813 if let Some(ops) = &self.ops {
2814 ops.validate("ops")?;
2815 }
2816 Ok(())
2817 }
2818}
2819
2820impl TokenBucketConfig {
2821 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
2823 if self.size == 0 {
2824 return Err(RateLimitConfigError::ZeroSize { bucket });
2825 }
2826 if self.refill_time_ms == 0 {
2827 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
2828 }
2829 Ok(())
2830 }
2831}
2832
2833impl fmt::Display for NetworkRateLimitDirection {
2834 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2835 match self {
2836 Self::Egress => f.write_str("egress"),
2837 Self::Ingress => f.write_str("ingress"),
2838 }
2839 }
2840}
2841
2842#[cfg(test)]
2847mod tests {
2848 use super::*;
2849
2850 fn tmpfs_mount(guest: &str) -> VolumeMount {
2851 VolumeMount::Tmpfs {
2852 guest: guest.to_owned(),
2853 size_mib: None,
2854 options: MountOptions::default(),
2855 }
2856 }
2857
2858 #[test]
2859 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
2860 let mut mounts = vec![
2861 tmpfs_mount("/workspace//persist/./logs/"),
2862 tmpfs_mount("/alpha/z"),
2863 tmpfs_mount("/workspace"),
2864 ];
2865
2866 canonicalize_volume_mounts(&mut mounts).unwrap();
2867
2868 assert_eq!(
2869 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
2870 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
2871 );
2872 }
2873
2874 #[test]
2875 fn volume_mounts_reject_duplicate_canonical_paths() {
2876 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
2877
2878 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
2879
2880 assert!(error.to_string().contains("same guest path: /data/cache"));
2881 }
2882
2883 #[test]
2884 fn volume_mounts_reject_parent_components_before_normalizing() {
2885 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
2886
2887 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
2888
2889 assert!(error.to_string().contains("must not contain '..'"));
2890 }
2891
2892 #[test]
2893 fn disk_image_format_from_extension() {
2894 assert_eq!(
2895 DiskImageFormat::from_extension("qcow2"),
2896 Some(DiskImageFormat::Qcow2)
2897 );
2898 assert_eq!(
2899 DiskImageFormat::from_extension("raw"),
2900 Some(DiskImageFormat::Raw)
2901 );
2902 assert_eq!(
2903 DiskImageFormat::from_extension("vmdk"),
2904 Some(DiskImageFormat::Vmdk)
2905 );
2906 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
2907 assert_eq!(DiskImageFormat::from_extension(""), None);
2908 }
2909
2910 #[test]
2911 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
2912 let resources: SandboxResources =
2913 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
2914
2915 assert_eq!(resources.cpus, 4);
2916 assert_eq!(resources.max_cpus, 4);
2917 assert_eq!(resources.memory_mib, 2048);
2918 assert_eq!(resources.max_memory_mib, 2048);
2919 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
2920 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
2921 assert_eq!(
2922 serde_json::to_value(resources).unwrap(),
2923 serde_json::json!({
2924 "cpus": 4,
2925 "memory_mib": 2048,
2926 "max_cpus": 4,
2927 "max_memory_mib": 2048
2928 })
2929 );
2930 }
2931
2932 #[test]
2933 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
2934 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
2935 assert!(inherited.get("cpu_placement").is_none());
2936
2937 for policy in [
2938 CpuPlacement::Auto,
2939 CpuPlacement::Spread,
2940 CpuPlacement::Compact,
2941 ] {
2942 let resources = SandboxResources {
2943 cpu_placement: policy,
2944 ..Default::default()
2945 };
2946 let json = serde_json::to_string(&resources).unwrap();
2947 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
2948
2949 assert_eq!(decoded.cpu_placement, policy);
2950 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
2951 }
2952 }
2953
2954 #[test]
2955 fn transparent_huge_page_policy_roundtrips_non_default() {
2956 let resources: SandboxResources = serde_json::from_str(
2957 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
2958 )
2959 .unwrap();
2960
2961 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
2962 assert_eq!(
2963 serde_json::to_value(resources).unwrap()["thp"],
2964 serde_json::json!("always")
2965 );
2966 assert_eq!(
2967 "never".parse::<TransparentHugePagePolicy>().unwrap(),
2968 TransparentHugePagePolicy::Never
2969 );
2970 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
2971 }
2972
2973 #[test]
2974 fn disk_image_format_display_roundtrip() {
2975 for format in [
2976 DiskImageFormat::Qcow2,
2977 DiskImageFormat::Raw,
2978 DiskImageFormat::Vmdk,
2979 ] {
2980 let rendered = format.to_string();
2981 let parsed: DiskImageFormat = rendered.parse().unwrap();
2982 assert_eq!(parsed, format);
2983 }
2984 }
2985
2986 #[test]
2987 fn disk_image_format_from_str_unknown() {
2988 assert!("ext4".parse::<DiskImageFormat>().is_err());
2989 }
2990
2991 #[test]
2992 fn log_source_effective_uses_default_user_program_sources() {
2993 assert_eq!(
2994 LogSource::effective(&[]),
2995 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
2996 );
2997 }
2998
2999 #[test]
3000 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3001 assert_eq!(
3002 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3003 vec![LogSource::Stdout, LogSource::System]
3004 );
3005 }
3006
3007 #[test]
3008 fn rlimit_resource_parses_case_insensitively() {
3009 assert_eq!(
3010 RlimitResource::try_from("NOFILE").unwrap(),
3011 RlimitResource::Nofile
3012 );
3013 assert!(RlimitResource::try_from("bogus").is_err());
3014 }
3015
3016 #[test]
3017 fn sandbox_policy_serde_roundtrip() {
3018 let policy = SandboxPolicy {
3019 ephemeral: true,
3020 max_duration_secs: Some(3600),
3021 idle_timeout_secs: Some(120),
3022 };
3023
3024 let json = serde_json::to_string(&policy).unwrap();
3025 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3026
3027 assert!(decoded.ephemeral);
3028 assert_eq!(decoded.max_duration_secs, Some(3600));
3029 assert_eq!(decoded.idle_timeout_secs, Some(120));
3030 }
3031
3032 #[test]
3033 fn sandbox_policy_defaults_to_persistent() {
3034 assert!(!SandboxPolicy::default().ephemeral);
3035 }
3036
3037 #[test]
3038 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3039 let decoded: SandboxPolicy =
3042 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3043 assert!(!decoded.ephemeral);
3044 assert_eq!(decoded.max_duration_secs, Some(60));
3045 }
3046
3047 #[test]
3048 fn sandbox_spec_default_uses_static_resource_defaults() {
3049 let spec = SandboxSpec::default();
3050
3051 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3052 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3053 assert_eq!(
3054 spec.runtime.metrics_sample_interval_ms,
3055 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3056 );
3057 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3058 }
3059
3060 #[test]
3061 fn deployment_profile_uses_stable_snake_case_wire_values() {
3062 assert_eq!(
3063 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3064 r#""multi_tenant""#
3065 );
3066 assert_eq!(
3067 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3068 DeploymentProfile::SingleTenant
3069 );
3070 }
3071
3072 #[test]
3073 fn sandbox_log_level_roundtrips_lowercase_values() {
3074 for (input, expected) in [
3075 ("error", SandboxLogLevel::Error),
3076 ("warn", SandboxLogLevel::Warn),
3077 ("info", SandboxLogLevel::Info),
3078 ("debug", SandboxLogLevel::Debug),
3079 ("trace", SandboxLogLevel::Trace),
3080 ] {
3081 let parsed: SandboxLogLevel = input.parse().unwrap();
3082 assert_eq!(parsed, expected);
3083 assert_eq!(parsed.as_str(), input);
3084 }
3085 }
3086}