From cee8c7f5cd80b410c69a8c83db1a198f66f55523 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Sun, 27 Aug 2023 17:47:17 -0300 Subject: [PATCH 1/7] detect client host endpoint --- Cargo.toml | 7 +- src/detect_host.rs | 201 +++++++++++++++++++++++++++++++++++++++++++++ src/docker.rs | 16 ++++ src/lib.rs | 2 + 4 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 src/detect_host.rs diff --git a/Cargo.toml b/Cargo.toml index bafb0f8..80f5cdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,11 @@ hyper = { version="0.14", features=["client", "http1", "tcp", "stream"] } hyper-openssl = { version="0.9", optional=true } openssl = { version="0.10", optional=true } +anyhow = { version="1.0", optional=true } +dirs = { version="5.0", optional=true } +hex = { version="0.4", optional=true } +sha2 = { version="0.10", optional=true } + [dev-dependencies] env_logger = "0.9" # Required for examples to run @@ -65,7 +70,7 @@ tls = ["containers-api/tls"] vendored-ssl = ["tls", "containers-api/vendored-ssl"] par-compress = ["containers-api/par-compress"] swarm = [] - +detect-host = ["dep:anyhow", "dep:dirs", "dep:hex", "dep:sha2"] # docs.rs-specific configuration [package.metadata.docs.rs] diff --git a/src/detect_host.rs b/src/detect_host.rs new file mode 100644 index 0000000..b84ada0 --- /dev/null +++ b/src/detect_host.rs @@ -0,0 +1,201 @@ +//! Auto detect the docker endpoint the same way docker-cli does +//! +//! Reference: + +use anyhow::Context; +use dirs::home_dir; +use env_vars::{DOCKER_CONFIG, DOCKER_CONTEXT, DOCKER_HOST}; +use sha2::{Digest, Sha256}; +use std::{fs, path::PathBuf, str::FromStr}; + +#[cfg(unix)] +pub const DEFAULT_DOCKER_ENDPOINT: &str = "unix:///var/run/docker.sock"; + +/// For windows the default endpoint is "npipe:////./pipe/docker_engine" +/// But currently this is not supported by docker-api, using to default tcp endpoint instead +/// https://github.com/vv9k/docker-api-rs/issues/57 +#[cfg(not(unix))] +pub const DEFAULT_DOCKER_ENDPOINT: &str = "tcp://127.0.0.1:2375"; + +/// List of environment variables supported by the `docker` command +pub(crate) mod env_vars { + use std::ffi::OsStr; + + /// The location of your client configuration files. + pub const DOCKER_CONFIG: &str = "DOCKER_CONFIG"; + + /// Name of the `docker context` to use (overrides `DOCKER_HOST` env var and default context set with `docker context use`) + pub const DOCKER_CONTEXT: &str = "DOCKER_CONTEXT"; + + /// Daemon socket to connect to. + pub const DOCKER_HOST: &str = "DOCKER_HOST"; + + /// Load an environment variable and verify if it's not empty + pub fn non_empty_var>(key: K) -> Option { + let value = std::env::var(key).ok()?; + if value.trim().is_empty() { + None + } else { + Some(value) + } + } +} + +enum EndpointError { + InvalidEndpoint, + CannotFindUserHomeDir, + InvalidJson { + filepath: PathBuf, + error: serde_json::Error, + }, + IOError(std::io::Error), +} + +/// Find the docker host the same way as docker-cli does +/// +/// # Steps +/// 1. Try to load the endpoint from the `DOCKER_CONTEXT` environment variable +/// 2. Try to load the endpoint from the `DOCKER_HOST` environment variable +/// 3. Try to load the endpoint from the `config.json` file +/// 4. Return the default endpoint +/// +/// # Fails when +/// * Cannot find docker config directory +/// * `DOCKER_CONTEXT` is defined and is invalid +/// * `config.js` file exists and fails to read or parse it +/// * `config.js` have the `currentContext` property defined, but fails to find the context endpoint +pub fn find_docker_host() -> Result { + // If defined, Load the endpoint from the `DOCKER_CONTEXT` environment variable + if let Some(context) = env_vars::non_empty_var(DOCKER_CONTEXT) { + let config_directory = docker_config_dir()?; + return host_from_context(&context, config_directory); + } + + // If defined, return the host from the `DOCKER_HOST` environment variable + if let Some(host) = env_vars::non_empty_var(DOCKER_HOST) { + return Ok(host); + } + + // If the config.json file exists, try to load the endpoint from it + let config_file = docker_config_dir().map(|config_dir| config_dir.join("config.json"))?; + if config_file.exists() { + let maybe_host = host_from_config_file(config_file)?; + return maybe_host.ok_or_else(|| EndpointError::InvalidEndpoint); + } + + // otherwise return the default endpoint + Ok(DEFAULT_DOCKER_ENDPOINT.to_string()) +} + +/// By default, the Docker-cli stores its configuration files in a directory called +/// `.docker` within your `$HOME` directory. the default location can be overridden by +/// the `DOCKER_CONFIG` environment variable. +/// +/// Reference: +/// https://github.com/docker/cli/blob/v24.0.5/man/docker-config-json.5.md +/// +/// Fails if the user home directory cannot be found +pub fn docker_config_dir() -> Result { + // Try to load the config directory from the `DOCKER_CONFIG` environment variable + if let Some(config_directory) = env_vars::non_empty_var(DOCKER_CONFIG).map(PathBuf::from) { + return Ok(config_directory); + } + + // Use the default config directory at $HOME/.docker/ + let Some(config_directory) = home_dir().map(|path| path.join(".docker/")) else { + return Err(EndpointError::CannotFindUserHomeDir) + }; + Ok(config_directory) +} + +/// Attempts to load the endpoint from the `.docker/config.json` file +/// +/// # Returns +/// * Ok(Some(host)) - if the config.js exists and contains currentContext field +/// * Ok(None) - if the config.js exists and not contain currentContext field +/// +/// # Fails when +/// * config.js doesn't exists +/// * cannot read or parse the config.js file +/// * the currentContext is defined, but fails to load the context endpoint +pub fn host_from_config_file(config_file: PathBuf) -> Result, EndpointError> { + // Read the config.json file and extract the current context + let config_file_json = file_to_json(&config_file)?; + + // Check if the config file has the property currentContext + let current_context = config_file_json + .get("currentContext") + .and_then(|value| value.as_str()) + .map(str::to_string); + + if let Some(context) = current_context { + let config_directory = config_file + .parent() + .map(|config_dir| config_dir.to_path_buf()) + .unwrap_or_default(); + let endpoint = host_from_context(&context, config_directory)?; + Ok(Some(endpoint)) + } else { + Ok(None) + } +} + +/// Load the Host of a given context, the context's host is located at: +/// UNIX: +/// - $HOME/.docker/contexts/meta//meta.json +/// Windows: +/// - %USERPROFILE%\.docker\contexts\meta\\meta.json +/// +/// Is possible to list contexts by running `docker context ls` +pub fn host_from_context( + context: &str, + mut config_directory: PathBuf, +) -> Result { + let metadata_filepath = { + // $HOME/.docker/contexts/meta//meta.json + let digest = sha256_digest(context); + config_directory.extend(["contexts", "meta", digest.as_str(), "meta.json"]); + config_directory + }; + + host_from_metadata_file(metadata_filepath) +} + +/// Parses the `meta.json` file and extract the docker endpoint +/// The endpoint is located at: `meta.Endpoints.Endpoints.docker.Host` +pub fn host_from_metadata_file(meta_filepath: PathBuf) -> Result { + let meta_json = file_to_json(&meta_filepath)?; + + let host = meta_json + .get("Endpoints") + .and_then(|value| value.get("Endpoints")) + .and_then(|value| value.get("docker")) + .and_then(|value| value.get("Host")) + .and_then(|value| value.as_str()) + .ok_or_else(|error| EndpointError::InvalidJson { + filepath: meta_filepath, + error, + })? + .to_string(); + + Ok(host) +} + +/// Parsers a file to a json value +fn file_to_json(filepath: &PathBuf) -> Result { + fs::read_to_string(filepath) + .map_err(|error| EndpointError::IOError(error))? + .parse::() + .map_err(|error| EndpointError::InvalidJson { + filepath: filepath.clone(), + error, + }) +} + +/// Returns the sha256 hex-digest of a given string +fn sha256_digest(name: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(name.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) +} diff --git a/src/docker.rs b/src/docker.rs index 4693752..cb4e07c 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -16,6 +16,7 @@ use crate::conn::get_https_connector; #[cfg(unix)] use crate::conn::get_unix_connector; +use crate::detect_host::DEFAULT_DOCKER_ENDPOINT; use futures_util::{ io::{AsyncRead, AsyncWrite}, stream::Stream, @@ -33,6 +34,21 @@ pub struct Docker { client: RequestClient, } +#[cfg(feature = "detect-host")] +impl Default for Docker { + fn default() -> Self { + use crate::detect_host::{find_docker_host, DEFAULT_DOCKER_ENDPOINT}; + // Try to connect to the configured host, otherwise connect to default endpoint + find_docker_host() + .ok() + .and_then(|endpoint| Self::new(endpoint).ok()) + .unwrap_or_else(|| { + Self::new(DEFAULT_DOCKER_ENDPOINT) + .expect("the default endpoint is always valid: qed") + }) + } +} + impl Docker { /// Creates a new Docker instance by automatically choosing appropriate connection type based /// on provided `uri`. diff --git a/src/lib.rs b/src/lib.rs index 730483c..7c3d0fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,8 @@ pub mod conn { pub use containers_api::conn::{Error, Transport, TtyChunk}; } pub mod docker; +#[cfg(feature = "detect-host")] +pub mod detect_host; pub mod errors; pub mod opts; From 7605b74f4de1dfc807bd9beb03e8c237aac6f696 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Sun, 27 Aug 2023 17:48:13 -0300 Subject: [PATCH 2/7] cargo fmt --- src/detect_host.rs | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/detect_host.rs b/src/detect_host.rs index b84ada0..b41437d 100644 --- a/src/detect_host.rs +++ b/src/detect_host.rs @@ -103,7 +103,7 @@ pub fn docker_config_dir() -> Result { // Use the default config directory at $HOME/.docker/ let Some(config_directory) = home_dir().map(|path| path.join(".docker/")) else { - return Err(EndpointError::CannotFindUserHomeDir) + return Err(EndpointError::CannotFindUserHomeDir); }; Ok(config_directory) } diff --git a/src/lib.rs b/src/lib.rs index 7c3d0fb..01bc32f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,9 +39,9 @@ pub mod conn { pub(crate) use containers_api::conn::*; pub use containers_api::conn::{Error, Transport, TtyChunk}; } -pub mod docker; #[cfg(feature = "detect-host")] pub mod detect_host; +pub mod docker; pub mod errors; pub mod opts; From 41bcd3f3fdfd0c7c3c6cdead30ff60f55b41810e Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Sun, 27 Aug 2023 17:54:58 -0300 Subject: [PATCH 3/7] Remove anyhow --- Cargo.toml | 3 +-- src/detect_host.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 80f5cdb..5416e11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ hyper = { version="0.14", features=["client", "http1", "tcp", "stream"] } hyper-openssl = { version="0.9", optional=true } openssl = { version="0.10", optional=true } -anyhow = { version="1.0", optional=true } dirs = { version="5.0", optional=true } hex = { version="0.4", optional=true } sha2 = { version="0.10", optional=true } @@ -70,7 +69,7 @@ tls = ["containers-api/tls"] vendored-ssl = ["tls", "containers-api/vendored-ssl"] par-compress = ["containers-api/par-compress"] swarm = [] -detect-host = ["dep:anyhow", "dep:dirs", "dep:hex", "dep:sha2"] +detect-host = ["dep:dirs", "dep:hex", "dep:sha2"] # docs.rs-specific configuration [package.metadata.docs.rs] diff --git a/src/detect_host.rs b/src/detect_host.rs index b41437d..f7fc2ec 100644 --- a/src/detect_host.rs +++ b/src/detect_host.rs @@ -2,7 +2,6 @@ //! //! Reference: -use anyhow::Context; use dirs::home_dir; use env_vars::{DOCKER_CONFIG, DOCKER_CONTEXT, DOCKER_HOST}; use sha2::{Digest, Sha256}; From 140c44f9eb49a2fd09d2d2a64a175c2aff1ae8d6 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Sun, 27 Aug 2023 18:08:22 -0300 Subject: [PATCH 4/7] Fix lint errors --- src/detect_host.rs | 8 ++++---- src/docker.rs | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/detect_host.rs b/src/detect_host.rs index f7fc2ec..fd7ef7c 100644 --- a/src/detect_host.rs +++ b/src/detect_host.rs @@ -4,8 +4,9 @@ use dirs::home_dir; use env_vars::{DOCKER_CONFIG, DOCKER_CONTEXT, DOCKER_HOST}; +use serde::de::Error as SerdeError; use sha2::{Digest, Sha256}; -use std::{fs, path::PathBuf, str::FromStr}; +use std::{fs, path::PathBuf}; #[cfg(unix)] pub const DEFAULT_DOCKER_ENDPOINT: &str = "unix:///var/run/docker.sock"; @@ -167,13 +168,12 @@ pub fn host_from_metadata_file(meta_filepath: PathBuf) -> Result Self { - use crate::detect_host::{find_docker_host, DEFAULT_DOCKER_ENDPOINT}; // Try to connect to the configured host, otherwise connect to default endpoint find_docker_host() .ok() From b9a91557b810ab3c1899d4030358b6064d21cf6c Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Sun, 27 Aug 2023 18:18:29 -0300 Subject: [PATCH 5/7] make EndpointError public --- src/detect_host.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/detect_host.rs b/src/detect_host.rs index fd7ef7c..9e42cac 100644 --- a/src/detect_host.rs +++ b/src/detect_host.rs @@ -41,8 +41,8 @@ pub(crate) mod env_vars { } } -enum EndpointError { - InvalidEndpoint, +#[derive(Debug)] +pub enum EndpointError { CannotFindUserHomeDir, InvalidJson { filepath: PathBuf, @@ -80,7 +80,7 @@ pub fn find_docker_host() -> Result { let config_file = docker_config_dir().map(|config_dir| config_dir.join("config.json"))?; if config_file.exists() { let maybe_host = host_from_config_file(config_file)?; - return maybe_host.ok_or_else(|| EndpointError::InvalidEndpoint); + return Ok(maybe_host.unwrap_or_else(|| DEFAULT_DOCKER_ENDPOINT.to_string())); } // otherwise return the default endpoint From 9c2e2b379b4df4f3c9bbbcfa5da6a8267d322e81 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Sun, 27 Aug 2023 18:22:24 -0300 Subject: [PATCH 6/7] Fix clippy warning --- src/detect_host.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detect_host.rs b/src/detect_host.rs index 9e42cac..6b0050f 100644 --- a/src/detect_host.rs +++ b/src/detect_host.rs @@ -183,7 +183,7 @@ pub fn host_from_metadata_file(meta_filepath: PathBuf) -> Result Result { fs::read_to_string(filepath) - .map_err(|error| EndpointError::IOError(error))? + .map_err(EndpointError::IOError)? .parse::() .map_err(|error| EndpointError::InvalidJson { filepath: filepath.clone(), From 27393242cd2adf8130ad65e52fb5a671d0da3fbc Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Sun, 27 Aug 2023 18:24:53 -0300 Subject: [PATCH 7/7] Fix typo --- src/detect_host.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/detect_host.rs b/src/detect_host.rs index 6b0050f..32d3535 100644 --- a/src/detect_host.rs +++ b/src/detect_host.rs @@ -62,8 +62,8 @@ pub enum EndpointError { /// # Fails when /// * Cannot find docker config directory /// * `DOCKER_CONTEXT` is defined and is invalid -/// * `config.js` file exists and fails to read or parse it -/// * `config.js` have the `currentContext` property defined, but fails to find the context endpoint +/// * `config.json` file exists and fails to read or parse it +/// * `config.json` have the `currentContext` property defined, but fails to find the context endpoint pub fn find_docker_host() -> Result { // If defined, Load the endpoint from the `DOCKER_CONTEXT` environment variable if let Some(context) = env_vars::non_empty_var(DOCKER_CONTEXT) { @@ -111,11 +111,11 @@ pub fn docker_config_dir() -> Result { /// Attempts to load the endpoint from the `.docker/config.json` file /// /// # Returns -/// * Ok(Some(host)) - if the config.js exists and contains currentContext field -/// * Ok(None) - if the config.js exists and not contain currentContext field +/// * Ok(Some(host)) - if the config.json exists and contains currentContext field +/// * Ok(None) - if the config.json exists and not contain currentContext field /// /// # Fails when -/// * config.js doesn't exists +/// * config.json doesn't exists /// * cannot read or parse the config.js file /// * the currentContext is defined, but fails to load the context endpoint pub fn host_from_config_file(config_file: PathBuf) -> Result, EndpointError> {