diff --git a/CHANGELOG.md b/CHANGELOG.md index 7258d801..c008a107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `libcnb-test`: + - Added `ContainerConfig::bind_mount` to support mounting a host machine file or directory into a container. ([#871](https://github.com/heroku/libcnb.rs/pull/871)) ## [0.24.0] - 2024-10-17 diff --git a/libcnb-test/src/container_config.rs b/libcnb-test/src/container_config.rs index 979e1f7c..5bba89b2 100644 --- a/libcnb-test/src/container_config.rs +++ b/libcnb-test/src/container_config.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; /// Config used when starting a container. /// @@ -31,6 +32,7 @@ pub struct ContainerConfig { pub(crate) command: Option>, pub(crate) env: HashMap, pub(crate) exposed_ports: HashSet, + pub(crate) bind_mounts: HashMap, } impl ContainerConfig { @@ -169,6 +171,37 @@ impl ContainerConfig { self } + /// Mount a host file or directory `source` into the container `target`. Useful for + /// integration tests that depend on persistent storage shared between container executions. + /// + /// See: [Docker Engine: Bind Mounts](https://docs.docker.com/engine/storage/bind-mounts/) + /// + /// # Example + /// ```no_run + /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner}; + /// + /// TestRunner::default().build( + /// BuildConfig::new("heroku/builder:22", "tests/fixtures/app"), + /// |context| { + /// // ... + /// context.start_container( + /// ContainerConfig::new().bind_mount("/shared/cache", "/workspace/cache"), + /// |container| { + /// // ... + /// }, + /// ); + /// }, + /// ); + /// ``` + pub fn bind_mount( + &mut self, + source: impl Into, + target: impl Into, + ) -> &mut Self { + self.bind_mounts.insert(source.into(), target.into()); + self + } + /// Adds or updates multiple environment variable mappings for the container. /// /// # Example diff --git a/libcnb-test/src/docker.rs b/libcnb-test/src/docker.rs index 38eb88a2..d1edf075 100644 --- a/libcnb-test/src/docker.rs +++ b/libcnb-test/src/docker.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; use std::process::Command; /// Represents a `docker run` command. @@ -13,6 +14,7 @@ pub(crate) struct DockerRunCommand { image_name: String, platform: Option, remove: bool, + bind_mounts: BTreeMap, } impl DockerRunCommand { @@ -27,6 +29,7 @@ impl DockerRunCommand { image_name: image_name.into(), platform: None, remove: false, + bind_mounts: BTreeMap::new(), } } @@ -67,6 +70,11 @@ impl DockerRunCommand { self.remove = remove; self } + + pub(crate) fn bind_mount>(&mut self, source: P, target: P) -> &mut Self { + self.bind_mounts.insert(source.into(), target.into()); + self + } } impl From for Command { @@ -98,6 +106,17 @@ impl From for Command { command.args(["--publish", &format!("127.0.0.1::{port}")]); } + for (source, target) in &docker_run_command.bind_mounts { + command.args([ + "--mount", + &format!( + "type=bind,source={},target={}", + source.to_string_lossy(), + target.to_string_lossy() + ), + ]); + } + command.arg(docker_run_command.image_name); if let Some(container_command) = docker_run_command.command { @@ -315,6 +334,8 @@ mod tests { docker_run_command.expose_port(55555); docker_run_command.platform("linux/amd64"); docker_run_command.remove(true); + docker_run_command.bind_mount(PathBuf::from("./test-cache"), PathBuf::from("/cache")); + docker_run_command.bind_mount("foo", "/bar"); let command: Command = docker_run_command.clone().into(); assert_eq!( @@ -337,6 +358,10 @@ mod tests { "127.0.0.1::12345", "--publish", "127.0.0.1::55555", + "--mount", + "type=bind,source=./test-cache,target=/cache", + "--mount", + "type=bind,source=foo,target=/bar", "my-image", "echo", "hello", diff --git a/libcnb-test/src/test_context.rs b/libcnb-test/src/test_context.rs index 807b349d..2b981371 100644 --- a/libcnb-test/src/test_context.rs +++ b/libcnb-test/src/test_context.rs @@ -112,6 +112,10 @@ impl<'a> TestContext<'a> { docker_run_command.expose_port(*port); }); + config.bind_mounts.iter().for_each(|(source, target)| { + docker_run_command.bind_mount(source, target); + }); + // We create the ContainerContext early to ensure the cleanup in ContainerContext::drop // is still performed even if the Docker command panics. let container_context = ContainerContext {