docker_wrapper/command/
tag.rs

1//! Docker tag command implementation.
2//!
3//! This module provides the `docker tag` command for creating tags for images.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9/// Docker tag command builder
10///
11/// Create a tag `TARGET_IMAGE` that refers to `SOURCE_IMAGE`.
12///
13/// # Example
14///
15/// ```no_run
16/// use docker_wrapper::TagCommand;
17///
18/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19/// // Tag an image with a new name
20/// TagCommand::new("myapp:latest", "myregistry.com/myapp:v1.0.0")
21///     .run()
22///     .await?;
23///
24/// // Tag with just a new tag on the same repository
25/// TagCommand::new("myapp:latest", "myapp:stable")
26///     .run()
27///     .await?;
28/// # Ok(())
29/// # }
30/// ```
31#[derive(Debug, Clone)]
32pub struct TagCommand {
33    /// Source image (name:tag or image ID)
34    source_image: String,
35    /// Target image (name:tag)
36    target_image: String,
37    /// Command executor
38    pub executor: CommandExecutor,
39}
40
41impl TagCommand {
42    /// Create a new tag command
43    ///
44    /// # Example
45    ///
46    /// ```
47    /// use docker_wrapper::TagCommand;
48    ///
49    /// // Tag an image for a registry
50    /// let cmd = TagCommand::new("myapp:latest", "docker.io/myuser/myapp:v1.0");
51    ///
52    /// // Create an alias tag
53    /// let cmd = TagCommand::new("nginx:1.21", "nginx:stable");
54    /// ```
55    #[must_use]
56    pub fn new(source_image: impl Into<String>, target_image: impl Into<String>) -> Self {
57        Self {
58            source_image: source_image.into(),
59            target_image: target_image.into(),
60            executor: CommandExecutor::new(),
61        }
62    }
63
64    /// Execute the tag command
65    ///
66    /// # Errors
67    /// Returns an error if:
68    /// - The Docker daemon is not running
69    /// - The source image doesn't exist
70    /// - The target image name is invalid
71    ///
72    /// # Example
73    ///
74    /// ```no_run
75    /// use docker_wrapper::TagCommand;
76    ///
77    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
78    /// let result = TagCommand::new("alpine:latest", "my-alpine:latest")
79    ///     .run()
80    ///     .await?;
81    ///
82    /// if result.success() {
83    ///     println!("Image tagged successfully");
84    /// }
85    /// # Ok(())
86    /// # }
87    /// ```
88    pub async fn run(&self) -> Result<TagResult> {
89        let output = self.execute().await?;
90        Ok(TagResult {
91            output,
92            source_image: self.source_image.clone(),
93            target_image: self.target_image.clone(),
94        })
95    }
96
97    /// Get a reference to the command executor
98    #[must_use]
99    pub fn get_executor(&self) -> &CommandExecutor {
100        &self.executor
101    }
102
103    /// Get a mutable reference to the command executor
104    #[must_use]
105    pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
106        &mut self.executor
107    }
108}
109
110#[async_trait]
111impl DockerCommand for TagCommand {
112    type Output = CommandOutput;
113
114    fn get_executor(&self) -> &CommandExecutor {
115        &self.executor
116    }
117
118    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
119        &mut self.executor
120    }
121
122    fn build_command_args(&self) -> Vec<String> {
123        let mut args = vec!["tag".to_string()];
124        args.push(self.source_image.clone());
125        args.push(self.target_image.clone());
126        // Add raw args from executor
127        args.extend(self.executor.raw_args.clone());
128        args
129    }
130
131    async fn execute(&self) -> Result<Self::Output> {
132        let args = self.build_command_args();
133        self.executor.execute_command("docker", args).await
134    }
135}
136
137/// Result from the tag command
138#[derive(Debug, Clone)]
139pub struct TagResult {
140    /// Raw command output
141    pub output: CommandOutput,
142    /// Source image that was tagged
143    pub source_image: String,
144    /// Target image name
145    pub target_image: String,
146}
147
148impl TagResult {
149    /// Check if the tag was successful
150    #[must_use]
151    pub fn success(&self) -> bool {
152        self.output.success
153    }
154
155    /// Get the source image
156    #[must_use]
157    pub fn source_image(&self) -> &str {
158        &self.source_image
159    }
160
161    /// Get the target image
162    #[must_use]
163    pub fn target_image(&self) -> &str {
164        &self.target_image
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_tag_basic() {
174        let cmd = TagCommand::new("alpine:latest", "my-alpine:latest");
175        let args = cmd.build_command_args();
176        assert_eq!(args, vec!["tag", "alpine:latest", "my-alpine:latest"]);
177    }
178
179    #[test]
180    fn test_tag_with_registry() {
181        let cmd = TagCommand::new("myapp:latest", "docker.io/myuser/myapp:v1.0.0");
182        let args = cmd.build_command_args();
183        assert_eq!(
184            args,
185            vec!["tag", "myapp:latest", "docker.io/myuser/myapp:v1.0.0"]
186        );
187    }
188
189    #[test]
190    fn test_tag_with_image_id() {
191        let cmd = TagCommand::new("sha256:abc123", "myimage:tagged");
192        let args = cmd.build_command_args();
193        assert_eq!(args, vec!["tag", "sha256:abc123", "myimage:tagged"]);
194    }
195
196    #[test]
197    fn test_tag_same_repo_different_tag() {
198        let cmd = TagCommand::new("nginx:1.21", "nginx:stable");
199        let args = cmd.build_command_args();
200        assert_eq!(args, vec!["tag", "nginx:1.21", "nginx:stable"]);
201    }
202}