Skip to content

Commit b10b48f

Browse files
authored
feat: allow configuring resource ulimits (#714)
Thanks again for working on this project! This exposes a way to configure resource ulimits (the [`--ulimit`](https://docs.docker.com/reference/cli/docker/container/run/#ulimit) flag for `docker run`). I'm relying on CI tests because I'm currently on Windows and didn't configure my environment to reflect #711, since it seems I'd need to install nasm now, as I received some errors regarding it.
1 parent f8d4a3d commit b10b48f

File tree

4 files changed

+91
-1
lines changed

4 files changed

+91
-1
lines changed

testcontainers/src/core/containers/request.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::{
66
time::Duration,
77
};
88

9+
use bollard_stubs::models::ResourcesUlimits;
10+
911
use crate::{
1012
core::{
1113
logs::consumer::LogConsumer, mounts::Mount, ports::ContainerPort, ContainerState,
@@ -27,6 +29,7 @@ pub struct ContainerRequest<I: Image> {
2729
pub(crate) hosts: BTreeMap<String, Host>,
2830
pub(crate) mounts: Vec<Mount>,
2931
pub(crate) ports: Option<Vec<PortMapping>>,
32+
pub(crate) ulimits: Option<Vec<ResourcesUlimits>>,
3033
pub(crate) privileged: bool,
3134
pub(crate) shm_size: Option<u64>,
3235
pub(crate) cgroupns_mode: Option<CgroupnsMode>,
@@ -168,6 +171,7 @@ impl<I: Image> From<I> for ContainerRequest<I> {
168171
hosts: BTreeMap::default(),
169172
mounts: Vec::new(),
170173
ports: None,
174+
ulimits: None,
171175
privileged: false,
172176
shm_size: None,
173177
cgroupns_mode: None,
@@ -208,6 +212,7 @@ impl<I: Image + Debug> Debug for ContainerRequest<I> {
208212
.field("hosts", &self.hosts)
209213
.field("mounts", &self.mounts)
210214
.field("ports", &self.ports)
215+
.field("ulimits", &self.ulimits)
211216
.field("privileged", &self.privileged)
212217
.field("shm_size", &self.shm_size)
213218
.field("cgroupns_mode", &self.cgroupns_mode)

testcontainers/src/core/image/image_ext.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::time::Duration;
22

3+
use bollard_stubs::models::ResourcesUlimits;
4+
35
use crate::{
46
core::{logs::consumer::LogConsumer, CgroupnsMode, ContainerPort, Host, Mount, PortMapping},
57
ContainerRequest, Image,
@@ -64,6 +66,16 @@ pub trait ImageExt<I: Image> {
6466
fn with_mapped_port(self, host_port: u16, container_port: ContainerPort)
6567
-> ContainerRequest<I>;
6668

69+
/// Adds a resource ulimit to the container.
70+
///
71+
/// # Examples
72+
/// ```rust,no_run
73+
/// use testcontainers::{GenericImage, ImageExt};
74+
///
75+
/// let image = GenericImage::new("image", "tag").with_ulimit("nofile", 65536, Some(65536));
76+
/// ```
77+
fn with_ulimit(self, name: &str, soft: i64, hard: Option<i64>) -> ContainerRequest<I>;
78+
6779
/// Sets the container to run in privileged mode.
6880
fn with_privileged(self, privileged: bool) -> ContainerRequest<I>;
6981

@@ -168,6 +180,21 @@ impl<RI: Into<ContainerRequest<I>>, I: Image> ImageExt<I> for RI {
168180
}
169181
}
170182

183+
fn with_ulimit(self, name: &str, soft: i64, hard: Option<i64>) -> ContainerRequest<I> {
184+
let container_req = self.into();
185+
let mut ulimits = container_req.ulimits.unwrap_or_default();
186+
ulimits.push(ResourcesUlimits {
187+
name: Some(name.into()),
188+
soft: Some(soft),
189+
hard,
190+
});
191+
192+
ContainerRequest {
193+
ulimits: Some(ulimits),
194+
..container_req
195+
}
196+
}
197+
171198
fn with_privileged(self, privileged: bool) -> ContainerRequest<I> {
172199
let container_req = self.into();
173200
ContainerRequest {

testcontainers/src/runners/async_runner.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bollard::{
55
container::{Config, CreateContainerOptions},
66
models::{HostConfig, PortBinding},
77
};
8-
use bollard_stubs::models::HostConfigCgroupnsModeEnum;
8+
use bollard_stubs::models::{HostConfigCgroupnsModeEnum, ResourcesUlimits};
99

1010
use crate::{
1111
core::{
@@ -172,6 +172,23 @@ where
172172
});
173173
}
174174

175+
// resource ulimits
176+
if let Some(ulimits) = &container_req.ulimits {
177+
config.host_config = config.host_config.map(|mut host_config| {
178+
host_config.ulimits = Some(
179+
ulimits
180+
.iter()
181+
.map(|ulimit| ResourcesUlimits {
182+
name: ulimit.name.clone(),
183+
soft: ulimit.soft,
184+
hard: ulimit.hard,
185+
})
186+
.collect(),
187+
);
188+
host_config
189+
});
190+
}
191+
175192
let cmd: Vec<_> = container_req.cmd().map(|v| v.to_string()).collect();
176193
if !cmd.is_empty() {
177194
config.cmd = Some(cmd);
@@ -546,6 +563,27 @@ mod tests {
546563
Ok(())
547564
}
548565

566+
#[tokio::test]
567+
async fn async_run_command_should_include_ulimits() -> anyhow::Result<()> {
568+
let image = GenericImage::new("hello-world", "latest");
569+
let container = image.with_ulimit("nofile", 123, Some(456)).start().await?;
570+
571+
let client = Client::lazy_client().await?;
572+
let container_details = client.inspect(container.id()).await?;
573+
574+
let ulimits = container_details
575+
.host_config
576+
.expect("HostConfig")
577+
.ulimits
578+
.expect("Privileged");
579+
580+
assert_eq!(ulimits.len(), 1);
581+
assert_eq!(ulimits[0].name, Some("nofile".into()));
582+
assert_eq!(ulimits[0].soft, Some(123));
583+
assert_eq!(ulimits[0].hard, Some(456));
584+
Ok(())
585+
}
586+
549587
#[tokio::test]
550588
async fn async_run_command_should_have_host_cgroupns_mode() -> anyhow::Result<()> {
551589
let image = GenericImage::new("hello-world", "latest");

testcontainers/src/runners/sync_runner.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,26 @@ mod tests {
285285
Ok(())
286286
}
287287

288+
#[test]
289+
fn sync_run_command_should_include_ulimits() -> anyhow::Result<()> {
290+
let image = GenericImage::new("hello-world", "latest");
291+
let container = image.with_ulimit("nofile", 123, Some(456)).start()?;
292+
293+
let container_details = inspect(container.id());
294+
295+
let ulimits = container_details
296+
.host_config
297+
.expect("HostConfig")
298+
.ulimits
299+
.expect("Privileged");
300+
301+
assert_eq!(ulimits.len(), 1);
302+
assert_eq!(ulimits[0].name, Some("nofile".into()));
303+
assert_eq!(ulimits[0].soft, Some(123));
304+
assert_eq!(ulimits[0].hard, Some(456));
305+
Ok(())
306+
}
307+
288308
#[test]
289309
fn sync_run_command_should_set_shared_memory_size() -> anyhow::Result<()> {
290310
let image = GenericImage::new("hello-world", "latest");

0 commit comments

Comments
 (0)