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 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub override_uid: Option<u32>,
286
287 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub override_gid: Option<u32>,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
297#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
298pub enum VolumeKind {
299 Directory,
301
302 Disk,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
309#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
310pub struct VolumeSpec {
311 pub name: String,
313
314 pub kind: VolumeKind,
316
317 pub quota_mib: Option<u32>,
319
320 pub capacity_mib: Option<u32>,
322
323 pub labels: Vec<(String, String)>,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
330#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
331pub enum NamedVolumeMode {
332 Existing,
334
335 Create,
337
338 EnsureExists,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
345#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
346pub struct NamedVolumeCreate {
347 pub mode: NamedVolumeMode,
349
350 pub name: String,
352
353 pub kind: VolumeKind,
355
356 pub quota_mib: Option<u32>,
358
359 pub capacity_mib: Option<u32>,
361
362 pub labels: Vec<(String, String)>,
364}
365
366#[derive(Clone)]
368#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
369#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
370#[cfg_attr(feature = "ts", ts(tag = "type"))]
371pub enum VolumeMount {
372 Bind {
374 #[cfg_attr(feature = "ts", ts(type = "string"))]
376 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
377 host: PathBuf,
378 guest: String,
380 options: MountOptions,
382 stat_virtualization: StatVirtualization,
384 host_permissions: HostPermissions,
386 follow_root_symlinks: bool,
393 quota_mib: Option<u32>,
399 },
400
401 Named {
403 name: String,
405 guest: String,
407 create: Option<NamedVolumeCreate>,
411 options: MountOptions,
413 stat_virtualization: StatVirtualization,
415 host_permissions: HostPermissions,
417 follow_root_symlinks: bool,
422 },
423
424 Tmpfs {
426 guest: String,
428 size_mib: Option<u32>,
430 options: MountOptions,
432 },
433
434 DiskImage {
436 #[cfg_attr(feature = "ts", ts(type = "string"))]
438 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
439 host: PathBuf,
440 guest: String,
442 format: DiskImageFormat,
444 fstype: Option<String>,
446 options: MountOptions,
448 },
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
454#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
455pub enum Patch {
456 Text {
458 path: String,
460 content: String,
462 mode: Option<u32>,
464 replace: bool,
466 },
467
468 File {
470 path: String,
472 content: Vec<u8>,
474 mode: Option<u32>,
476 replace: bool,
478 },
479
480 CopyFile {
482 #[cfg_attr(feature = "ts", ts(type = "string"))]
484 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
485 src: PathBuf,
486 dst: String,
488 mode: Option<u32>,
490 replace: bool,
492 },
493
494 CopyDir {
496 #[cfg_attr(feature = "ts", ts(type = "string"))]
498 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
499 src: PathBuf,
500 dst: String,
502 replace: bool,
504 },
505
506 Symlink {
508 target: String,
510 link: String,
512 replace: bool,
514 },
515
516 Mkdir {
518 path: String,
520 mode: Option<u32>,
522 },
523
524 Remove {
526 path: String,
528 },
529
530 Append {
532 path: String,
534 content: String,
536 },
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
547#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
548#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
549#[serde(default)]
550pub struct NetworkSpec {
551 pub enabled: bool,
553
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub interface: Option<InterfaceOverrides>,
557
558 pub ports: Vec<PublishedPortSpec>,
560
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub policy: Option<NetworkPolicy>,
564
565 #[serde(skip_serializing_if = "Option::is_none")]
567 pub dns: Option<DnsConfig>,
568
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub tls: Option<TlsConfig>,
572
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub secrets: Option<SecretsConfig>,
576
577 pub max_connections: Option<usize>,
579
580 #[serde(skip_serializing_if = "Option::is_none")]
582 pub rate_limiter: Option<NetworkRateLimiterConfig>,
583
584 pub trust_host_cas: bool,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
591#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
592pub struct PublishedPortSpec {
593 pub host_port: u16,
595
596 pub guest_port: u16,
598
599 #[serde(default)]
601 pub protocol: PortProtocol,
602
603 pub host_bind: String,
605}
606
607#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
609#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
610#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
611pub enum PortProtocol {
612 #[default]
614 #[serde(rename = "tcp")]
615 Tcp,
616
617 #[serde(rename = "udp")]
619 Udp,
620}
621
622#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
628#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
629#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
630#[serde(default)]
631pub struct VsockSpec {
632 pub routes: Vec<VsockRouteSpec>,
634}
635
636impl VsockSpec {
637 pub fn is_empty(&self) -> bool {
639 self.routes.is_empty()
640 }
641}
642
643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
645#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
646#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
647pub struct VsockRouteSpec {
648 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
650 pub host_socket: PathBuf,
651
652 pub port: u32,
654
655 #[serde(default)]
657 pub socket_type: VsockSocketType,
658}
659
660#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
662#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
663#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
664#[serde(rename_all = "snake_case")]
665pub enum VsockSocketType {
666 #[default]
668 Stream,
669
670 Dgram,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
680#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
681#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
682pub struct HandoffInit {
683 pub cmd: String,
687
688 #[serde(default)]
690 pub args: Vec<String>,
691
692 #[serde(default)]
694 pub env: Vec<(String, String)>,
695}
696
697#[derive(Debug, Default, Clone, Serialize, Deserialize)]
703#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
704#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
705pub struct SandboxPolicy {
706 #[serde(default)]
715 pub ephemeral: bool,
716
717 pub max_duration_secs: Option<u64>,
719
720 pub idle_timeout_secs: Option<u64>,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
735#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
736pub struct SnapshotSpec {
737 pub name: String,
739
740 #[serde(default)]
743 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
744 pub dest_dir: Option<PathBuf>,
745
746 pub source_sandbox: String,
748
749 pub labels: Vec<(String, String)>,
751
752 pub force: bool,
754
755 pub record_integrity: bool,
757
758 #[serde(default)]
764 pub resumable: bool,
765}
766
767#[derive(Debug, Default, Clone, Serialize, Deserialize)]
775#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
776#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
777#[serde(default)]
778pub struct SandboxSpec {
779 pub name: String,
781
782 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
784 pub image: RootfsSource,
785
786 pub resources: SandboxResources,
788
789 pub runtime: SandboxRuntimeOptions,
791
792 pub env: Vec<EnvVar>,
794
795 pub labels: BTreeMap<String, String>,
797
798 pub rlimits: Vec<Rlimit>,
800
801 pub mounts: Vec<VolumeMount>,
803
804 pub patches: Vec<Patch>,
806
807 pub network: NetworkSpec,
809
810 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
812 pub vsock: VsockSpec,
813
814 pub init: Option<HandoffInit>,
816
817 pub pull_policy: PullPolicy,
819
820 pub security_profile: SecurityProfile,
822
823 pub deployment_profile: DeploymentProfile,
829
830 pub lifecycle: SandboxPolicy,
832}
833
834#[derive(Debug, Clone, Serialize)]
836#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
837#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
838pub struct SandboxResources {
839 pub cpus: u8,
841
842 pub memory_mib: u32,
844
845 pub max_cpus: u8,
847
848 pub max_memory_mib: u32,
850
851 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
853 pub cpu_placement: CpuPlacement,
854
855 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub placement_profile: Option<String>,
858
859 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
861 pub thp: TransparentHugePagePolicy,
862}
863
864#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
866#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
867#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
868#[serde(rename_all = "lowercase")]
869pub enum CpuPlacement {
870 #[default]
872 Inherit,
873
874 Auto,
876
877 Spread,
879
880 Compact,
882}
883
884#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
886#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
887#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
888#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
889pub enum NumaPlacement {
890 PreferSingle,
892 StrictSingle,
894 Inherit,
896}
897
898#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
900#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
901#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
902#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
903pub enum MemoryPlacement {
904 FollowCpu,
906 Inherit,
908}
909
910#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
912#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
913#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
914#[serde(deny_unknown_fields)]
915pub struct PlacementProfile {
916 pub numa: NumaPlacement,
918 pub memory: MemoryPlacement,
920}
921
922#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
924#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
925#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
926#[serde(rename_all = "lowercase")]
927pub enum TransparentHugePagePolicy {
928 Always,
930
931 #[default]
933 Madvise,
934
935 Never,
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize)]
941#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
942#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
943#[serde(default)]
944pub struct SandboxRuntimeOptions {
945 pub workdir: Option<String>,
947
948 pub shell: Option<String>,
950
951 pub scripts: BTreeMap<String, String>,
953
954 pub entrypoint: Option<Vec<String>>,
956
957 pub cmd: Option<Vec<String>>,
959
960 pub hostname: Option<String>,
962
963 pub user: Option<String>,
965
966 pub log_level: Option<SandboxLogLevel>,
968
969 pub metrics_sample_interval_ms: Option<u64>,
971
972 pub disable_metrics_sample: bool,
974}
975
976#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
978#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
979#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
980pub struct EnvVar {
981 pub key: String,
983
984 pub value: String,
986}
987
988#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
990#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
991#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
992#[serde(rename_all = "lowercase")]
993pub enum SandboxLogLevel {
994 Error,
996
997 Warn,
999
1000 Info,
1002
1003 Debug,
1005
1006 Trace,
1008}
1009
1010#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1016#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1017#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1018pub enum RlimitResource {
1019 Cpu,
1021 Fsize,
1023 Data,
1025 Stack,
1027 Core,
1029 Rss,
1031 Nproc,
1033 Nofile,
1035 Memlock,
1037 As,
1039 Locks,
1041 Sigpending,
1043 Msgqueue,
1045 Nice,
1047 Rtprio,
1049 Rttime,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1055#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1056#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1057pub struct Rlimit {
1058 pub resource: RlimitResource,
1060
1061 pub soft: u64,
1063
1064 pub hard: u64,
1066}
1067
1068#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1074#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1075#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1076#[serde(rename_all = "lowercase")]
1077pub enum LogSource {
1078 Stdout,
1080
1081 Stderr,
1083
1084 Output,
1086
1087 System,
1089}
1090
1091impl DiskImageFormat {
1096 pub fn as_str(&self) -> &'static str {
1098 match self {
1099 Self::Qcow2 => "qcow2",
1100 Self::Raw => "raw",
1101 Self::Vmdk => "vmdk",
1102 }
1103 }
1104
1105 pub fn from_extension(ext: &str) -> Option<Self> {
1109 match ext {
1110 "qcow2" => Some(Self::Qcow2),
1111 "raw" => Some(Self::Raw),
1112 "vmdk" => Some(Self::Vmdk),
1113 _ => None,
1114 }
1115 }
1116}
1117
1118impl OciRootfsSource {
1119 pub fn new(reference: impl Into<String>) -> Self {
1121 Self {
1122 reference: reference.into(),
1123 root_disk: None,
1124 }
1125 }
1126}
1127
1128impl TransparentHugePagePolicy {
1129 pub fn is_madvise(&self) -> bool {
1131 matches!(self, Self::Madvise)
1132 }
1133
1134 pub fn as_str(self) -> &'static str {
1136 match self {
1137 Self::Always => "always",
1138 Self::Madvise => "madvise",
1139 Self::Never => "never",
1140 }
1141 }
1142}
1143
1144impl RootDisk {
1145 pub fn managed(size_mib: u32) -> Self {
1147 Self::Managed {
1148 size_mib: Some(size_mib),
1149 }
1150 }
1151
1152 pub fn tmpfs(size_mib: u32) -> Self {
1154 Self::Tmpfs {
1155 size_mib: Some(size_mib),
1156 }
1157 }
1158
1159 pub fn flat(size_mib: u32) -> Self {
1161 Self::Flat {
1162 size_mib: Some(size_mib),
1163 fstype: None,
1164 clone: FlatClone::Auto,
1165 }
1166 }
1167
1168 pub fn size_mib(&self) -> Option<u32> {
1170 match self {
1171 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1172 *size_mib
1173 }
1174 Self::DiskImage { .. } => None,
1175 }
1176 }
1177
1178 pub fn kind_str(&self) -> &'static str {
1180 match self {
1181 Self::Managed { .. } => "managed",
1182 Self::Tmpfs { .. } => "tmpfs",
1183 Self::DiskImage { .. } => "disk-image",
1184 Self::Flat { .. } => "flat",
1185 }
1186 }
1187
1188 pub fn is_managed(&self) -> bool {
1190 matches!(self, Self::Managed { .. })
1191 }
1192}
1193
1194impl FlatClone {
1195 pub const fn as_str(self) -> &'static str {
1197 match self {
1198 Self::Auto => "auto",
1199 Self::Copy => "copy",
1200 Self::Reflink => "reflink",
1201 }
1202 }
1203
1204 pub const fn is_auto(&self) -> bool {
1206 matches!(self, Self::Auto)
1207 }
1208}
1209
1210impl RootfsSource {
1211 pub fn oci(reference: impl Into<String>) -> Self {
1213 Self::Oci(OciRootfsSource::new(reference))
1214 }
1215
1216 pub fn oci_reference(&self) -> Option<&str> {
1218 match self {
1219 Self::Oci(oci) => Some(&oci.reference),
1220 _ => None,
1221 }
1222 }
1223
1224 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1226 match self {
1227 Self::Oci(oci) => oci.root_disk.as_ref(),
1228 _ => None,
1229 }
1230 }
1231
1232 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1235 match self {
1236 Self::Oci(oci) => match &oci.root_disk {
1237 Some(RootDisk::Managed { size_mib }) => *size_mib,
1238 Some(_) => None,
1239 None => None,
1240 },
1241 _ => None,
1242 }
1243 }
1244}
1245
1246impl EnvVar {
1247 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1249 Self {
1250 key: key.into(),
1251 value: value.into(),
1252 }
1253 }
1254
1255 pub fn as_pair(&self) -> (&str, &str) {
1257 (&self.key, &self.value)
1258 }
1259}
1260
1261impl VolumeKind {
1262 pub fn as_str(self) -> &'static str {
1264 match self {
1265 Self::Directory => "dir",
1266 Self::Disk => "disk",
1267 }
1268 }
1269
1270 pub fn from_db_value(value: &str) -> Self {
1272 match value {
1273 "disk" => Self::Disk,
1274 _ => Self::Directory,
1275 }
1276 }
1277}
1278
1279impl VolumeSpec {
1280 pub fn new(name: impl Into<String>) -> Self {
1282 Self {
1283 name: name.into(),
1284 kind: VolumeKind::Directory,
1285 quota_mib: None,
1286 capacity_mib: None,
1287 labels: Vec::new(),
1288 }
1289 }
1290}
1291
1292impl NamedVolumeCreate {
1293 pub fn mode(&self) -> NamedVolumeMode {
1295 self.mode
1296 }
1297
1298 pub fn name(&self) -> &str {
1300 &self.name
1301 }
1302
1303 pub fn kind(&self) -> VolumeKind {
1305 self.kind
1306 }
1307
1308 pub fn quota_mib(&self) -> Option<u32> {
1310 self.quota_mib
1311 }
1312
1313 pub fn capacity_mib(&self) -> Option<u32> {
1315 self.capacity_mib
1316 }
1317
1318 pub fn labels(&self) -> &[(String, String)] {
1320 &self.labels
1321 }
1322}
1323
1324impl VolumeMount {
1325 pub fn guest(&self) -> &str {
1327 match self {
1328 Self::Bind { guest, .. }
1329 | Self::Named { guest, .. }
1330 | Self::Tmpfs { guest, .. }
1331 | Self::DiskImage { guest, .. } => guest,
1332 }
1333 }
1334
1335 fn guest_mut(&mut self) -> &mut String {
1336 match self {
1337 Self::Bind { guest, .. }
1338 | Self::Named { guest, .. }
1339 | Self::Tmpfs { guest, .. }
1340 | Self::DiskImage { guest, .. } => guest,
1341 }
1342 }
1343
1344 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1346 match self {
1347 Self::Named { create, .. } => create.as_ref(),
1348 _ => None,
1349 }
1350 }
1351}
1352
1353pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1364 for mount in mounts.iter_mut() {
1365 let canonical = canonical_guest_mount_path(mount.guest())?;
1366 *mount.guest_mut() = canonical;
1367 }
1368
1369 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1370
1371 for pair in mounts.windows(2) {
1372 if pair[0].guest() == pair[1].guest() {
1373 return Err(TypesError::invalid_config(format!(
1374 "multiple volumes cannot mount the same guest path: {}",
1375 pair[0].guest()
1376 )));
1377 }
1378 }
1379
1380 Ok(())
1381}
1382
1383fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1384 let path = Utf8UnixPath::new(guest);
1385
1386 if !path.is_valid() {
1387 return Err(TypesError::invalid_config(format!(
1388 "guest mount path must be a valid Unix path: {guest}"
1389 )));
1390 }
1391 if !path.is_absolute() {
1392 return Err(TypesError::invalid_config(format!(
1393 "guest mount path must be absolute: {guest}"
1394 )));
1395 }
1396 if path
1397 .components()
1398 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1399 {
1400 return Err(TypesError::invalid_config(format!(
1401 "guest mount path must not contain '..': {guest}"
1402 )));
1403 }
1404 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1405 return Err(TypesError::invalid_config(format!(
1406 "guest mount path must not contain ':', ';', or ',': {guest}"
1407 )));
1408 }
1409
1410 let canonical = path.normalize().to_string();
1411 if canonical == "/" {
1412 return Err(TypesError::invalid_config(
1413 "cannot mount a volume at guest root /",
1414 ));
1415 }
1416
1417 Ok(canonical)
1418}
1419
1420fn guest_mount_order_key(guest: &str) -> (usize, String) {
1421 let path = Utf8UnixPath::new(guest);
1422 let depth = path.components().filter(Utf8Component::is_normal).count();
1423 (depth, guest.to_owned())
1424}
1425
1426impl RlimitResource {
1427 pub fn as_str(&self) -> &'static str {
1429 match self {
1430 Self::Cpu => "cpu",
1431 Self::Fsize => "fsize",
1432 Self::Data => "data",
1433 Self::Stack => "stack",
1434 Self::Core => "core",
1435 Self::Rss => "rss",
1436 Self::Nproc => "nproc",
1437 Self::Nofile => "nofile",
1438 Self::Memlock => "memlock",
1439 Self::As => "as",
1440 Self::Locks => "locks",
1441 Self::Sigpending => "sigpending",
1442 Self::Msgqueue => "msgqueue",
1443 Self::Nice => "nice",
1444 Self::Rtprio => "rtprio",
1445 Self::Rttime => "rttime",
1446 }
1447 }
1448}
1449
1450impl LogSource {
1451 pub fn effective(requested: &[Self]) -> Vec<Self> {
1453 if requested.is_empty() {
1454 vec![Self::Stdout, Self::Stderr, Self::Output]
1455 } else {
1456 let mut sources = requested.to_vec();
1457 sources.sort_by_key(|src| match src {
1458 Self::Stdout => 0,
1459 Self::Stderr => 1,
1460 Self::Output => 2,
1461 Self::System => 3,
1462 });
1463 sources.dedup();
1464 sources
1465 }
1466 }
1467}
1468
1469impl SandboxLogLevel {
1470 pub const fn as_str(self) -> &'static str {
1472 match self {
1473 Self::Error => "error",
1474 Self::Warn => "warn",
1475 Self::Info => "info",
1476 Self::Debug => "debug",
1477 Self::Trace => "trace",
1478 }
1479 }
1480}
1481
1482impl std::fmt::Display for DiskImageFormat {
1487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1488 f.write_str(self.as_str())
1489 }
1490}
1491
1492impl FromStr for DiskImageFormat {
1493 type Err = String;
1494
1495 fn from_str(s: &str) -> Result<Self, Self::Err> {
1496 match s {
1497 "qcow2" => Ok(Self::Qcow2),
1498 "raw" => Ok(Self::Raw),
1499 "vmdk" => Ok(Self::Vmdk),
1500 _ => Err(format!("unknown disk image format: {s}")),
1501 }
1502 }
1503}
1504
1505impl fmt::Display for TransparentHugePagePolicy {
1506 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1507 f.write_str(self.as_str())
1508 }
1509}
1510
1511impl FromStr for TransparentHugePagePolicy {
1512 type Err = String;
1513
1514 fn from_str(value: &str) -> Result<Self, Self::Err> {
1515 match value {
1516 "always" => Ok(Self::Always),
1517 "madvise" => Ok(Self::Madvise),
1518 "never" => Ok(Self::Never),
1519 _ => Err(format!(
1520 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1521 )),
1522 }
1523 }
1524}
1525
1526impl Default for RootfsSource {
1527 fn default() -> Self {
1528 Self::oci(String::new())
1529 }
1530}
1531
1532impl Default for SandboxResources {
1533 fn default() -> Self {
1534 Self {
1535 cpus: DEFAULT_SANDBOX_CPUS,
1536 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1537 max_cpus: DEFAULT_SANDBOX_CPUS,
1538 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1539 cpu_placement: CpuPlacement::Inherit,
1540 placement_profile: None,
1541 thp: TransparentHugePagePolicy::Madvise,
1542 }
1543 }
1544}
1545
1546impl<'de> Deserialize<'de> for SandboxResources {
1547 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1548 where
1549 D: serde::Deserializer<'de>,
1550 {
1551 #[derive(Deserialize)]
1552 struct RawResources {
1553 #[serde(default = "default_sandbox_cpus")]
1554 cpus: u8,
1555 #[serde(default = "default_sandbox_memory_mib")]
1556 memory_mib: u32,
1557 max_cpus: Option<u8>,
1558 max_memory_mib: Option<u32>,
1559 #[serde(default)]
1560 cpu_placement: CpuPlacement,
1561 #[serde(default)]
1562 placement_profile: Option<String>,
1563 #[serde(default)]
1564 thp: TransparentHugePagePolicy,
1565 }
1566
1567 let raw = RawResources::deserialize(deserializer)?;
1568 Ok(Self {
1569 cpus: raw.cpus,
1570 memory_mib: raw.memory_mib,
1571 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1575 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1576 cpu_placement: raw.cpu_placement,
1577 placement_profile: raw.placement_profile,
1578 thp: raw.thp,
1579 })
1580 }
1581}
1582
1583impl CpuPlacement {
1584 pub const fn is_inherit(&self) -> bool {
1586 matches!(self, Self::Inherit)
1587 }
1588}
1589
1590impl std::fmt::Display for CpuPlacement {
1591 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1592 f.write_str(match self {
1593 Self::Inherit => "inherit",
1594 Self::Auto => "auto",
1595 Self::Spread => "spread",
1596 Self::Compact => "compact",
1597 })
1598 }
1599}
1600
1601impl FromStr for CpuPlacement {
1602 type Err = String;
1603
1604 fn from_str(value: &str) -> Result<Self, Self::Err> {
1605 match value {
1606 "inherit" => Ok(Self::Inherit),
1607 "auto" => Ok(Self::Auto),
1608 "spread" => Ok(Self::Spread),
1609 "compact" => Ok(Self::Compact),
1610 _ => Err(format!(
1611 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1612 )),
1613 }
1614 }
1615}
1616
1617impl Default for SandboxRuntimeOptions {
1618 fn default() -> Self {
1619 Self {
1620 workdir: None,
1621 shell: None,
1622 scripts: BTreeMap::new(),
1623 entrypoint: None,
1624 cmd: None,
1625 hostname: None,
1626 user: None,
1627 log_level: None,
1628 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1629 disable_metrics_sample: false,
1630 }
1631 }
1632}
1633
1634impl Default for NetworkSpec {
1635 fn default() -> Self {
1636 Self {
1637 enabled: true,
1638 interface: None,
1639 ports: Vec::new(),
1640 policy: None,
1641 dns: None,
1642 tls: None,
1643 secrets: None,
1644 max_connections: None,
1645 rate_limiter: None,
1646 trust_host_cas: false,
1647 }
1648 }
1649}
1650
1651impl Default for PublishedPortSpec {
1652 fn default() -> Self {
1653 Self {
1654 host_port: 0,
1655 guest_port: 0,
1656 protocol: PortProtocol::Tcp,
1657 host_bind: "127.0.0.1".into(),
1658 }
1659 }
1660}
1661
1662impl From<(String, String)> for EnvVar {
1663 fn from((key, value): (String, String)) -> Self {
1664 Self { key, value }
1665 }
1666}
1667
1668impl From<EnvVar> for (String, String) {
1669 fn from(var: EnvVar) -> Self {
1670 (var.key, var.value)
1671 }
1672}
1673
1674impl FromStr for SandboxLogLevel {
1675 type Err = String;
1676
1677 fn from_str(s: &str) -> Result<Self, Self::Err> {
1678 match s {
1679 "error" => Ok(Self::Error),
1680 "warn" => Ok(Self::Warn),
1681 "info" => Ok(Self::Info),
1682 "debug" => Ok(Self::Debug),
1683 "trace" => Ok(Self::Trace),
1684 _ => Err(format!("unknown sandbox log level: {s}")),
1685 }
1686 }
1687}
1688
1689impl Serialize for VolumeMount {
1690 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1691 use serde::ser::SerializeMap;
1692
1693 match self {
1694 Self::Bind {
1695 host,
1696 guest,
1697 options,
1698 stat_virtualization,
1699 host_permissions,
1700 follow_root_symlinks,
1701 quota_mib,
1702 } => {
1703 let mut map = serializer.serialize_map(Some(8))?;
1704 map.serialize_entry("type", "Bind")?;
1705 map.serialize_entry("host", host)?;
1706 map.serialize_entry("guest", guest)?;
1707 map.serialize_entry("options", options)?;
1708 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1709 map.serialize_entry("host_permissions", host_permissions)?;
1710 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1711 map.serialize_entry("quota_mib", quota_mib)?;
1712 map.end()
1713 }
1714 Self::Named {
1715 name,
1716 guest,
1717 create: _,
1718 options,
1719 stat_virtualization,
1720 host_permissions,
1721 follow_root_symlinks,
1722 } => {
1723 let mut map = serializer.serialize_map(Some(7))?;
1724 map.serialize_entry("type", "Named")?;
1725 map.serialize_entry("name", name)?;
1726 map.serialize_entry("guest", guest)?;
1727 map.serialize_entry("options", options)?;
1728 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1729 map.serialize_entry("host_permissions", host_permissions)?;
1730 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1731 map.end()
1732 }
1733 Self::Tmpfs {
1734 guest,
1735 size_mib,
1736 options,
1737 } => {
1738 let mut map = serializer.serialize_map(Some(4))?;
1739 map.serialize_entry("type", "Tmpfs")?;
1740 map.serialize_entry("guest", guest)?;
1741 map.serialize_entry("size_mib", size_mib)?;
1742 map.serialize_entry("options", options)?;
1743 map.end()
1744 }
1745 Self::DiskImage {
1746 host,
1747 guest,
1748 format,
1749 fstype,
1750 options,
1751 } => {
1752 let mut map = serializer.serialize_map(Some(6))?;
1753 map.serialize_entry("type", "DiskImage")?;
1754 map.serialize_entry("host", host)?;
1755 map.serialize_entry("guest", guest)?;
1756 map.serialize_entry("format", format)?;
1757 map.serialize_entry("fstype", fstype)?;
1758 map.serialize_entry("options", options)?;
1759 map.end()
1760 }
1761 }
1762 }
1763}
1764
1765impl<'de> Deserialize<'de> for VolumeMount {
1766 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1767 fn default_strict() -> StatVirtualization {
1768 StatVirtualization::Strict
1769 }
1770
1771 fn default_private() -> HostPermissions {
1772 HostPermissions::Private
1773 }
1774
1775 #[derive(Deserialize)]
1776 #[serde(tag = "type")]
1777 enum VolumeMountHelper {
1778 Bind {
1779 host: PathBuf,
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 #[serde(default)]
1792 quota_mib: Option<u32>,
1793 },
1794 Named {
1795 name: String,
1796 guest: String,
1797 #[serde(default)]
1798 options: Option<MountOptions>,
1799 #[serde(default)]
1800 readonly: bool,
1801 #[serde(default = "default_strict")]
1802 stat_virtualization: StatVirtualization,
1803 #[serde(default = "default_private")]
1804 host_permissions: HostPermissions,
1805 #[serde(default)]
1806 follow_root_symlinks: bool,
1807 },
1808 Tmpfs {
1809 guest: String,
1810 #[serde(default)]
1811 size_mib: Option<u32>,
1812 #[serde(default)]
1813 options: Option<MountOptions>,
1814 #[serde(default)]
1815 readonly: bool,
1816 },
1817 DiskImage {
1818 host: PathBuf,
1819 guest: String,
1820 format: DiskImageFormat,
1821 #[serde(default)]
1822 fstype: Option<String>,
1823 #[serde(default)]
1824 options: Option<MountOptions>,
1825 #[serde(default)]
1826 readonly: bool,
1827 },
1828 }
1829
1830 let helper = VolumeMountHelper::deserialize(deserializer)?;
1831 Ok(match helper {
1832 VolumeMountHelper::Bind {
1833 host,
1834 guest,
1835 options,
1836 readonly,
1837 stat_virtualization,
1838 host_permissions,
1839 follow_root_symlinks,
1840 quota_mib,
1841 } => Self::Bind {
1842 host,
1843 guest,
1844 options: decode_mount_options(options, readonly),
1845 stat_virtualization,
1846 host_permissions,
1847 follow_root_symlinks,
1848 quota_mib,
1849 },
1850 VolumeMountHelper::Named {
1851 name,
1852 guest,
1853 options,
1854 readonly,
1855 stat_virtualization,
1856 host_permissions,
1857 follow_root_symlinks,
1858 } => Self::Named {
1859 name,
1860 guest,
1861 create: None,
1862 options: decode_mount_options(options, readonly),
1863 stat_virtualization,
1864 host_permissions,
1865 follow_root_symlinks,
1866 },
1867 VolumeMountHelper::Tmpfs {
1868 guest,
1869 size_mib,
1870 options,
1871 readonly,
1872 } => Self::Tmpfs {
1873 guest,
1874 size_mib,
1875 options: decode_mount_options(options, readonly),
1876 },
1877 VolumeMountHelper::DiskImage {
1878 host,
1879 guest,
1880 format,
1881 fstype,
1882 options,
1883 readonly,
1884 } => Self::DiskImage {
1885 host,
1886 guest,
1887 format,
1888 fstype,
1889 options: decode_mount_options(options, readonly),
1890 },
1891 })
1892 }
1893}
1894
1895impl fmt::Debug for VolumeMount {
1896 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1897 match self {
1898 Self::Bind {
1899 host,
1900 guest,
1901 options,
1902 stat_virtualization,
1903 host_permissions,
1904 follow_root_symlinks,
1905 quota_mib,
1906 } => f
1907 .debug_struct("Bind")
1908 .field("host", host)
1909 .field("guest", guest)
1910 .field("options", options)
1911 .field("stat_virtualization", stat_virtualization)
1912 .field("host_permissions", host_permissions)
1913 .field("follow_root_symlinks", follow_root_symlinks)
1914 .field("quota_mib", quota_mib)
1915 .finish(),
1916 Self::Named {
1917 name,
1918 guest,
1919 create,
1920 options,
1921 stat_virtualization,
1922 host_permissions,
1923 follow_root_symlinks,
1924 } => f
1925 .debug_struct("Named")
1926 .field("name", name)
1927 .field("guest", guest)
1928 .field("create", create)
1929 .field("options", options)
1930 .field("stat_virtualization", stat_virtualization)
1931 .field("host_permissions", host_permissions)
1932 .field("follow_root_symlinks", follow_root_symlinks)
1933 .finish(),
1934 Self::Tmpfs {
1935 guest,
1936 size_mib,
1937 options,
1938 } => f
1939 .debug_struct("Tmpfs")
1940 .field("guest", guest)
1941 .field("size_mib", size_mib)
1942 .field("options", options)
1943 .finish(),
1944 Self::DiskImage {
1945 host,
1946 guest,
1947 format,
1948 fstype,
1949 options,
1950 } => f
1951 .debug_struct("DiskImage")
1952 .field("host", host)
1953 .field("guest", guest)
1954 .field("format", format)
1955 .field("fstype", fstype)
1956 .field("options", options)
1957 .finish(),
1958 }
1959 }
1960}
1961
1962impl TryFrom<&str> for RlimitResource {
1964 type Error = String;
1965
1966 fn try_from(s: &str) -> Result<Self, Self::Error> {
1967 match s.to_ascii_lowercase().as_str() {
1968 "cpu" => Ok(Self::Cpu),
1969 "fsize" => Ok(Self::Fsize),
1970 "data" => Ok(Self::Data),
1971 "stack" => Ok(Self::Stack),
1972 "core" => Ok(Self::Core),
1973 "rss" => Ok(Self::Rss),
1974 "nproc" => Ok(Self::Nproc),
1975 "nofile" => Ok(Self::Nofile),
1976 "memlock" => Ok(Self::Memlock),
1977 "as" => Ok(Self::As),
1978 "locks" => Ok(Self::Locks),
1979 "sigpending" => Ok(Self::Sigpending),
1980 "msgqueue" => Ok(Self::Msgqueue),
1981 "nice" => Ok(Self::Nice),
1982 "rtprio" => Ok(Self::Rtprio),
1983 "rttime" => Ok(Self::Rttime),
1984 _ => Err(format!("unknown rlimit resource: {s}")),
1985 }
1986 }
1987}
1988
1989fn default_sandbox_cpus() -> u8 {
1994 DEFAULT_SANDBOX_CPUS
1995}
1996
1997fn default_sandbox_memory_mib() -> u32 {
1998 DEFAULT_SANDBOX_MEMORY_MIB
1999}
2000
2001fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2002 options.unwrap_or(MountOptions {
2003 readonly,
2004 ..MountOptions::default()
2005 })
2006}
2007
2008pub(crate) fn default_strict() -> StatVirtualization {
2010 StatVirtualization::Strict
2011}
2012
2013pub(crate) fn default_private() -> HostPermissions {
2015 HostPermissions::Private
2016}
2017
2018pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2020
2021#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2028#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2029#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2030pub struct SecretsConfig {
2031 #[serde(default)]
2033 pub secrets: Vec<SecretEntry>,
2034
2035 #[serde(default)]
2037 pub on_violation: ViolationAction,
2038}
2039
2040#[derive(Clone, Serialize, Deserialize)]
2045#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2046#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2047pub struct SecretEntry {
2048 pub env_var: String,
2054
2055 #[serde(default = "empty_secret_value")]
2064 #[cfg_attr(feature = "ts", ts(type = "string"))]
2065 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2066 pub value: Zeroizing<String>,
2067
2068 #[serde(default, skip_serializing_if = "Option::is_none")]
2072 pub source: Option<SecretSource>,
2073
2074 pub placeholder: String,
2079
2080 #[serde(default)]
2082 pub allowed_hosts: Vec<HostPattern>,
2083
2084 #[serde(default)]
2086 pub injection: SecretInjection,
2087
2088 #[serde(default, skip_serializing_if = "Option::is_none")]
2090 pub on_violation: Option<ViolationAction>,
2091
2092 #[serde(default = "default_true")]
2097 pub require_tls_identity: bool,
2098}
2099
2100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2102#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2103#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2104#[serde(rename_all = "kebab-case")]
2105pub enum HostPattern {
2106 #[serde(alias = "Exact")]
2108 Exact(String),
2109 #[serde(alias = "Wildcard")]
2111 Wildcard(String),
2112 #[serde(alias = "Any")]
2114 Any,
2115}
2116
2117#[derive(Debug, Clone, Serialize, Deserialize)]
2119#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2120#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2121pub struct SecretInjection {
2122 #[serde(default = "default_true")]
2124 pub headers: bool,
2125
2126 #[serde(default = "default_true")]
2128 pub basic_auth: bool,
2129
2130 #[serde(default)]
2132 pub query_params: bool,
2133
2134 #[serde(default)]
2142 pub body: bool,
2143}
2144
2145#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2147#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2148#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2149#[serde(rename_all = "kebab-case")]
2150pub enum ViolationAction {
2151 #[serde(alias = "Block")]
2153 Block,
2154 #[default]
2156 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2157 BlockAndLog,
2158 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2160 BlockAndTerminate,
2161 #[serde(alias = "Passthrough")]
2163 Passthrough(Vec<HostPattern>),
2164}
2165
2166#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2168pub enum SecretConfigError {
2169 #[error("secret #{secret_index}: env_var must not be empty")]
2171 EmptyEnvVar {
2172 secret_index: usize,
2174 },
2175
2176 #[error("secret #{secret_index}: env_var must not contain `=`")]
2178 EnvVarContainsEquals {
2179 secret_index: usize,
2181 },
2182
2183 #[error("secret #{secret_index}: env_var must not contain NUL")]
2185 EnvVarContainsNul {
2186 secret_index: usize,
2188 },
2189
2190 #[error("secret #{secret_index}: at least one allowed host is required")]
2192 MissingAllowedHosts {
2193 secret_index: usize,
2195 },
2196
2197 #[error("secret #{secret_index}: placeholder must not be empty")]
2199 EmptyPlaceholder {
2200 secret_index: usize,
2202 },
2203
2204 #[error(
2206 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2207 )]
2208 PlaceholderTooLong {
2209 secret_index: usize,
2211 actual_bytes: usize,
2213 max_bytes: usize,
2215 },
2216
2217 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2219 PlaceholderContainsNul {
2220 secret_index: usize,
2222 },
2223
2224 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2226 PlaceholderContainsLineBreak {
2227 secret_index: usize,
2229 },
2230}
2231
2232impl SecretsConfig {
2233 pub fn validate(&self) -> Result<(), SecretConfigError> {
2235 for (index, secret) in self.secrets.iter().enumerate() {
2236 secret.validate(index)?;
2237 }
2238 Ok(())
2239 }
2240}
2241
2242impl SecretEntry {
2243 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2245 validate_env_var(&self.env_var, secret_index)?;
2246
2247 if self.allowed_hosts.is_empty() {
2248 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2249 }
2250
2251 validate_placeholder(&self.placeholder, secret_index)
2252 }
2253}
2254
2255impl fmt::Debug for SecretEntry {
2257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2258 f.debug_struct("SecretEntry")
2259 .field("env_var", &self.env_var)
2260 .field("value", &"[REDACTED]")
2261 .field("source", &self.source)
2262 .field("placeholder", &self.placeholder)
2263 .field("allowed_hosts", &self.allowed_hosts)
2264 .field("injection", &self.injection)
2265 .field("on_violation", &self.on_violation)
2266 .field("require_tls_identity", &self.require_tls_identity)
2267 .finish()
2268 }
2269}
2270
2271impl HostPattern {
2272 pub fn parse(host: &str) -> Self {
2275 if host == "*" {
2276 HostPattern::Any
2277 } else if host.starts_with("*.") {
2278 HostPattern::Wildcard(host.to_string())
2279 } else {
2280 HostPattern::Exact(host.to_string())
2281 }
2282 }
2283
2284 pub fn matches(&self, hostname: &str) -> bool {
2289 match self {
2290 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2291 HostPattern::Wildcard(pattern) => {
2292 if let Some(suffix) = pattern.strip_prefix("*.") {
2293 hostname.eq_ignore_ascii_case(suffix)
2294 || (hostname.len() > suffix.len() + 1
2295 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2296 && hostname[hostname.len() - suffix.len()..]
2297 .eq_ignore_ascii_case(suffix))
2298 } else {
2299 hostname.eq_ignore_ascii_case(pattern)
2300 }
2301 }
2302 HostPattern::Any => true,
2303 }
2304 }
2305}
2306
2307impl Default for SecretInjection {
2308 fn default() -> Self {
2309 Self {
2310 headers: true,
2311 basic_auth: true,
2312 query_params: false,
2313 body: false,
2314 }
2315 }
2316}
2317
2318fn default_true() -> bool {
2319 true
2320}
2321
2322fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2323 if env_var.is_empty() {
2324 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2325 }
2326 if env_var.contains('=') {
2327 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2328 }
2329 if env_var.contains('\0') {
2330 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2331 }
2332 Ok(())
2333}
2334
2335fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2336 if placeholder.is_empty() {
2337 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2338 }
2339
2340 let actual_bytes = placeholder.len();
2341 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2342 return Err(SecretConfigError::PlaceholderTooLong {
2343 secret_index,
2344 actual_bytes,
2345 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2346 });
2347 }
2348
2349 if placeholder.contains('\0') {
2350 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2351 }
2352 if placeholder.contains('\r') || placeholder.contains('\n') {
2353 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2354 }
2355
2356 Ok(())
2357}
2358
2359#[derive(Debug, Clone, Serialize, Deserialize)]
2369#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2370#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2371pub struct TlsConfig {
2372 #[serde(default)]
2374 pub enabled: bool,
2375
2376 #[serde(default = "default_intercepted_ports")]
2378 pub intercepted_ports: Vec<u16>,
2379
2380 #[serde(default)]
2382 pub bypass: Vec<String>,
2383
2384 #[serde(default = "default_true")]
2386 pub verify_upstream: bool,
2387
2388 #[serde(default = "default_true")]
2391 pub block_quic_on_intercept: bool,
2392
2393 #[serde(default)]
2395 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2396 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2397 pub upstream_ca_cert: Vec<PathBuf>,
2398
2399 #[serde(default, alias = "scoped_upstream_ca_certs")]
2401 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2402
2403 #[serde(default)]
2405 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2406
2407 #[serde(default, alias = "ca")]
2410 pub intercept_ca: InterceptCaConfig,
2411
2412 #[serde(default)]
2414 pub cache: CertCacheConfig,
2415}
2416
2417#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2419#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2420#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2421pub struct InterceptCaConfig {
2422 #[serde(default)]
2425 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2426 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2427 pub cert_path: Option<PathBuf>,
2428
2429 #[serde(default)]
2432 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2433 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2434 pub key_path: Option<PathBuf>,
2435}
2436
2437#[derive(Debug, Clone, Serialize, Deserialize)]
2439#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2440#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2441pub struct CertCacheConfig {
2442 #[serde(default = "default_cache_capacity")]
2444 pub capacity: usize,
2445
2446 #[serde(default = "default_cert_validity_hours")]
2448 pub validity_hours: u64,
2449}
2450
2451#[derive(Debug, Clone, Serialize, Deserialize)]
2453#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2454#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2455pub struct ScopedUpstreamCaCert {
2456 pub pattern: String,
2458
2459 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2461 #[cfg_attr(feature = "ts", ts(type = "string"))]
2462 pub path: PathBuf,
2463}
2464
2465#[derive(Debug, Clone, Serialize, Deserialize)]
2467#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2468#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2469pub struct ScopedVerifyUpstream {
2470 pub pattern: String,
2472
2473 pub verify: bool,
2475}
2476
2477impl Default for TlsConfig {
2478 fn default() -> Self {
2479 Self {
2480 enabled: false,
2481 intercepted_ports: default_intercepted_ports(),
2482 bypass: Vec::new(),
2483 verify_upstream: true,
2484 block_quic_on_intercept: true,
2485 upstream_ca_cert: Vec::new(),
2486 scoped_upstream_ca_cert: Vec::new(),
2487 scoped_verify_upstream: Vec::new(),
2488 intercept_ca: InterceptCaConfig::default(),
2489 cache: CertCacheConfig::default(),
2490 }
2491 }
2492}
2493
2494impl Default for CertCacheConfig {
2495 fn default() -> Self {
2496 Self {
2497 capacity: default_cache_capacity(),
2498 validity_hours: default_cert_validity_hours(),
2499 }
2500 }
2501}
2502
2503fn default_intercepted_ports() -> Vec<u16> {
2504 vec![443]
2505}
2506
2507fn default_cache_capacity() -> usize {
2508 1000
2509}
2510
2511fn default_cert_validity_hours() -> u64 {
2512 24
2513}
2514
2515#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2521#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2522#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2523#[serde(rename_all = "snake_case")]
2524pub enum Action {
2525 Allow,
2527 Deny,
2529}
2530
2531#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2533#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2534#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2535#[serde(rename_all = "snake_case")]
2536pub enum Direction {
2537 Egress,
2539 Ingress,
2541 Any,
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 Protocol {
2551 Tcp,
2553 Udp,
2555 Icmpv4,
2557 Icmpv6,
2559}
2560
2561#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2563#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2564#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2565#[serde(rename_all = "snake_case")]
2566pub enum DestinationGroup {
2567 Public,
2569 Loopback,
2571 Private,
2573 LinkLocal,
2575 Metadata,
2577 Multicast,
2579 Host,
2581}
2582
2583#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2590#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2591#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2592#[serde(rename_all = "snake_case")]
2593pub enum Destination {
2594 Any,
2596 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2598 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2599 Domain(String),
2601 DomainSuffix(String),
2603 Group(DestinationGroup),
2605}
2606
2607#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2609#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2610#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2611pub struct PortRange {
2612 pub start: u16,
2614 pub end: u16,
2616}
2617
2618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2621#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2622#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2623pub struct Rule {
2624 pub direction: Direction,
2626 pub destination: Destination,
2628 #[serde(default)]
2630 pub protocols: Vec<Protocol>,
2631 #[serde(default)]
2633 pub ports: Vec<PortRange>,
2634 pub action: Action,
2636}
2637
2638#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2641#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2642#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2643pub struct NetworkPolicy {
2644 #[serde(default = "action_deny")]
2646 pub default_egress: Action,
2647 #[serde(default = "action_deny")]
2649 pub default_ingress: Action,
2650 #[serde(default)]
2652 pub rules: Vec<Rule>,
2653}
2654
2655fn action_deny() -> Action {
2658 Action::Deny
2659}
2660
2661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2667#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2668#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2669#[serde(default)]
2670pub struct DnsConfig {
2671 pub rebind_protection: bool,
2673 pub nameservers: Vec<String>,
2676 pub query_timeout_ms: u64,
2678}
2679
2680impl Default for DnsConfig {
2681 fn default() -> Self {
2682 Self {
2683 rebind_protection: true,
2684 nameservers: Vec::new(),
2685 query_timeout_ms: 5000,
2686 }
2687 }
2688}
2689
2690#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2694#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2695#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2696#[serde(default)]
2697pub struct InterfaceOverrides {
2698 #[serde(skip_serializing_if = "Option::is_none")]
2700 pub mac: Option<[u8; 6]>,
2701 #[serde(skip_serializing_if = "Option::is_none")]
2703 pub mtu: Option<u16>,
2704 #[serde(skip_serializing_if = "Option::is_none")]
2706 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2707 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2708 pub ipv4_address: Option<Ipv4Addr>,
2709 #[serde(skip_serializing_if = "Option::is_none")]
2711 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2712 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2713 pub ipv4_pool: Option<Ipv4Network>,
2714 #[serde(skip_serializing_if = "Option::is_none")]
2716 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2717 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2718 pub ipv6_address: Option<Ipv6Addr>,
2719 #[serde(skip_serializing_if = "Option::is_none")]
2721 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2722 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2723 pub ipv6_pool: Option<Ipv6Network>,
2724}
2725
2726fn empty_secret_value() -> Zeroizing<String> {
2727 Zeroizing::new(String::new())
2728}
2729
2730#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2736pub enum NetworkRateLimitDirection {
2737 Egress,
2739 Ingress,
2741}
2742
2743#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2745#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2746#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2747#[serde(default)]
2748pub struct NetworkRateLimiterConfig {
2749 #[serde(skip_serializing_if = "Option::is_none")]
2751 pub egress: Option<RateLimiterConfig>,
2752
2753 #[serde(skip_serializing_if = "Option::is_none")]
2755 pub ingress: Option<RateLimiterConfig>,
2756}
2757
2758#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2764#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2765#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2766#[serde(default)]
2767pub struct RateLimiterConfig {
2768 #[serde(skip_serializing_if = "Option::is_none")]
2770 pub bandwidth: Option<TokenBucketConfig>,
2771
2772 #[serde(skip_serializing_if = "Option::is_none")]
2774 pub ops: Option<TokenBucketConfig>,
2775}
2776
2777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2783#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2784#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2785pub struct TokenBucketConfig {
2786 pub size: u64,
2788
2789 pub refill_time_ms: u64,
2792
2793 #[serde(default)]
2795 pub one_time_burst: u64,
2796}
2797
2798#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2800pub enum RateLimitConfigError {
2801 #[error("rate limiter must configure at least one of bandwidth or ops")]
2803 EmptyLimiter,
2804
2805 #[error("{bucket} bucket: size must be greater than zero")]
2807 ZeroSize {
2808 bucket: &'static str,
2810 },
2811
2812 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
2814 ZeroRefillTime {
2815 bucket: &'static str,
2817 },
2818}
2819
2820impl RateLimiterConfig {
2821 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
2823 if self.bandwidth.is_none() && self.ops.is_none() {
2824 return Err(RateLimitConfigError::EmptyLimiter);
2825 }
2826 if let Some(bandwidth) = &self.bandwidth {
2827 bandwidth.validate("bandwidth")?;
2828 }
2829 if let Some(ops) = &self.ops {
2830 ops.validate("ops")?;
2831 }
2832 Ok(())
2833 }
2834}
2835
2836impl TokenBucketConfig {
2837 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
2839 if self.size == 0 {
2840 return Err(RateLimitConfigError::ZeroSize { bucket });
2841 }
2842 if self.refill_time_ms == 0 {
2843 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
2844 }
2845 Ok(())
2846 }
2847}
2848
2849impl fmt::Display for NetworkRateLimitDirection {
2850 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2851 match self {
2852 Self::Egress => f.write_str("egress"),
2853 Self::Ingress => f.write_str("ingress"),
2854 }
2855 }
2856}
2857
2858#[cfg(test)]
2863mod tests {
2864 use super::*;
2865
2866 fn tmpfs_mount(guest: &str) -> VolumeMount {
2867 VolumeMount::Tmpfs {
2868 guest: guest.to_owned(),
2869 size_mib: None,
2870 options: MountOptions::default(),
2871 }
2872 }
2873
2874 #[test]
2875 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
2876 let value = serde_json::to_value(MountOptions::default()).unwrap();
2877 assert!(value.get("override_uid").is_none());
2878 assert!(value.get("override_gid").is_none());
2879
2880 let decoded: MountOptions = serde_json::from_value(value).unwrap();
2881 assert_eq!(decoded.override_uid, None);
2882 assert_eq!(decoded.override_gid, None);
2883 }
2884
2885 #[test]
2886 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
2887 let mut mounts = vec![
2888 tmpfs_mount("/workspace//persist/./logs/"),
2889 tmpfs_mount("/alpha/z"),
2890 tmpfs_mount("/workspace"),
2891 ];
2892
2893 canonicalize_volume_mounts(&mut mounts).unwrap();
2894
2895 assert_eq!(
2896 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
2897 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
2898 );
2899 }
2900
2901 #[test]
2902 fn volume_mounts_reject_duplicate_canonical_paths() {
2903 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
2904
2905 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
2906
2907 assert!(error.to_string().contains("same guest path: /data/cache"));
2908 }
2909
2910 #[test]
2911 fn volume_mounts_reject_parent_components_before_normalizing() {
2912 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
2913
2914 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
2915
2916 assert!(error.to_string().contains("must not contain '..'"));
2917 }
2918
2919 #[test]
2920 fn disk_image_format_from_extension() {
2921 assert_eq!(
2922 DiskImageFormat::from_extension("qcow2"),
2923 Some(DiskImageFormat::Qcow2)
2924 );
2925 assert_eq!(
2926 DiskImageFormat::from_extension("raw"),
2927 Some(DiskImageFormat::Raw)
2928 );
2929 assert_eq!(
2930 DiskImageFormat::from_extension("vmdk"),
2931 Some(DiskImageFormat::Vmdk)
2932 );
2933 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
2934 assert_eq!(DiskImageFormat::from_extension(""), None);
2935 }
2936
2937 #[test]
2938 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
2939 let resources: SandboxResources =
2940 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
2941
2942 assert_eq!(resources.cpus, 4);
2943 assert_eq!(resources.max_cpus, 4);
2944 assert_eq!(resources.memory_mib, 2048);
2945 assert_eq!(resources.max_memory_mib, 2048);
2946 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
2947 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
2948 assert_eq!(
2949 serde_json::to_value(resources).unwrap(),
2950 serde_json::json!({
2951 "cpus": 4,
2952 "memory_mib": 2048,
2953 "max_cpus": 4,
2954 "max_memory_mib": 2048
2955 })
2956 );
2957 }
2958
2959 #[test]
2960 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
2961 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
2962 assert!(inherited.get("cpu_placement").is_none());
2963
2964 for policy in [
2965 CpuPlacement::Auto,
2966 CpuPlacement::Spread,
2967 CpuPlacement::Compact,
2968 ] {
2969 let resources = SandboxResources {
2970 cpu_placement: policy,
2971 ..Default::default()
2972 };
2973 let json = serde_json::to_string(&resources).unwrap();
2974 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
2975
2976 assert_eq!(decoded.cpu_placement, policy);
2977 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
2978 }
2979 }
2980
2981 #[test]
2982 fn transparent_huge_page_policy_roundtrips_non_default() {
2983 let resources: SandboxResources = serde_json::from_str(
2984 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
2985 )
2986 .unwrap();
2987
2988 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
2989 assert_eq!(
2990 serde_json::to_value(resources).unwrap()["thp"],
2991 serde_json::json!("always")
2992 );
2993 assert_eq!(
2994 "never".parse::<TransparentHugePagePolicy>().unwrap(),
2995 TransparentHugePagePolicy::Never
2996 );
2997 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
2998 }
2999
3000 #[test]
3001 fn disk_image_format_display_roundtrip() {
3002 for format in [
3003 DiskImageFormat::Qcow2,
3004 DiskImageFormat::Raw,
3005 DiskImageFormat::Vmdk,
3006 ] {
3007 let rendered = format.to_string();
3008 let parsed: DiskImageFormat = rendered.parse().unwrap();
3009 assert_eq!(parsed, format);
3010 }
3011 }
3012
3013 #[test]
3014 fn disk_image_format_from_str_unknown() {
3015 assert!("ext4".parse::<DiskImageFormat>().is_err());
3016 }
3017
3018 #[test]
3019 fn log_source_effective_uses_default_user_program_sources() {
3020 assert_eq!(
3021 LogSource::effective(&[]),
3022 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3023 );
3024 }
3025
3026 #[test]
3027 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3028 assert_eq!(
3029 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3030 vec![LogSource::Stdout, LogSource::System]
3031 );
3032 }
3033
3034 #[test]
3035 fn rlimit_resource_parses_case_insensitively() {
3036 assert_eq!(
3037 RlimitResource::try_from("NOFILE").unwrap(),
3038 RlimitResource::Nofile
3039 );
3040 assert!(RlimitResource::try_from("bogus").is_err());
3041 }
3042
3043 #[test]
3044 fn sandbox_policy_serde_roundtrip() {
3045 let policy = SandboxPolicy {
3046 ephemeral: true,
3047 max_duration_secs: Some(3600),
3048 idle_timeout_secs: Some(120),
3049 };
3050
3051 let json = serde_json::to_string(&policy).unwrap();
3052 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3053
3054 assert!(decoded.ephemeral);
3055 assert_eq!(decoded.max_duration_secs, Some(3600));
3056 assert_eq!(decoded.idle_timeout_secs, Some(120));
3057 }
3058
3059 #[test]
3060 fn sandbox_policy_defaults_to_persistent() {
3061 assert!(!SandboxPolicy::default().ephemeral);
3062 }
3063
3064 #[test]
3065 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3066 let decoded: SandboxPolicy =
3069 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3070 assert!(!decoded.ephemeral);
3071 assert_eq!(decoded.max_duration_secs, Some(60));
3072 }
3073
3074 #[test]
3075 fn sandbox_spec_default_uses_static_resource_defaults() {
3076 let spec = SandboxSpec::default();
3077
3078 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3079 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3080 assert_eq!(
3081 spec.runtime.metrics_sample_interval_ms,
3082 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3083 );
3084 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3085 }
3086
3087 #[test]
3088 fn deployment_profile_uses_stable_snake_case_wire_values() {
3089 assert_eq!(
3090 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3091 r#""multi_tenant""#
3092 );
3093 assert_eq!(
3094 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3095 DeploymentProfile::SingleTenant
3096 );
3097 }
3098
3099 #[test]
3100 fn sandbox_log_level_roundtrips_lowercase_values() {
3101 for (input, expected) in [
3102 ("error", SandboxLogLevel::Error),
3103 ("warn", SandboxLogLevel::Warn),
3104 ("info", SandboxLogLevel::Info),
3105 ("debug", SandboxLogLevel::Debug),
3106 ("trace", SandboxLogLevel::Trace),
3107 ] {
3108 let parsed: SandboxLogLevel = input.parse().unwrap();
3109 assert_eq!(parsed, expected);
3110 assert_eq!(parsed.as_str(), input);
3111 }
3112 }
3113}