talos_rust_client/
connector.rs

1//! Connection builder for Talos gRPC API with mTLS support
2
3use crate::error::{Error, Result};
4use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
5use tracing::{debug, instrument};
6
7/// Connection builder for Talos gRPC API
8#[derive(Debug, Clone)]
9pub struct TalosConnector {
10    endpoint: String,
11    ca_cert: Option<Vec<u8>>,
12    client_cert: Option<Vec<u8>>,
13    client_key: Option<Vec<u8>>,
14    server_name: Option<String>,
15}
16
17impl TalosConnector {
18    /// Create a new connection builder
19    ///
20    /// # Arguments
21    /// * `endpoint` - The Talos API endpoint (e.g., "https://192.168.1.100:50000")
22    pub fn new(endpoint: impl Into<String>) -> Self {
23        Self {
24            endpoint: endpoint.into(),
25            ca_cert: None,
26            client_cert: None,
27            client_key: None,
28            server_name: None,
29        }
30    }
31
32    /// Set the CA certificate in PEM format
33    pub fn ca_pem(mut self, pem: Vec<u8>) -> Self {
34        self.ca_cert = Some(pem);
35        self
36    }
37
38    /// Set the CA certificate from a file path
39    pub fn ca_pem_file(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
40        let pem = std::fs::read(path)?;
41        self.ca_cert = Some(pem);
42        Ok(self)
43    }
44
45    /// Set the client certificate in PEM format
46    pub fn cert_pem(mut self, pem: Vec<u8>) -> Self {
47        self.client_cert = Some(pem);
48        self
49    }
50
51    /// Set the client certificate from a file path
52    pub fn cert_pem_file(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
53        let pem = std::fs::read(path)?;
54        self.client_cert = Some(pem);
55        Ok(self)
56    }
57
58    /// Set the client private key in PEM format
59    pub fn key_pem(mut self, pem: Vec<u8>) -> Self {
60        self.client_key = Some(pem);
61        self
62    }
63
64    /// Set the client private key from a file path
65    pub fn key_pem_file(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
66        let pem = std::fs::read(path)?;
67        self.client_key = Some(pem);
68        Ok(self)
69    }
70
71    /// Set the server name for SNI (Server Name Indication)
72    pub fn server_name(mut self, name: impl Into<String>) -> Self {
73        self.server_name = Some(name.into());
74        self
75    }
76
77    /// Connect to the Talos API
78    #[instrument(skip(self))]
79    pub async fn connect(self) -> Result<Channel> {
80        debug!("Connecting to Talos API at {}", self.endpoint);
81
82        // Validate required fields
83        let ca_cert = self
84            .ca_cert
85            .ok_or_else(|| Error::MissingConfig("CA certificate".to_string()))?;
86        let client_cert = self
87            .client_cert
88            .ok_or_else(|| Error::MissingConfig("Client certificate".to_string()))?;
89        let client_key = self
90            .client_key
91            .ok_or_else(|| Error::MissingConfig("Client key".to_string()))?;
92
93        // Create tonic Certificate and Identity
94        let ca = Certificate::from_pem(ca_cert);
95        let identity = Identity::from_pem(client_cert, client_key);
96
97        // Configure TLS
98        let mut tls_config = ClientTlsConfig::new().ca_certificate(ca).identity(identity);
99
100        // Set domain name if provided, otherwise extract from endpoint
101        if let Some(domain) = self.server_name {
102            tls_config = tls_config.domain_name(domain);
103        } else {
104            // Extract domain from endpoint URL
105            if let Ok(url) = url::Url::parse(&self.endpoint) {
106                if let Some(host) = url.host_str() {
107                    tls_config = tls_config.domain_name(host);
108                }
109            }
110        }
111
112        let channel = Endpoint::from_shared(self.endpoint.clone())
113            .map_err(|e| Error::Other(format!("Invalid endpoint: {e}")))?
114            .tls_config(tls_config)
115            .map_err(|e| Error::TlsConfig(format!("Failed to set TLS config: {e}")))?
116            .connect()
117            .await?;
118
119        debug!("Successfully connected to Talos API");
120        Ok(channel)
121    }
122}