docker_wrapper/command/
logout.rs

1//! Docker logout command implementation
2//!
3//! This module provides functionality to log out from Docker registries.
4//! It supports logging out from specific registries or using the daemon default.
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 logging out from Docker registries
13///
14/// The `LogoutCommand` provides a builder pattern for constructing Docker logout commands
15/// to remove stored authentication credentials for Docker registries.
16///
17/// # Examples
18///
19/// ```rust
20/// use docker_wrapper::LogoutCommand;
21///
22/// // Logout from default registry (daemon-defined)
23/// let logout = LogoutCommand::new();
24///
25/// // Logout from specific registry
26/// let logout = LogoutCommand::new()
27///     .server("my-registry.com");
28/// ```
29#[derive(Debug, Clone)]
30pub struct LogoutCommand {
31    /// Registry server URL (None for daemon default)
32    server: Option<String>,
33    /// Command executor for running the command
34    executor: CommandExecutor,
35}
36
37/// Output from a logout command execution
38///
39/// Contains the raw output from the Docker logout command and provides
40/// convenience methods for checking logout status.
41#[derive(Debug, Clone)]
42pub struct LogoutOutput {
43    /// Raw output from the Docker command
44    pub output: CommandOutput,
45}
46
47impl LogoutCommand {
48    /// Creates a new logout command
49    ///
50    /// By default, logs out from the daemon-defined default registry
51    ///
52    /// # Examples
53    ///
54    /// ```rust
55    /// use docker_wrapper::LogoutCommand;
56    ///
57    /// let logout = LogoutCommand::new();
58    /// ```
59    #[must_use]
60    pub fn new() -> Self {
61        Self {
62            server: None,
63            executor: CommandExecutor::default(),
64        }
65    }
66
67    /// Sets the registry server to logout from
68    ///
69    /// If not specified, uses the daemon-defined default registry
70    ///
71    /// # Arguments
72    ///
73    /// * `server` - The registry server URL
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// use docker_wrapper::LogoutCommand;
79    ///
80    /// let logout = LogoutCommand::new()
81    ///     .server("gcr.io");
82    /// ```
83    #[must_use]
84    pub fn server(mut self, server: impl Into<String>) -> Self {
85        self.server = Some(server.into());
86        self
87    }
88
89    /// Sets a custom command executor
90    ///
91    /// # Arguments
92    ///
93    /// * `executor` - Custom command executor
94    #[must_use]
95    pub fn executor(mut self, executor: CommandExecutor) -> Self {
96        self.executor = executor;
97        self
98    }
99
100    /// Builds the command arguments for Docker logout
101    fn build_command_args(&self) -> Vec<String> {
102        let mut args = vec!["logout".to_string()];
103
104        // Add server if specified
105        if let Some(ref server) = self.server {
106            args.push(server.clone());
107        }
108
109        args
110    }
111
112    /// Gets the server (if set)
113    #[must_use]
114    pub fn get_server(&self) -> Option<&str> {
115        self.server.as_deref()
116    }
117}
118
119impl Default for LogoutCommand {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl LogoutOutput {
126    /// Returns true if the logout was successful
127    #[must_use]
128    pub fn success(&self) -> bool {
129        self.output.success
130    }
131
132    /// Returns true if the output indicates successful logout
133    #[must_use]
134    pub fn is_logged_out(&self) -> bool {
135        self.success()
136            && (self.output.stdout.contains("Removing login credentials")
137                || self.output.stdout.contains("Not logged in")
138                || self.output.stdout.is_empty() && self.output.stderr.is_empty())
139    }
140
141    /// Gets any warning messages from the logout output
142    #[must_use]
143    pub fn warnings(&self) -> Vec<&str> {
144        self.output
145            .stderr
146            .lines()
147            .filter(|line| line.to_lowercase().contains("warning"))
148            .collect()
149    }
150
151    /// Gets any info messages from the logout output
152    #[must_use]
153    pub fn info_messages(&self) -> Vec<&str> {
154        self.output
155            .stdout
156            .lines()
157            .filter(|line| !line.trim().is_empty())
158            .collect()
159    }
160}
161
162#[async_trait]
163impl DockerCommand for LogoutCommand {
164    type Output = LogoutOutput;
165
166    fn command_name(&self) -> &'static str {
167        "logout"
168    }
169
170    fn build_args(&self) -> Vec<String> {
171        self.build_command_args()
172    }
173
174    async fn execute(&self) -> Result<Self::Output> {
175        let output = self
176            .executor
177            .execute_command(self.command_name(), self.build_args())
178            .await?;
179
180        Ok(LogoutOutput { output })
181    }
182
183    fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
184        self.executor.add_arg(arg);
185        self
186    }
187
188    fn args<I, S>(&mut self, args: I) -> &mut Self
189    where
190        I: IntoIterator<Item = S>,
191        S: AsRef<OsStr>,
192    {
193        self.executor.add_args(args);
194        self
195    }
196
197    fn flag(&mut self, flag: &str) -> &mut Self {
198        self.executor.add_flag(flag);
199        self
200    }
201
202    fn option(&mut self, key: &str, value: &str) -> &mut Self {
203        self.executor.add_option(key, value);
204        self
205    }
206}
207
208impl fmt::Display for LogoutCommand {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        write!(f, "docker logout")?;
211
212        if let Some(ref server) = self.server {
213            write!(f, " {server}")?;
214        }
215
216        Ok(())
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_logout_command_basic() {
226        let logout = LogoutCommand::new();
227
228        assert_eq!(logout.get_server(), None);
229
230        let args = logout.build_command_args();
231        assert_eq!(args, vec!["logout"]);
232    }
233
234    #[test]
235    fn test_logout_command_with_server() {
236        let logout = LogoutCommand::new().server("gcr.io");
237
238        assert_eq!(logout.get_server(), Some("gcr.io"));
239
240        let args = logout.build_command_args();
241        assert_eq!(args, vec!["logout", "gcr.io"]);
242    }
243
244    #[test]
245    fn test_logout_command_with_private_registry() {
246        let logout = LogoutCommand::new().server("my-registry.example.com:5000");
247
248        let args = logout.build_command_args();
249        assert_eq!(args, vec!["logout", "my-registry.example.com:5000"]);
250    }
251
252    #[test]
253    fn test_logout_command_daemon_default() {
254        let logout = LogoutCommand::new();
255
256        // No server specified should use daemon default
257        assert_eq!(logout.get_server(), None);
258
259        let args = logout.build_command_args();
260        assert_eq!(args, vec!["logout"]);
261    }
262
263    #[test]
264    fn test_logout_command_display() {
265        let logout = LogoutCommand::new().server("example.com");
266
267        let display = format!("{logout}");
268        assert_eq!(display, "docker logout example.com");
269    }
270
271    #[test]
272    fn test_logout_command_display_no_server() {
273        let logout = LogoutCommand::new();
274
275        let display = format!("{logout}");
276        assert_eq!(display, "docker logout");
277    }
278
279    #[test]
280    fn test_logout_command_default() {
281        let logout = LogoutCommand::default();
282
283        assert_eq!(logout.get_server(), None);
284        let args = logout.build_command_args();
285        assert_eq!(args, vec!["logout"]);
286    }
287
288    #[test]
289    fn test_logout_output_success_with_credentials_removal() {
290        let output = CommandOutput {
291            stdout: "Removing login credentials for https://index.docker.io/v1/".to_string(),
292            stderr: String::new(),
293            exit_code: 0,
294            success: true,
295        };
296        let logout_output = LogoutOutput { output };
297
298        assert!(logout_output.success());
299        assert!(logout_output.is_logged_out());
300    }
301
302    #[test]
303    fn test_logout_output_success_not_logged_in() {
304        let output = CommandOutput {
305            stdout: "Not logged in to https://index.docker.io/v1/".to_string(),
306            stderr: String::new(),
307            exit_code: 0,
308            success: true,
309        };
310        let logout_output = LogoutOutput { output };
311
312        assert!(logout_output.success());
313        assert!(logout_output.is_logged_out());
314    }
315
316    #[test]
317    fn test_logout_output_success_empty() {
318        let output = CommandOutput {
319            stdout: String::new(),
320            stderr: String::new(),
321            exit_code: 0,
322            success: true,
323        };
324        let logout_output = LogoutOutput { output };
325
326        assert!(logout_output.success());
327        assert!(logout_output.is_logged_out());
328    }
329
330    #[test]
331    fn test_logout_output_warnings() {
332        let output = CommandOutput {
333            stdout: "Removing login credentials for registry".to_string(),
334            stderr: "WARNING: credentials may still be cached\ninfo: using default registry"
335                .to_string(),
336            exit_code: 0,
337            success: true,
338        };
339        let logout_output = LogoutOutput { output };
340
341        let warnings = logout_output.warnings();
342        assert_eq!(warnings.len(), 1);
343        assert!(warnings[0].contains("WARNING"));
344    }
345
346    #[test]
347    fn test_logout_output_info_messages() {
348        let output = CommandOutput {
349            stdout: "Removing login credentials for https://registry.example.com\nLogout completed"
350                .to_string(),
351            stderr: String::new(),
352            exit_code: 0,
353            success: true,
354        };
355        let logout_output = LogoutOutput { output };
356
357        let info = logout_output.info_messages();
358        assert_eq!(info.len(), 2);
359        assert!(info[0].contains("Removing login credentials"));
360        assert!(info[1].contains("Logout completed"));
361    }
362
363    #[test]
364    fn test_logout_output_failure() {
365        let output = CommandOutput {
366            stdout: String::new(),
367            stderr: "Error: unable to logout".to_string(),
368            exit_code: 1,
369            success: false,
370        };
371        let logout_output = LogoutOutput { output };
372
373        assert!(!logout_output.success());
374        assert!(!logout_output.is_logged_out());
375    }
376
377    #[test]
378    fn test_logout_command_name() {
379        let logout = LogoutCommand::new();
380        assert_eq!(logout.command_name(), "logout");
381    }
382
383    #[test]
384    fn test_logout_command_extensibility() {
385        let mut logout = LogoutCommand::new();
386
387        // Test the extension methods for future compatibility
388        logout
389            .arg("extra")
390            .args(vec!["more", "args"])
391            .flag("--verbose")
392            .option("key", "value");
393
394        // Command should still work normally
395        assert_eq!(logout.command_name(), "logout");
396    }
397
398    #[test]
399    fn test_logout_multiple_servers_concept() {
400        // Test that we can create logout commands for different servers
401        let daemon_default_logout = LogoutCommand::new();
402        let gcr_logout = LogoutCommand::new().server("gcr.io");
403        let private_logout = LogoutCommand::new().server("my-registry.com");
404
405        assert_eq!(daemon_default_logout.get_server(), None);
406        assert_eq!(gcr_logout.get_server(), Some("gcr.io"));
407        assert_eq!(private_logout.get_server(), Some("my-registry.com"));
408    }
409
410    #[test]
411    fn test_logout_builder_pattern() {
412        let logout = LogoutCommand::new().server("registry.example.com");
413
414        assert_eq!(logout.get_server(), Some("registry.example.com"));
415        assert_eq!(logout.command_name(), "logout");
416    }
417
418    #[test]
419    fn test_logout_various_server_formats() {
420        let test_cases = vec![
421            "gcr.io",
422            "registry-1.docker.io",
423            "localhost:5000",
424            "my-registry.com:443",
425            "registry.example.com/path",
426        ];
427
428        for server in test_cases {
429            let logout = LogoutCommand::new().server(server);
430            assert_eq!(logout.get_server(), Some(server));
431
432            let args = logout.build_command_args();
433            assert!(args.contains(&server.to_string()));
434        }
435    }
436}