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}