torrust_tracker_deployer_lib/adapters/docker/client.rs
1//! Docker CLI client implementation
2
3use std::path::Path;
4
5use super::error::DockerError;
6use crate::shared::command::CommandExecutor;
7
8/// Client for executing Docker CLI commands
9///
10/// This client wraps Docker CLI operations using our `CommandExecutor` collaborator,
11/// enabling testability and consistency with other external tool clients (Ansible,
12/// `OpenTofu`, LXD). Each Docker subcommand is exposed as a separate method.
13///
14/// # Architecture
15///
16/// The client uses `CommandExecutor` as a collaborator for actual command execution,
17/// following the same pattern as `AnsibleClient`, `TofuClient`, and `LxdClient`.
18///
19/// # Example
20///
21/// ```rust,no_run
22/// use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
23///
24/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
25/// let docker = DockerClient::new();
26///
27/// // Build an image
28/// docker.build_image("docker/app", "my-app", "latest")?;
29///
30/// // Check if it exists
31/// let exists = docker.image_exists("my-app", "latest")?;
32/// assert!(exists);
33/// # Ok(())
34/// # }
35/// ```
36#[derive(Debug)]
37pub struct DockerClient {
38 command_executor: CommandExecutor,
39}
40
41impl Default for DockerClient {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl DockerClient {
48 /// Create a new Docker client
49 ///
50 /// # Example
51 ///
52 /// ```rust
53 /// use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
54 ///
55 /// let docker = DockerClient::new();
56 /// ```
57 #[must_use]
58 pub fn new() -> Self {
59 Self {
60 command_executor: CommandExecutor::new(),
61 }
62 }
63
64 /// Build a Docker image from a Dockerfile directory
65 ///
66 /// Executes `docker build -t <name>:<tag> <path>` to build an image.
67 ///
68 /// # Arguments
69 ///
70 /// * `dockerfile_dir` - Path to directory containing the Dockerfile
71 /// * `image_name` - Name for the Docker image (e.g., "my-ssh-server")
72 /// * `image_tag` - Tag for the image (e.g., "latest")
73 ///
74 /// # Returns
75 ///
76 /// The build output on success
77 ///
78 /// # Errors
79 ///
80 /// Returns `DockerError::BuildFailed` if the build command fails
81 ///
82 /// # Example
83 ///
84 /// ```rust,no_run
85 /// # use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
86 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
87 /// # let docker = DockerClient::new();
88 /// docker.build_image("docker/ssh-server", "my-ssh", "latest")?;
89 /// # Ok(())
90 /// # }
91 /// ```
92 pub fn build_image<P: AsRef<Path>>(
93 &self,
94 dockerfile_dir: P,
95 image_name: &str,
96 image_tag: &str,
97 ) -> Result<String, DockerError> {
98 let image = format!("{image_name}:{image_tag}");
99 let path = dockerfile_dir.as_ref().display().to_string();
100 let args = vec!["build", "-t", &image, &path];
101
102 self.command_executor
103 .run_command("docker", &args, None)
104 .map(|result| result.stdout)
105 .map_err(|source| DockerError::BuildFailed { image, source })
106 }
107
108 /// List Docker images with optional repository filter
109 ///
110 /// Executes `docker images` with formatting to get structured output.
111 ///
112 /// # Arguments
113 ///
114 /// * `repository` - Optional repository name to filter by
115 ///
116 /// # Returns
117 ///
118 /// A vector of image information strings in format:
119 /// "repository:tag|id|size"
120 ///
121 /// # Errors
122 ///
123 /// Returns `DockerError::ListImagesFailed` if the command fails
124 ///
125 /// # Example
126 ///
127 /// ```rust,no_run
128 /// # use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
129 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
130 /// # let docker = DockerClient::new();
131 /// // List all images
132 /// let all_images = docker.list_images(None)?;
133 ///
134 /// // List specific repository
135 /// let ubuntu_images = docker.list_images(Some("ubuntu"))?;
136 /// # Ok(())
137 /// # }
138 /// ```
139 pub fn list_images(&self, repository: Option<&str>) -> Result<Vec<String>, DockerError> {
140 let format_str = "{{.Repository}}:{{.Tag}}|{{.ID}}|{{.Size}}";
141 let mut args = vec!["images", "--format", format_str];
142
143 if let Some(repo) = repository {
144 args.push(repo);
145 }
146
147 let result = self
148 .command_executor
149 .run_command("docker", &args, None)
150 .map_err(DockerError::ListImagesFailed)?;
151
152 Ok(result.stdout.lines().map(ToString::to_string).collect())
153 }
154
155 /// List Docker containers
156 ///
157 /// Executes `docker ps` with formatting to get structured output.
158 ///
159 /// # Arguments
160 ///
161 /// * `all` - If true, shows all containers (including stopped ones)
162 ///
163 /// # Returns
164 ///
165 /// A vector of container information strings in format:
166 /// "id|name|status"
167 ///
168 /// # Errors
169 ///
170 /// Returns `DockerError::ListContainersFailed` if the command fails
171 ///
172 /// # Example
173 ///
174 /// ```rust,no_run
175 /// # use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
176 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
177 /// # let docker = DockerClient::new();
178 /// // List only running containers
179 /// let running = docker.list_containers(false)?;
180 ///
181 /// // List all containers
182 /// let all_containers = docker.list_containers(true)?;
183 /// # Ok(())
184 /// # }
185 /// ```
186 pub fn list_containers(&self, all: bool) -> Result<Vec<String>, DockerError> {
187 let format_str = "{{.ID}}|{{.Names}}|{{.Status}}";
188 let mut args = vec!["ps", "--format", format_str];
189
190 if all {
191 args.push("-a");
192 }
193
194 let result = self
195 .command_executor
196 .run_command("docker", &args, None)
197 .map_err(DockerError::ListContainersFailed)?;
198
199 Ok(result.stdout.lines().map(ToString::to_string).collect())
200 }
201
202 /// Get logs from a Docker container
203 ///
204 /// Executes `docker logs <container-id>` to retrieve container logs.
205 ///
206 /// # Arguments
207 ///
208 /// * `container_id` - ID or name of the container
209 ///
210 /// # Returns
211 ///
212 /// The container's logs as a string
213 ///
214 /// # Errors
215 ///
216 /// Returns `DockerError::GetLogsFailed` if the command fails
217 ///
218 /// # Example
219 ///
220 /// ```rust,no_run
221 /// # use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
222 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
223 /// # let docker = DockerClient::new();
224 /// let logs = docker.get_container_logs("my-container")?;
225 /// println!("Container logs:\n{}", logs);
226 /// # Ok(())
227 /// # }
228 /// ```
229 pub fn get_container_logs(&self, container_id: &str) -> Result<String, DockerError> {
230 let args = vec!["logs", container_id];
231
232 self.command_executor
233 .run_command("docker", &args, None)
234 .map(|result| result.stdout)
235 .map_err(|source| DockerError::GetLogsFailed {
236 container_id: container_id.to_string(),
237 source,
238 })
239 }
240
241 /// Check if a Docker image exists locally
242 ///
243 /// Uses `list_images` to check for the presence of a specific image.
244 ///
245 /// # Arguments
246 ///
247 /// * `image_name` - Name of the image
248 /// * `image_tag` - Tag of the image
249 ///
250 /// # Returns
251 ///
252 /// `true` if the image exists, `false` otherwise
253 ///
254 /// # Errors
255 ///
256 /// Returns `DockerError::ListImagesFailed` if the command fails
257 ///
258 /// # Example
259 ///
260 /// ```rust,no_run
261 /// # use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
262 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
263 /// # let docker = DockerClient::new();
264 /// if docker.image_exists("ubuntu", "latest")? {
265 /// println!("Ubuntu image is available");
266 /// }
267 /// # Ok(())
268 /// # }
269 /// ```
270 pub fn image_exists(&self, image_name: &str, image_tag: &str) -> Result<bool, DockerError> {
271 let filter = format!("{image_name}:{image_tag}");
272 let images = self.list_images(Some(&filter))?;
273 Ok(!images.is_empty())
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn it_should_create_docker_client() {
283 let _docker = DockerClient::new();
284 }
285}