use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
#[derive(Debug, Clone)]
pub struct SwarmInitResult {
pub node_id: Option<String>,
pub worker_token: Option<String>,
pub manager_token: Option<String>,
pub output: String,
}
impl SwarmInitResult {
fn parse(output: &CommandOutput) -> Self {
let stdout = &output.stdout;
let mut node_id = None;
let mut worker_token = None;
let mut manager_token = None;
for line in stdout.lines() {
if line.contains("current node") && line.contains("is now a manager") {
if let Some(start) = line.find('(') {
if let Some(end) = line.find(')') {
node_id = Some(line[start + 1..end].to_string());
}
}
}
if line.contains("--token") {
let parts: Vec<&str> = line.split_whitespace().collect();
for (i, part) in parts.iter().enumerate() {
if *part == "--token" {
if let Some(token) = parts.get(i + 1) {
if stdout.contains("add a worker") && worker_token.is_none() {
worker_token = Some((*token).to_string());
} else if stdout.contains("add a manager") {
manager_token = Some((*token).to_string());
}
}
}
}
}
}
Self {
node_id,
worker_token,
manager_token,
output: stdout.clone(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SwarmInitCommand {
advertise_addr: Option<String>,
autolock: bool,
availability: Option<String>,
cert_expiry: Option<String>,
data_path_addr: Option<String>,
data_path_port: Option<u16>,
default_addr_pool: Vec<String>,
default_addr_pool_mask_length: Option<u8>,
dispatcher_heartbeat: Option<String>,
external_ca: Option<String>,
force_new_cluster: bool,
listen_addr: Option<String>,
max_snapshots: Option<u32>,
snapshot_interval: Option<u32>,
task_history_limit: Option<i32>,
pub executor: CommandExecutor,
}
impl SwarmInitCommand {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn advertise_addr(mut self, addr: impl Into<String>) -> Self {
self.advertise_addr = Some(addr.into());
self
}
#[must_use]
pub fn autolock(mut self) -> Self {
self.autolock = true;
self
}
#[must_use]
pub fn availability(mut self, availability: impl Into<String>) -> Self {
self.availability = Some(availability.into());
self
}
#[must_use]
pub fn cert_expiry(mut self, expiry: impl Into<String>) -> Self {
self.cert_expiry = Some(expiry.into());
self
}
#[must_use]
pub fn data_path_addr(mut self, addr: impl Into<String>) -> Self {
self.data_path_addr = Some(addr.into());
self
}
#[must_use]
pub fn data_path_port(mut self, port: u16) -> Self {
self.data_path_port = Some(port);
self
}
#[must_use]
pub fn default_addr_pool(mut self, pool: impl Into<String>) -> Self {
self.default_addr_pool.push(pool.into());
self
}
#[must_use]
pub fn default_addr_pool_mask_length(mut self, length: u8) -> Self {
self.default_addr_pool_mask_length = Some(length);
self
}
#[must_use]
pub fn dispatcher_heartbeat(mut self, heartbeat: impl Into<String>) -> Self {
self.dispatcher_heartbeat = Some(heartbeat.into());
self
}
#[must_use]
pub fn external_ca(mut self, url: impl Into<String>) -> Self {
self.external_ca = Some(url.into());
self
}
#[must_use]
pub fn force_new_cluster(mut self) -> Self {
self.force_new_cluster = true;
self
}
#[must_use]
pub fn listen_addr(mut self, addr: impl Into<String>) -> Self {
self.listen_addr = Some(addr.into());
self
}
#[must_use]
pub fn max_snapshots(mut self, count: u32) -> Self {
self.max_snapshots = Some(count);
self
}
#[must_use]
pub fn snapshot_interval(mut self, interval: u32) -> Self {
self.snapshot_interval = Some(interval);
self
}
#[must_use]
pub fn task_history_limit(mut self, limit: i32) -> Self {
self.task_history_limit = Some(limit);
self
}
fn build_args(&self) -> Vec<String> {
let mut args = vec!["swarm".to_string(), "init".to_string()];
if let Some(ref addr) = self.advertise_addr {
args.push("--advertise-addr".to_string());
args.push(addr.clone());
}
if self.autolock {
args.push("--autolock".to_string());
}
if let Some(ref availability) = self.availability {
args.push("--availability".to_string());
args.push(availability.clone());
}
if let Some(ref expiry) = self.cert_expiry {
args.push("--cert-expiry".to_string());
args.push(expiry.clone());
}
if let Some(ref addr) = self.data_path_addr {
args.push("--data-path-addr".to_string());
args.push(addr.clone());
}
if let Some(port) = self.data_path_port {
args.push("--data-path-port".to_string());
args.push(port.to_string());
}
for pool in &self.default_addr_pool {
args.push("--default-addr-pool".to_string());
args.push(pool.clone());
}
if let Some(length) = self.default_addr_pool_mask_length {
args.push("--default-addr-pool-mask-length".to_string());
args.push(length.to_string());
}
if let Some(ref heartbeat) = self.dispatcher_heartbeat {
args.push("--dispatcher-heartbeat".to_string());
args.push(heartbeat.clone());
}
if let Some(ref url) = self.external_ca {
args.push("--external-ca".to_string());
args.push(url.clone());
}
if self.force_new_cluster {
args.push("--force-new-cluster".to_string());
}
if let Some(ref addr) = self.listen_addr {
args.push("--listen-addr".to_string());
args.push(addr.clone());
}
if let Some(count) = self.max_snapshots {
args.push("--max-snapshots".to_string());
args.push(count.to_string());
}
if let Some(interval) = self.snapshot_interval {
args.push("--snapshot-interval".to_string());
args.push(interval.to_string());
}
if let Some(limit) = self.task_history_limit {
args.push("--task-history-limit".to_string());
args.push(limit.to_string());
}
args
}
}
#[async_trait]
impl DockerCommand for SwarmInitCommand {
type Output = SwarmInitResult;
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
fn build_command_args(&self) -> Vec<String> {
self.build_args()
}
async fn execute(&self) -> Result<Self::Output> {
let args = self.build_args();
let output = self.execute_command(args).await?;
Ok(SwarmInitResult::parse(&output))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_swarm_init_basic() {
let cmd = SwarmInitCommand::new();
let args = cmd.build_args();
assert_eq!(args, vec!["swarm", "init"]);
}
#[test]
fn test_swarm_init_with_advertise_addr() {
let cmd = SwarmInitCommand::new().advertise_addr("192.168.1.1:2377");
let args = cmd.build_args();
assert!(args.contains(&"--advertise-addr".to_string()));
assert!(args.contains(&"192.168.1.1:2377".to_string()));
}
#[test]
fn test_swarm_init_with_autolock() {
let cmd = SwarmInitCommand::new().autolock();
let args = cmd.build_args();
assert!(args.contains(&"--autolock".to_string()));
}
#[test]
fn test_swarm_init_all_options() {
let cmd = SwarmInitCommand::new()
.advertise_addr("192.168.1.1:2377")
.autolock()
.availability("active")
.cert_expiry("90d")
.data_path_addr("192.168.1.1")
.data_path_port(4789)
.default_addr_pool("10.10.0.0/16")
.default_addr_pool_mask_length(24)
.dispatcher_heartbeat("5s")
.force_new_cluster()
.listen_addr("0.0.0.0:2377")
.max_snapshots(5)
.snapshot_interval(10000)
.task_history_limit(10);
let args = cmd.build_args();
assert!(args.contains(&"--advertise-addr".to_string()));
assert!(args.contains(&"--autolock".to_string()));
assert!(args.contains(&"--availability".to_string()));
assert!(args.contains(&"--cert-expiry".to_string()));
assert!(args.contains(&"--data-path-addr".to_string()));
assert!(args.contains(&"--data-path-port".to_string()));
assert!(args.contains(&"--default-addr-pool".to_string()));
assert!(args.contains(&"--default-addr-pool-mask-length".to_string()));
assert!(args.contains(&"--dispatcher-heartbeat".to_string()));
assert!(args.contains(&"--force-new-cluster".to_string()));
assert!(args.contains(&"--listen-addr".to_string()));
assert!(args.contains(&"--max-snapshots".to_string()));
assert!(args.contains(&"--snapshot-interval".to_string()));
assert!(args.contains(&"--task-history-limit".to_string()));
}
}