Skip to content

Commit 7f089eb

Browse files
Add integration test support for Docker bind mounts (#871)
1 parent a3174bc commit 7f089eb

File tree

4 files changed

+66
-0
lines changed

4 files changed

+66
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
### Added
13+
14+
- `libcnb-test`:
15+
- 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))
1216

1317
## [0.24.0] - 2024-10-17
1418

libcnb-test/src/container_config.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::{HashMap, HashSet};
2+
use std::path::PathBuf;
23

34
/// Config used when starting a container.
45
///
@@ -31,6 +32,7 @@ pub struct ContainerConfig {
3132
pub(crate) command: Option<Vec<String>>,
3233
pub(crate) env: HashMap<String, String>,
3334
pub(crate) exposed_ports: HashSet<u16>,
35+
pub(crate) bind_mounts: HashMap<PathBuf, PathBuf>,
3436
}
3537

3638
impl ContainerConfig {
@@ -169,6 +171,37 @@ impl ContainerConfig {
169171
self
170172
}
171173

174+
/// Mount a host file or directory `source` into the container `target`. Useful for
175+
/// integration tests that depend on persistent storage shared between container executions.
176+
///
177+
/// See: [Docker Engine: Bind Mounts](https://docs.docker.com/engine/storage/bind-mounts/)
178+
///
179+
/// # Example
180+
/// ```no_run
181+
/// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
182+
///
183+
/// TestRunner::default().build(
184+
/// BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
185+
/// |context| {
186+
/// // ...
187+
/// context.start_container(
188+
/// ContainerConfig::new().bind_mount("/shared/cache", "/workspace/cache"),
189+
/// |container| {
190+
/// // ...
191+
/// },
192+
/// );
193+
/// },
194+
/// );
195+
/// ```
196+
pub fn bind_mount(
197+
&mut self,
198+
source: impl Into<PathBuf>,
199+
target: impl Into<PathBuf>,
200+
) -> &mut Self {
201+
self.bind_mounts.insert(source.into(), target.into());
202+
self
203+
}
204+
172205
/// Adds or updates multiple environment variable mappings for the container.
173206
///
174207
/// # Example

libcnb-test/src/docker.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::{BTreeMap, BTreeSet};
2+
use std::path::PathBuf;
23
use std::process::Command;
34

45
/// Represents a `docker run` command.
@@ -13,6 +14,7 @@ pub(crate) struct DockerRunCommand {
1314
image_name: String,
1415
platform: Option<String>,
1516
remove: bool,
17+
bind_mounts: BTreeMap<PathBuf, PathBuf>,
1618
}
1719

1820
impl DockerRunCommand {
@@ -27,6 +29,7 @@ impl DockerRunCommand {
2729
image_name: image_name.into(),
2830
platform: None,
2931
remove: false,
32+
bind_mounts: BTreeMap::new(),
3033
}
3134
}
3235

@@ -67,6 +70,11 @@ impl DockerRunCommand {
6770
self.remove = remove;
6871
self
6972
}
73+
74+
pub(crate) fn bind_mount<P: Into<PathBuf>>(&mut self, source: P, target: P) -> &mut Self {
75+
self.bind_mounts.insert(source.into(), target.into());
76+
self
77+
}
7078
}
7179

7280
impl From<DockerRunCommand> for Command {
@@ -98,6 +106,17 @@ impl From<DockerRunCommand> for Command {
98106
command.args(["--publish", &format!("127.0.0.1::{port}")]);
99107
}
100108

109+
for (source, target) in &docker_run_command.bind_mounts {
110+
command.args([
111+
"--mount",
112+
&format!(
113+
"type=bind,source={},target={}",
114+
source.to_string_lossy(),
115+
target.to_string_lossy()
116+
),
117+
]);
118+
}
119+
101120
command.arg(docker_run_command.image_name);
102121

103122
if let Some(container_command) = docker_run_command.command {
@@ -315,6 +334,8 @@ mod tests {
315334
docker_run_command.expose_port(55555);
316335
docker_run_command.platform("linux/amd64");
317336
docker_run_command.remove(true);
337+
docker_run_command.bind_mount(PathBuf::from("./test-cache"), PathBuf::from("/cache"));
338+
docker_run_command.bind_mount("foo", "/bar");
318339

319340
let command: Command = docker_run_command.clone().into();
320341
assert_eq!(
@@ -337,6 +358,10 @@ mod tests {
337358
"127.0.0.1::12345",
338359
"--publish",
339360
"127.0.0.1::55555",
361+
"--mount",
362+
"type=bind,source=./test-cache,target=/cache",
363+
"--mount",
364+
"type=bind,source=foo,target=/bar",
340365
"my-image",
341366
"echo",
342367
"hello",

libcnb-test/src/test_context.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ impl<'a> TestContext<'a> {
112112
docker_run_command.expose_port(*port);
113113
});
114114

115+
config.bind_mounts.iter().for_each(|(source, target)| {
116+
docker_run_command.bind_mount(source, target);
117+
});
118+
115119
// We create the ContainerContext early to ensure the cleanup in ContainerContext::drop
116120
// is still performed even if the Docker command panics.
117121
let container_context = ContainerContext {

0 commit comments

Comments
 (0)