docker_wrapper/command/
compose_build.rs1use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10#[allow(clippy::struct_excessive_bools)] pub struct ComposeBuildCommand {
12 pub executor: CommandExecutor,
14 pub config: ComposeConfig,
16 pub services: Vec<String>,
18 pub no_cache: bool,
20 pub pull: bool,
22 pub quiet: bool,
24 pub build_args: HashMap<String, String>,
26 pub parallel: bool,
28 pub memory: Option<String>,
30 pub progress: Option<ProgressOutput>,
32 pub ssh: Option<String>,
34}
35
36#[derive(Debug, Clone, Copy)]
38pub enum ProgressOutput {
39 Auto,
41 Plain,
43 Tty,
45}
46
47impl std::fmt::Display for ProgressOutput {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::Auto => write!(f, "auto"),
51 Self::Plain => write!(f, "plain"),
52 Self::Tty => write!(f, "tty"),
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ComposeBuildResult {
60 pub stdout: String,
62 pub stderr: String,
64 pub success: bool,
66 pub services: Vec<String>,
68}
69
70impl ComposeBuildCommand {
71 #[must_use]
73 pub fn new() -> Self {
74 Self {
75 executor: CommandExecutor::new(),
76 config: ComposeConfig::new(),
77 services: Vec::new(),
78 no_cache: false,
79 pull: false,
80 quiet: false,
81 build_args: HashMap::new(),
82 parallel: false,
83 memory: None,
84 progress: None,
85 ssh: None,
86 }
87 }
88
89 #[must_use]
91 pub fn service(mut self, service: impl Into<String>) -> Self {
92 self.services.push(service.into());
93 self
94 }
95
96 #[must_use]
98 pub fn services<I, S>(mut self, services: I) -> Self
99 where
100 I: IntoIterator<Item = S>,
101 S: Into<String>,
102 {
103 self.services.extend(services.into_iter().map(Into::into));
104 self
105 }
106
107 #[must_use]
109 pub fn no_cache(mut self) -> Self {
110 self.no_cache = true;
111 self
112 }
113
114 #[must_use]
116 pub fn pull(mut self) -> Self {
117 self.pull = true;
118 self
119 }
120
121 #[must_use]
123 pub fn quiet(mut self) -> Self {
124 self.quiet = true;
125 self
126 }
127
128 #[must_use]
130 pub fn build_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
131 self.build_args.insert(key.into(), value.into());
132 self
133 }
134
135 #[must_use]
137 pub fn build_args(mut self, args: HashMap<String, String>) -> Self {
138 self.build_args.extend(args);
139 self
140 }
141
142 #[must_use]
144 pub fn parallel(mut self) -> Self {
145 self.parallel = true;
146 self
147 }
148
149 #[must_use]
151 pub fn memory(mut self, memory: impl Into<String>) -> Self {
152 self.memory = Some(memory.into());
153 self
154 }
155
156 #[must_use]
158 pub fn progress(mut self, progress: ProgressOutput) -> Self {
159 self.progress = Some(progress);
160 self
161 }
162
163 #[must_use]
165 pub fn ssh(mut self, ssh: impl Into<String>) -> Self {
166 self.ssh = Some(ssh.into());
167 self
168 }
169}
170
171impl Default for ComposeBuildCommand {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177#[async_trait]
178impl DockerCommand for ComposeBuildCommand {
179 type Output = ComposeBuildResult;
180
181 fn get_executor(&self) -> &CommandExecutor {
182 &self.executor
183 }
184
185 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
186 &mut self.executor
187 }
188
189 fn build_command_args(&self) -> Vec<String> {
190 <Self as ComposeCommand>::build_command_args(self)
192 }
193
194 async fn execute(&self) -> Result<Self::Output> {
195 let args = <Self as ComposeCommand>::build_command_args(self);
196 let output = self.execute_command(args).await?;
197
198 Ok(ComposeBuildResult {
199 stdout: output.stdout,
200 stderr: output.stderr,
201 success: output.success,
202 services: self.services.clone(),
203 })
204 }
205}
206
207impl ComposeCommand for ComposeBuildCommand {
208 fn get_config(&self) -> &ComposeConfig {
209 &self.config
210 }
211
212 fn get_config_mut(&mut self) -> &mut ComposeConfig {
213 &mut self.config
214 }
215
216 fn subcommand(&self) -> &'static str {
217 "build"
218 }
219
220 fn build_subcommand_args(&self) -> Vec<String> {
221 let mut args = Vec::new();
222
223 if self.no_cache {
224 args.push("--no-cache".to_string());
225 }
226
227 if self.pull {
228 args.push("--pull".to_string());
229 }
230
231 if self.quiet {
232 args.push("--quiet".to_string());
233 }
234
235 if self.parallel {
236 args.push("--parallel".to_string());
237 }
238
239 for (key, value) in &self.build_args {
241 args.push("--build-arg".to_string());
242 args.push(format!("{key}={value}"));
243 }
244
245 if let Some(ref memory) = self.memory {
247 args.push("--memory".to_string());
248 args.push(memory.clone());
249 }
250
251 if let Some(progress) = self.progress {
253 args.push("--progress".to_string());
254 args.push(progress.to_string());
255 }
256
257 if let Some(ref ssh) = self.ssh {
259 args.push("--ssh".to_string());
260 args.push(ssh.clone());
261 }
262
263 args.extend(self.services.clone());
265
266 args
267 }
268}
269
270impl ComposeBuildResult {
271 #[must_use]
273 pub fn success(&self) -> bool {
274 self.success
275 }
276
277 #[must_use]
279 pub fn services(&self) -> &[String] {
280 &self.services
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_compose_build_basic() {
290 let cmd = ComposeBuildCommand::new();
291 let args = cmd.build_subcommand_args();
292 assert!(args.is_empty());
293
294 let full_args = ComposeCommand::build_command_args(&cmd);
295 assert_eq!(full_args[0], "compose");
296 assert!(full_args.contains(&"build".to_string()));
297 }
298
299 #[test]
300 fn test_compose_build_with_flags() {
301 let cmd = ComposeBuildCommand::new()
302 .no_cache()
303 .pull()
304 .quiet()
305 .parallel();
306
307 let args = cmd.build_subcommand_args();
308 assert!(args.contains(&"--no-cache".to_string()));
309 assert!(args.contains(&"--pull".to_string()));
310 assert!(args.contains(&"--quiet".to_string()));
311 assert!(args.contains(&"--parallel".to_string()));
312 }
313
314 #[test]
315 fn test_compose_build_with_services() {
316 let cmd = ComposeBuildCommand::new().service("web").service("db");
317
318 let args = cmd.build_subcommand_args();
319 assert!(args.contains(&"web".to_string()));
320 assert!(args.contains(&"db".to_string()));
321 }
322
323 #[test]
324 fn test_compose_build_with_build_args() {
325 let cmd = ComposeBuildCommand::new()
326 .build_arg("VERSION", "1.0")
327 .build_arg("ENV", "production");
328
329 let args = cmd.build_subcommand_args();
330 assert!(args.contains(&"--build-arg".to_string()));
331 let version_arg = "VERSION=1.0";
333 let env_arg = "ENV=production";
334 assert!(args.contains(&version_arg.to_string()) || args.contains(&env_arg.to_string()));
335 }
336
337 #[test]
338 fn test_compose_build_all_options() {
339 let cmd = ComposeBuildCommand::new()
340 .no_cache()
341 .pull()
342 .parallel()
343 .build_arg("VERSION", "2.0")
344 .memory("1g")
345 .progress(ProgressOutput::Plain)
346 .ssh("default")
347 .services(vec!["web", "worker"]);
348
349 let args = cmd.build_subcommand_args();
350 assert!(args.contains(&"--no-cache".to_string()));
351 assert!(args.contains(&"--pull".to_string()));
352 assert!(args.contains(&"--parallel".to_string()));
353 assert!(args.contains(&"--build-arg".to_string()));
354 assert!(args.contains(&"VERSION=2.0".to_string()));
355 assert!(args.contains(&"--memory".to_string()));
356 assert!(args.contains(&"1g".to_string()));
357 assert!(args.contains(&"--progress".to_string()));
358 assert!(args.contains(&"plain".to_string()));
359 assert!(args.contains(&"--ssh".to_string()));
360 assert!(args.contains(&"default".to_string()));
361 assert!(args.contains(&"web".to_string()));
362 assert!(args.contains(&"worker".to_string()));
363 }
364
365 #[test]
366 fn test_progress_output_display() {
367 assert_eq!(ProgressOutput::Auto.to_string(), "auto");
368 assert_eq!(ProgressOutput::Plain.to_string(), "plain");
369 assert_eq!(ProgressOutput::Tty.to_string(), "tty");
370 }
371
372 #[test]
373 fn test_compose_config_integration() {
374 let cmd = ComposeBuildCommand::new()
375 .file("docker-compose.yml")
376 .project_name("my-project")
377 .no_cache()
378 .service("web");
379
380 let args = ComposeCommand::build_command_args(&cmd);
381 assert!(args.contains(&"--file".to_string()));
382 assert!(args.contains(&"docker-compose.yml".to_string()));
383 assert!(args.contains(&"--project-name".to_string()));
384 assert!(args.contains(&"my-project".to_string()));
385 assert!(args.contains(&"--no-cache".to_string()));
386 assert!(args.contains(&"web".to_string()));
387 }
388}