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