docker_wrapper/compose/
up.rs

1//! Docker Compose up 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 up command builder
9#[derive(Debug, Clone)]
10#[allow(clippy::struct_excessive_bools)] // Multiple boolean flags are needed for compose up options
11pub struct ComposeUpCommand {
12    /// Base compose configuration
13    config: ComposeConfig,
14    /// Services to start (empty for all)
15    services: Vec<String>,
16    /// Run in detached mode
17    detach: bool,
18    /// Don't start linked services
19    no_deps: bool,
20    /// Force recreate containers
21    force_recreate: bool,
22    /// Recreate containers even if configuration unchanged
23    always_recreate_deps: bool,
24    /// Don't recreate containers
25    no_recreate: bool,
26    /// Don't build images
27    no_build: bool,
28    /// Don't start services
29    no_start: bool,
30    /// Build images before starting
31    build: bool,
32    /// Remove orphan containers
33    remove_orphans: bool,
34    /// Scale SERVICE to NUM instances
35    scale: Vec<(String, u32)>,
36    /// Timeout for container shutdown
37    timeout: Option<Duration>,
38    /// Exit code from first container that stops
39    exit_code_from: Option<String>,
40    /// Abort if containers are stopped
41    abort_on_container_exit: bool,
42    /// Attach to dependent containers
43    attach_dependencies: bool,
44    /// Recreate anonymous volumes
45    renew_anon_volumes: bool,
46    /// Wait for services to be healthy
47    wait: bool,
48    /// Maximum wait timeout
49    wait_timeout: Option<Duration>,
50    /// Pull image policy
51    pull: Option<PullPolicy>,
52}
53
54/// Image pull policy
55#[derive(Debug, Clone, Copy)]
56pub enum PullPolicy {
57    /// Always pull images
58    Always,
59    /// Never pull images
60    Never,
61    /// Pull missing images (default)
62    Missing,
63}
64
65impl std::fmt::Display for PullPolicy {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Self::Always => write!(f, "always"),
69            Self::Never => write!(f, "never"),
70            Self::Missing => write!(f, "missing"),
71        }
72    }
73}
74
75impl ComposeUpCommand {
76    /// Create a new compose up command
77    #[must_use]
78    pub fn new() -> Self {
79        Self {
80            config: ComposeConfig::new(),
81            services: Vec::new(),
82            detach: false,
83            no_deps: false,
84            force_recreate: false,
85            always_recreate_deps: false,
86            no_recreate: false,
87            no_build: false,
88            no_start: false,
89            build: false,
90            remove_orphans: false,
91            scale: Vec::new(),
92            timeout: None,
93            exit_code_from: None,
94            abort_on_container_exit: false,
95            attach_dependencies: false,
96            renew_anon_volumes: false,
97            wait: false,
98            wait_timeout: None,
99            pull: None,
100        }
101    }
102
103    /// Create with a specific compose configuration
104    #[must_use]
105    pub fn with_config(config: ComposeConfig) -> Self {
106        Self {
107            config,
108            ..Self::new()
109        }
110    }
111
112    /// Add a service to start
113    #[must_use]
114    pub fn service(mut self, service: impl Into<String>) -> Self {
115        self.services.push(service.into());
116        self
117    }
118
119    /// Add multiple services
120    #[must_use]
121    pub fn services<I, S>(mut self, services: I) -> Self
122    where
123        I: IntoIterator<Item = S>,
124        S: Into<String>,
125    {
126        self.services.extend(services.into_iter().map(Into::into));
127        self
128    }
129
130    /// Run in detached mode
131    #[must_use]
132    pub fn detach(mut self) -> Self {
133        self.detach = true;
134        self
135    }
136
137    /// Don't start linked services
138    #[must_use]
139    pub fn no_deps(mut self) -> Self {
140        self.no_deps = true;
141        self
142    }
143
144    /// Force recreate containers
145    #[must_use]
146    pub fn force_recreate(mut self) -> Self {
147        self.force_recreate = true;
148        self
149    }
150
151    /// Always recreate dependent containers
152    #[must_use]
153    pub fn always_recreate_deps(mut self) -> Self {
154        self.always_recreate_deps = true;
155        self
156    }
157
158    /// Don't recreate containers
159    #[must_use]
160    pub fn no_recreate(mut self) -> Self {
161        self.no_recreate = true;
162        self
163    }
164
165    /// Don't build images
166    #[must_use]
167    pub fn no_build(mut self) -> Self {
168        self.no_build = true;
169        self
170    }
171
172    /// Don't start services after creating
173    #[must_use]
174    pub fn no_start(mut self) -> Self {
175        self.no_start = true;
176        self
177    }
178
179    /// Build images before starting
180    #[must_use]
181    pub fn build(mut self) -> Self {
182        self.build = true;
183        self
184    }
185
186    /// Remove orphan containers
187    #[must_use]
188    pub fn remove_orphans(mut self) -> Self {
189        self.remove_orphans = true;
190        self
191    }
192
193    /// Scale a service to a specific number of instances
194    #[must_use]
195    pub fn scale(mut self, service: impl Into<String>, instances: u32) -> Self {
196        self.scale.push((service.into(), instances));
197        self
198    }
199
200    /// Set timeout for container shutdown
201    #[must_use]
202    pub fn timeout(mut self, timeout: Duration) -> Self {
203        self.timeout = Some(timeout);
204        self
205    }
206
207    /// Use exit code from specific container
208    #[must_use]
209    pub fn exit_code_from(mut self, service: impl Into<String>) -> Self {
210        self.exit_code_from = Some(service.into());
211        self
212    }
213
214    /// Abort when containers stop
215    #[must_use]
216    pub fn abort_on_container_exit(mut self) -> Self {
217        self.abort_on_container_exit = true;
218        self
219    }
220
221    /// Attach to dependent containers
222    #[must_use]
223    pub fn attach_dependencies(mut self) -> Self {
224        self.attach_dependencies = true;
225        self
226    }
227
228    /// Recreate anonymous volumes
229    #[must_use]
230    pub fn renew_anon_volumes(mut self) -> Self {
231        self.renew_anon_volumes = true;
232        self
233    }
234
235    /// Wait for services to be running/healthy
236    #[must_use]
237    pub fn wait(mut self) -> Self {
238        self.wait = true;
239        self
240    }
241
242    /// Set maximum wait timeout
243    #[must_use]
244    pub fn wait_timeout(mut self, timeout: Duration) -> Self {
245        self.wait_timeout = Some(timeout);
246        self
247    }
248
249    /// Set pull policy
250    #[must_use]
251    pub fn pull(mut self, policy: PullPolicy) -> Self {
252        self.pull = Some(policy);
253        self
254    }
255
256    /// Set compose file
257    #[must_use]
258    pub fn file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
259        self.config = self.config.file(path);
260        self
261    }
262
263    /// Set project name
264    #[must_use]
265    pub fn project_name(mut self, name: impl Into<String>) -> Self {
266        self.config = self.config.project_name(name);
267        self
268    }
269
270    /// Execute the compose up command
271    ///
272    /// # Errors
273    /// Returns an error if:
274    /// - Docker Compose is not installed
275    /// - Compose file is not found or invalid
276    /// - Service configuration errors
277    /// - Container creation/start fails
278    pub async fn run(&self) -> Result<ComposeUpResult> {
279        let output = self.execute().await?;
280
281        Ok(ComposeUpResult {
282            output,
283            services: self.services.clone(),
284            detached: self.detach,
285        })
286    }
287}
288
289impl Default for ComposeUpCommand {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295#[async_trait]
296impl ComposeCommand for ComposeUpCommand {
297    type Output = ComposeOutput;
298
299    fn subcommand(&self) -> &'static str {
300        "up"
301    }
302
303    fn build_args(&self) -> Vec<String> {
304        let mut args = Vec::new();
305
306        if self.detach {
307            args.push("--detach".to_string());
308        }
309
310        if self.no_deps {
311            args.push("--no-deps".to_string());
312        }
313
314        if self.force_recreate {
315            args.push("--force-recreate".to_string());
316        }
317
318        if self.always_recreate_deps {
319            args.push("--always-recreate-deps".to_string());
320        }
321
322        if self.no_recreate {
323            args.push("--no-recreate".to_string());
324        }
325
326        if self.no_build {
327            args.push("--no-build".to_string());
328        }
329
330        if self.no_start {
331            args.push("--no-start".to_string());
332        }
333
334        if self.build {
335            args.push("--build".to_string());
336        }
337
338        if self.remove_orphans {
339            args.push("--remove-orphans".to_string());
340        }
341
342        for (service, count) in &self.scale {
343            args.push("--scale".to_string());
344            args.push(format!("{service}={count}"));
345        }
346
347        if let Some(timeout) = self.timeout {
348            args.push("--timeout".to_string());
349            args.push(timeout.as_secs().to_string());
350        }
351
352        if let Some(ref service) = self.exit_code_from {
353            args.push("--exit-code-from".to_string());
354            args.push(service.clone());
355        }
356
357        if self.abort_on_container_exit {
358            args.push("--abort-on-container-exit".to_string());
359        }
360
361        if self.attach_dependencies {
362            args.push("--attach-dependencies".to_string());
363        }
364
365        if self.renew_anon_volumes {
366            args.push("--renew-anon-volumes".to_string());
367        }
368
369        if self.wait {
370            args.push("--wait".to_string());
371        }
372
373        if let Some(timeout) = self.wait_timeout {
374            args.push("--wait-timeout".to_string());
375            args.push(timeout.as_secs().to_string());
376        }
377
378        if let Some(ref pull) = self.pull {
379            args.push("--pull".to_string());
380            args.push(pull.to_string());
381        }
382
383        // Add service names at the end
384        args.extend(self.services.clone());
385
386        args
387    }
388
389    async fn execute(&self) -> Result<Self::Output> {
390        execute_compose_command(&self.config, self.subcommand(), self.build_args()).await
391    }
392
393    fn config(&self) -> &ComposeConfig {
394        &self.config
395    }
396}
397
398/// Result from compose up command
399#[derive(Debug, Clone)]
400pub struct ComposeUpResult {
401    /// Raw command output
402    pub output: ComposeOutput,
403    /// Services that were started
404    pub services: Vec<String>,
405    /// Whether running in detached mode
406    pub detached: bool,
407}
408
409impl ComposeUpResult {
410    /// Check if the command was successful
411    #[must_use]
412    pub fn success(&self) -> bool {
413        self.output.success
414    }
415
416    /// Get the services that were started
417    #[must_use]
418    pub fn services(&self) -> &[String] {
419        &self.services
420    }
421
422    /// Check if running in detached mode
423    #[must_use]
424    pub fn is_detached(&self) -> bool {
425        self.detached
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_compose_up_basic() {
435        let cmd = ComposeUpCommand::new();
436        let args = cmd.build_args();
437        assert!(args.is_empty());
438    }
439
440    #[test]
441    fn test_compose_up_detached() {
442        let cmd = ComposeUpCommand::new().detach();
443        let args = cmd.build_args();
444        assert_eq!(args, vec!["--detach"]);
445    }
446
447    #[test]
448    fn test_compose_up_with_services() {
449        let cmd = ComposeUpCommand::new().service("web").service("db");
450        let args = cmd.build_args();
451        assert_eq!(args, vec!["web", "db"]);
452    }
453
454    #[test]
455    fn test_compose_up_all_options() {
456        let cmd = ComposeUpCommand::new()
457            .detach()
458            .build()
459            .remove_orphans()
460            .scale("web", 3)
461            .wait()
462            .pull(PullPolicy::Always)
463            .service("web")
464            .service("db");
465
466        let args = cmd.build_args();
467        assert!(args.contains(&"--detach".to_string()));
468        assert!(args.contains(&"--build".to_string()));
469        assert!(args.contains(&"--remove-orphans".to_string()));
470        assert!(args.contains(&"--scale".to_string()));
471        assert!(args.contains(&"web=3".to_string()));
472        assert!(args.contains(&"--wait".to_string()));
473        assert!(args.contains(&"--pull".to_string()));
474        assert!(args.contains(&"always".to_string()));
475    }
476
477    #[test]
478    fn test_pull_policy_display() {
479        assert_eq!(PullPolicy::Always.to_string(), "always");
480        assert_eq!(PullPolicy::Never.to_string(), "never");
481        assert_eq!(PullPolicy::Missing.to_string(), "missing");
482    }
483}