scl_lib/
client.rs

1// SPDX-License-Identifier: EUPL-1.2
2pub use async_trait::async_trait;
3
4use reqwest::{
5    header::{self, HeaderMap, HeaderValue},
6    tls::CertificateRevocationList,
7    Certificate, Client, Identity, Response,
8};
9pub use reqwest::{Error, StatusCode};
10
11use std::ops::Deref;
12
13use crate::{
14    api_objects::{SclEvent, SclObject},
15    tls_config::TlsConfig,
16};
17
18use crate::LogError;
19
20/// This module contains a SclClient-specific error type and associated trait implementations.
21pub mod error {
22    /// A specialized `Result` type for SclClient operations.
23    pub type Result<T> = std::result::Result<T, ClientError>;
24
25    /// General error type for any SclClient errors.
26    #[derive(Debug)]
27    pub enum ClientError {
28        /// Custom error that does not belong to any other category.
29        Custom(String),
30        /// Wrapper for a `reqwest::Error`.
31        Http(reqwest::Error),
32        /// Wrapper for a `serde_json::Error`.
33        Decode(serde_json::Error),
34    }
35
36    impl std::fmt::Display for ClientError {
37        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38            use ClientError::*;
39            match self {
40                Custom(e) => e.fmt(f),
41                Http(e) => e.fmt(f),
42                Decode(e) => e.fmt(f),
43            }
44        }
45    }
46
47    impl std::error::Error for ClientError {}
48
49    impl From<reqwest::Error> for ClientError {
50        fn from(e: reqwest::Error) -> Self {
51            Self::Http(e)
52        }
53    }
54
55    impl From<serde_json::Error> for ClientError {
56        fn from(e: serde_json::Error) -> Self {
57            Self::Decode(e)
58        }
59    }
60}
61
62/// A trait for objects which can be passed as [EventHandler] to the `watch` function of a
63/// [SclClient].
64#[async_trait]
65pub trait EventHandler {
66    type Item: SclObject;
67    async fn handle_event(&mut self, event: SclEvent<Self::Item>);
68}
69
70/// A builder to construct a request object that can be used with the SclClient.
71///
72/// All members are optional and can be omitted. Specify them to create more complex requests.
73///
74/// # Examples
75///
76/// ```
77/// use scl_lib::api_objects::VirtualMachine;
78/// use scl_lib::client::SclRequest;
79///
80/// let req = SclRequest::<VirtualMachine>::new()
81///     .obj_name("vm-01")
82///     .sc("sc-01")
83///     .query(&[("node", "node-01")]);
84/// ```
85#[derive(Clone, Debug, Default)]
86pub struct SclRequest<'a, T: SclObject> {
87    pub obj_name: Option<String>,
88    pub sc_name: Option<String>,
89    pub body: Option<&'a T>,
90    pub query: &'a [(&'a str, &'a str)],
91}
92
93impl<'a, T: SclObject> SclRequest<'a, T> {
94    /// Creates and initializes a new [SclRequest] object.
95    pub fn new() -> Self {
96        Self {
97            obj_name: None,
98            body: None,
99            sc_name: None,
100            query: &[],
101        }
102    }
103
104    /// Sets the name of the request object to use as `T::path()/<name>` when constructing the URL.
105    pub fn obj_name(mut self, name: &str) -> Self {
106        self.obj_name = Some(name.into());
107        self
108    }
109
110    /// Adds a [SclObject] as body data to the request object.
111    pub fn body(mut self, obj: &'a T) -> Self {
112        self.body = Some(obj);
113        self
114    }
115
116    /// Sets the name of the the separation context to use as `/scs/<sc_name>` when constructing
117    /// the URL.
118    pub fn sc(mut self, sc_name: &str) -> Self {
119        self.sc_name = Some(sc_name.into());
120        self
121    }
122
123    /// Specify query parameters for the request.
124    pub fn query(mut self, query: &'a [(&'a str, &'a str)]) -> Self {
125        self.query = query;
126        self
127    }
128
129    /// Constructs a URL path from all member variables and returns it as [String].
130    pub fn url_path(&self) -> String {
131        T::api_endpoint(self.sc_name.as_deref(), self.obj_name.as_deref())
132    }
133}
134
135/// A wrapper object for a [Client] to provide convenient functions for interacting with the SCL
136/// API.
137///
138/// It implements the [Deref] trait so that custom requests can still be made directly via the
139/// wrapped client object.
140///
141/// # Examples
142///
143/// ```no_run
144/// use scl_lib::api_objects::VirtualMachine;
145/// use scl_lib::client::error::ClientError;
146/// use scl_lib::client::{reqwest_client, SclClient, SclRequest};
147///
148/// #[tokio::main]
149/// async fn main() -> Result<(), ClientError> {
150///     // Create client instance
151///     let client = {
152///         let reqwest_client = reqwest_client(None, None)
153///             .map_err(|e| ClientError::Custom(e.to_string()))?;
154///         SclClient::new("https://127.0.0.1:8008/api/v1".to_string(), reqwest_client)
155///     };
156///     // List all VMs of node "node-01"
157///     let vms: Vec<VirtualMachine> = client.list(
158///         &SclRequest::new().query(&[("node", "node-01")])
159///     ).await.unwrap();
160///     // Get the VM with the name "vm-01" of separation context "sc-01"
161///     let vm: VirtualMachine = client.show(
162///         &SclRequest::new().obj_name("vm-01").sc("sc-01")
163///     ).await.unwrap();
164///     // Update the virtual machine
165///     let _ = client.update(
166///         &SclRequest::new().obj_name(vm.metadata.name()).sc(&vm.separation_context).body(&vm)
167///     ).await.unwrap();
168///     // Delete the VM
169///     let _ = client.delete(
170///         &SclRequest::<VirtualMachine>::new().obj_name(vm.metadata.name())
171///     ).await.unwrap();
172///     Ok(())
173/// }
174/// ```
175#[derive(Clone)]
176pub struct SclClient {
177    pub api_url: String,
178    client: Client,
179}
180
181impl SclClient {
182    /// Creates a new [SclClient] from a URL string and an preconfigured [Client].
183    pub fn new(api_url: String, client: Client) -> Self {
184        Self { api_url, client }
185    }
186
187    /// Performs a HTTP GET request to the SCL API to fetch and return all [SclObject]s of type
188    /// `T`.
189    pub async fn list<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<Vec<T>, Error> {
190        let url = format!("{}{}", self.api_url, req.url_path());
191        self.client
192            .get(url)
193            .query(req.query)
194            .send()
195            .await
196            .log_err()?
197            .error_for_status()
198            .log_err()?
199            .json::<Vec<T>>()
200            .await
201    }
202
203    /// Performs a HTTP GET request to the SCL API to fetch and return a object of type `T`.
204    pub async fn show<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<T, Error> {
205        let url = format!("{}{}", self.api_url, req.url_path());
206        self.client
207            .get(url)
208            .send()
209            .await
210            .log_err()?
211            .error_for_status()
212            .log_err()?
213            .json::<T>()
214            .await
215    }
216
217    /// Performs a HTTP DELETE request to the SCL API to delete a object of type `T`.
218    pub async fn delete<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<(), Error> {
219        let url = format!("{}{}", self.api_url, req.url_path());
220        self.client
221            .delete(url)
222            .send()
223            .await
224            .log_err()?
225            .error_for_status()
226            .log_err()?;
227        Ok(())
228    }
229
230    /// Performs a HTTP POST request to the SCL API to create a object of type `T`.
231    pub async fn create<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<(), Error> {
232        let url = format!("{}{}", self.api_url, req.url_path());
233        match req.body {
234            Some(body) => self.client.post(url).json(body),
235            None => self.client.post(url),
236        }
237        .send()
238        .await
239        .log_err()?
240        .error_for_status()
241        .log_err()?;
242        Ok(())
243    }
244
245    /// Performs a HTTP PUT request to the SCL API to update a object of type `T`.
246    pub async fn update<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<(), Error> {
247        let url = format!("{}{}", self.api_url, req.url_path());
248        match req.body {
249            Some(body) => self.client.put(url).json(body),
250            None => self.client.put(url),
251        }
252        .send()
253        .await
254        .log_err()?
255        .error_for_status()
256        .log_err()?;
257        Ok(())
258    }
259
260    /// Performs an HTTP GET request to the watch endpoint of the item associated with the
261    /// EventHandler and calls the `handle` function for every received SclEvent.
262    ///
263    /// Invalid events are logged and then the function waits for further data. The function either
264    /// returns `Ok(())` when the bytestream ends, or returns an [reqwest::Error] if the connection
265    /// fails or breaks. For an automatic reconnect the function call should be executed in a
266    /// `loop{ }`.
267    pub async fn watch<T: EventHandler>(&self, mut handler: T) -> Result<(), reqwest::Error> {
268        let url = format!(
269            "{}/watch{}",
270            self.api_url,
271            T::Item::api_endpoint(None, None)
272        );
273        let mut resp = self.client.get(url).send().await?.error_for_status()?;
274        while let Some(bytes) = resp.chunk().await.log_err()? {
275            match serde_json::from_slice::<SclEvent<T::Item>>(&bytes) {
276                Ok(event) => handler.handle_event(event).await,
277                Err(err) => log::error!("{}", err),
278            };
279        }
280        Ok(())
281    }
282
283    /// Reads chunks of bytes from an open HTTP stream, parses them into an `SclEvent<T>` object
284    /// and then returns the result. Returns a `client::error::Error` if the stream fails to read
285    /// or the response body has been exhausted.
286    pub async fn recv_event<T: SclObject>(resp: &mut Response) -> error::Result<SclEvent<T>> {
287        if let Some(bytes) = resp.chunk().await.log_err()? {
288            Ok(serde_json::from_slice::<SclEvent<T>>(&bytes)?)
289        } else {
290            Err(error::ClientError::Custom(
291                "Empty Chunk: Response body has been exhausted.".to_string(),
292            ))
293        }
294    }
295}
296
297impl Deref for SclClient {
298    type Target = Client;
299
300    fn deref(&self) -> &Self::Target {
301        &self.client
302    }
303}
304
305pub fn reqwest_client(
306    additional_headers: Option<HeaderMap<HeaderValue>>,
307    tls: Option<&TlsConfig>,
308) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
309    let mut headers = HeaderMap::new();
310    // Set ACCEPT and CONTENT_TYPE headers to application/json
311    headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
312    headers.insert(
313        header::CONTENT_TYPE,
314        HeaderValue::from_static("application/json"),
315    );
316    // Add additional headers
317    if let Some(additional_headers) = additional_headers {
318        for (key, value) in additional_headers.iter() {
319            headers.insert(key, value.clone());
320        }
321    }
322
323    let mut client_builder = Client::builder().default_headers(headers);
324
325    if let Some(tls) = tls {
326        let ca_certs = Certificate::from_pem_bundle(&tls.ca_cert)?;
327        let mut id_buf = Vec::new();
328        id_buf.extend(&tls.client_cert);
329        id_buf.extend(&tls.client_key);
330        let identity = Identity::from_pem(&id_buf)?;
331        let crls = CertificateRevocationList::from_pem_bundle(&tls.crl)?;
332
333        client_builder = client_builder
334            .tls_backend_rustls()
335            .tls_certs_only(ca_certs)
336            .identity(identity)
337            .tls_crls_only(crls);
338    } else {
339        #[cfg(test)]
340        {
341            // Don't panic on empty CA certs even if we use reqwest with rustls feature flags.
342            client_builder = client_builder.tls_danger_accept_invalid_certs(true);
343        }
344    }
345
346    Ok(client_builder.build()?)
347}