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);
107
108 Ok(PortResult {
109 output,
110 container: self.container.clone(),
111 port_mappings,
112 })
113 }
114
115 fn parse_port_mappings(stdout: &str) -> Vec<PortMapping> {
117 let mut mappings = Vec::new();
118
119 for line in stdout.lines() {
120 let line = line.trim();
121 if line.is_empty() {
122 continue;
123 }
124
125 if let Some((container_part, host_part)) = line.split_once(" -> ") {
127 if let Some((port_str, protocol)) = container_part.split_once('/') {
128 if let Ok(container_port) = port_str.parse::<u16>() {
129 if let Some((host_ip, host_port_str)) = host_part.rsplit_once(':') {
130 if let Ok(host_port) = host_port_str.parse::<u16>() {
131 mappings.push(PortMapping {
132 container_port,
133 host_ip: host_ip.to_string(),
134 host_port,
135 protocol: protocol.to_string(),
136 });
137 }
138 }
139 }
140 }
141 }
142 }
143
144 mappings
145 }
146}
147
148#[async_trait]
149impl DockerCommand for PortCommand {
150 type Output = CommandOutput;
151
152 fn build_command_args(&self) -> Vec<String> {
153 let mut args = vec!["port".to_string(), self.container.clone()];
154
155 if let Some(port) = self.port {
156 args.push(port.to_string());
157 }
158
159 args.extend(self.executor.raw_args.clone());
160 args
161 }
162
163 fn get_executor(&self) -> &CommandExecutor {
164 &self.executor
165 }
166
167 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
168 &mut self.executor
169 }
170
171 async fn execute(&self) -> Result<Self::Output> {
172 let args = self.build_command_args();
173 let command_name = args[0].clone();
174 let command_args = args[1..].to_vec();
175 self.executor
176 .execute_command(&command_name, command_args)
177 .await
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct PortResult {
184 pub output: CommandOutput,
186 pub container: String,
188 pub port_mappings: Vec<PortMapping>,
190}
191
192impl PortResult {
193 #[must_use]
195 pub fn success(&self) -> bool {
196 self.output.success
197 }
198
199 #[must_use]
201 pub fn container(&self) -> &str {
202 &self.container
203 }
204
205 #[must_use]
207 pub fn port_mappings(&self) -> &[PortMapping] {
208 &self.port_mappings
209 }
210
211 #[must_use]
213 pub fn mapping_count(&self) -> usize {
214 self.port_mappings.len()
215 }
216}
217
218#[derive(Debug, Clone)]
220pub struct PortMapping {
221 pub container_port: u16,
223 pub host_ip: String,
225 pub host_port: u16,
227 pub protocol: String,
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_port_basic() {
237 let cmd = PortCommand::new("test-container");
238 let args = cmd.build_command_args();
239 assert_eq!(args, vec!["port", "test-container"]);
240 }
241
242 #[test]
243 fn test_port_with_specific_port() {
244 let cmd = PortCommand::new("test-container").port(80);
245 let args = cmd.build_command_args();
246 assert_eq!(args, vec!["port", "test-container", "80"]);
247 }
248
249 #[test]
250 fn test_parse_port_mappings() {
251 let output = "80/tcp -> 0.0.0.0:8080\n443/tcp -> 127.0.0.1:8443";
252 let mappings = PortCommand::parse_port_mappings(output);
253
254 assert_eq!(mappings.len(), 2);
255 assert_eq!(mappings[0].container_port, 80);
256 assert_eq!(mappings[0].host_ip, "0.0.0.0");
257 assert_eq!(mappings[0].host_port, 8080);
258 assert_eq!(mappings[0].protocol, "tcp");
259
260 assert_eq!(mappings[1].container_port, 443);
261 assert_eq!(mappings[1].host_ip, "127.0.0.1");
262 assert_eq!(mappings[1].host_port, 8443);
263 assert_eq!(mappings[1].protocol, "tcp");
264 }
265
266 #[test]
267 fn test_parse_port_mappings_empty() {
268 let mappings = PortCommand::parse_port_mappings("");
269 assert!(mappings.is_empty());
270 }
271}