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}