docker_wrapper/command/
compose_ps.rs1use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone)]
10pub struct ComposePsCommand {
11 pub executor: CommandExecutor,
13 pub config: ComposeConfig,
15 pub services: Vec<String>,
17 pub all: bool,
19 pub quiet: bool,
21 pub show_services: bool,
23 pub filter: Vec<String>,
25 pub format: Option<String>,
27 pub status: Option<Vec<ContainerStatus>>,
29}
30
31#[derive(Debug, Clone, Copy)]
33pub enum ContainerStatus {
34 Paused,
36 Restarting,
38 Running,
40 Stopped,
42}
43
44impl std::fmt::Display for ContainerStatus {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::Paused => write!(f, "paused"),
48 Self::Restarting => write!(f, "restarting"),
49 Self::Running => write!(f, "running"),
50 Self::Stopped => write!(f, "stopped"),
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ComposeContainerInfo {
58 #[serde(rename = "ID")]
60 pub id: String,
61 #[serde(rename = "Name")]
63 pub name: String,
64 #[serde(rename = "Service")]
66 pub service: String,
67 #[serde(rename = "State")]
69 pub state: String,
70 #[serde(rename = "Health")]
72 pub health: Option<String>,
73 #[serde(rename = "ExitCode")]
75 pub exit_code: Option<i32>,
76 #[serde(rename = "Publishers")]
78 pub publishers: Option<Vec<PortPublisher>>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct PortPublisher {
84 #[serde(rename = "TargetPort")]
86 pub target_port: u16,
87 #[serde(rename = "PublishedPort")]
89 pub published_port: Option<u16>,
90 #[serde(rename = "Protocol")]
92 pub protocol: String,
93}
94
95#[derive(Debug, Clone)]
97pub struct ComposePsResult {
98 pub stdout: String,
100 pub stderr: String,
102 pub success: bool,
104 pub containers: Vec<ComposeContainerInfo>,
106}
107
108impl ComposePsCommand {
109 #[must_use]
111 pub fn new() -> Self {
112 Self {
113 executor: CommandExecutor::new(),
114 config: ComposeConfig::new(),
115 services: Vec::new(),
116 all: false,
117 quiet: false,
118 show_services: false,
119 filter: Vec::new(),
120 format: None,
121 status: None,
122 }
123 }
124
125 #[must_use]
127 pub fn service(mut self, service: impl Into<String>) -> Self {
128 self.services.push(service.into());
129 self
130 }
131
132 #[must_use]
134 pub fn all(mut self) -> Self {
135 self.all = true;
136 self
137 }
138
139 #[must_use]
141 pub fn quiet(mut self) -> Self {
142 self.quiet = true;
143 self
144 }
145
146 #[must_use]
148 pub fn services(mut self) -> Self {
149 self.show_services = true;
150 self
151 }
152
153 #[must_use]
155 pub fn filter(mut self, filter: impl Into<String>) -> Self {
156 self.filter.push(filter.into());
157 self
158 }
159
160 #[must_use]
162 pub fn format(mut self, format: impl Into<String>) -> Self {
163 self.format = Some(format.into());
164 self
165 }
166
167 #[must_use]
169 pub fn status(mut self, status: ContainerStatus) -> Self {
170 self.status.get_or_insert_with(Vec::new).push(status);
171 self
172 }
173
174 #[must_use]
176 pub fn json(mut self) -> Self {
177 self.format = Some("json".to_string());
178 self
179 }
180
181 fn parse_json_output(stdout: &str) -> Vec<ComposeContainerInfo> {
183 stdout
184 .lines()
185 .filter(|line| !line.trim().is_empty())
186 .filter_map(|line| serde_json::from_str(line).ok())
187 .collect()
188 }
189}
190
191impl Default for ComposePsCommand {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197#[async_trait]
198impl DockerCommand for ComposePsCommand {
199 type Output = ComposePsResult;
200
201 fn get_executor(&self) -> &CommandExecutor {
202 &self.executor
203 }
204
205 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
206 &mut self.executor
207 }
208
209 fn build_command_args(&self) -> Vec<String> {
210 <Self as ComposeCommand>::build_command_args(self)
212 }
213
214 async fn execute(&self) -> Result<Self::Output> {
215 let args = <Self as ComposeCommand>::build_command_args(self);
216 let output = self.execute_command(args).await?;
217
218 let containers = if self.format.as_deref() == Some("json") {
220 Self::parse_json_output(&output.stdout)
221 } else {
222 Vec::new()
223 };
224
225 Ok(ComposePsResult {
226 stdout: output.stdout,
227 stderr: output.stderr,
228 success: output.success,
229 containers,
230 })
231 }
232}
233
234impl ComposeCommand for ComposePsCommand {
235 fn get_config(&self) -> &ComposeConfig {
236 &self.config
237 }
238
239 fn get_config_mut(&mut self) -> &mut ComposeConfig {
240 &mut self.config
241 }
242
243 fn subcommand(&self) -> &'static str {
244 "ps"
245 }
246
247 fn build_subcommand_args(&self) -> Vec<String> {
248 let mut args = Vec::new();
249
250 if self.all {
251 args.push("--all".to_string());
252 }
253
254 if self.quiet {
255 args.push("--quiet".to_string());
256 }
257
258 if self.show_services {
259 args.push("--services".to_string());
260 }
261
262 for filter in &self.filter {
263 args.push("--filter".to_string());
264 args.push(filter.clone());
265 }
266
267 if let Some(ref format) = self.format {
268 args.push("--format".to_string());
269 args.push(format.clone());
270 }
271
272 if let Some(ref statuses) = self.status {
273 for status in statuses {
274 args.push("--status".to_string());
275 args.push(status.to_string());
276 }
277 }
278
279 args.extend(self.services.clone());
281
282 args
283 }
284}
285
286impl ComposePsResult {
287 #[must_use]
289 pub fn success(&self) -> bool {
290 self.success
291 }
292
293 #[must_use]
295 pub fn containers(&self) -> &[ComposeContainerInfo] {
296 &self.containers
297 }
298
299 #[must_use]
301 pub fn container_ids(&self) -> Vec<String> {
302 if self.containers.is_empty() {
303 self.stdout
305 .lines()
306 .skip(1) .filter_map(|line| line.split_whitespace().next())
308 .map(String::from)
309 .collect()
310 } else {
311 self.containers.iter().map(|c| c.id.clone()).collect()
312 }
313 }
314
315 #[must_use]
317 pub fn stdout_lines(&self) -> Vec<&str> {
318 self.stdout.lines().collect()
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_compose_ps_basic() {
328 let cmd = ComposePsCommand::new();
329 let args = cmd.build_subcommand_args();
330 assert!(args.is_empty());
331
332 let full_args = ComposeCommand::build_command_args(&cmd);
333 assert_eq!(full_args[0], "compose");
334 assert!(full_args.contains(&"ps".to_string()));
335 }
336
337 #[test]
338 fn test_compose_ps_all() {
339 let cmd = ComposePsCommand::new().all();
340 let args = cmd.build_subcommand_args();
341 assert_eq!(args, vec!["--all"]);
342 }
343
344 #[test]
345 fn test_compose_ps_with_format() {
346 let cmd = ComposePsCommand::new().format("json").all();
347 let args = cmd.build_subcommand_args();
348 assert_eq!(args, vec!["--all", "--format", "json"]);
349 }
350
351 #[test]
352 fn test_compose_ps_with_filters() {
353 let cmd = ComposePsCommand::new()
354 .filter("status=running")
355 .quiet()
356 .service("web");
357 let args = cmd.build_subcommand_args();
358 assert_eq!(args, vec!["--quiet", "--filter", "status=running", "web"]);
359 }
360
361 #[test]
362 fn test_container_status_display() {
363 assert_eq!(ContainerStatus::Running.to_string(), "running");
364 assert_eq!(ContainerStatus::Stopped.to_string(), "stopped");
365 assert_eq!(ContainerStatus::Paused.to_string(), "paused");
366 assert_eq!(ContainerStatus::Restarting.to_string(), "restarting");
367 }
368
369 #[test]
370 fn test_compose_config_integration() {
371 let cmd = ComposePsCommand::new()
372 .file("docker-compose.yml")
373 .project_name("my-project")
374 .all();
375
376 let args = ComposeCommand::build_command_args(&cmd);
377 assert!(args.contains(&"--file".to_string()));
378 assert!(args.contains(&"docker-compose.yml".to_string()));
379 assert!(args.contains(&"--project-name".to_string()));
380 assert!(args.contains(&"my-project".to_string()));
381 assert!(args.contains(&"--all".to_string()));
382 }
383}