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}