docker_wrapper/compose/
create.rs

1//! Docker Compose create command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose create command
8///
9/// Create services without starting them.
10#[derive(Debug, Clone, Default)]
11#[allow(clippy::struct_excessive_bools)]
12pub struct ComposeCreateCommand {
13    /// Base configuration
14    pub config: ComposeConfig,
15    /// Build images before creating containers
16    pub build: bool,
17    /// Don't build images, even if missing
18    pub no_build: bool,
19    /// Force recreate containers
20    pub force_recreate: bool,
21    /// Don't recreate containers if they exist
22    pub no_recreate: bool,
23    /// Pull images before creating
24    pub pull: Option<PullPolicy>,
25    /// Remove orphaned containers
26    pub remove_orphans: bool,
27    /// Services to create
28    pub services: Vec<String>,
29}
30
31/// Pull policy for images
32#[derive(Debug, Clone, Copy)]
33pub enum PullPolicy {
34    /// Always pull images
35    Always,
36    /// Never pull images
37    Never,
38    /// Pull missing images (default)
39    Missing,
40    /// Pull images if local is older
41    Build,
42}
43
44impl PullPolicy {
45    /// Convert to command line argument
46    #[must_use]
47    pub fn as_arg(&self) -> &str {
48        match self {
49            Self::Always => "always",
50            Self::Never => "never",
51            Self::Missing => "missing",
52            Self::Build => "build",
53        }
54    }
55}
56
57/// Result from create command
58#[derive(Debug, Clone)]
59pub struct CreateResult {
60    /// Output from the command
61    pub output: String,
62    /// Whether the operation succeeded
63    pub success: bool,
64}
65
66impl ComposeCreateCommand {
67    /// Create a new create command
68    #[must_use]
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Add a compose file
74    #[must_use]
75    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
76        self.config.files.push(file.into());
77        self
78    }
79
80    /// Set project name
81    #[must_use]
82    pub fn project_name(mut self, name: impl Into<String>) -> Self {
83        self.config.project_name = Some(name.into());
84        self
85    }
86
87    /// Build images before creating
88    #[must_use]
89    pub fn build(mut self) -> Self {
90        self.build = true;
91        self
92    }
93
94    /// Don't build images
95    #[must_use]
96    pub fn no_build(mut self) -> Self {
97        self.no_build = true;
98        self
99    }
100
101    /// Force recreate containers
102    #[must_use]
103    pub fn force_recreate(mut self) -> Self {
104        self.force_recreate = true;
105        self
106    }
107
108    /// Don't recreate containers
109    #[must_use]
110    pub fn no_recreate(mut self) -> Self {
111        self.no_recreate = true;
112        self
113    }
114
115    /// Set pull policy
116    #[must_use]
117    pub fn pull(mut self, policy: PullPolicy) -> Self {
118        self.pull = Some(policy);
119        self
120    }
121
122    /// Remove orphaned containers
123    #[must_use]
124    pub fn remove_orphans(mut self) -> Self {
125        self.remove_orphans = true;
126        self
127    }
128
129    /// Add a service to create
130    #[must_use]
131    pub fn service(mut self, service: impl Into<String>) -> Self {
132        self.services.push(service.into());
133        self
134    }
135
136    /// Add multiple services to create
137    #[must_use]
138    pub fn services<I, S>(mut self, services: I) -> Self
139    where
140        I: IntoIterator<Item = S>,
141        S: Into<String>,
142    {
143        self.services.extend(services.into_iter().map(Into::into));
144        self
145    }
146
147    fn build_args(&self) -> Vec<String> {
148        let mut args = vec!["create".to_string()];
149
150        // Add flags
151        if self.build {
152            args.push("--build".to_string());
153        }
154        if self.no_build {
155            args.push("--no-build".to_string());
156        }
157        if self.force_recreate {
158            args.push("--force-recreate".to_string());
159        }
160        if self.no_recreate {
161            args.push("--no-recreate".to_string());
162        }
163        if self.remove_orphans {
164            args.push("--remove-orphans".to_string());
165        }
166
167        // Add pull policy
168        if let Some(pull) = &self.pull {
169            args.push("--pull".to_string());
170            args.push(pull.as_arg().to_string());
171        }
172
173        // Add services
174        args.extend(self.services.clone());
175
176        args
177    }
178}
179
180#[async_trait]
181impl ComposeCommand for ComposeCreateCommand {
182    type Output = CreateResult;
183
184    fn get_config(&self) -> &ComposeConfig {
185        &self.config
186    }
187
188    fn get_config_mut(&mut self) -> &mut ComposeConfig {
189        &mut self.config
190    }
191
192    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
193        let output = self.execute_compose_command(args).await?;
194
195        Ok(CreateResult {
196            output: output.stdout,
197            success: output.success,
198        })
199    }
200
201    async fn execute(&self) -> Result<Self::Output> {
202        let args = self.build_args();
203        self.execute_compose(args).await
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_create_command_basic() {
213        let cmd = ComposeCreateCommand::new();
214        let args = cmd.build_args();
215        assert_eq!(args[0], "create");
216    }
217
218    #[test]
219    fn test_create_command_with_build() {
220        let cmd = ComposeCreateCommand::new().build().force_recreate();
221        let args = cmd.build_args();
222        assert!(args.contains(&"--build".to_string()));
223        assert!(args.contains(&"--force-recreate".to_string()));
224    }
225
226    #[test]
227    fn test_create_command_with_pull() {
228        let cmd = ComposeCreateCommand::new()
229            .pull(PullPolicy::Always)
230            .no_recreate();
231        let args = cmd.build_args();
232        assert!(args.contains(&"--pull".to_string()));
233        assert!(args.contains(&"always".to_string()));
234        assert!(args.contains(&"--no-recreate".to_string()));
235    }
236
237    #[test]
238    fn test_create_command_with_services() {
239        let cmd = ComposeCreateCommand::new()
240            .service("web")
241            .service("db")
242            .remove_orphans();
243        let args = cmd.build_args();
244        assert!(args.contains(&"web".to_string()));
245        assert!(args.contains(&"db".to_string()));
246        assert!(args.contains(&"--remove-orphans".to_string()));
247    }
248}