docker_wrapper/command/swarm/
join_token.rs

1//! Docker swarm join-token command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Node role for join token retrieval
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SwarmNodeRole {
10    /// Worker node role
11    Worker,
12    /// Manager node role
13    Manager,
14}
15
16impl SwarmNodeRole {
17    /// Get the role as a string
18    #[must_use]
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            Self::Worker => "worker",
22            Self::Manager => "manager",
23        }
24    }
25}
26
27impl std::fmt::Display for SwarmNodeRole {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{}", self.as_str())
30    }
31}
32
33/// Result of swarm join-token command
34#[derive(Debug, Clone)]
35pub struct SwarmJoinTokenResult {
36    /// The join token
37    pub token: Option<String>,
38    /// The full join command (if not using --quiet)
39    pub join_command: Option<String>,
40    /// The role for which the token was retrieved
41    pub role: SwarmNodeRole,
42    /// Raw output from the command
43    pub output: String,
44}
45
46impl SwarmJoinTokenResult {
47    /// Parse the swarm join-token output
48    fn parse(output: &CommandOutput, role: SwarmNodeRole, quiet: bool) -> Self {
49        let stdout = output.stdout.trim();
50
51        if quiet {
52            // In quiet mode, output is just the token
53            Self {
54                token: Some(stdout.to_string()),
55                join_command: None,
56                role,
57                output: stdout.to_string(),
58            }
59        } else {
60            // Normal mode: parse the join command
61            // Output format:
62            // To add a worker to this swarm, run the following command:
63            //
64            //     docker swarm join --token SWMTKN-... 192.168.1.1:2377
65
66            let mut token = None;
67            let mut join_command = None;
68
69            for line in stdout.lines() {
70                let trimmed = line.trim();
71                if trimmed.starts_with("docker swarm join") {
72                    join_command = Some(trimmed.to_string());
73
74                    // Extract token from the join command
75                    let parts: Vec<&str> = trimmed.split_whitespace().collect();
76                    for (i, part) in parts.iter().enumerate() {
77                        if *part == "--token" {
78                            if let Some(t) = parts.get(i + 1) {
79                                token = Some((*t).to_string());
80                            }
81                        }
82                    }
83                }
84            }
85
86            Self {
87                token,
88                join_command,
89                role,
90                output: stdout.to_string(),
91            }
92        }
93    }
94}
95
96/// Docker swarm join-token command builder
97///
98/// Retrieves or rotates the join token for a swarm.
99#[derive(Debug, Clone)]
100pub struct SwarmJoinTokenCommand {
101    /// The role (worker or manager)
102    role: SwarmNodeRole,
103    /// Only display the token (no join command)
104    quiet: bool,
105    /// Rotate the join token
106    rotate: bool,
107    /// Command executor
108    pub executor: CommandExecutor,
109}
110
111impl SwarmJoinTokenCommand {
112    /// Create a new swarm join-token command for the specified role
113    #[must_use]
114    pub fn new(role: SwarmNodeRole) -> Self {
115        Self {
116            role,
117            quiet: false,
118            rotate: false,
119            executor: CommandExecutor::default(),
120        }
121    }
122
123    /// Create a command to get the worker join token
124    #[must_use]
125    pub fn worker() -> Self {
126        Self::new(SwarmNodeRole::Worker)
127    }
128
129    /// Create a command to get the manager join token
130    #[must_use]
131    pub fn manager() -> Self {
132        Self::new(SwarmNodeRole::Manager)
133    }
134
135    /// Only display the token (no join command)
136    #[must_use]
137    pub fn quiet(mut self) -> Self {
138        self.quiet = true;
139        self
140    }
141
142    /// Rotate the join token
143    #[must_use]
144    pub fn rotate(mut self) -> Self {
145        self.rotate = true;
146        self
147    }
148
149    /// Build the command arguments
150    fn build_args(&self) -> Vec<String> {
151        let mut args = vec!["swarm".to_string(), "join-token".to_string()];
152
153        if self.quiet {
154            args.push("--quiet".to_string());
155        }
156
157        if self.rotate {
158            args.push("--rotate".to_string());
159        }
160
161        args.push(self.role.as_str().to_string());
162
163        args
164    }
165}
166
167impl Default for SwarmJoinTokenCommand {
168    fn default() -> Self {
169        Self::worker()
170    }
171}
172
173#[async_trait]
174impl DockerCommand for SwarmJoinTokenCommand {
175    type Output = SwarmJoinTokenResult;
176
177    fn get_executor(&self) -> &CommandExecutor {
178        &self.executor
179    }
180
181    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
182        &mut self.executor
183    }
184
185    fn build_command_args(&self) -> Vec<String> {
186        self.build_args()
187    }
188
189    async fn execute(&self) -> Result<Self::Output> {
190        let args = self.build_args();
191        let output = self.execute_command(args).await?;
192        Ok(SwarmJoinTokenResult::parse(&output, self.role, self.quiet))
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_join_token_worker() {
202        let cmd = SwarmJoinTokenCommand::worker();
203        let args = cmd.build_args();
204        assert_eq!(args, vec!["swarm", "join-token", "worker"]);
205    }
206
207    #[test]
208    fn test_join_token_manager() {
209        let cmd = SwarmJoinTokenCommand::manager();
210        let args = cmd.build_args();
211        assert_eq!(args, vec!["swarm", "join-token", "manager"]);
212    }
213
214    #[test]
215    fn test_join_token_quiet() {
216        let cmd = SwarmJoinTokenCommand::worker().quiet();
217        let args = cmd.build_args();
218        assert!(args.contains(&"--quiet".to_string()));
219        assert!(args.contains(&"worker".to_string()));
220    }
221
222    #[test]
223    fn test_join_token_rotate() {
224        let cmd = SwarmJoinTokenCommand::manager().rotate();
225        let args = cmd.build_args();
226        assert!(args.contains(&"--rotate".to_string()));
227        assert!(args.contains(&"manager".to_string()));
228    }
229
230    #[test]
231    fn test_join_token_all_options() {
232        let cmd = SwarmJoinTokenCommand::worker().quiet().rotate();
233        let args = cmd.build_args();
234        assert_eq!(
235            args,
236            vec!["swarm", "join-token", "--quiet", "--rotate", "worker"]
237        );
238    }
239
240    #[test]
241    fn test_node_role_display() {
242        assert_eq!(SwarmNodeRole::Worker.to_string(), "worker");
243        assert_eq!(SwarmNodeRole::Manager.to_string(), "manager");
244    }
245}