docker_wrapper/command/
port.rs1use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9#[derive(Debug, Clone)]
33pub struct PortCommand {
34 container: String,
36 port: Option<u16>,
38 pub executor: CommandExecutor,
40}
41
42impl PortCommand {
43 #[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 #[must_use]
72 pub fn port(mut self, port: u16) -> Self {
73 self.port = Some(port);
74 self
75 }
76
77 pub async fn run(&self) -> Result<PortResult> {
103 let output = self.execute().await?;
104
105 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 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 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 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(), });
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#[derive(Debug, Clone)]
202pub struct PortResult {
203 pub output: CommandOutput,
205 pub container: String,
207 pub port_mappings: Vec<PortMapping>,
209}
210
211impl PortResult {
212 #[must_use]
214 pub fn success(&self) -> bool {
215 self.output.success
216 }
217
218 #[must_use]
220 pub fn container(&self) -> &str {
221 &self.container
222 }
223
224 #[must_use]
226 pub fn port_mappings(&self) -> &[PortMapping] {
227 &self.port_mappings
228 }
229
230 #[must_use]
232 pub fn mapping_count(&self) -> usize {
233 self.port_mappings.len()
234 }
235}
236
237#[derive(Debug, Clone)]
239pub struct PortMapping {
240 pub container_port: u16,
242 pub host_ip: String,
244 pub host_port: u16,
246 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 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 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 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 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 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}