Skip to content

Commit f70ad00

Browse files
authored
feat(docker-compose): support auto-detection of docker-compose client (#898)
- Introduce auto-client detection (local if exists, or containerised otherwise) as an explicit option - Enhance containerised client with additional option "project-directory" - it matters because compose is called from Docker, but volumes and configs in compose files will mount from host system Backward compatibility preserved
1 parent 79d110f commit f70ad00

File tree

4 files changed

+402
-42
lines changed

4 files changed

+402
-42
lines changed

docs/features/docker_compose.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,32 @@ compose.up().await?;
151151
- Consistent compose version across environments
152152
- Useful for CI/CD where Docker CLI might not be available
153153

154+
If your compose files use relative paths for bind mounts, set an explicit project directory so
155+
docker compose resolves those paths against the host location:
156+
157+
```rust
158+
use testcontainers::compose::{ContainerisedComposeOptions, DockerCompose};
159+
160+
let options = ContainerisedComposeOptions::new(&["/home/me/app/docker-compose.yml"])
161+
.with_project_directory("/home/me/app");
162+
163+
let mut compose = DockerCompose::with_containerised_client(options).await?;
164+
compose.up().await?;
165+
```
166+
167+
### Auto Client
168+
169+
Tries the local `docker compose` CLI first and falls back to the containerised client:
170+
171+
```rust
172+
use testcontainers::compose::{AutoComposeOptions, DockerCompose};
173+
174+
let options = AutoComposeOptions::new(&["docker-compose.yml"]);
175+
176+
let mut compose = DockerCompose::with_auto_client(options).await?;
177+
compose.up().await?;
178+
```
179+
154180
## Configuration Options
155181

156182
### Environment Variables

testcontainers/src/compose/client.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{fmt, path::PathBuf};
22

3-
use crate::compose::error::Result;
3+
use crate::compose::{error::Result, ContainerisedComposeOptions};
44

55
pub(super) mod containerised;
66
pub(super) mod local;
@@ -15,9 +15,9 @@ impl ComposeClient {
1515
ComposeClient::Local(local::LocalComposeCli::new(compose_files))
1616
}
1717

18-
pub(super) async fn new_containerised(compose_files: Vec<PathBuf>) -> Result<Self> {
18+
pub(super) async fn new_containerised(options: ContainerisedComposeOptions) -> Result<Self> {
1919
Ok(ComposeClient::Containerised(Box::new(
20-
containerised::ContainerisedComposeCli::new(compose_files).await?,
20+
containerised::ContainerisedComposeCli::new(options).await?,
2121
)))
2222
}
2323
}

testcontainers/src/compose/client/containerised.rs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
use std::path::PathBuf;
2-
31
use crate::{
42
compose::{
53
client::{ComposeInterface, DownCommand, UpCommand},
64
error::Result,
5+
ContainerisedComposeOptions,
76
},
8-
core::{CmdWaitFor, ExecCommand, Mount},
7+
core::{CmdWaitFor, ExecCommand},
98
images::docker_cli::DockerCli,
109
runners::AsyncRunner,
1110
ContainerAsync, ContainerRequest, ImageExt,
@@ -14,32 +13,33 @@ use crate::{
1413
pub(crate) struct ContainerisedComposeCli {
1514
container: ContainerAsync<DockerCli>,
1615
compose_files_in_container: Vec<String>,
16+
project_directory: Option<String>,
1717
}
1818

1919
impl ContainerisedComposeCli {
20-
pub(super) async fn new(compose_files: Vec<PathBuf>) -> Result<Self> {
20+
pub(super) async fn new(options: ContainerisedComposeOptions) -> Result<Self> {
21+
let (compose_files, project_directory) = options.into_parts();
2122
let mut image = ContainerRequest::from(DockerCli::new("/var/run/docker.sock"));
2223

2324
let compose_files_in_container: Vec<String> = compose_files
2425
.iter()
2526
.enumerate()
2627
.map(|(i, _)| format!("/docker-compose-{i}.yml"))
2728
.collect();
28-
let mounts: Vec<_> = compose_files
29-
.iter()
29+
for (path, file_name) in compose_files
30+
.into_iter()
3031
.zip(compose_files_in_container.iter())
31-
.filter_map(|(path, file_name)| path.to_str().map(|p| Mount::bind_mount(p, file_name)))
32-
.collect();
33-
34-
for mount in mounts {
35-
image = image.with_mount(mount);
32+
{
33+
image = image.with_copy_to(file_name, path);
3634
}
3735

3836
let container = image.start().await?;
37+
let project_directory = project_directory.map(|path| path.to_string_lossy().into_owned());
3938

4039
Ok(Self {
4140
container,
4241
compose_files_in_container,
42+
project_directory,
4343
})
4444
}
4545
}
@@ -52,12 +52,15 @@ impl ComposeInterface for ContainerisedComposeCli {
5252
cmd_parts.push(format!("{}={}", key, value));
5353
}
5454

55-
cmd_parts.extend([
56-
"docker".to_string(),
57-
"compose".to_string(),
58-
"--project-name".to_string(),
59-
command.project_name.clone(),
60-
]);
55+
cmd_parts.extend(["docker".to_string(), "compose".to_string()]);
56+
57+
if let Some(project_directory) = &self.project_directory {
58+
cmd_parts.push("--project-directory".to_string());
59+
cmd_parts.push(project_directory.clone());
60+
}
61+
62+
cmd_parts.push("--project-name".to_string());
63+
cmd_parts.push(command.project_name.clone());
6164

6265
for file in &self.compose_files_in_container {
6366
cmd_parts.push("-f".to_string());
@@ -87,13 +90,18 @@ impl ComposeInterface for ContainerisedComposeCli {
8790
}
8891

8992
async fn down(&self, command: DownCommand) -> Result<()> {
90-
let mut cmd = vec![
91-
"docker".to_string(),
92-
"compose".to_string(),
93+
let mut cmd = vec!["docker".to_string(), "compose".to_string()];
94+
95+
if let Some(project_directory) = &self.project_directory {
96+
cmd.push("--project-directory".to_string());
97+
cmd.push(project_directory.clone());
98+
}
99+
100+
cmd.extend([
93101
"--project-name".to_string(),
94102
command.project_name.clone(),
95103
"down".to_string(),
96-
];
104+
]);
97105

98106
if command.volumes {
99107
cmd.push("--volumes".to_string());

0 commit comments

Comments
 (0)