use crate::error::DockerError;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[cfg(feature = "deploy")]
use bollard::service::HostConfig;
#[cfg(feature = "deploy")]
use sysinfo::Disks;
#[cfg(feature = "deploy")]
use sysinfo::System;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemRequirements {
pub min_memory_gb: u64,
pub min_disk_gb: u64,
pub min_bandwidth_mbps: u64,
pub required_ports: Vec<u16>,
pub data_directory: String,
pub cpu_limit: Option<f64>, pub memory_limit: Option<String>, pub memory_swap: Option<String>, pub memory_reservation: Option<String>, pub cpu_shares: Option<i64>, pub cpuset_cpus: Option<String>, }
#[cfg(feature = "deploy")]
impl SystemRequirements {
pub fn check(&self) -> Result<(), DockerError> {
let mut sys = System::new_all();
sys.refresh_all();
let total_memory = sys.total_memory() / 1024 / 1024 / 1024; if total_memory < self.min_memory_gb {
return Err(DockerError::ValidationError(format!(
"Insufficient memory: {} GB available, {} GB required",
total_memory, self.min_memory_gb
)));
}
if let Some(limit) = &self.memory_limit {
let limit_bytes = parse_memory_string(limit)?;
let total_bytes = total_memory * 1024 * 1024 * 1024;
if limit_bytes > total_bytes {
return Err(DockerError::ValidationError(format!(
"Memory limit {} exceeds available memory {}GB",
limit, total_memory
)));
}
}
let data_path = Path::new(&self.data_directory);
let disks = Disks::new_with_refreshed_list();
if let Some(disk) = disks
.iter()
.find(|disk| data_path.starts_with(disk.mount_point().to_string_lossy().as_ref()))
{
let available_gb = disk.available_space() / 1024 / 1024 / 1024;
if available_gb < self.min_disk_gb {
return Err(DockerError::ValidationError(format!(
"Insufficient disk space: {} GB available, {} GB required",
available_gb, self.min_disk_gb
)));
}
}
for port in &self.required_ports {
if !is_port_available(*port) {
return Err(DockerError::ValidationError(format!(
"Port {} is already in use",
port
)));
}
}
Ok(())
}
#[must_use]
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
pub fn to_host_config(&self) -> HostConfig {
let mut host_config = HostConfig::default();
if let Some(memory) = &self.memory_limit {
host_config.memory = parse_memory_string(memory).ok().map(|v| v as i64);
}
if let Some(swap) = &self.memory_swap {
host_config.memory_swap = parse_memory_string(swap).ok().map(|v| v as i64);
}
if let Some(reservation) = &self.memory_reservation {
host_config.memory_reservation =
parse_memory_string(reservation).ok().map(|v| v as i64);
}
host_config.cpu_shares = self.cpu_shares;
host_config.cpuset_cpus = self.cpuset_cpus.clone();
if let Some(cpu) = self.cpu_limit {
host_config.nano_cpus = Some((cpu * 1e9) as i64);
}
host_config
}
}
fn is_port_available(port: u16) -> bool {
std::net::TcpListener::bind(("127.0.0.1", port)).is_ok()
}
pub fn parse_memory_string(memory: &str) -> Result<u64, DockerError> {
let len = memory.len();
let (num, unit) = memory.split_at(len - 1);
let base = num.parse::<u64>().map_err(|_| {
DockerError::InvalidResourceLimit(format!("Invalid memory value: {}", memory))
})?;
match unit.to_uppercase().as_str() {
"K" => Ok(base * 1024),
"M" => Ok(base * 1024 * 1024),
"G" => Ok(base * 1024 * 1024 * 1024),
_ => Err(DockerError::InvalidResourceLimit(format!(
"Invalid memory unit: {}",
unit
))),
}
}
#[cfg(test)]
mod tests {
use super::parse_memory_string;
use crate::error::DockerError;
#[test]
fn test_memory_string_parsing() {
assert_eq!(parse_memory_string("512M").unwrap(), 512 * 1024 * 1024);
assert_eq!(parse_memory_string("1G").unwrap(), 1024 * 1024 * 1024);
assert!(parse_memory_string("invalid").is_err());
}
#[test]
fn test_invalid_resource_limits() {
let memory_tests = vec![
("1X", "Invalid memory unit: X"),
("abc", "Invalid memory value: abc"),
("12.5G", "Invalid memory value: 12.5G"),
];
for (input, expected_error) in memory_tests {
let result = parse_memory_string(input);
assert!(matches!(
result,
Err(DockerError::InvalidResourceLimit(msg)) if msg.contains(expected_error)
));
}
}
}