Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/features/creating_container.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Creating containers

Build a container request by chaining `ImageExt` methods on an image, then start it with
`AsyncRunner` or `SyncRunner`.

```rust
use testcontainers::{
core::{IntoContainerPort, WaitFor},
runners::AsyncRunner,
GenericImage, ImageExt,
};

let _container = GenericImage::new("redis", "7.2.4")
.with_exposed_port(6379.tcp())
.with_env_var("DEBUG", "1")
.with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
.start()
.await
.unwrap();
```

## Common settings

- Ports and networking: use `with_exposed_port`, `with_mapped_port`, or `with_network`
(see [networking](networking.md)).
- Files and mounts: use `with_copy_to` and `with_mount` (see [files](files.md)).
- Readiness: use `with_wait_for` or health checks (see [wait strategies](wait_strategies.md)).
- Execution hooks: use `exec_after_start` or `exec_before_ready` on images
(see [exec commands](exec_commands.md)).

## Custom images

If you want a strongly-typed image with reusable defaults, define your own type and implement
`Image` for it, then use it like any other image (for example, `RedisImage` or `PostgresImage`).
Before rolling your own, check the community modules that already package popular services:
[`community modules`][community-modules] and the
[`testcontainers-rs-modules-community` repo][community-modules-repo].

```rust
use testcontainers::{
core::{ContainerPort, WaitFor},
runners::AsyncRunner,
Image,
};

#[derive(Debug, Clone)]
struct RedisImage {
ports: [ContainerPort; 1],
}

impl Default for RedisImage {
fn default() -> Self {
Self {
ports: [ContainerPort::Tcp(6379)],
}
}
}

impl Image for RedisImage {
fn name(&self) -> &str {
"redis"
}

fn tag(&self) -> &str {
"7.2.4"
}

fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stdout("Ready to accept connections")]
}

fn expose_ports(&self) -> &[ContainerPort] {
&self.ports
}
}

let _container = RedisImage::default().start().await.unwrap();
```

## Advanced settings

If you need to tweak Docker `HostConfig` fields that are not exposed by the high-level API, use
`ImageExt::with_host_config_modifier` to apply a single callback just before container creation.
The modifier runs after `testcontainers` fills in its defaults. If you call it multiple times,
the last modifier wins.

```rust
use testcontainers::{GenericImage, ImageExt};

let image = GenericImage::new("testcontainers/helloworld", "1.3.0")
.with_host_config_modifier(|host_config| {
host_config.cpu_period = Some(100_000);
host_config.cpu_quota = Some(200_000);
});
```

[community-modules]: ../quickstart/community_modules.md
[community-modules-repo]: https://github.com/testcontainers/testcontainers-rs-modules-community
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ nav:
- quickstart/community_modules.md
- Features:
- features/configuration.md
- features/creating_container.md
- features/wait_strategies.md
- features/exec_commands.md
- features/files.md
Expand Down
17 changes: 15 additions & 2 deletions testcontainers/src/core/containers/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ use std::{
collections::BTreeMap,
fmt::{Debug, Formatter},
net::IpAddr,
sync::Arc,
time::Duration,
};

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

use crate::{
core::{
Expand All @@ -18,6 +19,8 @@ use crate::{
Image, TestcontainersError,
};

pub(crate) type HostConfigModifier = Arc<dyn Fn(&mut HostConfig) + Send + Sync + 'static>;

/// Represents a request to start a container, allowing customization of the container.
#[must_use]
pub struct ContainerRequest<I: Image> {
Expand Down Expand Up @@ -49,6 +52,7 @@ pub struct ContainerRequest<I: Image> {
pub(crate) startup_timeout: Option<Duration>,
pub(crate) working_dir: Option<String>,
pub(crate) log_consumers: Vec<Box<dyn LogConsumer + 'static>>,
pub(crate) host_config_modifier: Option<HostConfigModifier>,
#[cfg(feature = "reusable-containers")]
pub(crate) reuse: crate::ReuseDirective,
pub(crate) user: Option<String>,
Expand Down Expand Up @@ -239,6 +243,10 @@ impl<I: Image> ContainerRequest<I> {
self.health_check.as_ref()
}

pub fn host_config_modifier(&self) -> Option<&HostConfigModifier> {
self.host_config_modifier.as_ref()
}

#[cfg(feature = "device-requests")]
pub fn device_requests(&self) -> Option<&[DeviceRequest]> {
self.device_requests.as_deref()
Expand Down Expand Up @@ -276,6 +284,7 @@ impl<I: Image> From<I> for ContainerRequest<I> {
startup_timeout: None,
working_dir: None,
log_consumers: vec![],
host_config_modifier: None,
#[cfg(feature = "reusable-containers")]
reuse: crate::ReuseDirective::Never,
user: None,
Expand Down Expand Up @@ -336,7 +345,11 @@ impl<I: Image + Debug> Debug for ContainerRequest<I> {
.field("working_dir", &self.working_dir)
.field("user", &self.user)
.field("ready_conditions", &self.ready_conditions)
.field("health_check", &self.health_check);
.field("health_check", &self.health_check)
.field(
"has_host_config_modifier",
&self.host_config_modifier.is_some(),
);

#[cfg(feature = "reusable-containers")]
repr.field("reusable", &self.reuse);
Expand Down
24 changes: 22 additions & 2 deletions testcontainers/src/core/image/image_ext.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::time::Duration;
use std::{sync::Arc, time::Duration};

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

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

/// Applies a custom modifier to the Docker `HostConfig` used for container creation.
///
/// The modifier runs after `testcontainers` finishes applying its defaults and settings.
/// If called multiple times, the last modifier replaces the previous one.
fn with_host_config_modifier(
self,
modifier: impl Fn(&mut HostConfig) + Send + Sync + 'static,
) -> ContainerRequest<I>;

/// Flag the container as being exempt from the default `testcontainers` remove-on-drop lifecycle,
/// indicating that the container should be kept running, and that executions with the same configuration
/// reuse it instead of starting a "fresh" container instance.
Expand Down Expand Up @@ -526,6 +535,17 @@ impl<RI: Into<ContainerRequest<I>>, I: Image> ImageExt<I> for RI {
container_req
}

fn with_host_config_modifier(
self,
modifier: impl Fn(&mut HostConfig) + Send + Sync + 'static,
) -> ContainerRequest<I> {
let container_req = self.into();
ContainerRequest {
host_config_modifier: Some(Arc::new(modifier)),
..container_req
}
}

#[cfg(feature = "reusable-containers")]
fn with_reuse(self, reuse: ReuseDirective) -> ContainerRequest<I> {
ContainerRequest {
Expand Down
7 changes: 7 additions & 0 deletions testcontainers/src/runners/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,13 @@ where
}
}

if let Some(modifier) = container_req.host_config_modifier() {
config.host_config = config.host_config.map(|mut host_config| {
modifier(&mut host_config);
host_config
});
}

let cmd: Vec<_> = container_req.cmd().map(|v| v.to_string()).collect();
if !cmd.is_empty() {
config.cmd = Some(cmd);
Expand Down
18 changes: 18 additions & 0 deletions testcontainers/src/runners/sync_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,24 @@ mod tests {
Ok(())
}

#[test]
fn sync_run_command_should_apply_host_config_modifier() -> anyhow::Result<()> {
let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
let container = image
.with_host_config_modifier(|host_config| {
host_config.cpu_period = Some(100_000);
host_config.cpu_quota = Some(200_000);
})
.start()?;

let container_details = inspect(container.id());
let host_config = container_details.host_config.expect("HostConfig");

assert_eq!(host_config.cpu_period, Some(100_000));
assert_eq!(host_config.cpu_quota, Some(200_000));
Ok(())
}

#[test]
fn sync_should_create_network_if_image_needs_it_and_drop_it_in_the_end() -> anyhow::Result<()> {
{
Expand Down
Loading