docker_wrapper/command/
create.rs

1//! Docker create command implementation.
2//!
3//! This module provides the `docker create` command for creating containers without starting them.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand, EnvironmentBuilder, PortBuilder};
6use crate::error::Result;
7use async_trait::async_trait;
8
9/// Docker create command builder
10#[allow(clippy::struct_excessive_bools)]
11///
12/// Create a new container without starting it. This is useful for preparing
13/// containers that will be started later.
14///
15/// # Example
16///
17/// ```no_run
18/// use docker_wrapper::CreateCommand;
19///
20/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21/// // Create a simple container
22/// let result = CreateCommand::new("alpine:latest")
23///     .name("my-container")
24///     .run()
25///     .await?;
26///
27/// println!("Created container: {}", result.container_id());
28/// # Ok(())
29/// # }
30/// ```
31#[derive(Debug, Clone)]
32pub struct CreateCommand {
33    /// Docker image to create container from
34    image: String,
35    /// Container name
36    name: Option<String>,
37    /// Command to run in container
38    command: Vec<String>,
39    /// Environment variables
40    env_builder: EnvironmentBuilder,
41    /// Port mappings
42    port_builder: PortBuilder,
43    /// Working directory
44    workdir: Option<String>,
45    /// User specification
46    user: Option<String>,
47    /// Hostname
48    hostname: Option<String>,
49    /// Attach to STDIN
50    attach_stdin: bool,
51    /// Attach to STDOUT  
52    attach_stdout: bool,
53    /// Attach to STDERR
54    attach_stderr: bool,
55    /// Keep STDIN open
56    interactive: bool,
57    /// Allocate a pseudo-TTY
58    tty: bool,
59    /// Volume mounts
60    volumes: Vec<String>,
61    /// Labels
62    labels: Vec<String>,
63    /// Memory limit
64    memory: Option<String>,
65    /// CPU limits
66    cpus: Option<String>,
67    /// Network mode
68    network: Option<String>,
69    /// Command executor
70    pub executor: CommandExecutor,
71}
72
73impl CreateCommand {
74    /// Create a new create command
75    ///
76    /// # Example
77    ///
78    /// ```
79    /// use docker_wrapper::CreateCommand;
80    ///
81    /// let cmd = CreateCommand::new("nginx:latest");
82    /// ```
83    #[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    /// Set the container name
109    ///
110    /// # Example
111    ///
112    /// ```
113    /// use docker_wrapper::CreateCommand;
114    ///
115    /// let cmd = CreateCommand::new("alpine:latest")
116    ///     .name("my-container");
117    /// ```
118    #[must_use]
119    pub fn name(mut self, name: impl Into<String>) -> Self {
120        self.name = Some(name.into());
121        self
122    }
123
124    /// Set the command to run in the container
125    ///
126    /// # Example
127    ///
128    /// ```
129    /// use docker_wrapper::CreateCommand;
130    ///
131    /// let cmd = CreateCommand::new("alpine:latest")
132    ///     .cmd(vec!["echo", "hello world"]);
133    /// ```
134    #[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    /// Add an environment variable
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// use docker_wrapper::CreateCommand;
146    ///
147    /// let cmd = CreateCommand::new("alpine:latest")
148    ///     .env("KEY", "value")
149    ///     .env("DEBUG", "true");
150    /// ```
151    #[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    /// Add a port mapping
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// use docker_wrapper::CreateCommand;
163    ///
164    /// let cmd = CreateCommand::new("nginx:latest")
165    ///     .port(8080, 80);
166    /// ```
167    #[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    /// Set working directory
174    #[must_use]
175    pub fn workdir(mut self, workdir: impl Into<String>) -> Self {
176        self.workdir = Some(workdir.into());
177        self
178    }
179
180    /// Set user
181    #[must_use]
182    pub fn user(mut self, user: impl Into<String>) -> Self {
183        self.user = Some(user.into());
184        self
185    }
186
187    /// Set hostname
188    #[must_use]
189    pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
190        self.hostname = Some(hostname.into());
191        self
192    }
193
194    /// Attach to STDIN
195    #[must_use]
196    pub fn attach_stdin(mut self) -> Self {
197        self.attach_stdin = true;
198        self
199    }
200
201    /// Attach to STDOUT
202    #[must_use]
203    pub fn attach_stdout(mut self) -> Self {
204        self.attach_stdout = true;
205        self
206    }
207
208    /// Attach to STDERR
209    #[must_use]
210    pub fn attach_stderr(mut self) -> Self {
211        self.attach_stderr = true;
212        self
213    }
214
215    /// Enable interactive mode
216    #[must_use]
217    pub fn interactive(mut self) -> Self {
218        self.interactive = true;
219        self
220    }
221
222    /// Allocate a pseudo-TTY
223    #[must_use]
224    pub fn tty(mut self) -> Self {
225        self.tty = true;
226        self
227    }
228
229    /// Add a volume mount
230    ///
231    /// # Example
232    ///
233    /// ```
234    /// use docker_wrapper::CreateCommand;
235    ///
236    /// let cmd = CreateCommand::new("alpine:latest")
237    ///     .volume("/host/path:/container/path")
238    ///     .volume("/host/data:/data:ro");
239    /// ```
240    #[must_use]
241    pub fn volume(mut self, volume: impl Into<String>) -> Self {
242        self.volumes.push(volume.into());
243        self
244    }
245
246    /// Add a label
247    #[must_use]
248    pub fn label(mut self, label: impl Into<String>) -> Self {
249        self.labels.push(label.into());
250        self
251    }
252
253    /// Set memory limit
254    #[must_use]
255    pub fn memory(mut self, memory: impl Into<String>) -> Self {
256        self.memory = Some(memory.into());
257        self
258    }
259
260    /// Set CPU limit
261    #[must_use]
262    pub fn cpus(mut self, cpus: impl Into<String>) -> Self {
263        self.cpus = Some(cpus.into());
264        self
265    }
266
267    /// Set network mode
268    #[must_use]
269    pub fn network(mut self, network: impl Into<String>) -> Self {
270        self.network = Some(network.into());
271        self
272    }
273
274    /// Execute the create command
275    ///
276    /// # Errors
277    /// Returns an error if:
278    /// - The Docker daemon is not running
279    /// - The specified image doesn't exist
280    /// - Invalid configuration options
281    ///
282    /// # Example
283    ///
284    /// ```no_run
285    /// use docker_wrapper::CreateCommand;
286    ///
287    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
288    /// let result = CreateCommand::new("alpine:latest")
289    ///     .name("test-container")
290    ///     .cmd(vec!["echo", "hello"])
291    ///     .run()
292    ///     .await?;
293    ///
294    /// if result.success() {
295    ///     println!("Created container: {}", result.container_id());
296    /// }
297    /// # Ok(())
298    /// # }
299    /// ```
300    pub async fn run(&self) -> Result<CreateResult> {
301        let output = self.execute().await?;
302
303        // Parse container ID from output
304        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        // Environment variables
334        args.extend(self.env_builder.build_args());
335
336        // Port mappings
337        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        // Add image
403        args.push(self.image.clone());
404
405        // Add command
406        args.extend(self.command.clone());
407
408        // Add raw arguments from executor
409        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/// Result from the create command
421#[derive(Debug, Clone)]
422pub struct CreateResult {
423    /// Raw command output
424    pub output: CommandOutput,
425    /// ID of the created container
426    pub container_id: String,
427}
428
429impl CreateResult {
430    /// Check if the create was successful
431    #[must_use]
432    pub fn success(&self) -> bool {
433        self.output.success && !self.container_id.is_empty()
434    }
435
436    /// Get the created container ID
437    #[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        // Verify key arguments are present
530        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}