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
15 changes: 7 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
[workspace]
resolver = "2"
members = [
"testcontainers",
"testimages",
]
members = ["testcontainers"]
exclude = ["testimages/no_expose_port", "testimages/simple-web-server"]

[workspace.package]
authors = ["Thomas Eizinger", "Artem Medvedev <i@ddtkey.com>", "Mervyn McCreight"]
authors = [
"Thomas Eizinger",
"Artem Medvedev <i@ddtkey.com>",
"Mervyn McCreight",
]
edition = "2021"
keywords = ["docker", "testcontainers"]
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/testcontainers/testcontainers-rs"
rust-version = "1.82"

[workspace.dependencies]
testimages = { path = "testimages" }
16 changes: 15 additions & 1 deletion docs/features/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,25 @@ Configuration is fetched in the following order:
3. else it will load the default Docker config file, which lives in the user's home, e.g. `~/.docker/config.json`.

## bollard, rustls and SSL Cryptography providers
`testcontainers` uses [`bollard`](https://docs.rs/bollard/latest/bollard/) to interact with the Docker API.

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

`bollard` in turn has options provided by `rustls` to configure its SSL cryptography providers.

The `testcontainers` feature flags to control this are as follows:

* `ring` - use `rustls` with `ring` as the cryptography provider (default)
* `aws-lc-rs` - use `rustls` with `aws-lc-rs` as the cryptography provider
* `ssl` - use `rustls` with a custom cryptography provider configuration - see [bollard](https://docs.rs/bollard/latest/bollard/#feature-flags) and [rustls](https://docs.rs/rustls/latest/rustls/#cryptography-providers) documentation for more.

## Podman Docker compatibility

Install `podman` + `podman-docker` packages for your system.

```sh
systemctl --user enable podman.socket
systemctl --user start podman.socket
systemctl --user status podman.socket

export DOCKER_HOST=unix:///run/user/$UID/podman/podman.sock
```
3 changes: 1 addition & 2 deletions testcontainers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ pretty_env_logger = "0.5"
reqwest = { version = "0.12.4", features = [
"blocking",
], default-features = false }
temp-dir = "0.1.13"
temp-dir = "0.1"
tempfile = "3.20"
testimages.workspace = true
tokio = { version = "1", features = ["macros"] }
8 changes: 4 additions & 4 deletions testcontainers/src/core/containers/async_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ mod tests {

#[tokio::test]
async fn async_logs_are_accessible() -> anyhow::Result<()> {
let image = GenericImage::new("testcontainers/helloworld", "1.1.0");
let image = GenericImage::new("testcontainers/helloworld", "1.2.0");
let container = image.start().await?;

let stderr = container.stderr(true);
Expand Down Expand Up @@ -608,7 +608,7 @@ mod tests {
("test-name", "async_containers_are_reused"),
];

let initial_image = GenericImage::new("testcontainers/helloworld", "1.1.0")
let initial_image = GenericImage::new("testcontainers/helloworld", "1.2.0")
.with_reuse(crate::ReuseDirective::CurrentSession)
.with_labels(labels);

Expand Down Expand Up @@ -672,7 +672,7 @@ mod tests {
("test-name", "async_reused_containers_are_not_confused"),
];

let initial_image = GenericImage::new("testcontainers/helloworld", "1.1.0")
let initial_image = GenericImage::new("testcontainers/helloworld", "1.2.0")
.with_reuse(ReuseDirective::Always)
.with_labels(labels);

Expand Down Expand Up @@ -732,7 +732,7 @@ mod tests {

let client = crate::core::client::docker_client_instance().await?;

let image = GenericImage::new("testcontainers/helloworld", "1.1.0")
let image = GenericImage::new("testcontainers/helloworld", "1.2.0")
.with_reuse(ReuseDirective::Always)
.with_labels([
("foo", "bar"),
Expand Down
2 changes: 1 addition & 1 deletion testcontainers/src/core/containers/sync_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ mod test {

#[test]
fn sync_logs_are_accessible() -> anyhow::Result<()> {
let image = GenericImage::new("testcontainers/helloworld", "1.1.0");
let image = GenericImage::new("testcontainers/helloworld", "1.2.0");
let container = image.start()?;

let stderr = container.stderr(true);
Expand Down
38 changes: 25 additions & 13 deletions testcontainers/src/runners/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,23 @@ mod tests {
use crate::{
core::{IntoContainerPort, WaitFor},
images::generic::GenericImage,
ImageExt,
runners::AsyncBuilder,
GenericBuildableImage, ImageExt,
};

async fn get_server_container() -> GenericImage {
let generic_image = GenericBuildableImage::new("simple_web_server", "latest")
// "Dockerfile" is included already, so adding the build context directory is all what is needed
.with_file(
std::fs::canonicalize("../testimages/simple_web_server").unwrap(),
".",
)
.build_image()
.await
.unwrap();
generic_image.with_wait_for(WaitFor::message_on_stdout("server is ready"))
}

/// Test that all user-supplied labels are added to containers started by `AsyncRunner::start`
#[tokio::test]
async fn async_start_should_apply_expected_labels() -> anyhow::Result<()> {
Expand Down Expand Up @@ -456,13 +470,11 @@ mod tests {

#[tokio::test]
async fn async_run_command_should_map_exposed_port() -> anyhow::Result<()> {
let image = GenericImage::new("simple_web_server", "latest")
.with_exposed_port(5000.tcp())
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
.with_wait_for(WaitFor::seconds(1));
let _ = pretty_env_logger::try_init();
let image = get_server_container().await.with_exposed_port(5000.tcp());
let container = image.start().await?;
container
.get_host_port_ipv4(5000)
.get_host_port_ipv4(5000.tcp())
.await
.expect("Port should be mapped");
Ok(())
Expand All @@ -477,8 +489,8 @@ mod tests {
let udp_port = 1000;
let sctp_port = 2000;

let generic_server = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
let generic_server = get_server_container()
.await
// Explicitly expose the port, which otherwise would not be available.
.with_exposed_port(udp_port.udp())
.with_exposed_port(sctp_port.sctp());
Expand Down Expand Up @@ -650,8 +662,8 @@ mod tests {
#[tokio::test]
async fn async_should_rely_on_network_mode_when_network_is_provided_and_settings_bridge_empty(
) -> anyhow::Result<()> {
let web_server = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
let web_server = get_server_container()
.await
.with_wait_for(WaitFor::seconds(1));

let container = web_server.clone().with_network("bridge").start().await?;
Expand All @@ -669,8 +681,8 @@ mod tests {

#[tokio::test]
async fn async_should_return_error_when_non_bridged_network_selected() -> anyhow::Result<()> {
let web_server = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
let web_server = get_server_container()
.await
.with_wait_for(WaitFor::seconds(1));

let container = web_server.clone().with_network("host").start().await?;
Expand Down Expand Up @@ -879,7 +891,7 @@ mod tests {

#[tokio::test]
async fn async_run_command_should_have_user() -> anyhow::Result<()> {
let image = GenericImage::new("simple_web_server", "latest");
let image = get_server_container().await;
let expected_user = "root";
let container = image.with_user(expected_user).start().await?;

Expand Down
28 changes: 18 additions & 10 deletions testcontainers/src/runners/sync_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ mod tests {
use crate::{
core::{client::Client, mounts::Mount, IntoContainerPort, WaitFor},
images::generic::GenericImage,
ImageExt,
runners::SyncBuilder,
GenericBuildableImage, ImageExt,
};

static RUNTIME: OnceLock<Runtime> = OnceLock::new();
Expand Down Expand Up @@ -118,6 +119,18 @@ mod tests {
runtime().block_on(client.network_exists(name)).unwrap()
}

fn get_server_container() -> GenericImage {
let generic_image = GenericBuildableImage::new("simple_web_server", "latest")
// "Dockerfile" is included already, so adding the build context directory is all what is needed
.with_file(
std::fs::canonicalize("../testimages/simple_web_server").unwrap(),
".",
)
.build_image()
.unwrap();
generic_image.with_wait_for(WaitFor::message_on_stdout("server is ready"))
}

#[derive(Default)]
struct HelloWorld {
mounts: Vec<Mount>,
Expand Down Expand Up @@ -165,9 +178,8 @@ mod tests {

#[test]
fn sync_run_command_should_map_exposed_port() -> anyhow::Result<()> {
let image = GenericImage::new("simple_web_server", "latest")
let image = get_server_container()
.with_exposed_port(5000.tcp())
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
.with_wait_for(WaitFor::seconds(1));
let container = image.start()?;
let res = container.get_host_port_ipv4(5000.tcp());
Expand Down Expand Up @@ -226,9 +238,7 @@ mod tests {
#[test]
fn sync_should_rely_on_network_mode_when_network_is_provided_and_settings_bridge_empty(
) -> anyhow::Result<()> {
let web_server = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
.with_wait_for(WaitFor::seconds(1));
let web_server = get_server_container().with_wait_for(WaitFor::seconds(1));

let container = web_server.clone().with_network("bridge").start()?;

Expand All @@ -238,9 +248,7 @@ mod tests {

#[test]
fn sync_should_return_error_when_non_bridged_network_selected() -> anyhow::Result<()> {
let web_server = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
.with_wait_for(WaitFor::seconds(1));
let web_server = get_server_container().with_wait_for(WaitFor::seconds(1));

let container = web_server.clone().with_network("host").start()?;

Expand All @@ -261,7 +269,7 @@ mod tests {

#[test]
fn sync_run_command_with_container_network_should_not_expose_ports() -> anyhow::Result<()> {
let _first_container = GenericImage::new("simple_web_server", "latest")
let _first_container = get_server_container()
.with_container_name("the_first_one")
.start()?;

Expand Down
53 changes: 34 additions & 19 deletions testcontainers/tests/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use testcontainers::{
wait::{ExitWaitStrategy, LogWaitStrategy},
CmdWaitFor, ExecCommand, WaitFor,
},
runners::AsyncRunner,
GenericImage, Image, ImageExt,
runners::{AsyncBuilder, AsyncRunner},
GenericBuildableImage, GenericImage, Image, ImageExt,
};
use tokio::io::AsyncReadExt;

Expand All @@ -35,6 +35,21 @@ impl Image for HelloWorld {
}
}

async fn get_server_container(msg: Option<WaitFor>) -> GenericImage {
let generic_image = GenericBuildableImage::new("simple_web_server", "latest")
// "Dockerfile" is included already, so adding the build context directory is all what is needed
.with_file(
std::fs::canonicalize("../testimages/simple_web_server").unwrap(),
".",
)
.build_image()
.await
.unwrap();

let msg = msg.unwrap_or(WaitFor::message_on_stdout("server is ready"));
generic_image.with_wait_for(msg)
}

#[tokio::test(flavor = "multi_thread")]
async fn bollard_can_run_hello_world_with_multi_thread() -> anyhow::Result<()> {
let _ = pretty_env_logger::try_init();
Expand Down Expand Up @@ -109,12 +124,14 @@ async fn start_containers_in_parallel() -> anyhow::Result<()> {
async fn async_run_exec() -> anyhow::Result<()> {
let _ = pretty_env_logger::try_init();

let image = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stderr("server will be listening to"))
.with_wait_for(WaitFor::log(
LogWaitStrategy::stdout("server is ready").with_times(2),
))
.with_wait_for(WaitFor::seconds(1));
let image = get_server_container(Some(WaitFor::message_on_stderr(
"server will be listening to",
)))
.await
.with_wait_for(WaitFor::log(
LogWaitStrategy::stdout("server is ready").with_times(2),
))
.with_wait_for(WaitFor::seconds(1));
let container = image.start().await?;

// exit regardless of the code
Expand Down Expand Up @@ -177,11 +194,12 @@ async fn async_wait_for_http() -> anyhow::Result<()> {

let _ = pretty_env_logger::try_init();

let image = GenericImage::new("simple_web_server", "latest")
.with_exposed_port(80.tcp())
.with_wait_for(WaitFor::http(
HttpWaitStrategy::new("/").with_expected_status_code(StatusCode::OK),
));
let waitfor_http_status =
WaitFor::http(HttpWaitStrategy::new("/").with_expected_status_code(StatusCode::OK));

let image = get_server_container(Some(waitfor_http_status))
.await
.with_exposed_port(80.tcp());
let _container = image.start().await?;
Ok(())
}
Expand All @@ -190,8 +208,8 @@ async fn async_wait_for_http() -> anyhow::Result<()> {
async fn async_run_exec_fails_due_to_unexpected_code() -> anyhow::Result<()> {
let _ = pretty_env_logger::try_init();

let image = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
let image = get_server_container(None)
.await
.with_wait_for(WaitFor::seconds(1));
let container = image.start().await?;

Expand Down Expand Up @@ -296,10 +314,7 @@ async fn async_container_exit_code() -> anyhow::Result<()> {
let _ = pretty_env_logger::try_init();

// Container that should run until manually quit
let container = GenericImage::new("simple_web_server", "latest")
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
.start()
.await?;
let container = get_server_container(None).await.start().await?;

assert_eq!(container.exit_code().await?, None);

Expand Down
13 changes: 10 additions & 3 deletions testcontainers/tests/dual_stack_host_ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ use std::net::{Ipv6Addr, TcpListener};

use testcontainers::{
core::{IntoContainerPort, WaitFor},
runners::SyncRunner,
GenericImage,
runners::{SyncBuilder, SyncRunner},
GenericBuildableImage,
};

/// Test the functionality of exposing container ports over both IPv4 and IPv6.
#[test]
fn test_ipv4_ipv6_host_ports() -> anyhow::Result<()> {
let _ = pretty_env_logger::try_init();

let image = GenericImage::new("simple_web_server", "latest")
let image = GenericBuildableImage::new("simple_web_server", "latest")
// "Dockerfile" is included already, so adding the build context directory is all what is needed
.with_file(
std::fs::canonicalize("../testimages/simple_web_server").unwrap(),
".",
)
.build_image()
.unwrap()
.with_wait_for(WaitFor::message_on_stdout("server is ready"))
.with_wait_for(WaitFor::seconds(1));

Expand Down
Loading
Loading