docker_wrapper/command/
login.rs1use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::fmt;
10
11#[derive(Debug, Clone)]
34pub struct LoginCommand {
35 username: String,
37 password: String,
39 registry: Option<String>,
41 password_stdin: bool,
43 pub executor: CommandExecutor,
45}
46
47#[derive(Debug, Clone)]
52pub struct LoginOutput {
53 pub output: CommandOutput,
55}
56
57impl LoginCommand {
58 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 #[must_use]
99 pub fn registry(mut self, registry: impl Into<String>) -> Self {
100 self.registry = Some(registry.into());
101 self
102 }
103
104 #[must_use]
118 pub fn password_stdin(mut self) -> Self {
119 self.password_stdin = true;
120 self
121 }
122
123 #[must_use]
129 pub fn executor(mut self, executor: CommandExecutor) -> Self {
130 self.executor = executor;
131 self
132 }
133
134 #[must_use]
136 pub fn get_username(&self) -> &str {
137 &self.username
138 }
139
140 #[must_use]
142 pub fn get_registry(&self) -> Option<&str> {
143 self.registry.as_deref()
144 }
145
146 #[must_use]
148 pub fn is_password_stdin(&self) -> bool {
149 self.password_stdin
150 }
151
152 #[must_use]
154 pub fn get_executor(&self) -> &CommandExecutor {
155 &self.executor
156 }
157
158 #[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 #[must_use]
174 pub fn success(&self) -> bool {
175 self.output.success
176 }
177
178 #[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 #[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 args.push("--username".to_string());
214 args.push(self.username.clone());
215
216 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 if let Some(ref registry) = self.registry {
226 args.push(registry.clone());
227 }
228
229 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 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}