docker_wrapper/command/
create.rs1use super::{CommandExecutor, CommandOutput, DockerCommand, EnvironmentBuilder, PortBuilder};
6use crate::error::Result;
7use async_trait::async_trait;
8
9#[allow(clippy::struct_excessive_bools)]
11#[derive(Debug, Clone)]
32pub struct CreateCommand {
33 image: String,
35 name: Option<String>,
37 command: Vec<String>,
39 env_builder: EnvironmentBuilder,
41 port_builder: PortBuilder,
43 workdir: Option<String>,
45 user: Option<String>,
47 hostname: Option<String>,
49 attach_stdin: bool,
51 attach_stdout: bool,
53 attach_stderr: bool,
55 interactive: bool,
57 tty: bool,
59 volumes: Vec<String>,
61 labels: Vec<String>,
63 memory: Option<String>,
65 cpus: Option<String>,
67 network: Option<String>,
69 pub executor: CommandExecutor,
71}
72
73impl CreateCommand {
74 #[must_use]
84 pub fn new(image: impl Into<String>) -> Self {
85 Self {
86 image: image.into(),
87 name: None,
88 command: Vec::new(),
89 env_builder: EnvironmentBuilder::new(),
90 port_builder: PortBuilder::new(),
91 workdir: None,
92 user: None,
93 hostname: None,
94 attach_stdin: false,
95 attach_stdout: false,
96 attach_stderr: false,
97 interactive: false,
98 tty: false,
99 volumes: Vec::new(),
100 labels: Vec::new(),
101 memory: None,
102 cpus: None,
103 network: None,
104 executor: CommandExecutor::new(),
105 }
106 }
107
108 #[must_use]
119 pub fn name(mut self, name: impl Into<String>) -> Self {
120 self.name = Some(name.into());
121 self
122 }
123
124 #[must_use]
135 pub fn cmd(mut self, command: Vec<impl Into<String>>) -> Self {
136 self.command = command.into_iter().map(Into::into).collect();
137 self
138 }
139
140 #[must_use]
152 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
153 self.env_builder = self.env_builder.var(key, value);
154 self
155 }
156
157 #[must_use]
168 pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
169 self.port_builder = self.port_builder.port(host_port, container_port);
170 self
171 }
172
173 #[must_use]
175 pub fn workdir(mut self, workdir: impl Into<String>) -> Self {
176 self.workdir = Some(workdir.into());
177 self
178 }
179
180 #[must_use]
182 pub fn user(mut self, user: impl Into<String>) -> Self {
183 self.user = Some(user.into());
184 self
185 }
186
187 #[must_use]
189 pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
190 self.hostname = Some(hostname.into());
191 self
192 }
193
194 #[must_use]
196 pub fn attach_stdin(mut self) -> Self {
197 self.attach_stdin = true;
198 self
199 }
200
201 #[must_use]
203 pub fn attach_stdout(mut self) -> Self {
204 self.attach_stdout = true;
205 self
206 }
207
208 #[must_use]
210 pub fn attach_stderr(mut self) -> Self {
211 self.attach_stderr = true;
212 self
213 }
214
215 #[must_use]
217 pub fn interactive(mut self) -> Self {
218 self.interactive = true;
219 self
220 }
221
222 #[must_use]
224 pub fn tty(mut self) -> Self {
225 self.tty = true;
226 self
227 }
228
229 #[must_use]
241 pub fn volume(mut self, volume: impl Into<String>) -> Self {
242 self.volumes.push(volume.into());
243 self
244 }
245
246 #[must_use]
248 pub fn label(mut self, label: impl Into<String>) -> Self {
249 self.labels.push(label.into());
250 self
251 }
252
253 #[must_use]
255 pub fn memory(mut self, memory: impl Into<String>) -> Self {
256 self.memory = Some(memory.into());
257 self
258 }
259
260 #[must_use]
262 pub fn cpus(mut self, cpus: impl Into<String>) -> Self {
263 self.cpus = Some(cpus.into());
264 self
265 }
266
267 #[must_use]
269 pub fn network(mut self, network: impl Into<String>) -> Self {
270 self.network = Some(network.into());
271 self
272 }
273
274 pub async fn run(&self) -> Result<CreateResult> {
301 let output = self.execute().await?;
302
303 let container_id = output.stdout.trim().to_string();
305
306 Ok(CreateResult {
307 output,
308 container_id,
309 })
310 }
311}
312
313#[async_trait]
314impl DockerCommand for CreateCommand {
315 type Output = CommandOutput;
316
317 fn get_executor(&self) -> &CommandExecutor {
318 &self.executor
319 }
320
321 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
322 &mut self.executor
323 }
324
325 fn build_command_args(&self) -> Vec<String> {
326 let mut args = vec!["create".to_string()];
327
328 if let Some(ref name) = self.name {
329 args.push("--name".to_string());
330 args.push(name.clone());
331 }
332
333 args.extend(self.env_builder.build_args());
335
336 args.extend(self.port_builder.build_args());
338
339 if let Some(ref workdir) = self.workdir {
340 args.push("--workdir".to_string());
341 args.push(workdir.clone());
342 }
343
344 if let Some(ref user) = self.user {
345 args.push("--user".to_string());
346 args.push(user.clone());
347 }
348
349 if let Some(ref hostname) = self.hostname {
350 args.push("--hostname".to_string());
351 args.push(hostname.clone());
352 }
353
354 if self.attach_stdin {
355 args.push("--attach".to_string());
356 args.push("STDIN".to_string());
357 }
358
359 if self.attach_stdout {
360 args.push("--attach".to_string());
361 args.push("STDOUT".to_string());
362 }
363
364 if self.attach_stderr {
365 args.push("--attach".to_string());
366 args.push("STDERR".to_string());
367 }
368
369 if self.interactive {
370 args.push("--interactive".to_string());
371 }
372
373 if self.tty {
374 args.push("--tty".to_string());
375 }
376
377 for volume in &self.volumes {
378 args.push("--volume".to_string());
379 args.push(volume.clone());
380 }
381
382 for label in &self.labels {
383 args.push("--label".to_string());
384 args.push(label.clone());
385 }
386
387 if let Some(ref memory) = self.memory {
388 args.push("--memory".to_string());
389 args.push(memory.clone());
390 }
391
392 if let Some(ref cpus) = self.cpus {
393 args.push("--cpus".to_string());
394 args.push(cpus.clone());
395 }
396
397 if let Some(ref network) = self.network {
398 args.push("--network".to_string());
399 args.push(network.clone());
400 }
401
402 args.push(self.image.clone());
404
405 args.extend(self.command.clone());
407
408 args.extend(self.executor.raw_args.clone());
410
411 args
412 }
413
414 async fn execute(&self) -> Result<Self::Output> {
415 let args = self.build_command_args();
416 self.execute_command(args).await
417 }
418}
419
420#[derive(Debug, Clone)]
422pub struct CreateResult {
423 pub output: CommandOutput,
425 pub container_id: String,
427}
428
429impl CreateResult {
430 #[must_use]
432 pub fn success(&self) -> bool {
433 self.output.success && !self.container_id.is_empty()
434 }
435
436 #[must_use]
438 pub fn container_id(&self) -> &str {
439 &self.container_id
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_create_basic() {
449 let cmd = CreateCommand::new("alpine:latest");
450 let args = cmd.build_command_args();
451 assert_eq!(args, vec!["create", "alpine:latest"]);
452 }
453
454 #[test]
455 fn test_create_with_name() {
456 let cmd = CreateCommand::new("alpine:latest").name("test-container");
457 let args = cmd.build_command_args();
458 assert_eq!(
459 args,
460 vec!["create", "--name", "test-container", "alpine:latest"]
461 );
462 }
463
464 #[test]
465 fn test_create_with_command() {
466 let cmd = CreateCommand::new("alpine:latest").cmd(vec!["echo", "hello"]);
467 let args = cmd.build_command_args();
468 assert_eq!(args, vec!["create", "alpine:latest", "echo", "hello"]);
469 }
470
471 #[test]
472 fn test_create_with_env() {
473 let cmd = CreateCommand::new("alpine:latest")
474 .env("KEY1", "value1")
475 .env("KEY2", "value2");
476 let args = cmd.build_command_args();
477 assert!(args.contains(&"--env".to_string()));
478 assert!(args.contains(&"KEY1=value1".to_string()));
479 assert!(args.contains(&"KEY2=value2".to_string()));
480 }
481
482 #[test]
483 fn test_create_with_ports() {
484 let cmd = CreateCommand::new("nginx:latest").port(8080, 80);
485 let args = cmd.build_command_args();
486 assert!(args.contains(&"--publish".to_string()));
487 assert!(args.contains(&"8080:80".to_string()));
488 }
489
490 #[test]
491 fn test_create_with_volumes() {
492 let cmd = CreateCommand::new("alpine:latest")
493 .volume("/host:/container")
494 .volume("/data:/app/data:ro");
495 let args = cmd.build_command_args();
496 assert!(args.contains(&"--volume".to_string()));
497 assert!(args.contains(&"/host:/container".to_string()));
498 assert!(args.contains(&"/data:/app/data:ro".to_string()));
499 }
500
501 #[test]
502 fn test_create_interactive_tty() {
503 let cmd = CreateCommand::new("alpine:latest").interactive().tty();
504 let args = cmd.build_command_args();
505 assert!(args.contains(&"--interactive".to_string()));
506 assert!(args.contains(&"--tty".to_string()));
507 }
508
509 #[test]
510 fn test_create_all_options() {
511 let cmd = CreateCommand::new("alpine:latest")
512 .name("test-container")
513 .cmd(vec!["sh", "-c", "echo hello"])
514 .env("DEBUG", "true")
515 .port(8080, 80)
516 .workdir("/app")
517 .user("1000:1000")
518 .hostname("test-host")
519 .interactive()
520 .tty()
521 .volume("/data:/app/data")
522 .label("version=1.0")
523 .memory("512m")
524 .cpus("0.5")
525 .network("bridge");
526
527 let args = cmd.build_command_args();
528
529 assert!(args.contains(&"--name".to_string()));
531 assert!(args.contains(&"test-container".to_string()));
532 assert!(args.contains(&"--workdir".to_string()));
533 assert!(args.contains(&"/app".to_string()));
534 assert!(args.contains(&"--interactive".to_string()));
535 assert!(args.contains(&"--tty".to_string()));
536 assert!(args.contains(&"alpine:latest".to_string()));
537 assert!(args.contains(&"sh".to_string()));
538 assert!(args.contains(&"-c".to_string()));
539 assert!(args.contains(&"echo hello".to_string()));
540 }
541}