opencode_cloud_core/docker/
client.rs1use bollard::Docker;
7use std::time::Duration;
8
9use super::error::DockerError;
10use crate::host::{HostConfig, SshTunnel};
11
12pub struct DockerClient {
14 inner: Docker,
15 _tunnel: Option<SshTunnel>,
17 host_name: Option<String>,
19}
20
21impl DockerClient {
22 pub fn new() -> Result<Self, DockerError> {
27 let docker = Docker::connect_with_local_defaults()
28 .map_err(|e| DockerError::Connection(e.to_string()))?;
29
30 Ok(Self {
31 inner: docker,
32 _tunnel: None,
33 host_name: None,
34 })
35 }
36
37 pub fn with_timeout(timeout_secs: u64) -> Result<Self, DockerError> {
42 let docker = Docker::connect_with_local_defaults()
43 .map_err(|e| DockerError::Connection(e.to_string()))?
44 .with_timeout(Duration::from_secs(timeout_secs));
45
46 Ok(Self {
47 inner: docker,
48 _tunnel: None,
49 host_name: None,
50 })
51 }
52
53 pub async fn connect_remote(host: &HostConfig, host_name: &str) -> Result<Self, DockerError> {
62 let tunnel = SshTunnel::new(host, host_name)
64 .map_err(|e| DockerError::Connection(format!("SSH tunnel failed: {e}")))?;
65
66 tunnel
68 .wait_ready()
69 .await
70 .map_err(|e| DockerError::Connection(format!("SSH tunnel not ready: {e}")))?;
71
72 let docker_url = tunnel.docker_url();
74 tracing::debug!("Connecting to remote Docker via {}", docker_url);
75
76 let max_attempts = 3;
78 let mut last_err = None;
79
80 for attempt in 0..max_attempts {
81 if attempt > 0 {
82 let delay = Duration::from_millis(100 * 2u64.pow(attempt));
83 tracing::debug!("Retry attempt {} after {:?}", attempt + 1, delay);
84 tokio::time::sleep(delay).await;
85 }
86
87 match Docker::connect_with_http(&docker_url, 120, bollard::API_DEFAULT_VERSION) {
88 Ok(docker) => {
89 match docker.ping().await {
91 Ok(_) => {
92 tracing::info!("Connected to Docker on {} via SSH tunnel", host_name);
93 return Ok(Self {
94 inner: docker,
95 _tunnel: Some(tunnel),
96 host_name: Some(host_name.to_string()),
97 });
98 }
99 Err(e) => {
100 tracing::debug!("Ping failed: {}", e);
101 last_err = Some(e.to_string());
102 }
103 }
104 }
105 Err(e) => {
106 tracing::debug!("Connection failed: {}", e);
107 last_err = Some(e.to_string());
108 }
109 }
110 }
111
112 Err(DockerError::Connection(format!(
113 "Failed to connect to Docker on {}: {}",
114 host_name,
115 last_err.unwrap_or_else(|| "unknown error".to_string())
116 )))
117 }
118
119 pub async fn connect_remote_with_timeout(
121 host: &HostConfig,
122 host_name: &str,
123 timeout_secs: u64,
124 ) -> Result<Self, DockerError> {
125 let tunnel = SshTunnel::new(host, host_name)
126 .map_err(|e| DockerError::Connection(format!("SSH tunnel failed: {e}")))?;
127
128 tunnel
129 .wait_ready()
130 .await
131 .map_err(|e| DockerError::Connection(format!("SSH tunnel not ready: {e}")))?;
132
133 let docker_url = tunnel.docker_url();
134
135 let docker =
136 Docker::connect_with_http(&docker_url, timeout_secs, bollard::API_DEFAULT_VERSION)
137 .map_err(|e| DockerError::Connection(e.to_string()))?;
138
139 docker.ping().await.map_err(DockerError::from)?;
141
142 Ok(Self {
143 inner: docker,
144 _tunnel: Some(tunnel),
145 host_name: Some(host_name.to_string()),
146 })
147 }
148
149 pub async fn verify_connection(&self) -> Result<(), DockerError> {
153 self.inner.ping().await.map_err(DockerError::from)?;
154 Ok(())
155 }
156
157 pub async fn version(&self) -> Result<String, DockerError> {
159 let version = self.inner.version().await.map_err(DockerError::from)?;
160
161 let version_str = format!(
162 "Docker {} (API {})",
163 version.version.unwrap_or_else(|| "unknown".to_string()),
164 version.api_version.unwrap_or_else(|| "unknown".to_string())
165 );
166
167 Ok(version_str)
168 }
169
170 pub fn host_name(&self) -> Option<&str> {
172 self.host_name.as_deref()
173 }
174
175 pub fn is_remote(&self) -> bool {
177 self._tunnel.is_some()
178 }
179
180 pub fn inner(&self) -> &Docker {
182 &self.inner
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn docker_client_creation_does_not_panic() {
192 let result = DockerClient::new();
195 drop(result);
197 }
198
199 #[test]
200 fn docker_client_with_timeout_does_not_panic() {
201 let result = DockerClient::with_timeout(600);
202 drop(result);
203 }
204
205 #[test]
206 fn test_host_name_methods() {
207 if let Ok(client) = DockerClient::new() {
209 assert!(client.host_name().is_none());
210 assert!(!client.is_remote());
211 }
212 }
213}