docker_wrapper/compose/
scale.rs

1//! Docker Compose scale command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8/// Docker Compose scale command
9///
10/// Scale services to specific number of instances.
11#[derive(Debug, Clone, Default)]
12pub struct ComposeScaleCommand {
13    /// Base configuration
14    pub config: ComposeConfig,
15    /// Service scale specifications (service=num)
16    pub scales: HashMap<String, u32>,
17    /// Don't start new containers
18    pub no_deps: bool,
19}
20
21/// Result from scale command
22#[derive(Debug, Clone)]
23pub struct ScaleResult {
24    /// Output from the command
25    pub output: String,
26    /// Whether the operation succeeded
27    pub success: bool,
28}
29
30impl ComposeScaleCommand {
31    /// Create a new scale command
32    #[must_use]
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Add a compose file
38    #[must_use]
39    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
40        self.config.files.push(file.into());
41        self
42    }
43
44    /// Set project name
45    #[must_use]
46    pub fn project_name(mut self, name: impl Into<String>) -> Self {
47        self.config.project_name = Some(name.into());
48        self
49    }
50
51    /// Scale a service to a specific number of instances
52    #[must_use]
53    pub fn scale(mut self, service: impl Into<String>, instances: u32) -> Self {
54        self.scales.insert(service.into(), instances);
55        self
56    }
57
58    /// Scale multiple services
59    #[must_use]
60    pub fn scales<I, S>(mut self, scales: I) -> Self
61    where
62        I: IntoIterator<Item = (S, u32)>,
63        S: Into<String>,
64    {
65        for (service, count) in scales {
66            self.scales.insert(service.into(), count);
67        }
68        self
69    }
70
71    /// Don't start dependency services
72    #[must_use]
73    pub fn no_deps(mut self) -> Self {
74        self.no_deps = true;
75        self
76    }
77
78    fn build_args(&self) -> Vec<String> {
79        let mut args = vec!["scale".to_string()];
80
81        // Add flags
82        if self.no_deps {
83            args.push("--no-deps".to_string());
84        }
85
86        // Add service scales
87        for (service, count) in &self.scales {
88            args.push(format!("{service}={count}"));
89        }
90
91        args
92    }
93}
94
95#[async_trait]
96impl ComposeCommand for ComposeScaleCommand {
97    type Output = ScaleResult;
98
99    fn get_config(&self) -> &ComposeConfig {
100        &self.config
101    }
102
103    fn get_config_mut(&mut self) -> &mut ComposeConfig {
104        &mut self.config
105    }
106
107    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
108        let output = self.execute_compose_command(args).await?;
109
110        Ok(ScaleResult {
111            output: output.stdout,
112            success: output.success,
113        })
114    }
115
116    async fn execute(&self) -> Result<Self::Output> {
117        let args = self.build_args();
118        self.execute_compose(args).await
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_scale_command_basic() {
128        let cmd = ComposeScaleCommand::new();
129        let args = cmd.build_args();
130        assert_eq!(args[0], "scale");
131    }
132
133    #[test]
134    fn test_scale_command_with_service() {
135        let cmd = ComposeScaleCommand::new()
136            .scale("web", 3)
137            .scale("worker", 5);
138        let args = cmd.build_args();
139        assert!(args.iter().any(|arg| arg == "web=3" || arg == "worker=5"));
140    }
141
142    #[test]
143    fn test_scale_command_with_no_deps() {
144        let cmd = ComposeScaleCommand::new().scale("web", 2).no_deps();
145        let args = cmd.build_args();
146        assert!(args.contains(&"--no-deps".to_string()));
147        assert!(args.iter().any(|arg| arg == "web=2"));
148    }
149
150    #[test]
151    fn test_scale_command_with_multiple() {
152        let scales = vec![("app", 4), ("cache", 2)];
153        let cmd = ComposeScaleCommand::new().scales(scales);
154        let args = cmd.build_args();
155        assert!(args.iter().any(|arg| arg == "app=4" || arg == "cache=2"));
156    }
157}