Skip to main content

talos_api_rs/resources/
bootstrap.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for bootstrap operations.
4//!
5//! Bootstrap initializes the etcd cluster on the first control-plane node.
6//! This operation should only be called ONCE per cluster.
7
8use crate::api::machine::{
9    Bootstrap as ProtoBootstrap, BootstrapRequest as ProtoRequest,
10    BootstrapResponse as ProtoResponse,
11};
12
13/// Request to bootstrap the etcd cluster.
14///
15/// Bootstrap initializes the etcd cluster on the first control-plane node.
16/// This should only be called ONCE when creating a new cluster.
17///
18/// # Example
19///
20/// ```no_run
21/// use talos_api_rs::resources::BootstrapRequest;
22///
23/// // Standard bootstrap (new cluster)
24/// let request = BootstrapRequest::new();
25///
26/// // Recovery from etcd snapshot
27/// let recovery_request = BootstrapRequest::builder()
28///     .recover_etcd(true)
29///     .build();
30/// ```
31#[derive(Debug, Clone, Copy, Default)]
32pub struct BootstrapRequest {
33    /// Enable etcd recovery from a snapshot.
34    /// The snapshot must be uploaded via `EtcdRecover` RPC before calling bootstrap.
35    pub recover_etcd: bool,
36    /// Skip hash verification on the etcd snapshot.
37    /// Enable this when recovering from a data directory copy.
38    pub recover_skip_hash_check: bool,
39}
40
41impl BootstrapRequest {
42    /// Create a new standard bootstrap request (no recovery).
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Create a builder for customizing the bootstrap request.
49    #[must_use]
50    pub fn builder() -> BootstrapRequestBuilder {
51        BootstrapRequestBuilder::default()
52    }
53
54    /// Create a recovery bootstrap request.
55    ///
56    /// Use this when restoring from an etcd snapshot.
57    #[must_use]
58    pub fn recovery() -> Self {
59        Self {
60            recover_etcd: true,
61            recover_skip_hash_check: false,
62        }
63    }
64
65    /// Create a recovery bootstrap request that skips hash verification.
66    ///
67    /// Use this when recovering from a data directory copy.
68    #[must_use]
69    pub fn recovery_skip_hash() -> Self {
70        Self {
71            recover_etcd: true,
72            recover_skip_hash_check: true,
73        }
74    }
75}
76
77impl From<BootstrapRequest> for ProtoRequest {
78    fn from(req: BootstrapRequest) -> Self {
79        ProtoRequest {
80            recover_etcd: req.recover_etcd,
81            recover_skip_hash_check: req.recover_skip_hash_check,
82        }
83    }
84}
85
86/// Builder for `BootstrapRequest`.
87#[derive(Debug, Clone, Copy, Default)]
88pub struct BootstrapRequestBuilder {
89    recover_etcd: bool,
90    recover_skip_hash_check: bool,
91}
92
93impl BootstrapRequestBuilder {
94    /// Enable etcd recovery from a snapshot.
95    #[must_use]
96    pub fn recover_etcd(mut self, recover: bool) -> Self {
97        self.recover_etcd = recover;
98        self
99    }
100
101    /// Skip hash verification on the etcd snapshot.
102    #[must_use]
103    pub fn recover_skip_hash_check(mut self, skip: bool) -> Self {
104        self.recover_skip_hash_check = skip;
105        self
106    }
107
108    /// Build the bootstrap request.
109    #[must_use]
110    pub fn build(self) -> BootstrapRequest {
111        BootstrapRequest {
112            recover_etcd: self.recover_etcd,
113            recover_skip_hash_check: self.recover_skip_hash_check,
114        }
115    }
116}
117
118/// Result of a bootstrap operation for a single node.
119#[derive(Debug, Clone)]
120pub struct BootstrapResult {
121    /// Node hostname (if available from metadata)
122    pub node: Option<String>,
123}
124
125impl From<ProtoBootstrap> for BootstrapResult {
126    fn from(proto: ProtoBootstrap) -> Self {
127        Self {
128            node: proto.metadata.map(|m| m.hostname),
129        }
130    }
131}
132
133/// Response from a bootstrap operation.
134#[derive(Debug, Clone)]
135pub struct BootstrapResponse {
136    /// Results from each node (typically just one for bootstrap)
137    pub results: Vec<BootstrapResult>,
138}
139
140impl From<ProtoResponse> for BootstrapResponse {
141    fn from(proto: ProtoResponse) -> Self {
142        Self {
143            results: proto.messages.into_iter().map(Into::into).collect(),
144        }
145    }
146}
147
148impl BootstrapResponse {
149    /// Check if the bootstrap succeeded.
150    #[must_use]
151    pub fn is_success(&self) -> bool {
152        !self.results.is_empty()
153    }
154
155    /// Get the first result (useful for single-node operations).
156    #[must_use]
157    pub fn first(&self) -> Option<&BootstrapResult> {
158        self.results.first()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_bootstrap_request_new() {
168        let request = BootstrapRequest::new();
169        assert!(!request.recover_etcd);
170        assert!(!request.recover_skip_hash_check);
171    }
172
173    #[test]
174    fn test_bootstrap_request_recovery() {
175        let request = BootstrapRequest::recovery();
176        assert!(request.recover_etcd);
177        assert!(!request.recover_skip_hash_check);
178    }
179
180    #[test]
181    fn test_bootstrap_request_recovery_skip_hash() {
182        let request = BootstrapRequest::recovery_skip_hash();
183        assert!(request.recover_etcd);
184        assert!(request.recover_skip_hash_check);
185    }
186
187    #[test]
188    fn test_bootstrap_request_builder() {
189        let request = BootstrapRequest::builder()
190            .recover_etcd(true)
191            .recover_skip_hash_check(true)
192            .build();
193
194        assert!(request.recover_etcd);
195        assert!(request.recover_skip_hash_check);
196    }
197
198    #[test]
199    fn test_proto_conversion() {
200        let request = BootstrapRequest::builder()
201            .recover_etcd(true)
202            .recover_skip_hash_check(false)
203            .build();
204
205        let proto: ProtoRequest = request.into();
206        assert!(proto.recover_etcd);
207        assert!(!proto.recover_skip_hash_check);
208    }
209
210    #[test]
211    fn test_bootstrap_response_is_success() {
212        let response = BootstrapResponse {
213            results: vec![BootstrapResult {
214                node: Some("controlplane-1".to_string()),
215            }],
216        };
217        assert!(response.is_success());
218
219        let empty_response = BootstrapResponse { results: vec![] };
220        assert!(!empty_response.is_success());
221    }
222}