docker_wrapper/command/
login.rs

1//! Docker login command implementation
2//!
3//! This module provides functionality to authenticate with Docker registries.
4//! It supports both Docker Hub and private registries with various authentication methods.
5
6use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::fmt;
10
11/// Command for authenticating with Docker registries
12///
13/// The `LoginCommand` provides a builder pattern for constructing Docker login commands
14/// with various authentication options including username/password, token-based auth,
15/// and different registry endpoints.
16///
17/// # Examples
18///
19/// ```rust
20/// use docker_wrapper::LoginCommand;
21///
22/// // Login to Docker Hub
23/// let login = LoginCommand::new("myusername", "mypassword");
24///
25/// // Login to private registry
26/// let login = LoginCommand::new("user", "pass")
27///     .registry("my-registry.com");
28///
29/// // Login with stdin password (more secure)
30/// let login = LoginCommand::new("user", "")
31///     .password_stdin();
32/// ```
33#[derive(Debug, Clone)]
34pub struct LoginCommand {
35    /// Username for authentication
36    username: String,
37    /// Password for authentication (empty if using stdin)
38    password: String,
39    /// Registry server URL (defaults to Docker Hub if None)
40    registry: Option<String>,
41    /// Whether to read password from stdin
42    password_stdin: bool,
43    /// Command executor for running the command
44    pub executor: CommandExecutor,
45}
46
47/// Output from a login command execution
48///
49/// Contains the raw output from the Docker login command and provides
50/// convenience methods for checking authentication status.
51#[derive(Debug, Clone)]
52pub struct LoginOutput {
53    /// Raw output from the Docker command
54    pub output: CommandOutput,
55}
56
57impl LoginCommand {
58    /// Creates a new login command with username and password
59    ///
60    /// # Arguments
61    ///
62    /// * `username` - The username for authentication
63    /// * `password` - The password for authentication (can be empty if using stdin)
64    ///
65    /// # Examples
66    ///
67    /// ```rust
68    /// use docker_wrapper::LoginCommand;
69    ///
70    /// let login = LoginCommand::new("myuser", "mypass");
71    /// ```
72    pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
73        Self {
74            username: username.into(),
75            password: password.into(),
76            registry: None,
77            password_stdin: false,
78            executor: CommandExecutor::default(),
79        }
80    }
81
82    /// Sets the registry server for authentication
83    ///
84    /// If not specified, defaults to Docker Hub (index.docker.io)
85    ///
86    /// # Arguments
87    ///
88    /// * `registry` - The registry server URL
89    ///
90    /// # Examples
91    ///
92    /// ```rust
93    /// use docker_wrapper::LoginCommand;
94    ///
95    /// let login = LoginCommand::new("user", "pass")
96    ///     .registry("gcr.io");
97    /// ```
98    #[must_use]
99    pub fn registry(mut self, registry: impl Into<String>) -> Self {
100        self.registry = Some(registry.into());
101        self
102    }
103
104    /// Enables reading password from stdin for security
105    ///
106    /// When enabled, the password field is ignored and Docker will
107    /// prompt for password input via stdin.
108    ///
109    /// # Examples
110    ///
111    /// ```rust
112    /// use docker_wrapper::LoginCommand;
113    ///
114    /// let login = LoginCommand::new("user", "")
115    ///     .password_stdin();
116    /// ```
117    #[must_use]
118    pub fn password_stdin(mut self) -> Self {
119        self.password_stdin = true;
120        self
121    }
122
123    /// Sets a custom command executor
124    ///
125    /// # Arguments
126    ///
127    /// * `executor` - Custom command executor
128    #[must_use]
129    pub fn executor(mut self, executor: CommandExecutor) -> Self {
130        self.executor = executor;
131        self
132    }
133
134    /// Gets the username
135    #[must_use]
136    pub fn get_username(&self) -> &str {
137        &self.username
138    }
139
140    /// Gets the registry (if set)
141    #[must_use]
142    pub fn get_registry(&self) -> Option<&str> {
143        self.registry.as_deref()
144    }
145
146    /// Returns true if password will be read from stdin
147    #[must_use]
148    pub fn is_password_stdin(&self) -> bool {
149        self.password_stdin
150    }
151
152    /// Get a reference to the command executor
153    #[must_use]
154    pub fn get_executor(&self) -> &CommandExecutor {
155        &self.executor
156    }
157
158    /// Get a mutable reference to the command executor
159    #[must_use]
160    pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
161        &mut self.executor
162    }
163}
164
165impl Default for LoginCommand {
166    fn default() -> Self {
167        Self::new("", "")
168    }
169}
170
171impl LoginOutput {
172    /// Returns true if the login was successful
173    #[must_use]
174    pub fn success(&self) -> bool {
175        self.output.success
176    }
177
178    /// Returns true if the output indicates successful authentication
179    #[must_use]
180    pub fn is_authenticated(&self) -> bool {
181        self.success()
182            && (self.output.stdout.contains("Login Succeeded")
183                || self.output.stdout.contains("login succeeded"))
184    }
185
186    /// Gets any warning messages from the login output
187    #[must_use]
188    pub fn warnings(&self) -> Vec<&str> {
189        self.output
190            .stderr
191            .lines()
192            .filter(|line| line.to_lowercase().contains("warning"))
193            .collect()
194    }
195}
196
197#[async_trait]
198impl DockerCommand for LoginCommand {
199    type Output = LoginOutput;
200
201    fn get_executor(&self) -> &CommandExecutor {
202        &self.executor
203    }
204
205    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
206        &mut self.executor
207    }
208
209    fn build_command_args(&self) -> Vec<String> {
210        let mut args = vec!["login".to_string()];
211
212        // Add username
213        args.push("--username".to_string());
214        args.push(self.username.clone());
215
216        // Add password option
217        if self.password_stdin {
218            args.push("--password-stdin".to_string());
219        } else {
220            args.push("--password".to_string());
221            args.push(self.password.clone());
222        }
223
224        // Add registry if specified
225        if let Some(ref registry) = self.registry {
226            args.push(registry.clone());
227        }
228
229        // Add raw args from executor
230        args.extend(self.executor.raw_args.clone());
231
232        args
233    }
234
235    async fn execute(&self) -> Result<Self::Output> {
236        let args = self.build_command_args();
237        let output = self.executor.execute_command("docker", args).await?;
238
239        Ok(LoginOutput { output })
240    }
241}
242
243impl fmt::Display for LoginCommand {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(f, "docker login")?;
246
247        if let Some(ref registry) = self.registry {
248            write!(f, " {registry}")?;
249        }
250
251        write!(f, " --username {}", self.username)?;
252
253        if self.password_stdin {
254            write!(f, " --password-stdin")?;
255        } else {
256            write!(f, " --password [HIDDEN]")?;
257        }
258
259        Ok(())
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_login_command_basic() {
269        let login = LoginCommand::new("testuser", "testpass");
270
271        assert_eq!(login.get_username(), "testuser");
272        assert_eq!(login.get_registry(), None);
273        assert!(!login.is_password_stdin());
274
275        let args = login.build_command_args();
276        assert_eq!(
277            args,
278            vec!["login", "--username", "testuser", "--password", "testpass"]
279        );
280    }
281
282    #[test]
283    fn test_login_command_with_registry() {
284        let login = LoginCommand::new("user", "pass").registry("gcr.io");
285
286        assert_eq!(login.get_registry(), Some("gcr.io"));
287
288        let args = login.build_command_args();
289        assert_eq!(
290            args,
291            vec![
292                "login",
293                "--username",
294                "user",
295                "--password",
296                "pass",
297                "gcr.io"
298            ]
299        );
300    }
301
302    #[test]
303    fn test_login_command_password_stdin() {
304        let login = LoginCommand::new("user", "ignored").password_stdin();
305
306        assert!(login.is_password_stdin());
307
308        let args = login.build_command_args();
309        assert_eq!(
310            args,
311            vec!["login", "--username", "user", "--password-stdin"]
312        );
313    }
314
315    #[test]
316    fn test_login_command_with_private_registry() {
317        let login = LoginCommand::new("admin", "secret").registry("my-registry.example.com:5000");
318
319        let args = login.build_command_args();
320        assert_eq!(
321            args,
322            vec![
323                "login",
324                "--username",
325                "admin",
326                "--password",
327                "secret",
328                "my-registry.example.com:5000"
329            ]
330        );
331    }
332
333    #[test]
334    fn test_login_command_docker_hub_default() {
335        let login = LoginCommand::new("dockeruser", "dockerpass");
336
337        // No registry specified should default to Docker Hub
338        assert_eq!(login.get_registry(), None);
339
340        let args = login.build_command_args();
341        assert!(!args.contains(&"index.docker.io".to_string()));
342    }
343
344    #[test]
345    fn test_login_command_display() {
346        let login = LoginCommand::new("testuser", "testpass").registry("example.com");
347
348        let display = format!("{login}");
349        assert!(display.contains("docker login"));
350        assert!(display.contains("example.com"));
351        assert!(display.contains("--username testuser"));
352        assert!(display.contains("--password [HIDDEN]"));
353        assert!(!display.contains("testpass"));
354    }
355
356    #[test]
357    fn test_login_command_display_stdin() {
358        let login = LoginCommand::new("testuser", "").password_stdin();
359
360        let display = format!("{login}");
361        assert!(display.contains("--password-stdin"));
362        assert!(!display.contains("[HIDDEN]"));
363    }
364
365    #[test]
366    fn test_login_command_default() {
367        let login = LoginCommand::default();
368
369        assert_eq!(login.get_username(), "");
370        assert_eq!(login.get_registry(), None);
371        assert!(!login.is_password_stdin());
372    }
373
374    #[test]
375    fn test_login_output_success_detection() {
376        let output = CommandOutput {
377            stdout: "Login Succeeded".to_string(),
378            stderr: String::new(),
379            exit_code: 0,
380            success: true,
381        };
382        let login_output = LoginOutput { output };
383
384        assert!(login_output.success());
385        assert!(login_output.is_authenticated());
386    }
387
388    #[test]
389    fn test_login_output_alternative_success_message() {
390        let output = CommandOutput {
391            stdout: "login succeeded for user@registry".to_string(),
392            stderr: String::new(),
393            exit_code: 0,
394            success: true,
395        };
396        let login_output = LoginOutput { output };
397
398        assert!(login_output.is_authenticated());
399    }
400
401    #[test]
402    fn test_login_output_warnings() {
403        let output = CommandOutput {
404            stdout: "Login Succeeded".to_string(),
405            stderr: "WARNING: login credentials saved in plaintext\ninfo: using default registry"
406                .to_string(),
407            exit_code: 0,
408            success: true,
409        };
410        let login_output = LoginOutput { output };
411
412        let warnings = login_output.warnings();
413        assert_eq!(warnings.len(), 1);
414        assert!(warnings[0].contains("WARNING"));
415    }
416
417    #[test]
418    fn test_login_output_failure() {
419        let output = CommandOutput {
420            stdout: String::new(),
421            stderr: "Error: authentication failed".to_string(),
422            exit_code: 1,
423            success: false,
424        };
425        let login_output = LoginOutput { output };
426
427        assert!(!login_output.success());
428        assert!(!login_output.is_authenticated());
429    }
430}