Skip to content

Commit b772702

Browse files
committed
feat: allow customizations of host-config
Addresses the needs of customizing specific docker params without expossing all available options. Initial discussion is here: #890
1 parent 79d110f commit b772702

File tree

5 files changed

+76
-3
lines changed

5 files changed

+76
-3
lines changed

docs/features/configuration.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ Configuration is fetched in the following order:
3232
2. `DOCKER_CONFIG` environment variable, as an alternative path to the directory containing Docker `config.json` file.
3333
3. else it will load the default Docker config file, which lives in the user's home, e.g. `~/.docker/config.json`.
3434

35+
## Advanced container configuration
36+
37+
If you need to tweak Docker `HostConfig` fields that are not exposed by the high-level API, use
38+
`ImageExt::with_host_config_modifier` to apply a single callback just before container creation.
39+
The modifier runs after `testcontainers` fills in its defaults.
40+
41+
```rust
42+
use testcontainers::{GenericImage, ImageExt};
43+
44+
let image = GenericImage::new("testcontainers/helloworld", "1.3.0")
45+
.with_host_config_modifier(|host_config| {
46+
host_config.cpu_period = Some(100_000);
47+
host_config.cpu_quota = Some(200_000);
48+
});
49+
```
50+
3551
## bollard, rustls and SSL Cryptography providers
3652

3753
`testcontainers` uses [`bollard`](https://docs.rs/bollard/latest/bollard/) to interact with the Docker API.

testcontainers/src/core/containers/request.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88

99
#[cfg(feature = "device-requests")]
1010
use bollard::models::DeviceRequest;
11-
use bollard::models::ResourcesUlimits;
11+
use bollard::models::{HostConfig, ResourcesUlimits};
1212

1313
use crate::{
1414
core::{
@@ -49,6 +49,7 @@ pub struct ContainerRequest<I: Image> {
4949
pub(crate) startup_timeout: Option<Duration>,
5050
pub(crate) working_dir: Option<String>,
5151
pub(crate) log_consumers: Vec<Box<dyn LogConsumer + 'static>>,
52+
pub(crate) host_config_modifier: Option<Box<dyn Fn(&mut HostConfig) + Send + Sync + 'static>>,
5253
#[cfg(feature = "reusable-containers")]
5354
pub(crate) reuse: crate::ReuseDirective,
5455
pub(crate) user: Option<String>,
@@ -239,6 +240,12 @@ impl<I: Image> ContainerRequest<I> {
239240
self.health_check.as_ref()
240241
}
241242

243+
pub fn host_config_modifier(
244+
&self,
245+
) -> Option<&(dyn Fn(&mut HostConfig) + Send + Sync + 'static)> {
246+
self.host_config_modifier.as_deref()
247+
}
248+
242249
#[cfg(feature = "device-requests")]
243250
pub fn device_requests(&self) -> Option<&[DeviceRequest]> {
244251
self.device_requests.as_deref()
@@ -276,6 +283,7 @@ impl<I: Image> From<I> for ContainerRequest<I> {
276283
startup_timeout: None,
277284
working_dir: None,
278285
log_consumers: vec![],
286+
host_config_modifier: None,
279287
#[cfg(feature = "reusable-containers")]
280288
reuse: crate::ReuseDirective::Never,
281289
user: None,
@@ -336,7 +344,11 @@ impl<I: Image + Debug> Debug for ContainerRequest<I> {
336344
.field("working_dir", &self.working_dir)
337345
.field("user", &self.user)
338346
.field("ready_conditions", &self.ready_conditions)
339-
.field("health_check", &self.health_check);
347+
.field("health_check", &self.health_check)
348+
.field(
349+
"has_host_config_modifier",
350+
&self.host_config_modifier.is_some(),
351+
);
340352

341353
#[cfg(feature = "reusable-containers")]
342354
repr.field("reusable", &self.reuse);

testcontainers/src/core/image/image_ext.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::time::Duration;
22

33
#[cfg(feature = "device-requests")]
44
use bollard::models::DeviceRequest;
5-
use bollard::models::ResourcesUlimits;
5+
use bollard::models::{HostConfig, ResourcesUlimits};
66

77
use crate::{
88
core::{
@@ -210,6 +210,15 @@ pub trait ImageExt<I: Image> {
210210
/// Allows to follow the container logs for the whole lifecycle of the container, starting from the creation.
211211
fn with_log_consumer(self, log_consumer: impl LogConsumer + 'static) -> ContainerRequest<I>;
212212

213+
/// Applies a custom modifier to the Docker `HostConfig` used for container creation.
214+
///
215+
/// The modifier runs after `testcontainers` finishes applying its defaults and settings.
216+
/// If called multiple times, the last modifier replaces the previous one.
217+
fn with_host_config_modifier(
218+
self,
219+
modifier: impl Fn(&mut HostConfig) + Send + Sync + 'static,
220+
) -> ContainerRequest<I>;
221+
213222
/// Flag the container as being exempt from the default `testcontainers` remove-on-drop lifecycle,
214223
/// indicating that the container should be kept running, and that executions with the same configuration
215224
/// reuse it instead of starting a "fresh" container instance.
@@ -526,6 +535,17 @@ impl<RI: Into<ContainerRequest<I>>, I: Image> ImageExt<I> for RI {
526535
container_req
527536
}
528537

538+
fn with_host_config_modifier(
539+
self,
540+
modifier: impl Fn(&mut HostConfig) + Send + Sync + 'static,
541+
) -> ContainerRequest<I> {
542+
let container_req = self.into();
543+
ContainerRequest {
544+
host_config_modifier: Some(Box::new(modifier)),
545+
..container_req
546+
}
547+
}
548+
529549
#[cfg(feature = "reusable-containers")]
530550
fn with_reuse(self, reuse: ReuseDirective) -> ContainerRequest<I> {
531551
ContainerRequest {

testcontainers/src/runners/async_runner.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ where
326326
}
327327
}
328328

329+
if let Some(modifier) = container_req.host_config_modifier() {
330+
config.host_config = config.host_config.map(|mut host_config| {
331+
modifier(&mut host_config);
332+
host_config
333+
});
334+
}
335+
329336
let cmd: Vec<_> = container_req.cmd().map(|v| v.to_string()).collect();
330337
if !cmd.is_empty() {
331338
config.cmd = Some(cmd);

testcontainers/src/runners/sync_runner.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,24 @@ mod tests {
329329
Ok(())
330330
}
331331

332+
#[test]
333+
fn sync_run_command_should_apply_host_config_modifier() -> anyhow::Result<()> {
334+
let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
335+
let container = image
336+
.with_host_config_modifier(|host_config| {
337+
host_config.cpu_period = Some(100_000);
338+
host_config.cpu_quota = Some(200_000);
339+
})
340+
.start()?;
341+
342+
let container_details = inspect(container.id());
343+
let host_config = container_details.host_config.expect("HostConfig");
344+
345+
assert_eq!(host_config.cpu_period, Some(100_000));
346+
assert_eq!(host_config.cpu_quota, Some(200_000));
347+
Ok(())
348+
}
349+
332350
#[test]
333351
fn sync_should_create_network_if_image_needs_it_and_drop_it_in_the_end() -> anyhow::Result<()> {
334352
{

0 commit comments

Comments
 (0)