docker_wrapper/command/
pull.rs

1//! Docker Pull Command Implementation
2//!
3//! This module provides a comprehensive implementation of the `docker pull` command,
4//! supporting all native Docker pull options for downloading images from registries.
5//!
6//! # Examples
7//!
8//! ## Basic Usage
9//!
10//! ```no_run
11//! use docker_wrapper::PullCommand;
12//! use docker_wrapper::DockerCommand;
13//!
14//! #[tokio::main]
15//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//!     // Basic pull of an image
17//!     let pull_cmd = PullCommand::new("nginx:latest");
18//!     let output = pull_cmd.execute().await?;
19//!     println!("Pull completed: {}", output.success);
20//!     Ok(())
21//! }
22//! ```
23//!
24//! ## Advanced Usage
25//!
26//! ```no_run
27//! use docker_wrapper::PullCommand;
28//! use docker_wrapper::DockerCommand;
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//!     // Pull all tags for a repository
33//!     let pull_cmd = PullCommand::new("alpine")
34//!         .all_tags()
35//!         .platform("linux/amd64")
36//!         .quiet();
37//!
38//!     let output = pull_cmd.execute().await?;
39//!     println!("All tags pulled: {}", output.success);
40//!     Ok(())
41//! }
42//! ```
43
44use super::{CommandExecutor, CommandOutput, DockerCommand};
45use crate::error::Result;
46use async_trait::async_trait;
47
48/// Docker Pull Command Builder
49///
50/// Implements the `docker pull` command for downloading images from registries.
51///
52/// # Docker Pull Overview
53///
54/// The pull command downloads images from Docker registries (like Docker Hub)
55/// to the local Docker daemon. It supports:
56/// - Single image pull by name and tag
57/// - All tags pull for a repository
58/// - Multi-platform image selection
59/// - Quiet mode for minimal output
60/// - Content trust verification control
61///
62/// # Image Naming
63///
64/// Images can be specified in several formats:
65/// - `image` - Defaults to latest tag
66/// - `image:tag` - Specific tag
67/// - `image@digest` - Specific digest
68/// - `registry/image:tag` - Specific registry
69/// - `registry:port/image:tag` - Custom registry port
70///
71/// # Examples
72///
73/// ```no_run
74/// use docker_wrapper::PullCommand;
75/// use docker_wrapper::DockerCommand;
76///
77/// #[tokio::main]
78/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
79///     // Pull latest nginx
80///     let output = PullCommand::new("nginx")
81///         .execute()
82///         .await?;
83///
84///     println!("Pull success: {}", output.success);
85///     Ok(())
86/// }
87/// ```
88#[derive(Debug, Clone)]
89pub struct PullCommand {
90    /// Image name with optional tag or digest
91    image: String,
92    /// Download all tagged images in the repository
93    all_tags: bool,
94    /// Skip image verification (disable content trust)
95    disable_content_trust: bool,
96    /// Set platform if server is multi-platform capable
97    platform: Option<String>,
98    /// Suppress verbose output
99    quiet: bool,
100    /// Command executor for handling raw arguments and execution
101    pub executor: CommandExecutor,
102}
103
104impl PullCommand {
105    /// Create a new `PullCommand` instance
106    ///
107    /// # Arguments
108    ///
109    /// * `image` - The image name to pull (e.g., "nginx:latest", "alpine", "redis:7.0")
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use docker_wrapper::PullCommand;
115    ///
116    /// let pull_cmd = PullCommand::new("nginx:latest");
117    /// ```
118    #[must_use]
119    pub fn new<S: Into<String>>(image: S) -> Self {
120        Self {
121            image: image.into(),
122            all_tags: false,
123            disable_content_trust: false,
124            platform: None,
125            quiet: false,
126            executor: CommandExecutor::new(),
127        }
128    }
129
130    /// Download all tagged images in the repository
131    ///
132    /// When enabled, pulls all available tags for the specified image repository.
133    /// Cannot be used with specific tags or digests.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use docker_wrapper::PullCommand;
139    ///
140    /// let pull_cmd = PullCommand::new("alpine")
141    ///     .all_tags();
142    /// ```
143    #[must_use]
144    pub fn all_tags(mut self) -> Self {
145        self.all_tags = true;
146        self
147    }
148
149    /// Skip image verification (disable content trust)
150    ///
151    /// By default, Docker may verify image signatures when content trust is enabled.
152    /// This option disables that verification.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use docker_wrapper::PullCommand;
158    ///
159    /// let pull_cmd = PullCommand::new("nginx:latest")
160    ///     .disable_content_trust();
161    /// ```
162    #[must_use]
163    pub fn disable_content_trust(mut self) -> Self {
164        self.disable_content_trust = true;
165        self
166    }
167
168    /// Set platform if server is multi-platform capable
169    ///
170    /// Specifies the platform for which to pull the image when the image
171    /// supports multiple platforms (architectures).
172    ///
173    /// Common platform values:
174    /// - `linux/amd64` - 64-bit Intel/AMD Linux
175    /// - `linux/arm64` - 64-bit ARM Linux
176    /// - `linux/arm/v7` - 32-bit ARM Linux
177    /// - `windows/amd64` - 64-bit Windows
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use docker_wrapper::PullCommand;
183    ///
184    /// let pull_cmd = PullCommand::new("nginx:latest")
185    ///     .platform("linux/arm64");
186    /// ```
187    #[must_use]
188    pub fn platform<S: Into<String>>(mut self, platform: S) -> Self {
189        self.platform = Some(platform.into());
190        self
191    }
192
193    /// Suppress verbose output
194    ///
195    /// Reduces the amount of output during the pull operation.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use docker_wrapper::PullCommand;
201    ///
202    /// let pull_cmd = PullCommand::new("nginx:latest")
203    ///     .quiet();
204    /// ```
205    #[must_use]
206    pub fn quiet(mut self) -> Self {
207        self.quiet = true;
208        self
209    }
210
211    /// Get the image name
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use docker_wrapper::PullCommand;
217    ///
218    /// let pull_cmd = PullCommand::new("nginx:latest");
219    /// assert_eq!(pull_cmd.get_image(), "nginx:latest");
220    /// ```
221    #[must_use]
222    pub fn get_image(&self) -> &str {
223        &self.image
224    }
225
226    /// Check if all tags mode is enabled
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// use docker_wrapper::PullCommand;
232    ///
233    /// let pull_cmd = PullCommand::new("alpine").all_tags();
234    /// assert!(pull_cmd.is_all_tags());
235    /// ```
236    #[must_use]
237    pub fn is_all_tags(&self) -> bool {
238        self.all_tags
239    }
240
241    /// Check if quiet mode is enabled
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use docker_wrapper::PullCommand;
247    ///
248    /// let pull_cmd = PullCommand::new("nginx").quiet();
249    /// assert!(pull_cmd.is_quiet());
250    /// ```
251    #[must_use]
252    pub fn is_quiet(&self) -> bool {
253        self.quiet
254    }
255
256    /// Get the platform if set
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use docker_wrapper::PullCommand;
262    ///
263    /// let pull_cmd = PullCommand::new("nginx").platform("linux/arm64");
264    /// assert_eq!(pull_cmd.get_platform(), Some("linux/arm64"));
265    /// ```
266    #[must_use]
267    pub fn get_platform(&self) -> Option<&str> {
268        self.platform.as_deref()
269    }
270
271    /// Check if content trust is disabled
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use docker_wrapper::PullCommand;
277    ///
278    /// let pull_cmd = PullCommand::new("nginx").disable_content_trust();
279    /// assert!(pull_cmd.is_content_trust_disabled());
280    /// ```
281    #[must_use]
282    pub fn is_content_trust_disabled(&self) -> bool {
283        self.disable_content_trust
284    }
285
286    /// Get a reference to the command executor
287    #[must_use]
288    pub fn get_executor(&self) -> &CommandExecutor {
289        &self.executor
290    }
291
292    /// Get a mutable reference to the command executor
293    #[must_use]
294    pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
295        &mut self.executor
296    }
297}
298
299impl Default for PullCommand {
300    fn default() -> Self {
301        Self::new("hello-world")
302    }
303}
304
305#[async_trait]
306impl DockerCommand for PullCommand {
307    type Output = CommandOutput;
308
309    fn get_executor(&self) -> &CommandExecutor {
310        &self.executor
311    }
312
313    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
314        &mut self.executor
315    }
316
317    fn build_command_args(&self) -> Vec<String> {
318        let mut args = vec!["pull".to_string()];
319
320        // Add all-tags flag
321        if self.all_tags {
322            args.push("--all-tags".to_string());
323        }
324
325        // Add disable-content-trust flag
326        if self.disable_content_trust {
327            args.push("--disable-content-trust".to_string());
328        }
329
330        // Add platform
331        if let Some(ref platform) = self.platform {
332            args.push("--platform".to_string());
333            args.push(platform.clone());
334        }
335
336        // Add quiet flag
337        if self.quiet {
338            args.push("--quiet".to_string());
339        }
340
341        // Add image name (must be last)
342        args.push(self.image.clone());
343
344        // Add raw args from executor
345        args.extend(self.executor.raw_args.clone());
346
347        args
348    }
349
350    async fn execute(&self) -> Result<Self::Output> {
351        let args = self.build_command_args();
352        self.executor.execute_command("docker", args).await
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_pull_command_basic() {
362        let pull_cmd = PullCommand::new("nginx:latest");
363        let args = pull_cmd.build_command_args();
364
365        assert_eq!(args, vec!["pull", "nginx:latest"]);
366        assert_eq!(pull_cmd.get_image(), "nginx:latest");
367        assert!(!pull_cmd.is_all_tags());
368        assert!(!pull_cmd.is_quiet());
369        assert!(!pull_cmd.is_content_trust_disabled());
370        assert_eq!(pull_cmd.get_platform(), None);
371    }
372
373    #[test]
374    fn test_pull_command_with_all_tags() {
375        let pull_cmd = PullCommand::new("alpine").all_tags();
376        let args = pull_cmd.build_command_args();
377
378        assert!(args.contains(&"--all-tags".to_string()));
379        assert!(args.contains(&"alpine".to_string()));
380        assert_eq!(args[0], "pull");
381        assert!(pull_cmd.is_all_tags());
382    }
383
384    #[test]
385    fn test_pull_command_with_platform() {
386        let pull_cmd = PullCommand::new("nginx:latest").platform("linux/arm64");
387        let args = pull_cmd.build_command_args();
388
389        assert!(args.contains(&"--platform".to_string()));
390        assert!(args.contains(&"linux/arm64".to_string()));
391        assert!(args.contains(&"nginx:latest".to_string()));
392        assert_eq!(args[0], "pull");
393        assert_eq!(pull_cmd.get_platform(), Some("linux/arm64"));
394    }
395
396    #[test]
397    fn test_pull_command_with_quiet() {
398        let pull_cmd = PullCommand::new("redis:7.0").quiet();
399        let args = pull_cmd.build_command_args();
400
401        assert!(args.contains(&"--quiet".to_string()));
402        assert!(args.contains(&"redis:7.0".to_string()));
403        assert_eq!(args[0], "pull");
404        assert!(pull_cmd.is_quiet());
405    }
406
407    #[test]
408    fn test_pull_command_disable_content_trust() {
409        let pull_cmd = PullCommand::new("ubuntu:22.04").disable_content_trust();
410        let args = pull_cmd.build_command_args();
411
412        assert!(args.contains(&"--disable-content-trust".to_string()));
413        assert!(args.contains(&"ubuntu:22.04".to_string()));
414        assert_eq!(args[0], "pull");
415        assert!(pull_cmd.is_content_trust_disabled());
416    }
417
418    #[test]
419    fn test_pull_command_all_options() {
420        let pull_cmd = PullCommand::new("postgres")
421            .all_tags()
422            .platform("linux/amd64")
423            .quiet()
424            .disable_content_trust();
425
426        let args = pull_cmd.build_command_args();
427
428        assert!(args.contains(&"--all-tags".to_string()));
429        assert!(args.contains(&"--platform".to_string()));
430        assert!(args.contains(&"linux/amd64".to_string()));
431        assert!(args.contains(&"--quiet".to_string()));
432        assert!(args.contains(&"--disable-content-trust".to_string()));
433        assert!(args.contains(&"postgres".to_string()));
434        assert_eq!(args[0], "pull");
435
436        // Verify helper methods
437        assert!(pull_cmd.is_all_tags());
438        assert!(pull_cmd.is_quiet());
439        assert!(pull_cmd.is_content_trust_disabled());
440        assert_eq!(pull_cmd.get_platform(), Some("linux/amd64"));
441        assert_eq!(pull_cmd.get_image(), "postgres");
442    }
443
444    #[test]
445    fn test_pull_command_with_registry() {
446        let pull_cmd = PullCommand::new("registry.hub.docker.com/library/nginx:alpine");
447        let args = pull_cmd.build_command_args();
448
449        assert_eq!(
450            args,
451            vec!["pull", "registry.hub.docker.com/library/nginx:alpine"]
452        );
453        assert_eq!(
454            pull_cmd.get_image(),
455            "registry.hub.docker.com/library/nginx:alpine"
456        );
457    }
458
459    #[test]
460    fn test_pull_command_with_digest() {
461        let pull_cmd = PullCommand::new(
462            "nginx@sha256:abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
463        );
464        let args = pull_cmd.build_command_args();
465
466        assert_eq!(
467            args,
468            vec![
469                "pull",
470                "nginx@sha256:abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab"
471            ]
472        );
473    }
474
475    #[test]
476    fn test_pull_command_order() {
477        let pull_cmd = PullCommand::new("alpine:3.18")
478            .quiet()
479            .platform("linux/arm64")
480            .all_tags();
481
482        let args = pull_cmd.build_command_args();
483
484        // Command should be first
485        assert_eq!(args[0], "pull");
486
487        // Image should be last
488        assert_eq!(args.last(), Some(&"alpine:3.18".to_string()));
489
490        // All options should be present
491        assert!(args.contains(&"--all-tags".to_string()));
492        assert!(args.contains(&"--platform".to_string()));
493        assert!(args.contains(&"linux/arm64".to_string()));
494        assert!(args.contains(&"--quiet".to_string()));
495    }
496
497    #[test]
498    fn test_pull_command_default() {
499        let pull_cmd = PullCommand::default();
500        assert_eq!(pull_cmd.get_image(), "hello-world");
501    }
502}