docker_wrapper/command/swarm/
ca.rs

1//! Docker swarm ca command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Result of swarm ca command
8#[derive(Debug, Clone)]
9pub struct SwarmCaResult {
10    /// The CA certificate (PEM format)
11    pub certificate: Option<String>,
12    /// Raw output from the command
13    pub output: String,
14}
15
16impl SwarmCaResult {
17    /// Parse the swarm ca output
18    fn parse(output: &CommandOutput) -> Self {
19        let stdout = output.stdout.trim();
20
21        // Check if output looks like a PEM certificate
22        let certificate = if stdout.contains("-----BEGIN CERTIFICATE-----") {
23            Some(stdout.to_string())
24        } else {
25            None
26        };
27
28        Self {
29            certificate,
30            output: stdout.to_string(),
31        }
32    }
33}
34
35/// Docker swarm ca command builder
36///
37/// Display and rotate the root CA certificate.
38#[derive(Debug, Clone, Default)]
39pub struct SwarmCaCommand {
40    /// Path to the PEM-formatted root CA certificate to use
41    ca_cert: Option<String>,
42    /// Path to the PEM-formatted root CA key to use
43    ca_key: Option<String>,
44    /// Validity period for node certificates (ns|us|ms|s|m|h)
45    cert_expiry: Option<String>,
46    /// Exit immediately instead of waiting for rotation to complete
47    detach: bool,
48    /// Specifications of external CA to use
49    external_ca: Option<String>,
50    /// Suppress progress output
51    quiet: bool,
52    /// Rotate the swarm CA (creates new cluster TLS certs)
53    rotate: bool,
54    /// Command executor
55    pub executor: CommandExecutor,
56}
57
58impl SwarmCaCommand {
59    /// Create a new swarm ca command
60    #[must_use]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Set the path to the root CA certificate
66    #[must_use]
67    pub fn ca_cert(mut self, path: impl Into<String>) -> Self {
68        self.ca_cert = Some(path.into());
69        self
70    }
71
72    /// Set the path to the root CA key
73    #[must_use]
74    pub fn ca_key(mut self, path: impl Into<String>) -> Self {
75        self.ca_key = Some(path.into());
76        self
77    }
78
79    /// Set the certificate expiry duration
80    #[must_use]
81    pub fn cert_expiry(mut self, expiry: impl Into<String>) -> Self {
82        self.cert_expiry = Some(expiry.into());
83        self
84    }
85
86    /// Exit immediately instead of waiting for rotation to complete
87    #[must_use]
88    pub fn detach(mut self) -> Self {
89        self.detach = true;
90        self
91    }
92
93    /// Set external CA specifications
94    #[must_use]
95    pub fn external_ca(mut self, spec: impl Into<String>) -> Self {
96        self.external_ca = Some(spec.into());
97        self
98    }
99
100    /// Suppress progress output
101    #[must_use]
102    pub fn quiet(mut self) -> Self {
103        self.quiet = true;
104        self
105    }
106
107    /// Rotate the swarm CA
108    #[must_use]
109    pub fn rotate(mut self) -> Self {
110        self.rotate = true;
111        self
112    }
113
114    /// Build the command arguments
115    fn build_args(&self) -> Vec<String> {
116        let mut args = vec!["swarm".to_string(), "ca".to_string()];
117
118        if let Some(ref path) = self.ca_cert {
119            args.push("--ca-cert".to_string());
120            args.push(path.clone());
121        }
122
123        if let Some(ref path) = self.ca_key {
124            args.push("--ca-key".to_string());
125            args.push(path.clone());
126        }
127
128        if let Some(ref expiry) = self.cert_expiry {
129            args.push("--cert-expiry".to_string());
130            args.push(expiry.clone());
131        }
132
133        if self.detach {
134            args.push("--detach".to_string());
135        }
136
137        if let Some(ref spec) = self.external_ca {
138            args.push("--external-ca".to_string());
139            args.push(spec.clone());
140        }
141
142        if self.quiet {
143            args.push("--quiet".to_string());
144        }
145
146        if self.rotate {
147            args.push("--rotate".to_string());
148        }
149
150        args
151    }
152}
153
154#[async_trait]
155impl DockerCommand for SwarmCaCommand {
156    type Output = SwarmCaResult;
157
158    fn get_executor(&self) -> &CommandExecutor {
159        &self.executor
160    }
161
162    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
163        &mut self.executor
164    }
165
166    fn build_command_args(&self) -> Vec<String> {
167        self.build_args()
168    }
169
170    async fn execute(&self) -> Result<Self::Output> {
171        let args = self.build_args();
172        let output = self.execute_command(args).await?;
173        Ok(SwarmCaResult::parse(&output))
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_swarm_ca_basic() {
183        let cmd = SwarmCaCommand::new();
184        let args = cmd.build_args();
185        assert_eq!(args, vec!["swarm", "ca"]);
186    }
187
188    #[test]
189    fn test_swarm_ca_rotate() {
190        let cmd = SwarmCaCommand::new().rotate();
191        let args = cmd.build_args();
192        assert!(args.contains(&"--rotate".to_string()));
193    }
194
195    #[test]
196    fn test_swarm_ca_with_cert_and_key() {
197        let cmd = SwarmCaCommand::new()
198            .ca_cert("/path/to/cert.pem")
199            .ca_key("/path/to/key.pem");
200        let args = cmd.build_args();
201        assert!(args.contains(&"--ca-cert".to_string()));
202        assert!(args.contains(&"/path/to/cert.pem".to_string()));
203        assert!(args.contains(&"--ca-key".to_string()));
204        assert!(args.contains(&"/path/to/key.pem".to_string()));
205    }
206
207    #[test]
208    fn test_swarm_ca_all_options() {
209        let cmd = SwarmCaCommand::new()
210            .ca_cert("/cert.pem")
211            .ca_key("/key.pem")
212            .cert_expiry("90d")
213            .detach()
214            .external_ca("protocol=cfssl,url=https://ca.example.com")
215            .quiet()
216            .rotate();
217
218        let args = cmd.build_args();
219        assert!(args.contains(&"--ca-cert".to_string()));
220        assert!(args.contains(&"--ca-key".to_string()));
221        assert!(args.contains(&"--cert-expiry".to_string()));
222        assert!(args.contains(&"--detach".to_string()));
223        assert!(args.contains(&"--external-ca".to_string()));
224        assert!(args.contains(&"--quiet".to_string()));
225        assert!(args.contains(&"--rotate".to_string()));
226    }
227}