Skip to content

Commit

Permalink
fix!: docker socket detection on unix (#721)
Browse files Browse the repository at this point in the history
Docker Desktop on macOS creates the socket in the user's home directory
from version
4.13.0.[[1](https://github.com/docker/for-mac/issues/6529)][[2](https://github.com/testcontainers/testcontainers-java/issues/6165)]

And the Docker team [has the intention to move
away](docker/for-mac#6529 (comment))
from the root-owned `/var/run/docker.sock`

There is a option named `Allow the default Docket Socket to be used
(requires password)` in the Docker Desktop, but the created symbolic
link `/var/run/docker.sock` [doesn't
persist](lando/lando#3533 (comment))
between OS restart or OS upgrade.

Without this patch or creating a symbolic link, macOS devs have to face
the ```called `Result::unwrap()` on an `Err` value:
Client(Init(SocketNotFoundError("/var/run/docker.sock")))``` error.

I have used `Cow` to avoid the need of cloning the fields of `Config`
struct. I am open to suggestion or other approaches.

Thanks!

---------

Co-authored-by: Artem Medvedev <[email protected]>
  • Loading branch information
mominul and DDtKey authored Aug 30, 2024
1 parent 5dff97f commit 202ec4a
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 7 deletions.
2 changes: 1 addition & 1 deletion testcontainers/src/core/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ impl Client {
}

pub(crate) async fn docker_hostname(&self) -> Result<url::Host, ClientError> {
let docker_host = self.config.docker_host();
let docker_host = &self.config.docker_host();
let docker_host_url = Url::from_str(docker_host)
.map_err(|e| ConfigurationError::InvalidDockerHost(e.to_string()))?;

Expand Down
4 changes: 2 additions & 2 deletions testcontainers/src/core/client/bollard_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::core::env;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2 * 60);

pub(super) fn init(config: &env::Config) -> Result<Docker, bollard::errors::Error> {
let host = config.docker_host();
let host = &config.docker_host();
let host_url = Url::from_str(host)?;

match host_url.scheme() {
Expand Down Expand Up @@ -36,7 +36,7 @@ fn connect_with_ssl(config: &env::Config) -> Result<Docker, bollard::errors::Err
let cert_path = config.cert_path().expect("cert path not found");

Docker::connect_with_ssl(
config.docker_host(),
&config.docker_host(),
&cert_path.join("key.pem"),
&cert_path.join("cert.pem"),
&cert_path.join("ca.pem"),
Expand Down
51 changes: 48 additions & 3 deletions testcontainers/src/core/env/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::{
borrow::Cow,
path::{Path, PathBuf},
str::FromStr,
};

use dirs::{home_dir, runtime_dir};

use crate::core::env::GetEnvValue;

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -125,12 +128,45 @@ impl Config {
/// 1. Docker host from the `tc.host` property in the `~/.testcontainers.properties` file.
/// 2. `DOCKER_HOST` environment variable.
/// 3. Docker host from the `docker.host` property in the `~/.testcontainers.properties` file.
/// 4. Else, the default Docker socket will be returned.
pub(crate) fn docker_host(&self) -> &str {
/// 4. Read the default Docker socket path, without the unix schema. E.g. `/var/run/docker.sock`.
/// 5. Read the rootless Docker socket path, checking in the following alternative locations:
/// 1. `${XDG_RUNTIME_DIR}/.docker/run/docker.sock`.
/// 2. `${HOME}/.docker/run/docker.sock`.
/// 3. `${HOME}/.docker/desktop/docker.sock`.
/// 6. The default Docker socket including schema will be returned if none of the above are set.
pub(crate) fn docker_host(&self) -> Cow<'_, str> {
self.tc_host
.as_deref()
.or(self.host.as_deref())
.unwrap_or(DEFAULT_DOCKER_HOST)
.map(Cow::Borrowed)
.unwrap_or_else(|| {
if cfg!(unix) {
validate_path("/var/run/docker.sock".into())
.or_else(|| {
runtime_dir().and_then(|dir| {
validate_path(format!("{}/.docker/run/docker.sock", dir.display()))
})
})
.or_else(|| {
home_dir().and_then(|dir| {
validate_path(format!("{}/.docker/run/docker.sock", dir.display()))
})
})
.or_else(|| {
home_dir().and_then(|dir| {
validate_path(format!(
"{}/.docker/desktop/docker.sock",
dir.display()
))
})
})
.map(|p| format!("unix://{p}"))
.map(Cow::Owned)
.unwrap_or(DEFAULT_DOCKER_HOST.into())
} else {
DEFAULT_DOCKER_HOST.into()
}
})
}

pub(crate) fn tls_verify(&self) -> bool {
Expand All @@ -150,6 +186,15 @@ impl Config {
}
}

/// Validate the path exists and return it if it does.
fn validate_path(path: String) -> Option<String> {
if Path::new(&path).exists() {
Some(path)
} else {
None
}
}

/// Read the Docker authentication configuration in the following order:
///
/// 1. `DOCKER_AUTH_CONFIG` environment variable, unmarshalling the string value from its JSON representation and using it as the Docker config.
Expand Down
7 changes: 6 additions & 1 deletion testcontainers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@
//! 1. Docker host from the `tc.host` property in the `~/.testcontainers.properties` file.
//! 2. `DOCKER_HOST` environment variable.
//! 3. Docker host from the "docker.host" property in the `~/.testcontainers.properties` file.
//! 4. Else, the default Docker socket will be returned.
//! 4. Read the default Docker socket path, without the unix schema. E.g. `/var/run/docker.sock`.
//! 5. Read the rootless Docker socket path, checking in the following alternative locations:
//! 1. `${XDG_RUNTIME_DIR}/.docker/run/docker.sock`.
//! 2. `${HOME}/.docker/run/docker.sock`.
//! 3. `${HOME}/.docker/desktop/docker.sock`.
//! 6. The default Docker socket including schema will be returned if none of the above are set.
//!
//! ### Docker authentication
//!
Expand Down

0 comments on commit 202ec4a

Please sign in to comment.