Skip to main content

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}