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