docker_wrapper/compose/
down.rs

1//! Docker Compose down command implementation.
2
3use super::{execute_compose_command, ComposeCommand, ComposeConfig, ComposeOutput};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::time::Duration;
7
8/// Docker Compose down command builder
9#[derive(Debug, Clone)]
10pub struct ComposeDownCommand {
11    /// Base compose configuration
12    config: ComposeConfig,
13    /// Remove images
14    remove_images: Option<RemoveImages>,
15    /// Remove named volumes
16    volumes: bool,
17    /// Remove orphan containers
18    remove_orphans: bool,
19    /// Timeout for container shutdown
20    timeout: Option<Duration>,
21    /// Services to stop (empty for all)
22    services: Vec<String>,
23}
24
25/// Image removal options for compose down
26#[derive(Debug, Clone, Copy)]
27pub enum RemoveImages {
28    /// Remove all images used by services
29    All,
30    /// Remove only images that don't have a custom tag
31    Local,
32}
33
34impl std::fmt::Display for RemoveImages {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::All => write!(f, "all"),
38            Self::Local => write!(f, "local"),
39        }
40    }
41}
42
43impl ComposeDownCommand {
44    /// Create a new compose down command
45    #[must_use]
46    pub fn new() -> Self {
47        Self {
48            config: ComposeConfig::new(),
49            remove_images: None,
50            volumes: false,
51            remove_orphans: false,
52            timeout: None,
53            services: Vec::new(),
54        }
55    }
56
57    /// Create with a specific compose configuration
58    #[must_use]
59    pub fn with_config(config: ComposeConfig) -> Self {
60        Self {
61            config,
62            ..Self::new()
63        }
64    }
65
66    /// Remove images (all or local)
67    #[must_use]
68    pub fn remove_images(mut self, policy: RemoveImages) -> Self {
69        self.remove_images = Some(policy);
70        self
71    }
72
73    /// Remove named volumes declared in the volumes section
74    #[must_use]
75    pub fn volumes(mut self) -> Self {
76        self.volumes = true;
77        self
78    }
79
80    /// Remove containers for services not defined in the compose file
81    #[must_use]
82    pub fn remove_orphans(mut self) -> Self {
83        self.remove_orphans = true;
84        self
85    }
86
87    /// Set timeout for container shutdown
88    #[must_use]
89    pub fn timeout(mut self, timeout: Duration) -> Self {
90        self.timeout = Some(timeout);
91        self
92    }
93
94    /// Add a service to stop
95    #[must_use]
96    pub fn service(mut self, service: impl Into<String>) -> Self {
97        self.services.push(service.into());
98        self
99    }
100
101    /// Add multiple services
102    #[must_use]
103    pub fn services<I, S>(mut self, services: I) -> Self
104    where
105        I: IntoIterator<Item = S>,
106        S: Into<String>,
107    {
108        self.services.extend(services.into_iter().map(Into::into));
109        self
110    }
111
112    /// Set compose file
113    #[must_use]
114    pub fn file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
115        self.config = self.config.file(path);
116        self
117    }
118
119    /// Set project name
120    #[must_use]
121    pub fn project_name(mut self, name: impl Into<String>) -> Self {
122        self.config = self.config.project_name(name);
123        self
124    }
125
126    /// Execute the compose down command
127    ///
128    /// # Errors
129    /// Returns an error if:
130    /// - Docker Compose is not installed
131    /// - Compose file is not found
132    /// - Container stop/removal fails
133    pub async fn run(&self) -> Result<ComposeDownResult> {
134        let output = self.execute().await?;
135
136        Ok(ComposeDownResult {
137            output,
138            removed_volumes: self.volumes,
139            removed_images: self.remove_images.is_some(),
140        })
141    }
142}
143
144impl Default for ComposeDownCommand {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150#[async_trait]
151impl ComposeCommand for ComposeDownCommand {
152    type Output = ComposeOutput;
153
154    fn subcommand(&self) -> &'static str {
155        "down"
156    }
157
158    fn build_args(&self) -> Vec<String> {
159        let mut args = Vec::new();
160
161        if let Some(ref remove) = self.remove_images {
162            args.push("--rmi".to_string());
163            args.push(remove.to_string());
164        }
165
166        if self.volumes {
167            args.push("--volumes".to_string());
168        }
169
170        if self.remove_orphans {
171            args.push("--remove-orphans".to_string());
172        }
173
174        if let Some(timeout) = self.timeout {
175            args.push("--timeout".to_string());
176            args.push(timeout.as_secs().to_string());
177        }
178
179        // Add service names at the end
180        args.extend(self.services.clone());
181
182        args
183    }
184
185    async fn execute(&self) -> Result<Self::Output> {
186        execute_compose_command(&self.config, self.subcommand(), self.build_args()).await
187    }
188
189    fn config(&self) -> &ComposeConfig {
190        &self.config
191    }
192}
193
194/// Result from compose down command
195#[derive(Debug, Clone)]
196pub struct ComposeDownResult {
197    /// Raw command output
198    pub output: ComposeOutput,
199    /// Whether volumes were removed
200    pub removed_volumes: bool,
201    /// Whether images were removed
202    pub removed_images: bool,
203}
204
205impl ComposeDownResult {
206    /// Check if the command was successful
207    #[must_use]
208    pub fn success(&self) -> bool {
209        self.output.success
210    }
211
212    /// Check if volumes were removed
213    #[must_use]
214    pub fn volumes_removed(&self) -> bool {
215        self.removed_volumes
216    }
217
218    /// Check if images were removed
219    #[must_use]
220    pub fn images_removed(&self) -> bool {
221        self.removed_images
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_compose_down_basic() {
231        let cmd = ComposeDownCommand::new();
232        let args = cmd.build_args();
233        assert!(args.is_empty());
234    }
235
236    #[test]
237    fn test_compose_down_with_volumes() {
238        let cmd = ComposeDownCommand::new().volumes();
239        let args = cmd.build_args();
240        assert_eq!(args, vec!["--volumes"]);
241    }
242
243    #[test]
244    fn test_compose_down_remove_images() {
245        let cmd = ComposeDownCommand::new().remove_images(RemoveImages::All);
246        let args = cmd.build_args();
247        assert_eq!(args, vec!["--rmi", "all"]);
248    }
249
250    #[test]
251    fn test_compose_down_all_options() {
252        let cmd = ComposeDownCommand::new()
253            .remove_images(RemoveImages::Local)
254            .volumes()
255            .remove_orphans()
256            .timeout(Duration::from_secs(30))
257            .service("web")
258            .service("db");
259
260        let args = cmd.build_args();
261        assert_eq!(
262            args,
263            vec![
264                "--rmi",
265                "local",
266                "--volumes",
267                "--remove-orphans",
268                "--timeout",
269                "30",
270                "web",
271                "db"
272            ]
273        );
274    }
275
276    #[test]
277    fn test_remove_images_display() {
278        assert_eq!(RemoveImages::All.to_string(), "all");
279        assert_eq!(RemoveImages::Local.to_string(), "local");
280    }
281}