docker_wrapper/command/swarm/
init.rs1use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7#[derive(Debug, Clone)]
9pub struct SwarmInitResult {
10 pub node_id: Option<String>,
12 pub worker_token: Option<String>,
14 pub manager_token: Option<String>,
16 pub output: String,
18}
19
20impl SwarmInitResult {
21 fn parse(output: &CommandOutput) -> Self {
23 let stdout = &output.stdout;
24
25 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 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 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 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#[derive(Debug, Clone, Default)]
74pub struct SwarmInitCommand {
75 advertise_addr: Option<String>,
77 autolock: bool,
79 availability: Option<String>,
81 cert_expiry: Option<String>,
83 data_path_addr: Option<String>,
85 data_path_port: Option<u16>,
87 default_addr_pool: Vec<String>,
89 default_addr_pool_mask_length: Option<u8>,
91 dispatcher_heartbeat: Option<String>,
93 external_ca: Option<String>,
95 force_new_cluster: bool,
97 listen_addr: Option<String>,
99 max_snapshots: Option<u32>,
101 snapshot_interval: Option<u32>,
103 task_history_limit: Option<i32>,
105 pub executor: CommandExecutor,
107}
108
109impl SwarmInitCommand {
110 #[must_use]
112 pub fn new() -> Self {
113 Self::default()
114 }
115
116 #[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 #[must_use]
125 pub fn autolock(mut self) -> Self {
126 self.autolock = true;
127 self
128 }
129
130 #[must_use]
132 pub fn availability(mut self, availability: impl Into<String>) -> Self {
133 self.availability = Some(availability.into());
134 self
135 }
136
137 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
188 pub fn force_new_cluster(mut self) -> Self {
189 self.force_new_cluster = true;
190 self
191 }
192
193 #[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 #[must_use]
202 pub fn max_snapshots(mut self, count: u32) -> Self {
203 self.max_snapshots = Some(count);
204 self
205 }
206
207 #[must_use]
209 pub fn snapshot_interval(mut self, interval: u32) -> Self {
210 self.snapshot_interval = Some(interval);
211 self
212 }
213
214 #[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 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}