docker_wrapper/command/swarm/
init.rs

1//! Docker swarm init command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Result of swarm init command
8#[derive(Debug, Clone)]
9pub struct SwarmInitResult {
10    /// The node ID of the manager node
11    pub node_id: Option<String>,
12    /// The worker join token
13    pub worker_token: Option<String>,
14    /// The manager join token
15    pub manager_token: Option<String>,
16    /// Raw output from the command
17    pub output: String,
18}
19
20impl SwarmInitResult {
21    /// Parse the swarm init output
22    fn parse(output: &CommandOutput) -> Self {
23        let stdout = &output.stdout;
24
25        // Try to extract tokens from the output
26        // Output format:
27        // Swarm initialized: current node (nodeId) is now a manager.
28        // To add a worker to this swarm, run the following command:
29        //     docker swarm join --token <token> <ip>:<port>
30
31        let mut node_id = None;
32        let mut worker_token = None;
33        let mut manager_token = None;
34
35        for line in stdout.lines() {
36            if line.contains("current node") && line.contains("is now a manager") {
37                // Extract node ID from parentheses
38                if let Some(start) = line.find('(') {
39                    if let Some(end) = line.find(')') {
40                        node_id = Some(line[start + 1..end].to_string());
41                    }
42                }
43            }
44
45            if line.contains("--token") {
46                // Extract token from the join command
47                let parts: Vec<&str> = line.split_whitespace().collect();
48                for (i, part) in parts.iter().enumerate() {
49                    if *part == "--token" {
50                        if let Some(token) = parts.get(i + 1) {
51                            // Determine if this is worker or manager token based on context
52                            if stdout.contains("add a worker") && worker_token.is_none() {
53                                worker_token = Some((*token).to_string());
54                            } else if stdout.contains("add a manager") {
55                                manager_token = Some((*token).to_string());
56                            }
57                        }
58                    }
59                }
60            }
61        }
62
63        Self {
64            node_id,
65            worker_token,
66            manager_token,
67            output: stdout.clone(),
68        }
69    }
70}
71
72/// Docker swarm init command builder
73#[derive(Debug, Clone, Default)]
74pub struct SwarmInitCommand {
75    /// Advertised address (format: <ip|interface>[:port])
76    advertise_addr: Option<String>,
77    /// Enable manager auto-lock
78    autolock: bool,
79    /// Availability of the node (active, pause, drain)
80    availability: Option<String>,
81    /// Validity period for node certificates (ns|us|ms|s|m|h)
82    cert_expiry: Option<String>,
83    /// Address or interface to use for data path traffic
84    data_path_addr: Option<String>,
85    /// Port to use for data path traffic
86    data_path_port: Option<u16>,
87    /// Default address pool in CIDR format
88    default_addr_pool: Vec<String>,
89    /// Default address pool subnet mask length
90    default_addr_pool_mask_length: Option<u8>,
91    /// Dispatcher heartbeat period (ns|us|ms|s|m|h)
92    dispatcher_heartbeat: Option<String>,
93    /// External CA URL (format: `protocol://url`)
94    external_ca: Option<String>,
95    /// Force create a new cluster from current state
96    force_new_cluster: bool,
97    /// Listen address (format: <ip|interface>[:port])
98    listen_addr: Option<String>,
99    /// Number of snapshots to keep beyond current snapshot
100    max_snapshots: Option<u32>,
101    /// Log entries to keep after compacting raft log
102    snapshot_interval: Option<u32>,
103    /// Task history retention limit
104    task_history_limit: Option<i32>,
105    /// Command executor
106    pub executor: CommandExecutor,
107}
108
109impl SwarmInitCommand {
110    /// Create a new swarm init command
111    #[must_use]
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Set the advertised address
117    #[must_use]
118    pub fn advertise_addr(mut self, addr: impl Into<String>) -> Self {
119        self.advertise_addr = Some(addr.into());
120        self
121    }
122
123    /// Enable manager auto-lock
124    #[must_use]
125    pub fn autolock(mut self) -> Self {
126        self.autolock = true;
127        self
128    }
129
130    /// Set the availability of the node
131    #[must_use]
132    pub fn availability(mut self, availability: impl Into<String>) -> Self {
133        self.availability = Some(availability.into());
134        self
135    }
136
137    /// Set the certificate expiry duration
138    #[must_use]
139    pub fn cert_expiry(mut self, expiry: impl Into<String>) -> Self {
140        self.cert_expiry = Some(expiry.into());
141        self
142    }
143
144    /// Set the data path address
145    #[must_use]
146    pub fn data_path_addr(mut self, addr: impl Into<String>) -> Self {
147        self.data_path_addr = Some(addr.into());
148        self
149    }
150
151    /// Set the data path port
152    #[must_use]
153    pub fn data_path_port(mut self, port: u16) -> Self {
154        self.data_path_port = Some(port);
155        self
156    }
157
158    /// Add a default address pool
159    #[must_use]
160    pub fn default_addr_pool(mut self, pool: impl Into<String>) -> Self {
161        self.default_addr_pool.push(pool.into());
162        self
163    }
164
165    /// Set the default address pool mask length
166    #[must_use]
167    pub fn default_addr_pool_mask_length(mut self, length: u8) -> Self {
168        self.default_addr_pool_mask_length = Some(length);
169        self
170    }
171
172    /// Set the dispatcher heartbeat period
173    #[must_use]
174    pub fn dispatcher_heartbeat(mut self, heartbeat: impl Into<String>) -> Self {
175        self.dispatcher_heartbeat = Some(heartbeat.into());
176        self
177    }
178
179    /// Set the external CA URL
180    #[must_use]
181    pub fn external_ca(mut self, url: impl Into<String>) -> Self {
182        self.external_ca = Some(url.into());
183        self
184    }
185
186    /// Force create a new cluster from current state
187    #[must_use]
188    pub fn force_new_cluster(mut self) -> Self {
189        self.force_new_cluster = true;
190        self
191    }
192
193    /// Set the listen address
194    #[must_use]
195    pub fn listen_addr(mut self, addr: impl Into<String>) -> Self {
196        self.listen_addr = Some(addr.into());
197        self
198    }
199
200    /// Set the maximum number of snapshots to keep
201    #[must_use]
202    pub fn max_snapshots(mut self, count: u32) -> Self {
203        self.max_snapshots = Some(count);
204        self
205    }
206
207    /// Set the snapshot interval
208    #[must_use]
209    pub fn snapshot_interval(mut self, interval: u32) -> Self {
210        self.snapshot_interval = Some(interval);
211        self
212    }
213
214    /// Set the task history retention limit
215    #[must_use]
216    pub fn task_history_limit(mut self, limit: i32) -> Self {
217        self.task_history_limit = Some(limit);
218        self
219    }
220
221    /// Build the command arguments
222    fn build_args(&self) -> Vec<String> {
223        let mut args = vec!["swarm".to_string(), "init".to_string()];
224
225        if let Some(ref addr) = self.advertise_addr {
226            args.push("--advertise-addr".to_string());
227            args.push(addr.clone());
228        }
229
230        if self.autolock {
231            args.push("--autolock".to_string());
232        }
233
234        if let Some(ref availability) = self.availability {
235            args.push("--availability".to_string());
236            args.push(availability.clone());
237        }
238
239        if let Some(ref expiry) = self.cert_expiry {
240            args.push("--cert-expiry".to_string());
241            args.push(expiry.clone());
242        }
243
244        if let Some(ref addr) = self.data_path_addr {
245            args.push("--data-path-addr".to_string());
246            args.push(addr.clone());
247        }
248
249        if let Some(port) = self.data_path_port {
250            args.push("--data-path-port".to_string());
251            args.push(port.to_string());
252        }
253
254        for pool in &self.default_addr_pool {
255            args.push("--default-addr-pool".to_string());
256            args.push(pool.clone());
257        }
258
259        if let Some(length) = self.default_addr_pool_mask_length {
260            args.push("--default-addr-pool-mask-length".to_string());
261            args.push(length.to_string());
262        }
263
264        if let Some(ref heartbeat) = self.dispatcher_heartbeat {
265            args.push("--dispatcher-heartbeat".to_string());
266            args.push(heartbeat.clone());
267        }
268
269        if let Some(ref url) = self.external_ca {
270            args.push("--external-ca".to_string());
271            args.push(url.clone());
272        }
273
274        if self.force_new_cluster {
275            args.push("--force-new-cluster".to_string());
276        }
277
278        if let Some(ref addr) = self.listen_addr {
279            args.push("--listen-addr".to_string());
280            args.push(addr.clone());
281        }
282
283        if let Some(count) = self.max_snapshots {
284            args.push("--max-snapshots".to_string());
285            args.push(count.to_string());
286        }
287
288        if let Some(interval) = self.snapshot_interval {
289            args.push("--snapshot-interval".to_string());
290            args.push(interval.to_string());
291        }
292
293        if let Some(limit) = self.task_history_limit {
294            args.push("--task-history-limit".to_string());
295            args.push(limit.to_string());
296        }
297
298        args
299    }
300}
301
302#[async_trait]
303impl DockerCommand for SwarmInitCommand {
304    type Output = SwarmInitResult;
305
306    fn get_executor(&self) -> &CommandExecutor {
307        &self.executor
308    }
309
310    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
311        &mut self.executor
312    }
313
314    fn build_command_args(&self) -> Vec<String> {
315        self.build_args()
316    }
317
318    async fn execute(&self) -> Result<Self::Output> {
319        let args = self.build_args();
320        let output = self.execute_command(args).await?;
321        Ok(SwarmInitResult::parse(&output))
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_swarm_init_basic() {
331        let cmd = SwarmInitCommand::new();
332        let args = cmd.build_args();
333        assert_eq!(args, vec!["swarm", "init"]);
334    }
335
336    #[test]
337    fn test_swarm_init_with_advertise_addr() {
338        let cmd = SwarmInitCommand::new().advertise_addr("192.168.1.1:2377");
339        let args = cmd.build_args();
340        assert!(args.contains(&"--advertise-addr".to_string()));
341        assert!(args.contains(&"192.168.1.1:2377".to_string()));
342    }
343
344    #[test]
345    fn test_swarm_init_with_autolock() {
346        let cmd = SwarmInitCommand::new().autolock();
347        let args = cmd.build_args();
348        assert!(args.contains(&"--autolock".to_string()));
349    }
350
351    #[test]
352    fn test_swarm_init_all_options() {
353        let cmd = SwarmInitCommand::new()
354            .advertise_addr("192.168.1.1:2377")
355            .autolock()
356            .availability("active")
357            .cert_expiry("90d")
358            .data_path_addr("192.168.1.1")
359            .data_path_port(4789)
360            .default_addr_pool("10.10.0.0/16")
361            .default_addr_pool_mask_length(24)
362            .dispatcher_heartbeat("5s")
363            .force_new_cluster()
364            .listen_addr("0.0.0.0:2377")
365            .max_snapshots(5)
366            .snapshot_interval(10000)
367            .task_history_limit(10);
368
369        let args = cmd.build_args();
370        assert!(args.contains(&"--advertise-addr".to_string()));
371        assert!(args.contains(&"--autolock".to_string()));
372        assert!(args.contains(&"--availability".to_string()));
373        assert!(args.contains(&"--cert-expiry".to_string()));
374        assert!(args.contains(&"--data-path-addr".to_string()));
375        assert!(args.contains(&"--data-path-port".to_string()));
376        assert!(args.contains(&"--default-addr-pool".to_string()));
377        assert!(args.contains(&"--default-addr-pool-mask-length".to_string()));
378        assert!(args.contains(&"--dispatcher-heartbeat".to_string()));
379        assert!(args.contains(&"--force-new-cluster".to_string()));
380        assert!(args.contains(&"--listen-addr".to_string()));
381        assert!(args.contains(&"--max-snapshots".to_string()));
382        assert!(args.contains(&"--snapshot-interval".to_string()));
383        assert!(args.contains(&"--task-history-limit".to_string()));
384    }
385}