docker_wrapper/compose/
ps.rs1use super::{execute_compose_command, ComposeCommand, ComposeConfig, ComposeOutput};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone)]
10pub struct ComposePsCommand {
11 config: ComposeConfig,
13 services: Vec<String>,
15 all: bool,
17 quiet: bool,
19 show_services: bool,
21 filter: Vec<String>,
23 format: Option<String>,
25 status: Option<Vec<ContainerStatus>>,
27}
28
29#[derive(Debug, Clone, Copy)]
31pub enum ContainerStatus {
32 Paused,
34 Restarting,
36 Running,
38 Stopped,
40}
41
42impl std::fmt::Display for ContainerStatus {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Paused => write!(f, "paused"),
46 Self::Restarting => write!(f, "restarting"),
47 Self::Running => write!(f, "running"),
48 Self::Stopped => write!(f, "stopped"),
49 }
50 }
51}
52
53impl ComposePsCommand {
54 #[must_use]
56 pub fn new() -> Self {
57 Self {
58 config: ComposeConfig::new(),
59 services: Vec::new(),
60 all: false,
61 quiet: false,
62 show_services: false,
63 filter: Vec::new(),
64 format: None,
65 status: None,
66 }
67 }
68
69 #[must_use]
71 pub fn with_config(config: ComposeConfig) -> Self {
72 Self {
73 config,
74 ..Self::new()
75 }
76 }
77
78 #[must_use]
80 pub fn service(mut self, service: impl Into<String>) -> Self {
81 self.services.push(service.into());
82 self
83 }
84
85 #[must_use]
87 pub fn all(mut self) -> Self {
88 self.all = true;
89 self
90 }
91
92 #[must_use]
94 pub fn quiet(mut self) -> Self {
95 self.quiet = true;
96 self
97 }
98
99 #[must_use]
101 pub fn services(mut self) -> Self {
102 self.show_services = true;
103 self
104 }
105
106 #[must_use]
108 pub fn filter(mut self, filter: impl Into<String>) -> Self {
109 self.filter.push(filter.into());
110 self
111 }
112
113 #[must_use]
115 pub fn format(mut self, format: impl Into<String>) -> Self {
116 self.format = Some(format.into());
117 self
118 }
119
120 #[must_use]
122 pub fn status(mut self, status: ContainerStatus) -> Self {
123 self.status.get_or_insert_with(Vec::new).push(status);
124 self
125 }
126
127 #[must_use]
129 pub fn json(mut self) -> Self {
130 self.format = Some("json".to_string());
131 self
132 }
133
134 #[must_use]
136 pub fn file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
137 self.config = self.config.file(path);
138 self
139 }
140
141 #[must_use]
143 pub fn project_name(mut self, name: impl Into<String>) -> Self {
144 self.config = self.config.project_name(name);
145 self
146 }
147
148 pub async fn run(&self) -> Result<ComposePsResult> {
156 let output = self.execute().await?;
157
158 let containers = if self.format.as_deref() == Some("json") {
160 Self::parse_json_output(&output.stdout)
161 } else {
162 Vec::new()
163 };
164
165 Ok(ComposePsResult { output, containers })
166 }
167
168 fn parse_json_output(stdout: &str) -> Vec<ComposeContainerInfo> {
170 stdout
171 .lines()
172 .filter(|line| !line.trim().is_empty())
173 .filter_map(|line| serde_json::from_str(line).ok())
174 .collect()
175 }
176}
177
178impl Default for ComposePsCommand {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184#[async_trait]
185impl ComposeCommand for ComposePsCommand {
186 type Output = ComposeOutput;
187
188 fn subcommand(&self) -> &'static str {
189 "ps"
190 }
191
192 fn build_args(&self) -> Vec<String> {
193 let mut args = Vec::new();
194
195 if self.all {
196 args.push("--all".to_string());
197 }
198
199 if self.quiet {
200 args.push("--quiet".to_string());
201 }
202
203 if self.show_services {
204 args.push("--services".to_string());
205 }
206
207 for filter in &self.filter {
208 args.push("--filter".to_string());
209 args.push(filter.clone());
210 }
211
212 if let Some(ref format) = self.format {
213 args.push("--format".to_string());
214 args.push(format.clone());
215 }
216
217 if let Some(ref statuses) = self.status {
218 for status in statuses {
219 args.push("--status".to_string());
220 args.push(status.to_string());
221 }
222 }
223
224 args.extend(self.services.clone());
226
227 args
228 }
229
230 async fn execute(&self) -> Result<Self::Output> {
231 execute_compose_command(&self.config, self.subcommand(), self.build_args()).await
232 }
233
234 fn config(&self) -> &ComposeConfig {
235 &self.config
236 }
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ComposeContainerInfo {
242 #[serde(rename = "ID")]
244 pub id: String,
245 #[serde(rename = "Name")]
247 pub name: String,
248 #[serde(rename = "Service")]
250 pub service: String,
251 #[serde(rename = "State")]
253 pub state: String,
254 #[serde(rename = "Health")]
256 pub health: Option<String>,
257 #[serde(rename = "ExitCode")]
259 pub exit_code: Option<i32>,
260 #[serde(rename = "Publishers")]
262 pub publishers: Option<Vec<PortPublisher>>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct PortPublisher {
268 #[serde(rename = "TargetPort")]
270 pub target_port: u16,
271 #[serde(rename = "PublishedPort")]
273 pub published_port: Option<u16>,
274 #[serde(rename = "Protocol")]
276 pub protocol: String,
277}
278
279#[derive(Debug, Clone)]
281pub struct ComposePsResult {
282 pub output: ComposeOutput,
284 pub containers: Vec<ComposeContainerInfo>,
286}
287
288impl ComposePsResult {
289 #[must_use]
291 pub fn success(&self) -> bool {
292 self.output.success
293 }
294
295 #[must_use]
297 pub fn containers(&self) -> &[ComposeContainerInfo] {
298 &self.containers
299 }
300
301 #[must_use]
303 pub fn container_ids(&self) -> Vec<String> {
304 if self.containers.is_empty() {
305 self.output
307 .stdout_lines()
308 .into_iter()
309 .skip(1) .filter_map(|line| line.split_whitespace().next())
311 .map(String::from)
312 .collect()
313 } else {
314 self.containers.iter().map(|c| c.id.clone()).collect()
315 }
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_compose_ps_basic() {
325 let cmd = ComposePsCommand::new();
326 let args = cmd.build_args();
327 assert!(args.is_empty());
328 }
329
330 #[test]
331 fn test_compose_ps_all() {
332 let cmd = ComposePsCommand::new().all();
333 let args = cmd.build_args();
334 assert_eq!(args, vec!["--all"]);
335 }
336
337 #[test]
338 fn test_compose_ps_with_format() {
339 let cmd = ComposePsCommand::new().format("json").all();
340 let args = cmd.build_args();
341 assert_eq!(args, vec!["--all", "--format", "json"]);
342 }
343
344 #[test]
345 fn test_compose_ps_with_filters() {
346 let cmd = ComposePsCommand::new()
347 .filter("status=running")
348 .quiet()
349 .service("web");
350 let args = cmd.build_args();
351 assert_eq!(args, vec!["--quiet", "--filter", "status=running", "web"]);
352 }
353
354 #[test]
355 fn test_container_status_display() {
356 assert_eq!(ContainerStatus::Running.to_string(), "running");
357 assert_eq!(ContainerStatus::Stopped.to_string(), "stopped");
358 assert_eq!(ContainerStatus::Paused.to_string(), "paused");
359 assert_eq!(ContainerStatus::Restarting.to_string(), "restarting");
360 }
361}