torrust_tracker_deployer_lib/testing/integration/ssh_server/debug.rs
1//! Debug utilities for Docker container troubleshooting
2
3use std::sync::Arc;
4
5use crate::adapters::docker::DockerClient;
6use crate::testing::network::PortUsageChecker;
7
8// Import constants only for the convenience function
9use super::constants::{SSH_SERVER_IMAGE_NAME, SSH_SERVER_IMAGE_TAG};
10
11// ============================================================================
12// PUBLIC API - Structured Debug Data
13// ============================================================================
14
15/// Debug information collected about Docker containers
16///
17/// This struct holds structured information about Docker state for troubleshooting
18/// SSH server container issues. Each field contains either successfully collected
19/// data or an error message explaining what went wrong.
20#[derive(Debug)]
21pub struct DockerDebugInfo {
22 /// Docker client for executing commands
23 docker: Arc<DockerClient>,
24
25 /// Output from `docker ps -a` listing all containers
26 pub all_containers: Result<String, String>,
27
28 /// Docker images matching the SSH server image name
29 pub ssh_images: Result<String, String>,
30
31 /// Information about containers using the SSH server image
32 pub ssh_containers: Result<Vec<ContainerInfo>, String>,
33
34 /// Port usage information for the SSH port
35 pub port_usage: Result<Vec<String>, String>,
36
37 /// Docker image name that was searched for
38 image_name: String,
39
40 /// Docker image tag that was searched for
41 image_tag: String,
42}
43
44/// Information about a specific Docker container
45#[derive(Debug, Clone)]
46pub struct ContainerInfo {
47 /// Container ID
48 pub id: String,
49
50 /// Full status line from docker ps
51 pub status: String,
52
53 /// Container logs (last 20 lines)
54 pub logs: Result<String, String>,
55}
56
57// ============================================================================
58// PUBLIC API - Debug Info Collection
59// ============================================================================
60
61impl DockerDebugInfo {
62 /// Create a new `DockerDebugInfo` and collect all diagnostic information
63 ///
64 /// This constructor runs various Docker commands to gather diagnostic information
65 /// when SSH connectivity tests fail. It collects container status, logs,
66 /// and port usage information.
67 ///
68 /// # Arguments
69 ///
70 /// * `docker` - Docker client for executing commands
71 /// * `container_port` - The host port that the SSH container is mapped to
72 /// * `image_name` - Image name to filter by (e.g., "torrust-ssh-server")
73 /// * `image_tag` - Image tag to filter by (e.g., "latest")
74 ///
75 /// # Returns
76 ///
77 /// A `DockerDebugInfo` struct containing all collected information. Each field
78 /// is a `Result` that either contains the successfully collected data or an
79 /// error message explaining what went wrong.
80 ///
81 /// # Example
82 ///
83 /// ```rust,no_run
84 /// use std::sync::Arc;
85 /// use torrust_tracker_deployer_lib::adapters::docker::DockerClient;
86 /// use torrust_tracker_deployer_lib::testing::integration::ssh_server::DockerDebugInfo;
87 ///
88 /// let docker = Arc::new(DockerClient::new());
89 /// let debug_info = DockerDebugInfo::new(docker, 2222, "torrust-ssh-server", "latest");
90 /// debug_info.print();
91 /// ```
92 #[must_use]
93 pub fn new(
94 docker: Arc<DockerClient>,
95 container_port: u16,
96 image_name: &str,
97 image_tag: &str,
98 ) -> Self {
99 let mut instance = Self {
100 docker,
101 all_containers: Ok(String::new()),
102 ssh_images: Ok(String::new()),
103 ssh_containers: Ok(Vec::new()),
104 port_usage: Ok(Vec::new()),
105 image_name: image_name.to_string(),
106 image_tag: image_tag.to_string(),
107 };
108
109 // Collect debug information using instance methods
110 instance.all_containers = instance.list_all_containers();
111 instance.ssh_images = instance.list_ssh_images(&instance.image_name.clone());
112 instance.ssh_containers = instance.find_ssh_containers();
113 instance.port_usage =
114 PortUsageChecker::check_port(container_port).map_err(|e| e.to_string());
115
116 instance
117 }
118
119 /// List all Docker containers
120 ///
121 /// Uses `DockerClient::list_containers(true)` to list all containers (running and stopped).
122 fn list_all_containers(&self) -> Result<String, String> {
123 self.docker
124 .list_containers(true)
125 .map(|containers| containers.join("\n"))
126 .map_err(|e| format!("Failed to list containers: {e}"))
127 }
128
129 /// List SSH server Docker images
130 ///
131 /// Uses `DockerClient::list_images` filtered by SSH server image name.
132 fn list_ssh_images(&self, image_name: &str) -> Result<String, String> {
133 self.docker
134 .list_images(Some(image_name))
135 .map(|images| images.join("\n"))
136 .map_err(|e| format!("Failed to list images: {e}"))
137 }
138
139 /// Find containers using the SSH server image
140 ///
141 /// Uses `DockerClient::list_containers` and filters by image.
142 /// Also fetches logs for each matching container.
143 fn find_ssh_containers(&self) -> Result<Vec<ContainerInfo>, String> {
144 // TODO: Filter by image when DockerClient supports image info in list_containers
145 // For now, we list all containers
146
147 let all_containers = self
148 .docker
149 .list_containers(true)
150 .map_err(|e| format!("Failed to list containers: {e}"))?;
151
152 let mut containers = Vec::new();
153
154 // Filter containers by image and collect their info
155 for container_line in all_containers {
156 // Container format from DockerClient: "id|name|status"
157 if let Some(container_id) = container_line.split('|').next() {
158 // For now, we include all containers
159 // TODO: Filter by image when DockerClient supports image info
160 containers.push(ContainerInfo {
161 id: container_id.to_string(),
162 status: container_line.clone(),
163 logs: self.get_container_logs(container_id),
164 });
165 }
166 }
167
168 Ok(containers)
169 }
170
171 /// Get logs for a specific container
172 ///
173 /// Uses `DockerClient::get_container_logs` to retrieve logs.
174 /// Note: `DockerClient` doesn't support --tail yet, so we get all logs.
175 fn get_container_logs(&self, container_id: &str) -> Result<String, String> {
176 self.docker
177 .get_container_logs(container_id)
178 .map_err(|e| format!("Failed to get container logs: {e}"))
179 }
180
181 /// Get a reference to the Docker client
182 ///
183 /// This allows access to the underlying Docker client for additional operations
184 /// if needed after debug info has been collected.
185 #[must_use]
186 pub fn docker(&self) -> &Arc<DockerClient> {
187 &self.docker
188 }
189
190 /// Print the debug information in a formatted way
191 ///
192 /// Prints all collected debug information to stdout in a human-readable format.
193 pub fn print(&self) {
194 println!("\n=== Docker Debug Information ===");
195 self.print_all_containers();
196 self.print_ssh_images();
197 self.print_ssh_containers_and_logs();
198 self.print_port_usage();
199 println!("=== End Docker Debug Information ===\n");
200 }
201
202 /// Print all Docker containers
203 fn print_all_containers(&self) {
204 match &self.all_containers {
205 Ok(containers) => {
206 println!("Docker containers (docker ps -a):");
207 println!("{containers}");
208 }
209 Err(e) => {
210 println!("Failed to list containers: {e}");
211 }
212 }
213 }
214
215 /// Print SSH server images
216 fn print_ssh_images(&self) {
217 match &self.ssh_images {
218 Ok(images) => {
219 println!("\nDocker images for {}:", self.image_name);
220 println!("{images}");
221 }
222 Err(e) => {
223 println!("Failed to list images: {e}");
224 }
225 }
226 }
227
228 /// Print SSH containers and their logs
229 fn print_ssh_containers_and_logs(&self) {
230 match &self.ssh_containers {
231 Ok(containers) => {
232 let image_tag = format!("{}:{}", self.image_name, self.image_tag);
233 println!("\nContainers using {image_tag}:");
234
235 if containers.is_empty() {
236 println!("No containers found");
237 } else {
238 for container in containers {
239 println!("Container {}: {}", container.id, container.status);
240
241 match &container.logs {
242 Ok(logs) => {
243 println!("\nContainer logs for {}:", container.id);
244 println!("{logs}");
245 }
246 Err(e) => {
247 println!("Failed to get logs for {}: {e}", container.id);
248 }
249 }
250 }
251 }
252 }
253 Err(e) => {
254 println!("Failed to filter containers: {e}");
255 }
256 }
257 }
258
259 /// Print port usage information
260 fn print_port_usage(&self) {
261 println!("\nPort information:");
262 match &self.port_usage {
263 Ok(lines) => {
264 for line in lines {
265 println!("{line}");
266 }
267 }
268 Err(e) => {
269 println!("Failed to check port usage: {e}");
270 }
271 }
272 }
273}
274
275// ============================================================================
276// PUBLIC API - Convenience Function
277// ============================================================================
278
279/// Debug helper function to collect and print Docker container information
280///
281/// This is a convenience function that collects all Docker debug information
282/// and prints it to stdout. For programmatic access to the structured data,
283/// use [`DockerDebugInfo::new`] instead.
284///
285/// This function runs various Docker commands to help diagnose issues when SSH
286/// connectivity tests fail in CI environments. It prints container status, logs,
287/// and other useful debugging information.
288///
289/// # Arguments
290///
291/// * `container_port` - The host port that the SSH container is mapped to
292///
293/// # Usage
294///
295/// This function is typically called when SSH connectivity tests fail to help
296/// diagnose what's happening with the Docker containers in CI environments.
297///
298/// ```rust
299/// use torrust_tracker_deployer_lib::testing::integration::ssh_server::print_docker_debug_info;
300///
301/// // In a test when SSH connectivity fails:
302/// print_docker_debug_info(2222);
303/// ```
304pub fn print_docker_debug_info(container_port: u16) {
305 let docker = Arc::new(DockerClient::new());
306 let debug_info = DockerDebugInfo::new(
307 docker,
308 container_port,
309 SSH_SERVER_IMAGE_NAME,
310 SSH_SERVER_IMAGE_TAG,
311 );
312 debug_info.print();
313}
314
315// ============================================================================
316// TESTS
317// ============================================================================
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn it_should_collect_docker_debug_info() {
325 // This test verifies that the new method doesn't panic
326 // Actual Docker commands may fail in test environment, which is expected
327 let docker = Arc::new(DockerClient::new());
328 let debug_info = DockerDebugInfo::new(docker, 2222, "test-image", "latest");
329
330 // Verify structure exists (even if commands failed)
331 assert!(debug_info.all_containers.is_ok() || debug_info.all_containers.is_err());
332 assert!(debug_info.ssh_images.is_ok() || debug_info.ssh_images.is_err());
333 assert!(debug_info.ssh_containers.is_ok() || debug_info.ssh_containers.is_err());
334 assert!(debug_info.port_usage.is_ok() || debug_info.port_usage.is_err());
335 }
336
337 #[test]
338 fn it_should_print_without_panicking() {
339 // This test verifies that printing doesn't panic
340 let docker = Arc::new(DockerClient::new());
341 let debug_info = DockerDebugInfo::new(docker, 2222, "test-image", "latest");
342 debug_info.print();
343 // If we get here without panicking, test passes
344 }
345}