Skip to main content

ironflow_ops_docker/
client.rs

1//! [`DockerClient`] -- central handle wrapping a [`bollard::Docker`] connection.
2
3use bollard::Docker;
4use ironflow_core::error::OperationError;
5use ironflow_core::operation::OperationContext;
6
7/// A handle to a Docker daemon connection.
8///
9/// Wraps [`bollard::Docker`] and provides construction helpers for common
10/// connection modes (local socket, TCP with optional TLS).
11///
12/// # Examples
13///
14/// ```no_run
15/// use ironflow_ops_docker::DockerClient;
16///
17/// # fn example() -> Result<(), ironflow_core::error::OperationError> {
18/// let client = DockerClient::connect_local()?;
19/// # Ok(())
20/// # }
21/// ```
22#[derive(Clone)]
23pub struct DockerClient {
24    docker: Docker,
25}
26
27impl DockerClient {
28    /// Wrap an existing [`bollard::Docker`] connection.
29    ///
30    /// # Examples
31    ///
32    /// ```no_run
33    /// use ironflow_ops_docker::DockerClient;
34    /// use bollard::Docker;
35    ///
36    /// # fn example() -> Result<(), ironflow_core::error::OperationError> {
37    /// let docker = Docker::connect_with_local_defaults()
38    ///     .map_err(|e| ironflow_core::error::OperationError::External {
39    ///         origin: "docker".to_string(),
40    ///         message: e.to_string(),
41    ///     })?;
42    /// let client = DockerClient::new(docker);
43    /// # Ok(())
44    /// # }
45    /// ```
46    pub fn new(docker: Docker) -> Self {
47        Self { docker }
48    }
49
50    /// Connect to the local Docker daemon using platform defaults.
51    ///
52    /// On Unix this uses the Unix socket at `/var/run/docker.sock`.
53    /// On Windows this uses the named pipe.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`OperationError::External`] if the connection cannot be
58    /// established.
59    ///
60    /// # Examples
61    ///
62    /// ```no_run
63    /// use ironflow_ops_docker::DockerClient;
64    ///
65    /// # fn example() -> Result<(), ironflow_core::error::OperationError> {
66    /// let client = DockerClient::connect_local()?;
67    /// # Ok(())
68    /// # }
69    /// ```
70    pub fn connect_local() -> Result<Self, OperationError> {
71        let docker =
72            Docker::connect_with_local_defaults().map_err(|e| OperationError::External {
73                origin: "docker".to_string(),
74                message: e.to_string(),
75            })?;
76        Ok(Self { docker })
77    }
78
79    /// Connect to a Docker daemon at the given host URL.
80    ///
81    /// Supports `unix://`, `tcp://`, and `http://` schemes.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`OperationError::External`] if the connection cannot be
86    /// established.
87    ///
88    /// # Examples
89    ///
90    /// ```no_run
91    /// use ironflow_ops_docker::DockerClient;
92    ///
93    /// # fn example() -> Result<(), ironflow_core::error::OperationError> {
94    /// let client = DockerClient::connect_with_url("tcp://localhost:2375", 120)?;
95    /// # Ok(())
96    /// # }
97    /// ```
98    pub fn connect_with_url(url: &str, timeout_secs: u64) -> Result<Self, OperationError> {
99        let docker = Docker::connect_with_http(url, timeout_secs, bollard::API_DEFAULT_VERSION)
100            .map_err(|e| OperationError::External {
101                origin: "docker".to_string(),
102                message: e.to_string(),
103            })?;
104        Ok(Self { docker })
105    }
106
107    /// Build a [`DockerClient`] from an [`OperationContext`].
108    ///
109    /// Reads `docker_host` from the context to determine the connection mode.
110    /// If `docker_host` is not set, connects to the local daemon using
111    /// platform defaults.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`OperationError::External`] if the connection fails.
116    ///
117    /// # Examples
118    ///
119    /// ```no_run
120    /// use ironflow_ops_docker::DockerClient;
121    /// use ironflow_core::operation::{OperationContext, NoopSecretResolver};
122    /// use std::sync::Arc;
123    ///
124    /// # async fn example() -> Result<(), ironflow_core::error::OperationError> {
125    /// let ctx = OperationContext::new(Arc::new(NoopSecretResolver));
126    /// let client = DockerClient::from_context(&ctx)?;
127    /// # Ok(())
128    /// # }
129    /// ```
130    pub fn from_context(_ctx: &OperationContext) -> Result<Self, OperationError> {
131        Self::connect_local()
132    }
133
134    /// Get a reference to the underlying [`bollard::Docker`] connection.
135    pub fn docker(&self) -> &Docker {
136        &self.docker
137    }
138
139    /// Consume the client and return the underlying [`bollard::Docker`] connection.
140    pub fn into_inner(self) -> Docker {
141        self.docker
142    }
143}
144
145impl std::fmt::Debug for DockerClient {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("DockerClient")
148            .field("connected", &true)
149            .finish()
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn debug_does_not_leak_internals() {
159        let client = DockerClient::connect_local();
160        if let Ok(c) = client {
161            let debug = format!("{c:?}");
162            assert!(debug.contains("DockerClient"));
163            assert!(!debug.contains("docker.sock"));
164        }
165    }
166}