Skip to main content

talos_api_rs/resources/
configuration.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for configuration-related operations.
4//!
5//! This module provides ergonomic builders and types for working with
6//! Talos machine configuration.
7
8use crate::api::machine::{
9    apply_configuration_request::Mode as ProtoMode, ApplyConfiguration as ProtoApplyConfiguration,
10    ApplyConfigurationRequest as ProtoRequest, ApplyConfigurationResponse as ProtoResponse,
11};
12use std::time::Duration;
13
14/// Mode for applying configuration changes.
15///
16/// Determines how the node handles configuration updates.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum ApplyMode {
19    /// Reboot the node after applying configuration.
20    Reboot,
21    /// Automatically determine the best mode based on changes.
22    #[default]
23    Auto,
24    /// Apply configuration without rebooting (if possible).
25    NoReboot,
26    /// Stage the configuration for next boot.
27    Staged,
28    /// Try the configuration temporarily; revert if not confirmed.
29    Try,
30}
31
32impl From<ApplyMode> for i32 {
33    fn from(mode: ApplyMode) -> Self {
34        match mode {
35            ApplyMode::Reboot => ProtoMode::Reboot as i32,
36            ApplyMode::Auto => ProtoMode::Auto as i32,
37            ApplyMode::NoReboot => ProtoMode::NoReboot as i32,
38            ApplyMode::Staged => ProtoMode::Staged as i32,
39            ApplyMode::Try => ProtoMode::Try as i32,
40        }
41    }
42}
43
44impl From<i32> for ApplyMode {
45    fn from(value: i32) -> Self {
46        match value {
47            0 => ApplyMode::Reboot,
48            1 => ApplyMode::Auto,
49            2 => ApplyMode::NoReboot,
50            3 => ApplyMode::Staged,
51            4 => ApplyMode::Try,
52            _ => ApplyMode::Auto, // Default fallback
53        }
54    }
55}
56
57impl std::fmt::Display for ApplyMode {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            ApplyMode::Reboot => write!(f, "reboot"),
61            ApplyMode::Auto => write!(f, "auto"),
62            ApplyMode::NoReboot => write!(f, "no-reboot"),
63            ApplyMode::Staged => write!(f, "staged"),
64            ApplyMode::Try => write!(f, "try"),
65        }
66    }
67}
68
69/// Builder for creating configuration apply requests.
70///
71/// # Example
72///
73/// ```no_run
74/// use talos_api_rs::resources::ApplyConfigurationRequest;
75/// use talos_api_rs::resources::ApplyMode;
76///
77/// let request = ApplyConfigurationRequest::builder()
78///     .config_yaml("machine:\n  type: worker")
79///     .mode(ApplyMode::NoReboot)
80///     .dry_run(true)
81///     .build();
82/// ```
83#[derive(Debug, Clone, Default)]
84pub struct ApplyConfigurationRequest {
85    /// Raw configuration data (YAML bytes)
86    pub data: Vec<u8>,
87    /// Mode for applying configuration
88    pub mode: ApplyMode,
89    /// If true, validate only without applying
90    pub dry_run: bool,
91    /// Timeout for try mode (optional)
92    pub try_mode_timeout: Option<Duration>,
93}
94
95impl ApplyConfigurationRequest {
96    /// Create a new builder for `ApplyConfigurationRequest`.
97    #[must_use]
98    pub fn builder() -> ApplyConfigurationRequestBuilder {
99        ApplyConfigurationRequestBuilder::default()
100    }
101
102    /// Create a request from raw YAML configuration.
103    #[must_use]
104    pub fn from_yaml(yaml: impl AsRef<str>) -> Self {
105        Self {
106            data: yaml.as_ref().as_bytes().to_vec(),
107            mode: ApplyMode::Auto,
108            dry_run: false,
109            try_mode_timeout: None,
110        }
111    }
112
113    /// Create a request from raw bytes.
114    #[must_use]
115    pub fn from_bytes(data: Vec<u8>) -> Self {
116        Self {
117            data,
118            mode: ApplyMode::Auto,
119            dry_run: false,
120            try_mode_timeout: None,
121        }
122    }
123}
124
125impl From<ApplyConfigurationRequest> for ProtoRequest {
126    fn from(req: ApplyConfigurationRequest) -> Self {
127        ProtoRequest {
128            data: req.data,
129            mode: req.mode.into(),
130            dry_run: req.dry_run,
131            try_mode_timeout: req.try_mode_timeout.map(|d| prost_types::Duration {
132                seconds: d.as_secs() as i64,
133                nanos: d.subsec_nanos() as i32,
134            }),
135        }
136    }
137}
138
139/// Builder for `ApplyConfigurationRequest`.
140#[derive(Debug, Clone, Default)]
141pub struct ApplyConfigurationRequestBuilder {
142    data: Vec<u8>,
143    mode: ApplyMode,
144    dry_run: bool,
145    try_mode_timeout: Option<Duration>,
146}
147
148impl ApplyConfigurationRequestBuilder {
149    /// Set the configuration from a YAML string.
150    #[must_use]
151    pub fn config_yaml(mut self, yaml: impl AsRef<str>) -> Self {
152        self.data = yaml.as_ref().as_bytes().to_vec();
153        self
154    }
155
156    /// Set the configuration from raw bytes.
157    #[must_use]
158    pub fn config_bytes(mut self, data: Vec<u8>) -> Self {
159        self.data = data;
160        self
161    }
162
163    /// Set the configuration from a file path.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the file cannot be read.
168    pub fn config_file(mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
169        self.data = std::fs::read(path)?;
170        Ok(self)
171    }
172
173    /// Set the apply mode.
174    #[must_use]
175    pub fn mode(mut self, mode: ApplyMode) -> Self {
176        self.mode = mode;
177        self
178    }
179
180    /// Enable dry-run mode (validate only, don't apply).
181    #[must_use]
182    pub fn dry_run(mut self, dry_run: bool) -> Self {
183        self.dry_run = dry_run;
184        self
185    }
186
187    /// Set the timeout for try mode.
188    #[must_use]
189    pub fn try_mode_timeout(mut self, timeout: Duration) -> Self {
190        self.try_mode_timeout = Some(timeout);
191        self
192    }
193
194    /// Build the request.
195    #[must_use]
196    pub fn build(self) -> ApplyConfigurationRequest {
197        ApplyConfigurationRequest {
198            data: self.data,
199            mode: self.mode,
200            dry_run: self.dry_run,
201            try_mode_timeout: self.try_mode_timeout,
202        }
203    }
204}
205
206/// Result of applying a configuration.
207#[derive(Debug, Clone)]
208pub struct ApplyConfigurationResult {
209    /// Node identifier (if available)
210    pub node: Option<String>,
211    /// Configuration validation warnings
212    pub warnings: Vec<String>,
213    /// Mode that was actually applied
214    pub mode: ApplyMode,
215    /// Human-readable description of the result
216    pub mode_details: String,
217}
218
219impl From<ProtoApplyConfiguration> for ApplyConfigurationResult {
220    fn from(proto: ProtoApplyConfiguration) -> Self {
221        Self {
222            node: proto.metadata.map(|m| m.hostname),
223            warnings: proto.warnings,
224            mode: proto.mode.into(),
225            mode_details: proto.mode_details,
226        }
227    }
228}
229
230/// Response from applying configuration.
231#[derive(Debug, Clone)]
232pub struct ApplyConfigurationResponse {
233    /// Results from each node
234    pub results: Vec<ApplyConfigurationResult>,
235}
236
237impl From<ProtoResponse> for ApplyConfigurationResponse {
238    fn from(proto: ProtoResponse) -> Self {
239        Self {
240            results: proto.messages.into_iter().map(Into::into).collect(),
241        }
242    }
243}
244
245impl ApplyConfigurationResponse {
246    /// Check if all nodes applied the configuration successfully (no warnings).
247    #[must_use]
248    pub fn is_success(&self) -> bool {
249        self.results.iter().all(|r| r.warnings.is_empty())
250    }
251
252    /// Get all warnings from all nodes.
253    #[must_use]
254    pub fn all_warnings(&self) -> Vec<&str> {
255        self.results
256            .iter()
257            .flat_map(|r| r.warnings.iter().map(String::as_str))
258            .collect()
259    }
260
261    /// Get the first result (useful for single-node operations).
262    #[must_use]
263    pub fn first(&self) -> Option<&ApplyConfigurationResult> {
264        self.results.first()
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_apply_mode_conversion() {
274        assert_eq!(i32::from(ApplyMode::Reboot), 0);
275        assert_eq!(i32::from(ApplyMode::Auto), 1);
276        assert_eq!(i32::from(ApplyMode::NoReboot), 2);
277        assert_eq!(i32::from(ApplyMode::Staged), 3);
278        assert_eq!(i32::from(ApplyMode::Try), 4);
279
280        assert_eq!(ApplyMode::from(0), ApplyMode::Reboot);
281        assert_eq!(ApplyMode::from(1), ApplyMode::Auto);
282        assert_eq!(ApplyMode::from(2), ApplyMode::NoReboot);
283        assert_eq!(ApplyMode::from(3), ApplyMode::Staged);
284        assert_eq!(ApplyMode::from(4), ApplyMode::Try);
285    }
286
287    #[test]
288    fn test_builder_pattern() {
289        let request = ApplyConfigurationRequest::builder()
290            .config_yaml("machine:\n  type: worker")
291            .mode(ApplyMode::NoReboot)
292            .dry_run(true)
293            .try_mode_timeout(Duration::from_secs(60))
294            .build();
295
296        assert_eq!(request.data, b"machine:\n  type: worker");
297        assert_eq!(request.mode, ApplyMode::NoReboot);
298        assert!(request.dry_run);
299        assert_eq!(request.try_mode_timeout, Some(Duration::from_secs(60)));
300    }
301
302    #[test]
303    fn test_from_yaml() {
304        let request = ApplyConfigurationRequest::from_yaml("test: config");
305        assert_eq!(request.data, b"test: config");
306        assert_eq!(request.mode, ApplyMode::Auto);
307        assert!(!request.dry_run);
308    }
309
310    #[test]
311    fn test_proto_conversion() {
312        let request = ApplyConfigurationRequest::builder()
313            .config_yaml("test")
314            .mode(ApplyMode::Staged)
315            .dry_run(true)
316            .try_mode_timeout(Duration::from_secs(120))
317            .build();
318
319        let proto: ProtoRequest = request.into();
320        assert_eq!(proto.data, b"test");
321        assert_eq!(proto.mode, ProtoMode::Staged as i32);
322        assert!(proto.dry_run);
323        assert!(proto.try_mode_timeout.is_some());
324    }
325
326    #[test]
327    fn test_apply_mode_display() {
328        assert_eq!(ApplyMode::Reboot.to_string(), "reboot");
329        assert_eq!(ApplyMode::Auto.to_string(), "auto");
330        assert_eq!(ApplyMode::NoReboot.to_string(), "no-reboot");
331        assert_eq!(ApplyMode::Staged.to_string(), "staged");
332        assert_eq!(ApplyMode::Try.to_string(), "try");
333    }
334}