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}