Skip to main content

talos_api_rs/client/
node_target.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Node targeting support for multi-node operations
4//!
5//! This module provides functionality to target specific nodes when making
6//! Talos API calls. By default, API calls go to the endpoint you're connected to,
7//! but you can use the `x-talos-node` gRPC metadata header to route requests
8//! to specific nodes in the cluster.
9//!
10//! # Example
11//!
12//! ```ignore
13//! use talos_api_rs::{TalosClient, TalosClientConfig};
14//! use talos_api_rs::client::NodeTarget;
15//!
16//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! let client = TalosClient::new(TalosClientConfig::default()).await?;
18//!
19//! // Target a specific node
20//! let target = NodeTarget::single("192.168.1.10");
21//! let hostname = client.with_node(target).hostname().await?;
22//!
23//! // Target multiple nodes (cluster-wide)
24//! let targets = NodeTarget::multiple(vec!["192.168.1.10", "192.168.1.11"]);
25//! # Ok(())
26//! # }
27//! ```
28
29use tonic::metadata::{Ascii, MetadataValue};
30use tonic::Request;
31
32/// The gRPC metadata key for node targeting
33pub const NODE_METADATA_KEY: &str = "x-talos-node";
34
35/// Represents a target node or set of nodes for API operations
36#[derive(Debug, Clone, PartialEq, Eq, Default)]
37pub enum NodeTarget {
38    /// No specific target - use the connected endpoint
39    #[default]
40    Default,
41    /// Target a single node by IP or hostname
42    Single(String),
43    /// Target multiple nodes (for cluster-wide operations)
44    Multiple(Vec<String>),
45}
46
47impl NodeTarget {
48    /// Create a target for the default (connected) node
49    #[must_use]
50    pub fn none() -> Self {
51        Self::Default
52    }
53
54    /// Create a target for a single node
55    #[must_use]
56    pub fn single(node: impl Into<String>) -> Self {
57        Self::Single(node.into())
58    }
59
60    /// Create a target for multiple nodes
61    #[must_use]
62    pub fn multiple(nodes: impl IntoIterator<Item = impl Into<String>>) -> Self {
63        Self::Multiple(nodes.into_iter().map(Into::into).collect())
64    }
65
66    /// Create from a comma-separated string
67    #[must_use]
68    pub fn from_csv(csv: &str) -> Self {
69        let nodes: Vec<String> = csv
70            .split(',')
71            .map(|s| s.trim().to_string())
72            .filter(|s| !s.is_empty())
73            .collect();
74
75        match nodes.len() {
76            0 => Self::Default,
77            1 => Self::Single(nodes.into_iter().next().unwrap()),
78            _ => Self::Multiple(nodes),
79        }
80    }
81
82    /// Check if this is the default target
83    #[must_use]
84    pub fn is_default(&self) -> bool {
85        matches!(self, Self::Default)
86    }
87
88    /// Check if this targets a single node
89    #[must_use]
90    pub fn is_single(&self) -> bool {
91        matches!(self, Self::Single(_))
92    }
93
94    /// Check if this targets multiple nodes
95    #[must_use]
96    pub fn is_multiple(&self) -> bool {
97        matches!(self, Self::Multiple(_))
98    }
99
100    /// Get the nodes as a slice (empty for Default)
101    #[must_use]
102    pub fn nodes(&self) -> &[String] {
103        match self {
104            Self::Default => &[],
105            Self::Single(node) => std::slice::from_ref(node),
106            Self::Multiple(nodes) => nodes,
107        }
108    }
109
110    /// Get the first node, if any
111    #[must_use]
112    pub fn first(&self) -> Option<&str> {
113        match self {
114            Self::Default => None,
115            Self::Single(node) => Some(node),
116            Self::Multiple(nodes) => nodes.first().map(String::as_str),
117        }
118    }
119
120    /// Convert to comma-separated string for gRPC metadata
121    #[must_use]
122    pub fn to_csv(&self) -> Option<String> {
123        match self {
124            Self::Default => None,
125            Self::Single(node) => Some(node.clone()),
126            Self::Multiple(nodes) => Some(nodes.join(",")),
127        }
128    }
129
130    /// Apply node targeting to a gRPC request
131    pub fn apply_to_request<T>(&self, mut request: Request<T>) -> Request<T> {
132        if let Some(node_value) = self.to_csv() {
133            if let Ok(metadata_value) = node_value.parse::<MetadataValue<Ascii>>() {
134                request
135                    .metadata_mut()
136                    .insert(NODE_METADATA_KEY, metadata_value);
137            }
138        }
139        request
140    }
141}
142
143impl From<&str> for NodeTarget {
144    fn from(s: &str) -> Self {
145        if s.is_empty() {
146            Self::Default
147        } else if s.contains(',') {
148            Self::from_csv(s)
149        } else {
150            Self::Single(s.to_string())
151        }
152    }
153}
154
155impl From<String> for NodeTarget {
156    fn from(s: String) -> Self {
157        Self::from(s.as_str())
158    }
159}
160
161impl From<Vec<String>> for NodeTarget {
162    fn from(nodes: Vec<String>) -> Self {
163        match nodes.len() {
164            0 => Self::Default,
165            1 => Self::Single(nodes.into_iter().next().unwrap()),
166            _ => Self::Multiple(nodes),
167        }
168    }
169}
170
171impl From<Option<String>> for NodeTarget {
172    fn from(opt: Option<String>) -> Self {
173        match opt {
174            Some(s) => Self::from(s),
175            None => Self::Default,
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_node_target_default() {
186        let target = NodeTarget::none();
187        assert!(target.is_default());
188        assert!(!target.is_single());
189        assert!(!target.is_multiple());
190        assert_eq!(target.nodes(), &[] as &[String]);
191        assert_eq!(target.first(), None);
192        assert_eq!(target.to_csv(), None);
193    }
194
195    #[test]
196    fn test_node_target_single() {
197        let target = NodeTarget::single("192.168.1.10");
198        assert!(!target.is_default());
199        assert!(target.is_single());
200        assert!(!target.is_multiple());
201        assert_eq!(target.nodes(), &["192.168.1.10".to_string()]);
202        assert_eq!(target.first(), Some("192.168.1.10"));
203        assert_eq!(target.to_csv(), Some("192.168.1.10".to_string()));
204    }
205
206    #[test]
207    fn test_node_target_multiple() {
208        let target = NodeTarget::multiple(vec!["192.168.1.10", "192.168.1.11"]);
209        assert!(!target.is_default());
210        assert!(!target.is_single());
211        assert!(target.is_multiple());
212        assert_eq!(
213            target.nodes(),
214            &["192.168.1.10".to_string(), "192.168.1.11".to_string()]
215        );
216        assert_eq!(target.first(), Some("192.168.1.10"));
217        assert_eq!(
218            target.to_csv(),
219            Some("192.168.1.10,192.168.1.11".to_string())
220        );
221    }
222
223    #[test]
224    fn test_node_target_from_csv() {
225        assert!(NodeTarget::from_csv("").is_default());
226        assert_eq!(
227            NodeTarget::from_csv("192.168.1.10"),
228            NodeTarget::Single("192.168.1.10".to_string())
229        );
230        assert_eq!(
231            NodeTarget::from_csv("192.168.1.10, 192.168.1.11"),
232            NodeTarget::Multiple(vec!["192.168.1.10".to_string(), "192.168.1.11".to_string()])
233        );
234    }
235
236    #[test]
237    fn test_node_target_from_str() {
238        let target: NodeTarget = "192.168.1.10".into();
239        assert_eq!(target, NodeTarget::Single("192.168.1.10".to_string()));
240
241        let target: NodeTarget = "192.168.1.10,192.168.1.11".into();
242        assert!(target.is_multiple());
243    }
244
245    #[test]
246    fn test_node_target_from_vec() {
247        let target: NodeTarget = vec!["192.168.1.10".to_string()].into();
248        assert!(target.is_single());
249
250        let target: NodeTarget =
251            vec!["192.168.1.10".to_string(), "192.168.1.11".to_string()].into();
252        assert!(target.is_multiple());
253
254        let target: NodeTarget = Vec::<String>::new().into();
255        assert!(target.is_default());
256    }
257
258    #[test]
259    fn test_apply_to_request() {
260        let target = NodeTarget::single("192.168.1.10");
261        let request = Request::new(());
262        let request = target.apply_to_request(request);
263
264        let metadata = request.metadata().get(NODE_METADATA_KEY);
265        assert!(metadata.is_some());
266        assert_eq!(metadata.unwrap().to_str().unwrap(), "192.168.1.10");
267    }
268
269    #[test]
270    fn test_apply_to_request_multiple() {
271        let target = NodeTarget::multiple(vec!["10.0.0.1", "10.0.0.2"]);
272        let request = Request::new(());
273        let request = target.apply_to_request(request);
274
275        let metadata = request.metadata().get(NODE_METADATA_KEY);
276        assert!(metadata.is_some());
277        assert_eq!(metadata.unwrap().to_str().unwrap(), "10.0.0.1,10.0.0.2");
278    }
279
280    #[test]
281    fn test_apply_to_request_default() {
282        let target = NodeTarget::default();
283        let request = Request::new(());
284        let request = target.apply_to_request(request);
285
286        let metadata = request.metadata().get(NODE_METADATA_KEY);
287        assert!(metadata.is_none());
288    }
289}