diff --git a/testcontainers/src/buildables/generic.rs b/testcontainers/src/buildables/generic.rs index 2ec7218f..fe8d4b8d 100644 --- a/testcontainers/src/buildables/generic.rs +++ b/testcontainers/src/buildables/generic.rs @@ -5,14 +5,76 @@ use crate::{ BuildableImage, GenericImage, }; +/// A generic implementation of [`BuildableImage`] for building custom Docker images. +/// +/// `GenericBuildableImage` provides a fluent interface for constructing Docker images from +/// Dockerfiles and build contexts. It supports adding files and directories from the filesystem, +/// embedding data directly, and customizing the build process. +/// +/// # Build Context Management +/// +/// The build context is managed through a [`BuildContextBuilder`] that collects all files +/// and data needed for the Docker build. Files are automatically packaged into a TAR archive +/// that gets sent to the Docker daemon. +/// +/// # Example: Basic Image Build +/// +/// ```rust,no_run +/// use testcontainers::{GenericBuildableImage, runners::AsyncBuilder}; +/// +/// #[tokio::test] +/// async fn test_hello() -> anyhow::Result<()> { +/// let image = GenericBuildableImage::new("hello-world", "latest") +/// .with_dockerfile_string( +/// r#"FROM alpine:latest +/// COPY hello.sh /usr/local/bin/ +/// RUN chmod +x /usr/local/bin/hello.sh +/// CMD ["/usr/local/bin/hello.sh"]"# +/// ) +/// .with_data( +/// "#!/bin/sh\necho 'Hello from custom image!'", +/// "./hello.sh" +/// ) +/// .build_image().await?; +/// // start container +/// // use it +/// } +/// ``` +/// +/// # Example: Multi-File Build Context +/// +/// ```rust,no_run +/// use testcontainers::{GenericBuildableImage, runners::AsyncBuilder}; +/// +/// #[tokio::test] +/// async fn test_webapp() -> anyhow::Result<()> { +/// let image = GenericBuildableImage::new("web-app", "1.0") +/// .with_dockerfile("./Dockerfile") +/// .with_file("./package.json", "./package.json") +/// .with_file("./src", "./src") +/// .with_data(vec![0x00, 0x01, 0x02], "./data.dat") +/// .build_image().await?; +/// // start container +/// // use it +/// } +/// ``` #[derive(Debug)] pub struct GenericBuildableImage { + /// The name of the Docker image to be built name: String, + /// The tag assigned to the built image and passed down to the [`Image`] tag: String, + /// Wrapped builder for managing the build context build_context_builder: BuildContextBuilder, } impl GenericBuildableImage { + /// Creates a new buildable image with the specified name and tag. + /// + /// # Arguments + /// + /// * `name` - The name for the Docker image (e.g., "my-app", "registry.com/service") + /// * `tag` - The tag for the image (e.g., "latest", "1.0", "dev") pub fn new(name: impl Into, tag: impl Into) -> Self { Self { name: name.into(), @@ -21,21 +83,54 @@ impl GenericBuildableImage { } } + /// Adds a Dockerfile from the filesystem to the build context. + /// + /// # Arguments + /// + /// * `source` - Path to the Dockerfile on the local filesystem pub fn with_dockerfile(mut self, source: impl Into) -> Self { self.build_context_builder = self.build_context_builder.with_dockerfile(source); self } + /// Adds a Dockerfile from a string to the build context. + /// + /// This is useful for generating Dockerfiles programmatically or embedding + /// simple Dockerfiles directly in test code. + /// + /// # Arguments + /// + /// * `content` - The complete Dockerfile content as a string pub fn with_dockerfile_string(mut self, content: impl Into) -> Self { self.build_context_builder = self.build_context_builder.with_dockerfile_string(content); self } + /// Adds a file or directory from the filesystem to the build context. + /// + /// Be aware, that if you don't add the Dockerfile with the specific `with_dockerfile()` + /// or `with_dockerfile_string()` functions it has to be named `Dockerfile`in the build + /// context. Containerfile won't be recognized! + /// + /// # Arguments + /// + /// * `source` - Path to the file or directory on the local filesystem + /// * `target` - Path where the file should be placed in the build context pub fn with_file(mut self, source: impl Into, target: impl Into) -> Self { self.build_context_builder = self.build_context_builder.with_file(source, target); self } + /// Adds data directly to the build context as a file. + /// + /// This method allows you to embed file content directly without requiring + /// files to exist on the filesystem. Useful for generated content, templates, + /// or small configuration files. + /// + /// # Arguments + /// + /// * `data` - The file content as bytes + /// * `target` - Path where the file should be placed in the build context pub fn with_data(mut self, data: impl Into>, target: impl Into) -> Self { self.build_context_builder = self.build_context_builder.with_data(data, target); self diff --git a/testcontainers/src/core/build_context.rs b/testcontainers/src/core/build_context.rs index 3cd4bf67..7729a648 100644 --- a/testcontainers/src/core/build_context.rs +++ b/testcontainers/src/core/build_context.rs @@ -2,36 +2,106 @@ use std::path::PathBuf; use crate::{core::copy::CopyToContainerCollection, CopyToContainer}; +/// Builder for managing Docker BuildKit build contexts. +/// +/// A build context contains all the files and data that Docker needs to build an image. +/// This includes the Dockerfile, source code, configuration files, and any other materials +/// referenced by the Dockerfile's `COPY` or `ADD` instructions. +/// More information see: +/// +/// The `BuildContextBuilder` collects these materials and packages them into a TAR archive +/// that can be sent to the Docker daemon for building. +/// +/// # Example +/// +/// ```rust,no_run +/// use testcontainers::core::BuildContextBuilder; +/// +/// let context = BuildContextBuilder::default() +/// .with_dockerfile_string("FROM alpine:latest\nCOPY app /usr/local/bin/") +/// .with_file("./target/release/app", "./app") +/// .with_data(b"#!/bin/sh\necho 'Hello World'", "./hello.sh") +/// .collect(); +/// ``` #[derive(Debug, Default, Clone)] pub struct BuildContextBuilder { build_context_parts: Vec, } impl BuildContextBuilder { + /// Adds a Dockerfile from the filesystem to the build context. + /// + /// # Arguments + /// + /// * `source` - Path to the Dockerfile on the local filesystem pub fn with_dockerfile(self, source: impl Into) -> Self { self.with_file(source.into(), "Dockerfile") } + /// Adds a Dockerfile from a string to the build context. + /// + /// This is useful for generating Dockerfiles programmatically or embedding + /// simple Dockerfiles directly in test code. + /// + /// # Arguments + /// + /// * `content` - The complete Dockerfile content as a string pub fn with_dockerfile_string(self, content: impl Into) -> Self { self.with_data(content.into(), "Dockerfile") } + /// Adds a file or directory from the filesystem to the build context. + /// + /// Be aware, that if you don't add the Dockerfile with the specific `with_dockerfile()` + /// or `with_dockerfile_string()` functions it has to be named `Dockerfile`in the build + /// context. Containerfile won't be recognized! + /// + /// # Arguments + /// + /// * `source` - Path to the file or directory on the local filesystem + /// * `target` - Path where the file should be placed in the build context pub fn with_file(mut self, source: impl Into, target: impl Into) -> Self { self.build_context_parts .push(CopyToContainer::new(source.into(), target)); self } + /// Adds data directly to the build context as a file. + /// + /// This method allows you to embed file content directly without requiring + /// files to exist on the filesystem. Useful for generated content, templates, + /// or small configuration files. + /// + /// # Arguments + /// + /// * `data` - The file content as bytes + /// * `target` - Path where the file should be placed in the build context pub fn with_data(mut self, data: impl Into>, target: impl Into) -> Self { self.build_context_parts .push(CopyToContainer::new(data.into(), target)); self } + /// Consumes the builder and returns the collected build context. + /// + /// This method finalizes the build context and returns a [`CopyToContainerCollection`] + /// that can be converted to a TAR archive for Docker. + /// + /// # Returns + /// + /// A [`CopyToContainerCollection`] containing all the build context materials. pub fn collect(self) -> CopyToContainerCollection { CopyToContainerCollection::new(self.build_context_parts) } + /// Returns the build context without consuming the builder. + /// + /// This method creates a clone of the current build context state, allowing + /// the builder to be reused or modified further. + /// + /// # Returns + /// + /// A [`CopyToContainerCollection`] containing all the current build context materials. pub fn as_copy_to_container_collection(&self) -> CopyToContainerCollection { CopyToContainerCollection::new(self.build_context_parts.clone()) } diff --git a/testcontainers/src/core/buildable.rs b/testcontainers/src/core/buildable.rs index cfbdf6fd..93d86339 100644 --- a/testcontainers/src/core/buildable.rs +++ b/testcontainers/src/core/buildable.rs @@ -1,10 +1,73 @@ use crate::{core::copy::CopyToContainerCollection, Image}; +/// Trait for images that can be built from within your tests or testcontainer libraries. +/// +/// Unlike the [`Image`] trait which represents existing Docker images, `BuildableImage` +/// represents images that need to be constructed from a, possibly even dynamic, `Dockerfile`` +/// and the needed Docker build context. +/// +/// If you want to dynamically create Dockerfiles look at Dockerfile generator crates like: +/// +/// +/// The build process, executed by [`crate::runners::SyncBuilder`] / [`crate::runners::AsyncBuilder`], follows these steps: +/// 1. Collect build context via `build_context()` which will be tarred and sent to buildkit. +/// 2. Generate image descriptor via `descriptor()` which will be passed to the container +/// 3. Build the Docker image using the Docker API +/// 4. Convert to runnable [`Image`] via `into_image()` which consumes the `BuildableImage` +/// into an `Image` +/// +/// # Example +/// +/// ```rust +/// use testcontainers::{GenericBuildableImage, runners::AsyncBuilder}; +/// +/// #[tokio::test] +/// async fn test_example() -> anyhow::Result<()> { +/// let image = GenericBuildableImage::new("example-tc", "1.0") +/// .with_dockerfile_string("FROM alpine:latest\nRUN echo 'hello'") +/// .build_image().await?; +/// // start container +/// // use it +/// } +/// ``` +/// pub trait BuildableImage { + /// The type of [`Image`] that this buildable image produces after building. type Built: Image; + /// Returns the build context containing all files and data needed to build the image. + /// + /// The build context consist of at least the `Dockerfile` and needs all the resources + /// referred to by the Dockerfile. + /// This is more or less equivalent to the directory you would pass to `docker build`. + /// + /// + /// + /// For creating build contexts, use the [`crate::core::BuildContextBuilder`] API when not using + /// [`crate::GenericBuildableImage`], which wraps `BuildContextBuilder` builder functions. + /// + /// # Returns + /// + /// A [`CopyToContainerCollection`] containing the build context in a form we + /// can send it to buildkit. fn build_context(&self) -> CopyToContainerCollection; + + /// Returns the image descriptor (name:tag) that will be assigned to the built image and be + /// passed down to the container for running. + /// + /// # Returns + /// + /// A string in the format "name:tag" that uniquely identifies the built image. fn descriptor(&self) -> String; + /// Consumes this buildable image and converts it into a runnable [`Image`]. + /// + /// This method is called after the Docker image has been successfully built. + /// It transforms the build specification into a standard [`Image`] that can be + /// started as a container. + /// + /// # Returns + /// + /// An [`Image`] instance configured to run the built Docker image. fn into_image(self) -> Self::Built; } diff --git a/testcontainers/src/images/generic.rs b/testcontainers/src/images/generic.rs index 7849ec79..c981114a 100644 --- a/testcontainers/src/images/generic.rs +++ b/testcontainers/src/images/generic.rs @@ -12,14 +12,12 @@ use crate::{ /// /// For example: /// -/// ``` +/// ```rust,ignore /// use testcontainers::{ /// core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage, ImageExt /// }; /// -/// # /* /// #[tokio::test] -/// # */ /// async fn test_redis() { /// let container = GenericImage::new("redis", "7.2.4") /// .with_exposed_port(6379.tcp()) diff --git a/testcontainers/src/lib.rs b/testcontainers/src/lib.rs index 70ad6293..a77cb080 100644 --- a/testcontainers/src/lib.rs +++ b/testcontainers/src/lib.rs @@ -22,7 +22,13 @@ //! //! Unsurprisingly, working with testcontainers is very similar to working with Docker itself. //! -//! 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. +//! If you need to build an image first, then you need to define the [`BuildableImage`] that specifies the build context +//! and the Dockerfile, then call the `build_image` method on it from either the [`AsyncBuilder`] or [`SyncBuilder`] trait. +//! This will yield an [`Image`] you could actually start. +//! +//! If you already have a Docker image you can just define your [`Image`] that you want to run, and then simply call the +//! `start` method on it from either the [`AsyncRunner`] or [`SyncRunner`] trait. +//! //! This will return you [`ContainerAsync`] or [`Container`] respectively. //! Containers implement `Drop`. As soon as they go out of scope, the underlying docker container is removed. //! To disable this behavior, you can set ENV variable `TESTCONTAINERS_COMMAND` to `keep`. @@ -60,7 +66,8 @@ //! # Ecosystem //! //! `testcontainers` is the core crate that provides an API for working with containers in a test environment. -//! The only image that is provided by the core crate is the [`GenericImage`], which is a simple wrapper around any docker image. +//! The only buildable image and image implementations that are provided by the core crate are the [`GenericBuildableImage`] +//! and [`GenericImage`], respectively. //! //! 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`]. //! @@ -70,6 +77,8 @@ //! //! [tc_website]: https://testcontainers.org //! [`Docker`]: https://docker.com +//! [`AsyncBuilder`]: runners::AsyncBuilder +//! [`SyncBuilder`]: runners::SyncBuilder //! [`AsyncRunner`]: runners::AsyncRunner //! [`SyncRunner`]: runners::SyncRunner //! [`testcontainers-modules`]: https://crates.io/crates/testcontainers-modules diff --git a/testcontainers/src/runners/async_builder.rs b/testcontainers/src/runners/async_builder.rs index 4861194b..839b699d 100644 --- a/testcontainers/src/runners/async_builder.rs +++ b/testcontainers/src/runners/async_builder.rs @@ -11,6 +11,29 @@ pub trait AsyncBuilder { } #[async_trait] +/// Helper trait to build Docker images asynchronously from [`BuildableImage`] instances. +/// +/// Provides an asynchronous interface for building custom Docker images within test environments. +/// This trait is automatically implemented for any type that implements [`BuildableImage`] + [`Send`]. +/// +/// # Example +/// +/// ```rust,no_run +/// use testcontainers::{core::WaitFor, runners::AsyncBuilder, runners::AsyncRunner, GenericBuildableImage}; +/// +/// #[test] +/// async fn test_custom_image() -> anyhow::Result<()> { +/// let image = GenericBuildableImage::new("my-test-app", "latest") +/// .with_dockerfile_string("FROM alpine:latest\nRUN echo 'hello'") +/// .build_image()?.await; +/// // Use the built image in containers +/// let container = image +/// .with_wait_for(WaitFor::message_on_stdout("Hello from test!")) +/// .start()?.await; +/// +/// Ok(()) +/// } +/// ``` impl AsyncBuilder for T where T: BuildableImage + Send, diff --git a/testcontainers/src/runners/sync_builder.rs b/testcontainers/src/runners/sync_builder.rs index d2e7b8ea..72001322 100644 --- a/testcontainers/src/runners/sync_builder.rs +++ b/testcontainers/src/runners/sync_builder.rs @@ -1,5 +1,28 @@ use crate::{core::error::Result, runners::sync_runner::lazy_sync_runner, BuildableImage}; +/// Helper trait to build Docker images synchronously from [`BuildableImage`] instances. +/// +/// Provides a blocking interface for building custom Docker images within test environments. +/// This trait is automatically implemented for any type that implements [`BuildableImage`] + [`Send`]. +/// +/// # Example +/// +/// ```rust,no_run +/// use testcontainers::{core::WaitFor, runners::SyncBuilder, runners::SyncRunner, GenericBuildableImage}; +/// +/// #[test] +/// fn test_custom_image() -> anyhow::Result<()> { +/// let image = GenericBuildableImage::new("my-test-app", "latest") +/// .with_dockerfile_string("FROM alpine:latest\nRUN echo 'hello'") +/// .build_image()?; +/// // Use the built image in containers +/// let container = image +/// .with_wait_for(WaitFor::message_on_stdout("Hello from test!")) +/// .start()?; +/// +/// Ok(()) +/// } +/// ``` pub trait SyncBuilder { fn build_image(self) -> Result; }