1use 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 ComposeRunCommand {
12 pub executor: CommandExecutor,
14 pub config: ComposeConfig,
16 pub service: String,
18 pub command: Vec<String>,
20 pub detach: bool,
22 pub rm: bool,
24 pub no_deps: bool,
26 pub no_tty: bool,
28 pub interactive: bool,
30 pub entrypoint: Option<String>,
32 pub env: HashMap<String, String>,
34 pub labels: HashMap<String, String>,
36 pub name: Option<String>,
38 pub publish: Vec<String>,
40 pub user: Option<String>,
42 pub workdir: Option<String>,
44 pub volumes: Vec<String>,
46 pub volume_rm: bool,
48}
49
50#[derive(Debug, Clone)]
52pub struct ComposeRunResult {
53 pub stdout: String,
55 pub stderr: String,
57 pub success: bool,
59 pub exit_code: i32,
61 pub service: String,
63 pub detached: bool,
65}
66
67impl ComposeRunCommand {
68 #[must_use]
70 pub fn new(service: impl Into<String>) -> Self {
71 Self {
72 executor: CommandExecutor::new(),
73 config: ComposeConfig::new(),
74 service: service.into(),
75 command: Vec::new(),
76 detach: false,
77 rm: false,
78 no_deps: false,
79 no_tty: false,
80 interactive: false,
81 entrypoint: None,
82 env: HashMap::new(),
83 labels: HashMap::new(),
84 name: None,
85 publish: Vec::new(),
86 user: None,
87 workdir: None,
88 volumes: Vec::new(),
89 volume_rm: false,
90 }
91 }
92
93 #[must_use]
95 pub fn cmd<I, S>(mut self, command: I) -> Self
96 where
97 I: IntoIterator<Item = S>,
98 S: Into<String>,
99 {
100 self.command = command.into_iter().map(Into::into).collect();
101 self
102 }
103
104 #[must_use]
106 pub fn arg(mut self, arg: impl Into<String>) -> Self {
107 self.command.push(arg.into());
108 self
109 }
110
111 #[must_use]
113 pub fn args<I, S>(mut self, args: I) -> Self
114 where
115 I: IntoIterator<Item = S>,
116 S: Into<String>,
117 {
118 self.command.extend(args.into_iter().map(Into::into));
119 self
120 }
121
122 #[must_use]
124 pub fn detach(mut self) -> Self {
125 self.detach = true;
126 self
127 }
128
129 #[must_use]
131 pub fn rm(mut self) -> Self {
132 self.rm = true;
133 self
134 }
135
136 #[must_use]
138 pub fn no_deps(mut self) -> Self {
139 self.no_deps = true;
140 self
141 }
142
143 #[must_use]
145 pub fn no_tty(mut self) -> Self {
146 self.no_tty = true;
147 self
148 }
149
150 #[must_use]
152 pub fn interactive(mut self) -> Self {
153 self.interactive = true;
154 self
155 }
156
157 #[must_use]
159 pub fn entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
160 self.entrypoint = Some(entrypoint.into());
161 self
162 }
163
164 #[must_use]
166 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
167 self.env.insert(key.into(), value.into());
168 self
169 }
170
171 #[must_use]
173 pub fn envs(mut self, env_vars: HashMap<String, String>) -> Self {
174 self.env.extend(env_vars);
175 self
176 }
177
178 #[must_use]
180 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
181 self.labels.insert(key.into(), value.into());
182 self
183 }
184
185 #[must_use]
187 pub fn labels(mut self, labels: HashMap<String, String>) -> Self {
188 self.labels.extend(labels);
189 self
190 }
191
192 #[must_use]
194 pub fn name(mut self, name: impl Into<String>) -> Self {
195 self.name = Some(name.into());
196 self
197 }
198
199 #[must_use]
201 pub fn publish(mut self, publish: impl Into<String>) -> Self {
202 self.publish.push(publish.into());
203 self
204 }
205
206 #[must_use]
208 pub fn user(mut self, user: impl Into<String>) -> Self {
209 self.user = Some(user.into());
210 self
211 }
212
213 #[must_use]
215 pub fn workdir(mut self, workdir: impl Into<String>) -> Self {
216 self.workdir = Some(workdir.into());
217 self
218 }
219
220 #[must_use]
222 pub fn volume(mut self, volume: impl Into<String>) -> Self {
223 self.volumes.push(volume.into());
224 self
225 }
226
227 #[must_use]
229 pub fn volume_rm(mut self) -> Self {
230 self.volume_rm = true;
231 self
232 }
233}
234
235#[async_trait]
236impl DockerCommand for ComposeRunCommand {
237 type Output = ComposeRunResult;
238
239 fn get_executor(&self) -> &CommandExecutor {
240 &self.executor
241 }
242
243 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
244 &mut self.executor
245 }
246
247 fn build_command_args(&self) -> Vec<String> {
248 <Self as ComposeCommand>::build_command_args(self)
250 }
251
252 async fn execute(&self) -> Result<Self::Output> {
253 let args = <Self as ComposeCommand>::build_command_args(self);
254 let output = self.execute_command(args).await?;
255
256 Ok(ComposeRunResult {
257 stdout: output.stdout,
258 stderr: output.stderr,
259 success: output.success,
260 exit_code: output.exit_code,
261 service: self.service.clone(),
262 detached: self.detach,
263 })
264 }
265}
266
267impl ComposeCommand for ComposeRunCommand {
268 fn get_config(&self) -> &ComposeConfig {
269 &self.config
270 }
271
272 fn get_config_mut(&mut self) -> &mut ComposeConfig {
273 &mut self.config
274 }
275
276 fn subcommand(&self) -> &'static str {
277 "run"
278 }
279
280 fn build_subcommand_args(&self) -> Vec<String> {
281 let mut args = Vec::new();
282
283 if self.detach {
284 args.push("--detach".to_string());
285 }
286
287 if self.rm {
288 args.push("--rm".to_string());
289 }
290
291 if self.no_deps {
292 args.push("--no-deps".to_string());
293 }
294
295 if self.no_tty {
296 args.push("--no-TTY".to_string());
297 }
298
299 if self.interactive {
300 args.push("--interactive".to_string());
301 }
302
303 if let Some(ref entrypoint) = self.entrypoint {
305 args.push("--entrypoint".to_string());
306 args.push(entrypoint.clone());
307 }
308
309 for (key, value) in &self.env {
311 args.push("--env".to_string());
312 args.push(format!("{key}={value}"));
313 }
314
315 for (key, value) in &self.labels {
317 args.push("--label".to_string());
318 args.push(format!("{key}={value}"));
319 }
320
321 if let Some(ref name) = self.name {
323 args.push("--name".to_string());
324 args.push(name.clone());
325 }
326
327 for publish in &self.publish {
329 args.push("--publish".to_string());
330 args.push(publish.clone());
331 }
332
333 if let Some(ref user) = self.user {
335 args.push("--user".to_string());
336 args.push(user.clone());
337 }
338
339 if let Some(ref workdir) = self.workdir {
341 args.push("--workdir".to_string());
342 args.push(workdir.clone());
343 }
344
345 for volume in &self.volumes {
347 args.push("--volume".to_string());
348 args.push(volume.clone());
349 }
350
351 if self.volume_rm {
352 args.push("--volume".to_string());
353 args.push("rm".to_string());
354 }
355
356 args.push(self.service.clone());
358
359 args.extend(self.command.clone());
361
362 args
363 }
364}
365
366impl ComposeRunResult {
367 #[must_use]
369 pub fn success(&self) -> bool {
370 self.success
371 }
372
373 #[must_use]
375 pub fn exit_code(&self) -> i32 {
376 self.exit_code
377 }
378
379 #[must_use]
381 pub fn service(&self) -> &str {
382 &self.service
383 }
384
385 #[must_use]
387 pub fn is_detached(&self) -> bool {
388 self.detached
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_compose_run_basic() {
398 let cmd = ComposeRunCommand::new("web");
399 let args = cmd.build_subcommand_args();
400 assert!(args.contains(&"web".to_string()));
401
402 let full_args = ComposeCommand::build_command_args(&cmd);
403 assert_eq!(full_args[0], "compose");
404 assert!(full_args.contains(&"run".to_string()));
405 assert!(full_args.contains(&"web".to_string()));
406 }
407
408 #[test]
409 fn test_compose_run_with_command() {
410 let cmd = ComposeRunCommand::new("worker").cmd(vec!["python", "script.py"]);
411
412 let args = cmd.build_subcommand_args();
413 assert!(args.contains(&"worker".to_string()));
414 assert!(args.contains(&"python".to_string()));
415 assert!(args.contains(&"script.py".to_string()));
416 }
417
418 #[test]
419 fn test_compose_run_with_flags() {
420 let cmd = ComposeRunCommand::new("app")
421 .detach()
422 .rm()
423 .no_deps()
424 .interactive();
425
426 let args = cmd.build_subcommand_args();
427 assert!(args.contains(&"--detach".to_string()));
428 assert!(args.contains(&"--rm".to_string()));
429 assert!(args.contains(&"--no-deps".to_string()));
430 assert!(args.contains(&"--interactive".to_string()));
431 }
432
433 #[test]
434 fn test_compose_run_with_env_and_labels() {
435 let cmd = ComposeRunCommand::new("test")
436 .env("NODE_ENV", "development")
437 .env("DEBUG", "true")
438 .label("version", "1.0")
439 .label("component", "api");
440
441 let args = cmd.build_subcommand_args();
442 assert!(args.contains(&"--env".to_string()));
443 assert!(args.contains(&"NODE_ENV=development".to_string()));
444 assert!(args.contains(&"DEBUG=true".to_string()));
445 assert!(args.contains(&"--label".to_string()));
446 assert!(args.contains(&"version=1.0".to_string()));
447 assert!(args.contains(&"component=api".to_string()));
448 }
449
450 #[test]
451 fn test_compose_run_all_options() {
452 let cmd = ComposeRunCommand::new("database")
453 .detach()
454 .rm()
455 .name("test-db")
456 .user("postgres")
457 .workdir("/app")
458 .volume("/data:/var/lib/postgresql/data")
459 .publish("5432:5432")
460 .entrypoint("docker-entrypoint.sh")
461 .cmd(vec!["postgres"])
462 .env("POSTGRES_DB", "testdb")
463 .label("env", "test");
464
465 let args = cmd.build_subcommand_args();
466
467 assert!(args.contains(&"--detach".to_string()));
469 assert!(args.contains(&"--rm".to_string()));
470
471 assert!(args.contains(&"--name".to_string()));
473 assert!(args.contains(&"test-db".to_string()));
474 assert!(args.contains(&"--user".to_string()));
475 assert!(args.contains(&"postgres".to_string()));
476 assert!(args.contains(&"--workdir".to_string()));
477 assert!(args.contains(&"/app".to_string()));
478 assert!(args.contains(&"--volume".to_string()));
479 assert!(args.contains(&"/data:/var/lib/postgresql/data".to_string()));
480 assert!(args.contains(&"--publish".to_string()));
481 assert!(args.contains(&"5432:5432".to_string()));
482 assert!(args.contains(&"--entrypoint".to_string()));
483 assert!(args.contains(&"docker-entrypoint.sh".to_string()));
484
485 assert!(args.contains(&"database".to_string()));
487 assert!(args.contains(&"postgres".to_string()));
488
489 assert!(args.contains(&"POSTGRES_DB=testdb".to_string()));
491 assert!(args.contains(&"env=test".to_string()));
492 }
493
494 #[test]
495 fn test_compose_config_integration() {
496 let cmd = ComposeRunCommand::new("worker")
497 .file("docker-compose.yml")
498 .project_name("my-project")
499 .rm()
500 .cmd(vec!["python", "worker.py"]);
501
502 let args = ComposeCommand::build_command_args(&cmd);
503 assert!(args.contains(&"--file".to_string()));
504 assert!(args.contains(&"docker-compose.yml".to_string()));
505 assert!(args.contains(&"--project-name".to_string()));
506 assert!(args.contains(&"my-project".to_string()));
507 assert!(args.contains(&"--rm".to_string()));
508 assert!(args.contains(&"worker".to_string()));
509 assert!(args.contains(&"python".to_string()));
510 assert!(args.contains(&"worker.py".to_string()));
511 }
512}