docker_wrapper/command/
compose_up.rs

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