diff --git a/testcontainers/src/core/containers/request.rs b/testcontainers/src/core/containers/request.rs index ddfa1a69..06aa949b 100644 --- a/testcontainers/src/core/containers/request.rs +++ b/testcontainers/src/core/containers/request.rs @@ -6,6 +6,8 @@ use std::{ time::Duration, }; +use bollard_stubs::models::ResourcesUlimits; + use crate::{ core::{ logs::consumer::LogConsumer, mounts::Mount, ports::ContainerPort, ContainerState, @@ -27,6 +29,7 @@ pub struct ContainerRequest { pub(crate) hosts: BTreeMap, pub(crate) mounts: Vec, pub(crate) ports: Option>, + pub(crate) ulimits: Option>, pub(crate) privileged: bool, pub(crate) shm_size: Option, pub(crate) cgroupns_mode: Option, @@ -168,6 +171,7 @@ impl From for ContainerRequest { hosts: BTreeMap::default(), mounts: Vec::new(), ports: None, + ulimits: None, privileged: false, shm_size: None, cgroupns_mode: None, @@ -208,6 +212,7 @@ impl Debug for ContainerRequest { .field("hosts", &self.hosts) .field("mounts", &self.mounts) .field("ports", &self.ports) + .field("ulimits", &self.ulimits) .field("privileged", &self.privileged) .field("shm_size", &self.shm_size) .field("cgroupns_mode", &self.cgroupns_mode) diff --git a/testcontainers/src/core/image/image_ext.rs b/testcontainers/src/core/image/image_ext.rs index 54cc5e8d..67dc9a42 100644 --- a/testcontainers/src/core/image/image_ext.rs +++ b/testcontainers/src/core/image/image_ext.rs @@ -1,5 +1,7 @@ use std::time::Duration; +use bollard_stubs::models::ResourcesUlimits; + use crate::{ core::{logs::consumer::LogConsumer, CgroupnsMode, ContainerPort, Host, Mount, PortMapping}, ContainerRequest, Image, @@ -64,6 +66,16 @@ pub trait ImageExt { fn with_mapped_port(self, host_port: u16, container_port: ContainerPort) -> ContainerRequest; + /// Adds a resource ulimit to the container. + /// + /// # Examples + /// ```rust,no_run + /// use testcontainers::{GenericImage, ImageExt}; + /// + /// let image = GenericImage::new("image", "tag").with_ulimit("nofile", 65536, Some(65536)); + /// ``` + fn with_ulimit(self, name: &str, soft: i64, hard: Option) -> ContainerRequest; + /// Sets the container to run in privileged mode. fn with_privileged(self, privileged: bool) -> ContainerRequest; @@ -168,6 +180,21 @@ impl>, I: Image> ImageExt for RI { } } + fn with_ulimit(self, name: &str, soft: i64, hard: Option) -> ContainerRequest { + let container_req = self.into(); + let mut ulimits = container_req.ulimits.unwrap_or_default(); + ulimits.push(ResourcesUlimits { + name: Some(name.into()), + soft: Some(soft), + hard, + }); + + ContainerRequest { + ulimits: Some(ulimits), + ..container_req + } + } + fn with_privileged(self, privileged: bool) -> ContainerRequest { let container_req = self.into(); ContainerRequest { diff --git a/testcontainers/src/runners/async_runner.rs b/testcontainers/src/runners/async_runner.rs index 50d6c3ba..209aced3 100644 --- a/testcontainers/src/runners/async_runner.rs +++ b/testcontainers/src/runners/async_runner.rs @@ -5,7 +5,7 @@ use bollard::{ container::{Config, CreateContainerOptions}, models::{HostConfig, PortBinding}, }; -use bollard_stubs::models::HostConfigCgroupnsModeEnum; +use bollard_stubs::models::{HostConfigCgroupnsModeEnum, ResourcesUlimits}; use crate::{ core::{ @@ -172,6 +172,23 @@ where }); } + // resource ulimits + if let Some(ulimits) = &container_req.ulimits { + config.host_config = config.host_config.map(|mut host_config| { + host_config.ulimits = Some( + ulimits + .iter() + .map(|ulimit| ResourcesUlimits { + name: ulimit.name.clone(), + soft: ulimit.soft, + hard: ulimit.hard, + }) + .collect(), + ); + host_config + }); + } + let cmd: Vec<_> = container_req.cmd().map(|v| v.to_string()).collect(); if !cmd.is_empty() { config.cmd = Some(cmd); @@ -546,6 +563,27 @@ mod tests { Ok(()) } + #[tokio::test] + async fn async_run_command_should_include_ulimits() -> anyhow::Result<()> { + let image = GenericImage::new("hello-world", "latest"); + let container = image.with_ulimit("nofile", 123, Some(456)).start().await?; + + let client = Client::lazy_client().await?; + let container_details = client.inspect(container.id()).await?; + + let ulimits = container_details + .host_config + .expect("HostConfig") + .ulimits + .expect("Privileged"); + + assert_eq!(ulimits.len(), 1); + assert_eq!(ulimits[0].name, Some("nofile".into())); + assert_eq!(ulimits[0].soft, Some(123)); + assert_eq!(ulimits[0].hard, Some(456)); + Ok(()) + } + #[tokio::test] async fn async_run_command_should_have_host_cgroupns_mode() -> anyhow::Result<()> { let image = GenericImage::new("hello-world", "latest"); diff --git a/testcontainers/src/runners/sync_runner.rs b/testcontainers/src/runners/sync_runner.rs index ea6e5651..9860eef6 100644 --- a/testcontainers/src/runners/sync_runner.rs +++ b/testcontainers/src/runners/sync_runner.rs @@ -285,6 +285,26 @@ mod tests { Ok(()) } + #[test] + fn sync_run_command_should_include_ulimits() -> anyhow::Result<()> { + let image = GenericImage::new("hello-world", "latest"); + let container = image.with_ulimit("nofile", 123, Some(456)).start()?; + + let container_details = inspect(container.id()); + + let ulimits = container_details + .host_config + .expect("HostConfig") + .ulimits + .expect("Privileged"); + + assert_eq!(ulimits.len(), 1); + assert_eq!(ulimits[0].name, Some("nofile".into())); + assert_eq!(ulimits[0].soft, Some(123)); + assert_eq!(ulimits[0].hard, Some(456)); + Ok(()) + } + #[test] fn sync_run_command_should_set_shared_memory_size() -> anyhow::Result<()> { let image = GenericImage::new("hello-world", "latest");