diff --git a/libcnb-test/src/container_config.rs b/libcnb-test/src/container_config.rs index 979e1f7c..c3dcf48c 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::{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) volumes: HashMap, } impl ContainerConfig { @@ -169,6 +171,37 @@ impl ContainerConfig { self } + /// Mounts a named volume `source` into the container `destination`. Useful for integration + /// tests that depend on persistent storage shared between container executions. + /// + /// See: [Docker CLI, Mount Volume](https://docs.docker.com/reference/cli/docker/container/run/#volume) + /// + /// # Example + /// ```no_run + /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner}; + /// use std::path::PathBuf; + /// + /// TestRunner::default().build( + /// BuildConfig::new("heroku/builder:22", "tests/fixtures/app"), + /// |context| { + /// // ... + /// context.start_container( + /// ContainerConfig::new().volume(PathBuf::from("/shared/cache"), PathBuf::from("/workspace/cache")), + /// |container| { + /// // ... + /// }, + /// ); + /// }, + /// ); + /// ``` + pub fn volume>(&mut self, source: P, destination: P) -> &mut Self { + self.volumes.insert( + source.as_ref().to_path_buf(), + destination.as_ref().to_path_buf(), + ); + 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..46ac8fb5 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. @@ -9,6 +10,7 @@ pub(crate) struct DockerRunCommand { detach: bool, entrypoint: Option, env: BTreeMap, + volumes: BTreeMap, exposed_ports: BTreeSet, image_name: String, platform: Option, @@ -23,6 +25,7 @@ impl DockerRunCommand { detach: false, entrypoint: None, env: BTreeMap::new(), + volumes: BTreeMap::new(), exposed_ports: BTreeSet::new(), image_name: image_name.into(), platform: None, @@ -53,6 +56,15 @@ impl DockerRunCommand { self } + pub(crate) fn volume( + &mut self, + source: impl Into, + destination: impl Into, + ) -> &mut Self { + self.volumes.insert(source.into(), destination.into()); + self + } + pub(crate) fn expose_port(&mut self, port: u16) -> &mut Self { self.exposed_ports.insert(port); self @@ -98,6 +110,17 @@ impl From for Command { command.args(["--publish", &format!("127.0.0.1::{port}")]); } + for (source, destination) in &docker_run_command.volumes { + command.args([ + "--volume", + &format!( + "{}:{}", + source.to_string_lossy(), + destination.to_string_lossy() + ), + ]); + } + command.arg(docker_run_command.image_name); if let Some(container_command) = docker_run_command.command { @@ -311,6 +334,7 @@ mod tests { docker_run_command.entrypoint("/usr/bin/bash"); docker_run_command.env("BAR", "2"); docker_run_command.env("FOO", "1"); + docker_run_command.volume(PathBuf::from("./test-cache"), PathBuf::from("/cache")); docker_run_command.expose_port(12345); docker_run_command.expose_port(55555); docker_run_command.platform("linux/amd64"); @@ -337,6 +361,8 @@ mod tests { "127.0.0.1::12345", "--publish", "127.0.0.1::55555", + "--volume", + "./test-cache:/cache", "my-image", "echo", "hello", diff --git a/libcnb-test/src/test_context.rs b/libcnb-test/src/test_context.rs index 807b349d..5573f5a4 100644 --- a/libcnb-test/src/test_context.rs +++ b/libcnb-test/src/test_context.rs @@ -108,6 +108,10 @@ impl<'a> TestContext<'a> { docker_run_command.env(key, value); }); + config.volumes.iter().for_each(|(source, destination)| { + docker_run_command.volume(source, destination); + }); + config.exposed_ports.iter().for_each(|port| { docker_run_command.expose_port(*port); });