docker_wrapper/command/swarm/
join.rs

1//! Docker swarm join command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Result of swarm join command
8#[derive(Debug, Clone)]
9pub struct SwarmJoinResult {
10    /// Whether the join was successful
11    pub success: bool,
12    /// Raw output from the command
13    pub output: String,
14}
15
16impl SwarmJoinResult {
17    /// Parse the swarm join output
18    fn parse(output: &CommandOutput) -> Self {
19        Self {
20            success: output.success,
21            output: output.stdout.clone(),
22        }
23    }
24}
25
26/// Docker swarm join command builder
27#[derive(Debug, Clone)]
28pub struct SwarmJoinCommand {
29    /// The host:port of the manager to join
30    remote_addr: String,
31    /// Token for joining the swarm
32    token: String,
33    /// Advertised address (format: <ip|interface>[:port])
34    advertise_addr: Option<String>,
35    /// Availability of the node (active, pause, drain)
36    availability: Option<String>,
37    /// Address or interface to use for data path traffic
38    data_path_addr: Option<String>,
39    /// Listen address (format: <ip|interface>[:port])
40    listen_addr: Option<String>,
41    /// Command executor
42    pub executor: CommandExecutor,
43}
44
45impl SwarmJoinCommand {
46    /// Create a new swarm join command
47    ///
48    /// # Arguments
49    /// * `token` - The join token (worker or manager)
50    /// * `remote_addr` - The address of a manager node (host:port)
51    #[must_use]
52    pub fn new(token: impl Into<String>, remote_addr: impl Into<String>) -> Self {
53        Self {
54            remote_addr: remote_addr.into(),
55            token: token.into(),
56            advertise_addr: None,
57            availability: None,
58            data_path_addr: None,
59            listen_addr: None,
60            executor: CommandExecutor::new(),
61        }
62    }
63
64    /// Set the advertised address
65    #[must_use]
66    pub fn advertise_addr(mut self, addr: impl Into<String>) -> Self {
67        self.advertise_addr = Some(addr.into());
68        self
69    }
70
71    /// Set the availability of the node
72    #[must_use]
73    pub fn availability(mut self, availability: impl Into<String>) -> Self {
74        self.availability = Some(availability.into());
75        self
76    }
77
78    /// Set the data path address
79    #[must_use]
80    pub fn data_path_addr(mut self, addr: impl Into<String>) -> Self {
81        self.data_path_addr = Some(addr.into());
82        self
83    }
84
85    /// Set the listen address
86    #[must_use]
87    pub fn listen_addr(mut self, addr: impl Into<String>) -> Self {
88        self.listen_addr = Some(addr.into());
89        self
90    }
91
92    /// Build the command arguments
93    fn build_args(&self) -> Vec<String> {
94        let mut args = vec!["swarm".to_string(), "join".to_string()];
95
96        if let Some(ref addr) = self.advertise_addr {
97            args.push("--advertise-addr".to_string());
98            args.push(addr.clone());
99        }
100
101        if let Some(ref availability) = self.availability {
102            args.push("--availability".to_string());
103            args.push(availability.clone());
104        }
105
106        if let Some(ref addr) = self.data_path_addr {
107            args.push("--data-path-addr".to_string());
108            args.push(addr.clone());
109        }
110
111        if let Some(ref addr) = self.listen_addr {
112            args.push("--listen-addr".to_string());
113            args.push(addr.clone());
114        }
115
116        // Token must come before remote address
117        args.push("--token".to_string());
118        args.push(self.token.clone());
119
120        // Remote address is the last positional argument
121        args.push(self.remote_addr.clone());
122
123        args
124    }
125}
126
127#[async_trait]
128impl DockerCommand for SwarmJoinCommand {
129    type Output = SwarmJoinResult;
130
131    fn get_executor(&self) -> &CommandExecutor {
132        &self.executor
133    }
134
135    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
136        &mut self.executor
137    }
138
139    fn build_command_args(&self) -> Vec<String> {
140        self.build_args()
141    }
142
143    async fn execute(&self) -> Result<Self::Output> {
144        let args = self.build_args();
145        let output = self.execute_command(args).await?;
146        Ok(SwarmJoinResult::parse(&output))
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_swarm_join_basic() {
156        let cmd = SwarmJoinCommand::new("SWMTKN-1-xxx", "192.168.1.1:2377");
157        let args = cmd.build_args();
158        assert_eq!(args[0], "swarm");
159        assert_eq!(args[1], "join");
160        assert!(args.contains(&"--token".to_string()));
161        assert!(args.contains(&"SWMTKN-1-xxx".to_string()));
162        assert!(args.contains(&"192.168.1.1:2377".to_string()));
163    }
164
165    #[test]
166    fn test_swarm_join_with_options() {
167        let cmd = SwarmJoinCommand::new("SWMTKN-1-xxx", "192.168.1.1:2377")
168            .advertise_addr("192.168.1.2:2377")
169            .availability("active")
170            .data_path_addr("192.168.1.2")
171            .listen_addr("0.0.0.0:2377");
172
173        let args = cmd.build_args();
174        assert!(args.contains(&"--advertise-addr".to_string()));
175        assert!(args.contains(&"--availability".to_string()));
176        assert!(args.contains(&"--data-path-addr".to_string()));
177        assert!(args.contains(&"--listen-addr".to_string()));
178    }
179}