Skip to content

Commit 4b04ccd

Browse files
authored
Merge branch 'testcontainers:main' into aa/alias
2 parents d712f72 + 392d634 commit 4b04ccd

File tree

7 files changed

+286
-5
lines changed

7 files changed

+286
-5
lines changed

testcontainers/src/buildables/generic.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,76 @@ use crate::{
55
BuildableImage, GenericImage,
66
};
77

8+
/// A generic implementation of [`BuildableImage`] for building custom Docker images.
9+
///
10+
/// `GenericBuildableImage` provides a fluent interface for constructing Docker images from
11+
/// Dockerfiles and build contexts. It supports adding files and directories from the filesystem,
12+
/// embedding data directly, and customizing the build process.
13+
///
14+
/// # Build Context Management
15+
///
16+
/// The build context is managed through a [`BuildContextBuilder`] that collects all files
17+
/// and data needed for the Docker build. Files are automatically packaged into a TAR archive
18+
/// that gets sent to the Docker daemon.
19+
///
20+
/// # Example: Basic Image Build
21+
///
22+
/// ```rust,no_run
23+
/// use testcontainers::{GenericBuildableImage, runners::AsyncBuilder};
24+
///
25+
/// #[tokio::test]
26+
/// async fn test_hello() -> anyhow::Result<()> {
27+
/// let image = GenericBuildableImage::new("hello-world", "latest")
28+
/// .with_dockerfile_string(
29+
/// r#"FROM alpine:latest
30+
/// COPY hello.sh /usr/local/bin/
31+
/// RUN chmod +x /usr/local/bin/hello.sh
32+
/// CMD ["/usr/local/bin/hello.sh"]"#
33+
/// )
34+
/// .with_data(
35+
/// "#!/bin/sh\necho 'Hello from custom image!'",
36+
/// "./hello.sh"
37+
/// )
38+
/// .build_image().await?;
39+
/// // start container
40+
/// // use it
41+
/// }
42+
/// ```
43+
///
44+
/// # Example: Multi-File Build Context
45+
///
46+
/// ```rust,no_run
47+
/// use testcontainers::{GenericBuildableImage, runners::AsyncBuilder};
48+
///
49+
/// #[tokio::test]
50+
/// async fn test_webapp() -> anyhow::Result<()> {
51+
/// let image = GenericBuildableImage::new("web-app", "1.0")
52+
/// .with_dockerfile("./Dockerfile")
53+
/// .with_file("./package.json", "./package.json")
54+
/// .with_file("./src", "./src")
55+
/// .with_data(vec![0x00, 0x01, 0x02], "./data.dat")
56+
/// .build_image().await?;
57+
/// // start container
58+
/// // use it
59+
/// }
60+
/// ```
861
#[derive(Debug)]
962
pub struct GenericBuildableImage {
63+
/// The name of the Docker image to be built
1064
name: String,
65+
/// The tag assigned to the built image and passed down to the [`Image`]
1166
tag: String,
67+
/// Wrapped builder for managing the build context
1268
build_context_builder: BuildContextBuilder,
1369
}
1470

1571
impl GenericBuildableImage {
72+
/// Creates a new buildable image with the specified name and tag.
73+
///
74+
/// # Arguments
75+
///
76+
/// * `name` - The name for the Docker image (e.g., "my-app", "registry.com/service")
77+
/// * `tag` - The tag for the image (e.g., "latest", "1.0", "dev")
1678
pub fn new(name: impl Into<String>, tag: impl Into<String>) -> Self {
1779
Self {
1880
name: name.into(),
@@ -21,21 +83,54 @@ impl GenericBuildableImage {
2183
}
2284
}
2385

86+
/// Adds a Dockerfile from the filesystem to the build context.
87+
///
88+
/// # Arguments
89+
///
90+
/// * `source` - Path to the Dockerfile on the local filesystem
2491
pub fn with_dockerfile(mut self, source: impl Into<PathBuf>) -> Self {
2592
self.build_context_builder = self.build_context_builder.with_dockerfile(source);
2693
self
2794
}
2895

96+
/// Adds a Dockerfile from a string to the build context.
97+
///
98+
/// This is useful for generating Dockerfiles programmatically or embedding
99+
/// simple Dockerfiles directly in test code.
100+
///
101+
/// # Arguments
102+
///
103+
/// * `content` - The complete Dockerfile content as a string
29104
pub fn with_dockerfile_string(mut self, content: impl Into<String>) -> Self {
30105
self.build_context_builder = self.build_context_builder.with_dockerfile_string(content);
31106
self
32107
}
33108

109+
/// Adds a file or directory from the filesystem to the build context.
110+
///
111+
/// Be aware, that if you don't add the Dockerfile with the specific `with_dockerfile()`
112+
/// or `with_dockerfile_string()` functions it has to be named `Dockerfile`in the build
113+
/// context. Containerfile won't be recognized!
114+
///
115+
/// # Arguments
116+
///
117+
/// * `source` - Path to the file or directory on the local filesystem
118+
/// * `target` - Path where the file should be placed in the build context
34119
pub fn with_file(mut self, source: impl Into<PathBuf>, target: impl Into<String>) -> Self {
35120
self.build_context_builder = self.build_context_builder.with_file(source, target);
36121
self
37122
}
38123

124+
/// Adds data directly to the build context as a file.
125+
///
126+
/// This method allows you to embed file content directly without requiring
127+
/// files to exist on the filesystem. Useful for generated content, templates,
128+
/// or small configuration files.
129+
///
130+
/// # Arguments
131+
///
132+
/// * `data` - The file content as bytes
133+
/// * `target` - Path where the file should be placed in the build context
39134
pub fn with_data(mut self, data: impl Into<Vec<u8>>, target: impl Into<String>) -> Self {
40135
self.build_context_builder = self.build_context_builder.with_data(data, target);
41136
self

testcontainers/src/core/build_context.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,106 @@ use std::path::PathBuf;
22

33
use crate::{core::copy::CopyToContainerCollection, CopyToContainer};
44

5+
/// Builder for managing Docker BuildKit build contexts.
6+
///
7+
/// A build context contains all the files and data that Docker needs to build an image.
8+
/// This includes the Dockerfile, source code, configuration files, and any other materials
9+
/// referenced by the Dockerfile's `COPY` or `ADD` instructions.
10+
/// More information see: <https://docs.docker.com/build/concepts/context/>
11+
///
12+
/// The `BuildContextBuilder` collects these materials and packages them into a TAR archive
13+
/// that can be sent to the Docker daemon for building.
14+
///
15+
/// # Example
16+
///
17+
/// ```rust,no_run
18+
/// use testcontainers::core::BuildContextBuilder;
19+
///
20+
/// let context = BuildContextBuilder::default()
21+
/// .with_dockerfile_string("FROM alpine:latest\nCOPY app /usr/local/bin/")
22+
/// .with_file("./target/release/app", "./app")
23+
/// .with_data(b"#!/bin/sh\necho 'Hello World'", "./hello.sh")
24+
/// .collect();
25+
/// ```
526
#[derive(Debug, Default, Clone)]
627
pub struct BuildContextBuilder {
728
build_context_parts: Vec<CopyToContainer>,
829
}
930

1031
impl BuildContextBuilder {
32+
/// Adds a Dockerfile from the filesystem to the build context.
33+
///
34+
/// # Arguments
35+
///
36+
/// * `source` - Path to the Dockerfile on the local filesystem
1137
pub fn with_dockerfile(self, source: impl Into<PathBuf>) -> Self {
1238
self.with_file(source.into(), "Dockerfile")
1339
}
1440

41+
/// Adds a Dockerfile from a string to the build context.
42+
///
43+
/// This is useful for generating Dockerfiles programmatically or embedding
44+
/// simple Dockerfiles directly in test code.
45+
///
46+
/// # Arguments
47+
///
48+
/// * `content` - The complete Dockerfile content as a string
1549
pub fn with_dockerfile_string(self, content: impl Into<String>) -> Self {
1650
self.with_data(content.into(), "Dockerfile")
1751
}
1852

53+
/// Adds a file or directory from the filesystem to the build context.
54+
///
55+
/// Be aware, that if you don't add the Dockerfile with the specific `with_dockerfile()`
56+
/// or `with_dockerfile_string()` functions it has to be named `Dockerfile`in the build
57+
/// context. Containerfile won't be recognized!
58+
///
59+
/// # Arguments
60+
///
61+
/// * `source` - Path to the file or directory on the local filesystem
62+
/// * `target` - Path where the file should be placed in the build context
1963
pub fn with_file(mut self, source: impl Into<PathBuf>, target: impl Into<String>) -> Self {
2064
self.build_context_parts
2165
.push(CopyToContainer::new(source.into(), target));
2266
self
2367
}
2468

69+
/// Adds data directly to the build context as a file.
70+
///
71+
/// This method allows you to embed file content directly without requiring
72+
/// files to exist on the filesystem. Useful for generated content, templates,
73+
/// or small configuration files.
74+
///
75+
/// # Arguments
76+
///
77+
/// * `data` - The file content as bytes
78+
/// * `target` - Path where the file should be placed in the build context
2579
pub fn with_data(mut self, data: impl Into<Vec<u8>>, target: impl Into<String>) -> Self {
2680
self.build_context_parts
2781
.push(CopyToContainer::new(data.into(), target));
2882
self
2983
}
3084

85+
/// Consumes the builder and returns the collected build context.
86+
///
87+
/// This method finalizes the build context and returns a [`CopyToContainerCollection`]
88+
/// that can be converted to a TAR archive for Docker.
89+
///
90+
/// # Returns
91+
///
92+
/// A [`CopyToContainerCollection`] containing all the build context materials.
3193
pub fn collect(self) -> CopyToContainerCollection {
3294
CopyToContainerCollection::new(self.build_context_parts)
3395
}
3496

97+
/// Returns the build context without consuming the builder.
98+
///
99+
/// This method creates a clone of the current build context state, allowing
100+
/// the builder to be reused or modified further.
101+
///
102+
/// # Returns
103+
///
104+
/// A [`CopyToContainerCollection`] containing all the current build context materials.
35105
pub fn as_copy_to_container_collection(&self) -> CopyToContainerCollection {
36106
CopyToContainerCollection::new(self.build_context_parts.clone())
37107
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,73 @@
11
use crate::{core::copy::CopyToContainerCollection, Image};
22

3+
/// Trait for images that can be built from within your tests or testcontainer libraries.
4+
///
5+
/// Unlike the [`Image`] trait which represents existing Docker images, `BuildableImage`
6+
/// represents images that need to be constructed from a, possibly even dynamic, `Dockerfile``
7+
/// and the needed Docker build context.
8+
///
9+
/// If you want to dynamically create Dockerfiles look at Dockerfile generator crates like:
10+
/// <https://crates.io/crates/dockerfile_builder>
11+
///
12+
/// The build process, executed by [`crate::runners::SyncBuilder`] / [`crate::runners::AsyncBuilder`], follows these steps:
13+
/// 1. Collect build context via `build_context()` which will be tarred and sent to buildkit.
14+
/// 2. Generate image descriptor via `descriptor()` which will be passed to the container
15+
/// 3. Build the Docker image using the Docker API
16+
/// 4. Convert to runnable [`Image`] via `into_image()` which consumes the `BuildableImage`
17+
/// into an `Image`
18+
///
19+
/// # Example
20+
///
21+
/// ```rust
22+
/// use testcontainers::{GenericBuildableImage, runners::AsyncBuilder};
23+
///
24+
/// #[tokio::test]
25+
/// async fn test_example() -> anyhow::Result<()> {
26+
/// let image = GenericBuildableImage::new("example-tc", "1.0")
27+
/// .with_dockerfile_string("FROM alpine:latest\nRUN echo 'hello'")
28+
/// .build_image().await?;
29+
/// // start container
30+
/// // use it
31+
/// }
32+
/// ```
33+
///
334
pub trait BuildableImage {
35+
/// The type of [`Image`] that this buildable image produces after building.
436
type Built: Image;
537

38+
/// Returns the build context containing all files and data needed to build the image.
39+
///
40+
/// The build context consist of at least the `Dockerfile` and needs all the resources
41+
/// referred to by the Dockerfile.
42+
/// This is more or less equivalent to the directory you would pass to `docker build`.
43+
///
44+
/// <https://docs.docker.com/build/concepts/context/>
45+
///
46+
/// For creating build contexts, use the [`crate::core::BuildContextBuilder`] API when not using
47+
/// [`crate::GenericBuildableImage`], which wraps `BuildContextBuilder` builder functions.
48+
///
49+
/// # Returns
50+
///
51+
/// A [`CopyToContainerCollection`] containing the build context in a form we
52+
/// can send it to buildkit.
653
fn build_context(&self) -> CopyToContainerCollection;
54+
55+
/// Returns the image descriptor (name:tag) that will be assigned to the built image and be
56+
/// passed down to the container for running.
57+
///
58+
/// # Returns
59+
///
60+
/// A string in the format "name:tag" that uniquely identifies the built image.
761
fn descriptor(&self) -> String;
862

63+
/// Consumes this buildable image and converts it into a runnable [`Image`].
64+
///
65+
/// This method is called after the Docker image has been successfully built.
66+
/// It transforms the build specification into a standard [`Image`] that can be
67+
/// started as a container.
68+
///
69+
/// # Returns
70+
///
71+
/// An [`Image`] instance configured to run the built Docker image.
972
fn into_image(self) -> Self::Built;
1073
}

testcontainers/src/images/generic.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@ use crate::{
1212
///
1313
/// For example:
1414
///
15-
/// ```
15+
/// ```rust,ignore
1616
/// use testcontainers::{
1717
/// core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage, ImageExt
1818
/// };
1919
///
20-
/// # /*
2120
/// #[tokio::test]
22-
/// # */
2321
/// async fn test_redis() {
2422
/// let container = GenericImage::new("redis", "7.2.4")
2523
/// .with_exposed_port(6379.tcp())

testcontainers/src/lib.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@
2222
//!
2323
//! Unsurprisingly, working with testcontainers is very similar to working with Docker itself.
2424
//!
25-
//! First, you need to define the [`Image`] that you want to run, and then simply call the `start` method on it from either the [`AsyncRunner`] or [`SyncRunner`] trait.
25+
//! If you need to build an image first, then you need to define the [`BuildableImage`] that specifies the build context
26+
//! and the Dockerfile, then call the `build_image` method on it from either the [`AsyncBuilder`] or [`SyncBuilder`] trait.
27+
//! This will yield an [`Image`] you could actually start.
28+
//!
29+
//! If you already have a Docker image you can just define your [`Image`] that you want to run, and then simply call the
30+
//! `start` method on it from either the [`AsyncRunner`] or [`SyncRunner`] trait.
31+
//!
2632
//! This will return you [`ContainerAsync`] or [`Container`] respectively.
2733
//! Containers implement `Drop`. As soon as they go out of scope, the underlying docker container is removed.
2834
//! To disable this behavior, you can set ENV variable `TESTCONTAINERS_COMMAND` to `keep`.
@@ -60,7 +66,8 @@
6066
//! # Ecosystem
6167
//!
6268
//! `testcontainers` is the core crate that provides an API for working with containers in a test environment.
63-
//! The only image that is provided by the core crate is the [`GenericImage`], which is a simple wrapper around any docker image.
69+
//! The only buildable image and image implementations that are provided by the core crate are the [`GenericBuildableImage`]
70+
//! and [`GenericImage`], respectively.
6471
//!
6572
//! However, it does not provide ready-to-use modules, you can implement your [`Image`]s using the library directly or use community supported [`testcontainers-modules`].
6673
//!
@@ -70,6 +77,8 @@
7077
//!
7178
//! [tc_website]: https://testcontainers.org
7279
//! [`Docker`]: https://docker.com
80+
//! [`AsyncBuilder`]: runners::AsyncBuilder
81+
//! [`SyncBuilder`]: runners::SyncBuilder
7382
//! [`AsyncRunner`]: runners::AsyncRunner
7483
//! [`SyncRunner`]: runners::SyncRunner
7584
//! [`testcontainers-modules`]: https://crates.io/crates/testcontainers-modules

testcontainers/src/runners/async_builder.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@ pub trait AsyncBuilder<B: BuildableImage> {
1111
}
1212

1313
#[async_trait]
14+
/// Helper trait to build Docker images asynchronously from [`BuildableImage`] instances.
15+
///
16+
/// Provides an asynchronous interface for building custom Docker images within test environments.
17+
/// This trait is automatically implemented for any type that implements [`BuildableImage`] + [`Send`].
18+
///
19+
/// # Example
20+
///
21+
/// ```rust,no_run
22+
/// use testcontainers::{core::WaitFor, runners::AsyncBuilder, runners::AsyncRunner, GenericBuildableImage};
23+
///
24+
/// #[test]
25+
/// async fn test_custom_image() -> anyhow::Result<()> {
26+
/// let image = GenericBuildableImage::new("my-test-app", "latest")
27+
/// .with_dockerfile_string("FROM alpine:latest\nRUN echo 'hello'")
28+
/// .build_image()?.await;
29+
/// // Use the built image in containers
30+
/// let container = image
31+
/// .with_wait_for(WaitFor::message_on_stdout("Hello from test!"))
32+
/// .start()?.await;
33+
///
34+
/// Ok(())
35+
/// }
36+
/// ```
1437
impl<T> AsyncBuilder<T> for T
1538
where
1639
T: BuildableImage + Send,

0 commit comments

Comments
 (0)