docker_wrapper/command/
port.rs

1//! Docker port command implementation.
2//!
3//! This module provides the `docker port` command for listing port mappings.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9/// Docker port command builder
10///
11/// List port mappings or a specific mapping for a container.
12///
13/// # Example
14///
15/// ```no_run
16/// use docker_wrapper::PortCommand;
17///
18/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19/// // List all port mappings
20/// let ports = PortCommand::new("my-container")
21///     .run()
22///     .await?;
23///
24/// // Get specific port mapping
25/// let port = PortCommand::new("my-container")
26///     .port(80)
27///     .run()
28///     .await?;
29/// # Ok(())
30/// # }
31/// ```
32#[derive(Debug, Clone)]
33pub struct PortCommand {
34    /// Container name or ID
35    container: String,
36    /// Specific port to query
37    port: Option<u16>,
38    /// Command executor
39    pub executor: CommandExecutor,
40}
41
42impl PortCommand {
43    /// Create a new port command
44    ///
45    /// # Example
46    ///
47    /// ```
48    /// use docker_wrapper::PortCommand;
49    ///
50    /// let cmd = PortCommand::new("my-container");
51    /// ```
52    #[must_use]
53    pub fn new(container: impl Into<String>) -> Self {
54        Self {
55            container: container.into(),
56            port: None,
57            executor: CommandExecutor::new(),
58        }
59    }
60
61    /// Query specific port mapping
62    ///
63    /// # Example
64    ///
65    /// ```
66    /// use docker_wrapper::PortCommand;
67    ///
68    /// let cmd = PortCommand::new("my-container")
69    ///     .port(80);
70    /// ```
71    #[must_use]
72    pub fn port(mut self, port: u16) -> Self {
73        self.port = Some(port);
74        self
75    }
76
77    /// Execute the port command
78    ///
79    /// # Errors
80    /// Returns an error if:
81    /// - The Docker daemon is not running
82    /// - The container doesn't exist
83    ///
84    /// # Example
85    ///
86    /// ```no_run
87    /// use docker_wrapper::PortCommand;
88    ///
89    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
90    /// let result = PortCommand::new("my-container")
91    ///     .run()
92    ///     .await?;
93    ///
94    /// if result.success() {
95    ///     for mapping in result.port_mappings() {
96    ///         println!("{}:{} -> {}", mapping.host_ip, mapping.host_port, mapping.container_port);
97    ///     }
98    /// }
99    /// # Ok(())
100    /// # }
101    /// ```
102    pub async fn run(&self) -> Result<PortResult> {
103        let output = self.execute().await?;
104
105        // Parse port mappings from output, passing the queried port for simple format parsing
106        let port_mappings = Self::parse_port_mappings(&output.stdout, self.port);
107
108        Ok(PortResult {
109            output,
110            container: self.container.clone(),
111            port_mappings,
112        })
113    }
114
115    /// Parse port mappings from command output.
116    ///
117    /// Handles two formats:
118    /// - Full format (all ports): `80/tcp -> 0.0.0.0:8080`
119    /// - Simple format (specific port query): `0.0.0.0:8080`
120    ///
121    /// When `queried_port` is provided and the simple format is detected,
122    /// the container port is inferred from the queried port.
123    fn parse_port_mappings(stdout: &str, queried_port: Option<u16>) -> Vec<PortMapping> {
124        let mut mappings = Vec::new();
125
126        for line in stdout.lines() {
127            let line = line.trim();
128            if line.is_empty() {
129                continue;
130            }
131
132            // Try full format first: "80/tcp -> 0.0.0.0:8080"
133            if let Some((container_part, host_part)) = line.split_once(" -> ") {
134                if let Some((port_str, protocol)) = container_part.split_once('/') {
135                    if let Ok(container_port) = port_str.parse::<u16>() {
136                        if let Some((host_ip, host_port_str)) = host_part.rsplit_once(':') {
137                            if let Ok(host_port) = host_port_str.parse::<u16>() {
138                                mappings.push(PortMapping {
139                                    container_port,
140                                    host_ip: host_ip.to_string(),
141                                    host_port,
142                                    protocol: protocol.to_string(),
143                                });
144                            }
145                        }
146                    }
147                }
148            } else if let Some(container_port) = queried_port {
149                // Simple format (specific port query): "0.0.0.0:8080" or "[::]:8080"
150                if let Some((host_ip, host_port_str)) = line.rsplit_once(':') {
151                    if let Ok(host_port) = host_port_str.parse::<u16>() {
152                        mappings.push(PortMapping {
153                            container_port,
154                            host_ip: host_ip.to_string(),
155                            host_port,
156                            protocol: "tcp".to_string(), // Default to tcp when not specified
157                        });
158                    }
159                }
160            }
161        }
162
163        mappings
164    }
165}
166
167#[async_trait]
168impl DockerCommand for PortCommand {
169    type Output = CommandOutput;
170
171    fn build_command_args(&self) -> Vec<String> {
172        let mut args = vec!["port".to_string(), self.container.clone()];
173
174        if let Some(port) = self.port {
175            args.push(port.to_string());
176        }
177
178        args.extend(self.executor.raw_args.clone());
179        args
180    }
181
182    fn get_executor(&self) -> &CommandExecutor {
183        &self.executor
184    }
185
186    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
187        &mut self.executor
188    }
189
190    async fn execute(&self) -> Result<Self::Output> {
191        let args = self.build_command_args();
192        let command_name = args[0].clone();
193        let command_args = args[1..].to_vec();
194        self.executor
195            .execute_command(&command_name, command_args)
196            .await
197    }
198}
199
200/// Result from the port command
201#[derive(Debug, Clone)]
202pub struct PortResult {
203    /// Raw command output
204    pub output: CommandOutput,
205    /// Container that was queried
206    pub container: String,
207    /// Parsed port mappings
208    pub port_mappings: Vec<PortMapping>,
209}
210
211impl PortResult {
212    /// Check if the port command was successful
213    #[must_use]
214    pub fn success(&self) -> bool {
215        self.output.success
216    }
217
218    /// Get the container name
219    #[must_use]
220    pub fn container(&self) -> &str {
221        &self.container
222    }
223
224    /// Get the port mappings
225    #[must_use]
226    pub fn port_mappings(&self) -> &[PortMapping] {
227        &self.port_mappings
228    }
229
230    /// Get port mapping count
231    #[must_use]
232    pub fn mapping_count(&self) -> usize {
233        self.port_mappings.len()
234    }
235}
236
237/// Port mapping information
238#[derive(Debug, Clone)]
239pub struct PortMapping {
240    /// Container port
241    pub container_port: u16,
242    /// Host IP address
243    pub host_ip: String,
244    /// Host port
245    pub host_port: u16,
246    /// Protocol (tcp/udp)
247    pub protocol: String,
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_port_basic() {
256        let cmd = PortCommand::new("test-container");
257        let args = cmd.build_command_args();
258        assert_eq!(args, vec!["port", "test-container"]);
259    }
260
261    #[test]
262    fn test_port_with_specific_port() {
263        let cmd = PortCommand::new("test-container").port(80);
264        let args = cmd.build_command_args();
265        assert_eq!(args, vec!["port", "test-container", "80"]);
266    }
267
268    #[test]
269    fn test_parse_port_mappings_full_format() {
270        let output = "80/tcp -> 0.0.0.0:8080\n443/tcp -> 127.0.0.1:8443";
271        let mappings = PortCommand::parse_port_mappings(output, None);
272
273        assert_eq!(mappings.len(), 2);
274        assert_eq!(mappings[0].container_port, 80);
275        assert_eq!(mappings[0].host_ip, "0.0.0.0");
276        assert_eq!(mappings[0].host_port, 8080);
277        assert_eq!(mappings[0].protocol, "tcp");
278
279        assert_eq!(mappings[1].container_port, 443);
280        assert_eq!(mappings[1].host_ip, "127.0.0.1");
281        assert_eq!(mappings[1].host_port, 8443);
282        assert_eq!(mappings[1].protocol, "tcp");
283    }
284
285    #[test]
286    fn test_parse_port_mappings_simple_format() {
287        // Format returned when querying a specific port: docker port <container> 6379
288        let output = "0.0.0.0:40998\n[::]:40998";
289        let mappings = PortCommand::parse_port_mappings(output, Some(6379));
290
291        assert_eq!(mappings.len(), 2);
292
293        // IPv4 mapping
294        assert_eq!(mappings[0].container_port, 6379);
295        assert_eq!(mappings[0].host_ip, "0.0.0.0");
296        assert_eq!(mappings[0].host_port, 40998);
297        assert_eq!(mappings[0].protocol, "tcp");
298
299        // IPv6 mapping
300        assert_eq!(mappings[1].container_port, 6379);
301        assert_eq!(mappings[1].host_ip, "[::]");
302        assert_eq!(mappings[1].host_port, 40998);
303        assert_eq!(mappings[1].protocol, "tcp");
304    }
305
306    #[test]
307    fn test_parse_port_mappings_simple_format_without_queried_port() {
308        // Without queried_port, simple format lines are ignored
309        let output = "0.0.0.0:40998\n[::]:40998";
310        let mappings = PortCommand::parse_port_mappings(output, None);
311
312        assert!(mappings.is_empty());
313    }
314
315    #[test]
316    fn test_parse_port_mappings_empty() {
317        let mappings = PortCommand::parse_port_mappings("", None);
318        assert!(mappings.is_empty());
319    }
320
321    #[test]
322    fn test_parse_port_mappings_mixed_format() {
323        // In practice this wouldn't happen, but test robustness
324        let output = "80/tcp -> 0.0.0.0:8080\n0.0.0.0:9000";
325        let mappings = PortCommand::parse_port_mappings(output, Some(443));
326
327        assert_eq!(mappings.len(), 2);
328        assert_eq!(mappings[0].container_port, 80);
329        assert_eq!(mappings[0].host_port, 8080);
330        assert_eq!(mappings[1].container_port, 443);
331        assert_eq!(mappings[1].host_port, 9000);
332    }
333}