use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const DEFAULT_SANDBOX_CPUS: u8 = 1;
pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum DiskImageFormat {
Qcow2,
Raw,
Vmdk,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum RootfsSource {
Bind(
#[cfg_attr(feature = "ts", ts(type = "string"))]
PathBuf,
),
Oci(OciRootfsSource),
DiskImage {
#[cfg_attr(feature = "ts", ts(type = "string"))]
path: PathBuf,
format: DiskImageFormat,
fstype: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct OciRootfsSource {
pub reference: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upper_size_mib: Option<u32>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum PullPolicy {
#[default]
IfMissing,
Always,
Never,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum StatVirtualization {
Strict,
Relaxed,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum HostPermissions {
Private,
Mirror,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum SecurityProfile {
#[default]
Default,
Restricted,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct MountOptions {
pub readonly: bool,
pub noexec: bool,
pub nosuid: bool,
pub nodev: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum VolumeKind {
Directory,
Disk,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct VolumeSpec {
pub name: String,
pub kind: VolumeKind,
pub quota_mib: Option<u32>,
pub capacity_mib: Option<u32>,
pub labels: Vec<(String, String)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum NamedVolumeMode {
Existing,
Create,
EnsureExists,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct NamedVolumeCreate {
pub mode: NamedVolumeMode,
pub name: String,
pub kind: VolumeKind,
pub quota_mib: Option<u32>,
pub capacity_mib: Option<u32>,
pub labels: Vec<(String, String)>,
}
#[derive(Clone)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts", ts(tag = "type"))]
pub enum VolumeMount {
Bind {
#[cfg_attr(feature = "ts", ts(type = "string"))]
host: PathBuf,
guest: String,
options: MountOptions,
stat_virtualization: StatVirtualization,
host_permissions: HostPermissions,
quota_mib: Option<u32>,
},
Named {
name: String,
guest: String,
create: Option<NamedVolumeCreate>,
options: MountOptions,
stat_virtualization: StatVirtualization,
host_permissions: HostPermissions,
},
Tmpfs {
guest: String,
size_mib: Option<u32>,
options: MountOptions,
},
DiskImage {
#[cfg_attr(feature = "ts", ts(type = "string"))]
host: PathBuf,
guest: String,
format: DiskImageFormat,
fstype: Option<String>,
options: MountOptions,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum Patch {
Text {
path: String,
content: String,
mode: Option<u32>,
replace: bool,
},
File {
path: String,
content: Vec<u8>,
mode: Option<u32>,
replace: bool,
},
CopyFile {
#[cfg_attr(feature = "ts", ts(type = "string"))]
src: PathBuf,
dst: String,
mode: Option<u32>,
replace: bool,
},
CopyDir {
#[cfg_attr(feature = "ts", ts(type = "string"))]
src: PathBuf,
dst: String,
replace: bool,
},
Symlink {
target: String,
link: String,
replace: bool,
},
Mkdir {
path: String,
mode: Option<u32>,
},
Remove {
path: String,
},
Append {
path: String,
content: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct NetworkSpec {
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub interface: Option<Value>,
pub ports: Vec<PublishedPortSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secrets: Option<Value>,
pub max_connections: Option<usize>,
pub trust_host_cas: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct PublishedPortSpec {
pub host_port: u16,
pub guest_port: u16,
#[serde(default)]
pub protocol: PortProtocol,
pub host_bind: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum PortProtocol {
#[default]
#[serde(rename = "tcp")]
Tcp,
#[serde(rename = "udp")]
Udp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct HandoffInit {
#[cfg_attr(feature = "ts", ts(type = "string"))]
pub cmd: PathBuf,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Vec<(String, String)>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct SandboxPolicy {
#[serde(default)]
pub ephemeral: bool,
pub max_duration_secs: Option<u64>,
pub idle_timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum SnapshotDestination {
Name(String),
Path(
#[cfg_attr(feature = "ts", ts(type = "string"))]
PathBuf,
),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct SnapshotSpec {
pub source_sandbox: String,
pub destination: SnapshotDestination,
pub labels: Vec<(String, String)>,
pub force: bool,
pub record_integrity: bool,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct SandboxSpec {
pub name: String,
pub image: RootfsSource,
pub resources: SandboxResources,
pub runtime: SandboxRuntimeOptions,
pub env: Vec<EnvVar>,
pub labels: BTreeMap<String, String>,
pub rlimits: Vec<Rlimit>,
pub mounts: Vec<VolumeMount>,
pub patches: Vec<Patch>,
pub network: NetworkSpec,
pub init: Option<HandoffInit>,
pub pull_policy: PullPolicy,
pub security_profile: SecurityProfile,
pub lifecycle: SandboxPolicy,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct SandboxResources {
pub cpus: u8,
pub memory_mib: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct SandboxRuntimeOptions {
pub workdir: Option<String>,
pub shell: Option<String>,
pub scripts: BTreeMap<String, String>,
pub entrypoint: Option<Vec<String>>,
pub cmd: Option<Vec<String>>,
pub hostname: Option<String>,
pub user: Option<String>,
pub log_level: Option<SandboxLogLevel>,
pub metrics_sample_interval_ms: Option<u64>,
pub disable_metrics_sample: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct EnvVar {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum SandboxLogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub enum RlimitResource {
Cpu,
Fsize,
Data,
Stack,
Core,
Rss,
Nproc,
Nofile,
Memlock,
As,
Locks,
Sigpending,
Msgqueue,
Nice,
Rtprio,
Rttime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Rlimit {
pub resource: RlimitResource,
pub soft: u64,
pub hard: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum LogSource {
Stdout,
Stderr,
Output,
System,
}
impl DiskImageFormat {
pub fn as_str(&self) -> &'static str {
match self {
Self::Qcow2 => "qcow2",
Self::Raw => "raw",
Self::Vmdk => "vmdk",
}
}
pub fn from_extension(ext: &str) -> Option<Self> {
match ext {
"qcow2" => Some(Self::Qcow2),
"raw" => Some(Self::Raw),
"vmdk" => Some(Self::Vmdk),
_ => None,
}
}
}
impl OciRootfsSource {
pub fn new(reference: impl Into<String>) -> Self {
Self {
reference: reference.into(),
upper_size_mib: None,
}
}
}
impl RootfsSource {
pub fn oci(reference: impl Into<String>) -> Self {
Self::Oci(OciRootfsSource::new(reference))
}
pub fn oci_reference(&self) -> Option<&str> {
match self {
Self::Oci(oci) => Some(&oci.reference),
_ => None,
}
}
pub fn oci_upper_size_mib(&self) -> Option<u32> {
match self {
Self::Oci(oci) => oci.upper_size_mib,
_ => None,
}
}
}
impl EnvVar {
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
pub fn as_pair(&self) -> (&str, &str) {
(&self.key, &self.value)
}
}
impl VolumeKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Directory => "dir",
Self::Disk => "disk",
}
}
pub fn from_db_value(value: &str) -> Self {
match value {
"disk" => Self::Disk,
_ => Self::Directory,
}
}
}
impl VolumeSpec {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
kind: VolumeKind::Directory,
quota_mib: None,
capacity_mib: None,
labels: Vec::new(),
}
}
}
impl NamedVolumeCreate {
pub fn mode(&self) -> NamedVolumeMode {
self.mode
}
pub fn name(&self) -> &str {
&self.name
}
pub fn kind(&self) -> VolumeKind {
self.kind
}
pub fn quota_mib(&self) -> Option<u32> {
self.quota_mib
}
pub fn capacity_mib(&self) -> Option<u32> {
self.capacity_mib
}
pub fn labels(&self) -> &[(String, String)] {
&self.labels
}
}
impl VolumeMount {
pub fn guest(&self) -> &str {
match self {
Self::Bind { guest, .. }
| Self::Named { guest, .. }
| Self::Tmpfs { guest, .. }
| Self::DiskImage { guest, .. } => guest,
}
}
pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
match self {
Self::Named { create, .. } => create.as_ref(),
_ => None,
}
}
}
impl RlimitResource {
pub fn as_str(&self) -> &'static str {
match self {
Self::Cpu => "cpu",
Self::Fsize => "fsize",
Self::Data => "data",
Self::Stack => "stack",
Self::Core => "core",
Self::Rss => "rss",
Self::Nproc => "nproc",
Self::Nofile => "nofile",
Self::Memlock => "memlock",
Self::As => "as",
Self::Locks => "locks",
Self::Sigpending => "sigpending",
Self::Msgqueue => "msgqueue",
Self::Nice => "nice",
Self::Rtprio => "rtprio",
Self::Rttime => "rttime",
}
}
}
impl LogSource {
pub fn effective(requested: &[Self]) -> Vec<Self> {
if requested.is_empty() {
vec![Self::Stdout, Self::Stderr, Self::Output]
} else {
let mut sources = requested.to_vec();
sources.sort_by_key(|src| match src {
Self::Stdout => 0,
Self::Stderr => 1,
Self::Output => 2,
Self::System => 3,
});
sources.dedup();
sources
}
}
}
impl SandboxLogLevel {
pub const fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warn => "warn",
Self::Info => "info",
Self::Debug => "debug",
Self::Trace => "trace",
}
}
}
impl std::fmt::Display for DiskImageFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for DiskImageFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"qcow2" => Ok(Self::Qcow2),
"raw" => Ok(Self::Raw),
"vmdk" => Ok(Self::Vmdk),
_ => Err(format!("unknown disk image format: {s}")),
}
}
}
impl Default for RootfsSource {
fn default() -> Self {
Self::oci(String::new())
}
}
impl Default for SandboxResources {
fn default() -> Self {
Self {
cpus: DEFAULT_SANDBOX_CPUS,
memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
}
}
}
impl Default for SandboxRuntimeOptions {
fn default() -> Self {
Self {
workdir: None,
shell: None,
scripts: BTreeMap::new(),
entrypoint: None,
cmd: None,
hostname: None,
user: None,
log_level: None,
metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
disable_metrics_sample: false,
}
}
}
impl Default for NetworkSpec {
fn default() -> Self {
Self {
enabled: true,
interface: None,
ports: Vec::new(),
policy: None,
dns: None,
tls: None,
secrets: None,
max_connections: None,
trust_host_cas: false,
}
}
}
impl Default for PublishedPortSpec {
fn default() -> Self {
Self {
host_port: 0,
guest_port: 0,
protocol: PortProtocol::Tcp,
host_bind: "127.0.0.1".into(),
}
}
}
impl From<(String, String)> for EnvVar {
fn from((key, value): (String, String)) -> Self {
Self { key, value }
}
}
impl From<EnvVar> for (String, String) {
fn from(var: EnvVar) -> Self {
(var.key, var.value)
}
}
impl FromStr for SandboxLogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"error" => Ok(Self::Error),
"warn" => Ok(Self::Warn),
"info" => Ok(Self::Info),
"debug" => Ok(Self::Debug),
"trace" => Ok(Self::Trace),
_ => Err(format!("unknown sandbox log level: {s}")),
}
}
}
impl Serialize for VolumeMount {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Self::Bind {
host,
guest,
options,
stat_virtualization,
host_permissions,
quota_mib,
} => {
let mut map = serializer.serialize_map(Some(7))?;
map.serialize_entry("type", "Bind")?;
map.serialize_entry("host", host)?;
map.serialize_entry("guest", guest)?;
map.serialize_entry("options", options)?;
map.serialize_entry("stat_virtualization", stat_virtualization)?;
map.serialize_entry("host_permissions", host_permissions)?;
map.serialize_entry("quota_mib", quota_mib)?;
map.end()
}
Self::Named {
name,
guest,
create: _,
options,
stat_virtualization,
host_permissions,
} => {
let mut map = serializer.serialize_map(Some(6))?;
map.serialize_entry("type", "Named")?;
map.serialize_entry("name", name)?;
map.serialize_entry("guest", guest)?;
map.serialize_entry("options", options)?;
map.serialize_entry("stat_virtualization", stat_virtualization)?;
map.serialize_entry("host_permissions", host_permissions)?;
map.end()
}
Self::Tmpfs {
guest,
size_mib,
options,
} => {
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry("type", "Tmpfs")?;
map.serialize_entry("guest", guest)?;
map.serialize_entry("size_mib", size_mib)?;
map.serialize_entry("options", options)?;
map.end()
}
Self::DiskImage {
host,
guest,
format,
fstype,
options,
} => {
let mut map = serializer.serialize_map(Some(6))?;
map.serialize_entry("type", "DiskImage")?;
map.serialize_entry("host", host)?;
map.serialize_entry("guest", guest)?;
map.serialize_entry("format", format)?;
map.serialize_entry("fstype", fstype)?;
map.serialize_entry("options", options)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for VolumeMount {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
fn default_strict() -> StatVirtualization {
StatVirtualization::Strict
}
fn default_private() -> HostPermissions {
HostPermissions::Private
}
#[derive(Deserialize)]
#[serde(tag = "type")]
enum VolumeMountHelper {
Bind {
host: PathBuf,
guest: String,
#[serde(default)]
options: Option<MountOptions>,
#[serde(default)]
readonly: bool,
#[serde(default = "default_strict")]
stat_virtualization: StatVirtualization,
#[serde(default = "default_private")]
host_permissions: HostPermissions,
#[serde(default)]
quota_mib: Option<u32>,
},
Named {
name: String,
guest: String,
#[serde(default)]
options: Option<MountOptions>,
#[serde(default)]
readonly: bool,
#[serde(default = "default_strict")]
stat_virtualization: StatVirtualization,
#[serde(default = "default_private")]
host_permissions: HostPermissions,
},
Tmpfs {
guest: String,
#[serde(default)]
size_mib: Option<u32>,
#[serde(default)]
options: Option<MountOptions>,
#[serde(default)]
readonly: bool,
},
DiskImage {
host: PathBuf,
guest: String,
format: DiskImageFormat,
#[serde(default)]
fstype: Option<String>,
#[serde(default)]
options: Option<MountOptions>,
#[serde(default)]
readonly: bool,
},
}
let helper = VolumeMountHelper::deserialize(deserializer)?;
Ok(match helper {
VolumeMountHelper::Bind {
host,
guest,
options,
readonly,
stat_virtualization,
host_permissions,
quota_mib,
} => Self::Bind {
host,
guest,
options: decode_mount_options(options, readonly),
stat_virtualization,
host_permissions,
quota_mib,
},
VolumeMountHelper::Named {
name,
guest,
options,
readonly,
stat_virtualization,
host_permissions,
} => Self::Named {
name,
guest,
create: None,
options: decode_mount_options(options, readonly),
stat_virtualization,
host_permissions,
},
VolumeMountHelper::Tmpfs {
guest,
size_mib,
options,
readonly,
} => Self::Tmpfs {
guest,
size_mib,
options: decode_mount_options(options, readonly),
},
VolumeMountHelper::DiskImage {
host,
guest,
format,
fstype,
options,
readonly,
} => Self::DiskImage {
host,
guest,
format,
fstype,
options: decode_mount_options(options, readonly),
},
})
}
}
impl fmt::Debug for VolumeMount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bind {
host,
guest,
options,
stat_virtualization,
host_permissions,
quota_mib,
} => f
.debug_struct("Bind")
.field("host", host)
.field("guest", guest)
.field("options", options)
.field("stat_virtualization", stat_virtualization)
.field("host_permissions", host_permissions)
.field("quota_mib", quota_mib)
.finish(),
Self::Named {
name,
guest,
create,
options,
stat_virtualization,
host_permissions,
} => f
.debug_struct("Named")
.field("name", name)
.field("guest", guest)
.field("create", create)
.field("options", options)
.field("stat_virtualization", stat_virtualization)
.field("host_permissions", host_permissions)
.finish(),
Self::Tmpfs {
guest,
size_mib,
options,
} => f
.debug_struct("Tmpfs")
.field("guest", guest)
.field("size_mib", size_mib)
.field("options", options)
.finish(),
Self::DiskImage {
host,
guest,
format,
fstype,
options,
} => f
.debug_struct("DiskImage")
.field("host", host)
.field("guest", guest)
.field("format", format)
.field("fstype", fstype)
.field("options", options)
.finish(),
}
}
}
impl TryFrom<&str> for RlimitResource {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s.to_ascii_lowercase().as_str() {
"cpu" => Ok(Self::Cpu),
"fsize" => Ok(Self::Fsize),
"data" => Ok(Self::Data),
"stack" => Ok(Self::Stack),
"core" => Ok(Self::Core),
"rss" => Ok(Self::Rss),
"nproc" => Ok(Self::Nproc),
"nofile" => Ok(Self::Nofile),
"memlock" => Ok(Self::Memlock),
"as" => Ok(Self::As),
"locks" => Ok(Self::Locks),
"sigpending" => Ok(Self::Sigpending),
"msgqueue" => Ok(Self::Msgqueue),
"nice" => Ok(Self::Nice),
"rtprio" => Ok(Self::Rtprio),
"rttime" => Ok(Self::Rttime),
_ => Err(format!("unknown rlimit resource: {s}")),
}
}
}
fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
options.unwrap_or(MountOptions {
readonly,
..MountOptions::default()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disk_image_format_from_extension() {
assert_eq!(
DiskImageFormat::from_extension("qcow2"),
Some(DiskImageFormat::Qcow2)
);
assert_eq!(
DiskImageFormat::from_extension("raw"),
Some(DiskImageFormat::Raw)
);
assert_eq!(
DiskImageFormat::from_extension("vmdk"),
Some(DiskImageFormat::Vmdk)
);
assert_eq!(DiskImageFormat::from_extension("ext4"), None);
assert_eq!(DiskImageFormat::from_extension(""), None);
}
#[test]
fn disk_image_format_display_roundtrip() {
for format in [
DiskImageFormat::Qcow2,
DiskImageFormat::Raw,
DiskImageFormat::Vmdk,
] {
let rendered = format.to_string();
let parsed: DiskImageFormat = rendered.parse().unwrap();
assert_eq!(parsed, format);
}
}
#[test]
fn disk_image_format_from_str_unknown() {
assert!("ext4".parse::<DiskImageFormat>().is_err());
}
#[test]
fn log_source_effective_uses_default_user_program_sources() {
assert_eq!(
LogSource::effective(&[]),
vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
);
}
#[test]
fn log_source_effective_sorts_and_deduplicates_requested_sources() {
assert_eq!(
LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
vec![LogSource::Stdout, LogSource::System]
);
}
#[test]
fn rlimit_resource_parses_case_insensitively() {
assert_eq!(
RlimitResource::try_from("NOFILE").unwrap(),
RlimitResource::Nofile
);
assert!(RlimitResource::try_from("bogus").is_err());
}
#[test]
fn sandbox_policy_serde_roundtrip() {
let policy = SandboxPolicy {
ephemeral: true,
max_duration_secs: Some(3600),
idle_timeout_secs: Some(120),
};
let json = serde_json::to_string(&policy).unwrap();
let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
assert!(decoded.ephemeral);
assert_eq!(decoded.max_duration_secs, Some(3600));
assert_eq!(decoded.idle_timeout_secs, Some(120));
}
#[test]
fn sandbox_policy_defaults_to_persistent() {
assert!(!SandboxPolicy::default().ephemeral);
}
#[test]
fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
let decoded: SandboxPolicy =
serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
assert!(!decoded.ephemeral);
assert_eq!(decoded.max_duration_secs, Some(60));
}
#[test]
fn sandbox_spec_default_uses_static_resource_defaults() {
let spec = SandboxSpec::default();
assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
assert_eq!(
spec.runtime.metrics_sample_interval_ms,
Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
);
}
#[test]
fn sandbox_log_level_roundtrips_lowercase_values() {
for (input, expected) in [
("error", SandboxLogLevel::Error),
("warn", SandboxLogLevel::Warn),
("info", SandboxLogLevel::Info),
("debug", SandboxLogLevel::Debug),
("trace", SandboxLogLevel::Trace),
] {
let parsed: SandboxLogLevel = input.parse().unwrap();
assert_eq!(parsed, expected);
assert_eq!(parsed.as_str(), input);
}
}
}