1use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::time::Duration;
7
8#[derive(Debug, Clone)]
10#[allow(clippy::struct_excessive_bools)] pub struct ComposeUpCommand {
12 pub executor: CommandExecutor,
14 pub config: ComposeConfig,
16 pub services: Vec<String>,
18 pub detach: bool,
20 pub no_deps: bool,
22 pub force_recreate: bool,
24 pub always_recreate_deps: bool,
26 pub no_recreate: bool,
28 pub no_build: bool,
30 pub no_start: bool,
32 pub build: bool,
34 pub remove_orphans: bool,
36 pub scale: Vec<(String, u32)>,
38 pub timeout: Option<Duration>,
40 pub exit_code_from: Option<String>,
42 pub abort_on_container_exit: bool,
44 pub attach_dependencies: bool,
46 pub renew_anon_volumes: bool,
48 pub wait: bool,
50 pub wait_timeout: Option<Duration>,
52 pub pull: Option<PullPolicy>,
54}
55
56#[derive(Debug, Clone, Copy)]
58pub enum PullPolicy {
59 Always,
61 Never,
63 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#[derive(Debug, Clone)]
79pub struct ComposeUpResult {
80 pub stdout: String,
82 pub stderr: String,
84 pub success: bool,
86 pub services: Vec<String>,
88 pub detached: bool,
90}
91
92impl ComposeUpCommand {
93 #[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 #[must_use]
123 pub fn service(mut self, service: impl Into<String>) -> Self {
124 self.services.push(service.into());
125 self
126 }
127
128 #[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 #[must_use]
141 pub fn detach(mut self) -> Self {
142 self.detach = true;
143 self
144 }
145
146 #[must_use]
148 pub fn no_deps(mut self) -> Self {
149 self.no_deps = true;
150 self
151 }
152
153 #[must_use]
155 pub fn force_recreate(mut self) -> Self {
156 self.force_recreate = true;
157 self
158 }
159
160 #[must_use]
162 pub fn always_recreate_deps(mut self) -> Self {
163 self.always_recreate_deps = true;
164 self
165 }
166
167 #[must_use]
169 pub fn no_recreate(mut self) -> Self {
170 self.no_recreate = true;
171 self
172 }
173
174 #[must_use]
176 pub fn no_build(mut self) -> Self {
177 self.no_build = true;
178 self
179 }
180
181 #[must_use]
183 pub fn no_start(mut self) -> Self {
184 self.no_start = true;
185 self
186 }
187
188 #[must_use]
190 pub fn build(mut self) -> Self {
191 self.build = true;
192 self
193 }
194
195 #[must_use]
197 pub fn remove_orphans(mut self) -> Self {
198 self.remove_orphans = true;
199 self
200 }
201
202 #[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 #[must_use]
211 pub fn timeout(mut self, timeout: Duration) -> Self {
212 self.timeout = Some(timeout);
213 self
214 }
215
216 #[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 #[must_use]
225 pub fn abort_on_container_exit(mut self) -> Self {
226 self.abort_on_container_exit = true;
227 self
228 }
229
230 #[must_use]
232 pub fn attach_dependencies(mut self) -> Self {
233 self.attach_dependencies = true;
234 self
235 }
236
237 #[must_use]
239 pub fn renew_anon_volumes(mut self) -> Self {
240 self.renew_anon_volumes = true;
241 self
242 }
243
244 #[must_use]
246 pub fn wait(mut self) -> Self {
247 self.wait = true;
248 self
249 }
250
251 #[must_use]
253 pub fn wait_timeout(mut self, timeout: Duration) -> Self {
254 self.wait_timeout = Some(timeout);
255 self
256 }
257
258 #[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 <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 args.extend(self.services.clone());
398
399 args
400 }
401}
402
403impl ComposeUpResult {
404 #[must_use]
406 pub fn success(&self) -> bool {
407 self.success
408 }
409
410 #[must_use]
412 pub fn services(&self) -> &[String] {
413 &self.services
414 }
415
416 #[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}