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