docker_wrapper/command/
push.rs

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