selectel_mks/
lib.rs

1use error::Error;
2use std::time::Duration;
3use tokio::time::timeout;
4use url::Url;
5
6// Hyper imports.
7use hyper::body::Buf;
8use hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT};
9use hyper::{Method, Request};
10#[cfg(feature = "rustls")]
11type HttpsConnector = hyper_rustls::HttpsConnector<hyper::client::HttpConnector>;
12#[cfg(feature = "rust-native-tls")]
13use hyper_tls;
14#[cfg(feature = "rust-native-tls")]
15type HttpsConnector = hyper_tls::HttpsConnector<hyper::client::HttpConnector>;
16
17pub mod error;
18pub mod resource_url;
19
20pub mod cluster;
21pub mod kubeversion;
22pub mod node;
23pub mod nodegroup;
24pub mod task;
25
26// Environment variables from Cargo.
27static PKG_NAME: &str = env!("CARGO_PKG_NAME");
28static PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
29
30/// `Client` struct is used to make calls to the MKS API.
31pub struct Client {
32    client: hyper::Client<HttpsConnector>,
33    token: String,
34    base_endpoint: url::Url,
35    user_agent: String,
36    timeout: Duration,
37}
38
39impl Client {
40    /// Construct the new Client struct with default configuration.
41    ///
42    /// Use `Builder` to configure the client.
43    pub fn new(base_endpoint: &str, token: &str) -> Result<Client, Error> {
44        Client::with_builder(base_endpoint, token, Client::builder())
45    }
46
47    fn with_builder(base_endpoint: &str, token: &str, builder: Builder) -> Result<Client, Error> {
48        // Check token.
49        if token.is_empty() {
50            return Err(Error::EmptyTokenError);
51        }
52        let token = String::from(token);
53
54        // Check base endpoint.
55        let base_endpoint = Url::parse(base_endpoint).map_err(|_| Error::EndpointError)?;
56
57        // Use the provided Hyper client or configure a new one.
58        let client = match builder.client {
59            Some(client) => client,
60            None => {
61                #[cfg(feature = "rustls")]
62                let client = hyper::Client::builder().build(HttpsConnector::new());
63                #[cfg(feature = "rust-native-tls")]
64                let client = hyper::Client::builder().build(HttpsConnector::new()?);
65
66                client
67            }
68        };
69
70        Ok(Client {
71            client,
72            token,
73            base_endpoint,
74            user_agent: Client::user_agent(),
75            timeout: builder.timeout,
76        })
77    }
78
79    fn user_agent() -> String {
80        format!("{}/{}", PKG_NAME, PKG_VERSION)
81    }
82
83    /// Get a default builder.
84    pub fn builder() -> Builder {
85        Builder::default()
86    }
87
88    // Prepare a new request.
89    fn new_request(
90        &self,
91        method: Method,
92        path: &str,
93        body: Option<String>,
94    ) -> Result<Request<hyper::Body>, Error> {
95        // Build a final Hyper URI.
96        let uri = self.make_uri(path)?;
97
98        // Prepare a new Hyper request.
99        let mut req = Request::new(hyper::Body::empty());
100        *req.method_mut() = method;
101        *req.uri_mut() = uri;
102
103        // Add user-agent header.
104        req.headers_mut().insert(
105            USER_AGENT,
106            HeaderValue::from_str(&self.user_agent).map_err(|_| Error::RequestError)?,
107        );
108
109        // Add x-auth-token header.
110        req.headers_mut().insert(
111            "x-auth-token",
112            HeaderValue::from_str(&self.token).map_err(|_| Error::RequestError)?,
113        );
114
115        // Add body into the new request if it's provided.
116        if let Some(body) = body {
117            // Add content-length header if body is provided.
118            let len =
119                HeaderValue::from_str(&body.len().to_string()).map_err(|_| Error::RequestError)?;
120            req.headers_mut().insert(CONTENT_LENGTH, len);
121
122            // Add content-type header if body is provided.
123            req.headers_mut().insert(
124                CONTENT_TYPE,
125                HeaderValue::from_str("application/json").map_err(|_| Error::RequestError)?,
126            );
127
128            *req.body_mut() = hyper::Body::from(body);
129        }
130
131        Ok(req)
132    }
133
134    #[tokio::main]
135    async fn do_request(&self, req: hyper::Request<hyper::Body>) -> Result<String, Error> {
136        let duration = self.timeout;
137        let handle = async {
138            let raw_resp = self.client.request(req).await?;
139
140            let status = raw_resp.status();
141            let body = hyper::body::aggregate(raw_resp).await?.to_bytes();
142            let body = String::from_utf8_lossy(&body);
143
144            Ok::<_, hyper::Error>((body.to_string(), status))
145        };
146
147        let raw_resp = timeout(duration, handle).await??;
148
149        let (body, status) = raw_resp;
150
151        if !status.is_success() {
152            return Err(Error::HttpError(status.as_u16(), body));
153        }
154
155        Ok(body)
156    }
157
158    fn make_uri(&self, path: &str) -> Result<hyper::Uri, Error> {
159        let url = self
160            .base_endpoint
161            .clone()
162            .join(path)
163            .map_err(|_| Error::UrlError)?;
164
165        url.as_str()
166            .parse::<hyper::Uri>()
167            .map_err(|_| Error::UrlError)
168    }
169}
170
171/// Methods to work with clusters.
172impl Client {
173    /// Get a cluster.
174    pub fn get_cluster(&self, cluster_id: &str) -> Result<cluster::schemas::Cluster, Error> {
175        cluster::api::get(self, cluster_id)
176    }
177
178    /// List clusters.
179    pub fn list_clusters(&self) -> Result<Vec<cluster::schemas::Cluster>, Error> {
180        cluster::api::list(self)
181    }
182
183    /// Create a cluster.
184    pub fn create_cluster(
185        &self,
186        opts: &cluster::schemas::CreateOpts,
187    ) -> Result<cluster::schemas::Cluster, Error> {
188        cluster::api::create(self, opts)
189    }
190
191    /// Delete a cluster.
192    pub fn delete_cluster(&self, cluster_id: &str) -> Result<(), Error> {
193        cluster::api::delete(self, cluster_id)
194    }
195}
196
197/// Methods to work with Kubernetes versions.
198impl Client {
199    /// List all Kubernetes versions.
200    pub fn list_kube_versions(&self) -> Result<Vec<kubeversion::schemas::KubeVersion>, Error> {
201        kubeversion::api::list(self)
202    }
203}
204
205/// Methods to work with nodes.
206impl Client {
207    /// Get a cluster node.
208    pub fn get_node(
209        &self,
210        cluster_id: &str,
211        nodegroup_id: &str,
212        node_id: &str,
213    ) -> Result<node::schemas::Node, Error> {
214        node::api::get(self, cluster_id, nodegroup_id, node_id)
215    }
216
217    /// Reinstall a cluster node.
218    pub fn reinstall_node(
219        &self,
220        cluster_id: &str,
221        nodegroup_id: &str,
222        node_id: &str,
223    ) -> Result<(), Error> {
224        node::api::reinstall(self, cluster_id, nodegroup_id, node_id)
225    }
226}
227
228/// Methods to work with nodegroups.
229impl Client {
230    /// Get a cluster nodegroup.
231    pub fn get_nodegroup(
232        &self,
233        cluster_id: &str,
234        nodegroup_id: &str,
235    ) -> Result<nodegroup::schemas::Nodegroup, Error> {
236        nodegroup::api::get(self, cluster_id, nodegroup_id)
237    }
238
239    /// List cluster nodegroups.
240    pub fn list_nodegroups(
241        &self,
242        cluster_id: &str,
243    ) -> Result<Vec<nodegroup::schemas::Nodegroup>, Error> {
244        nodegroup::api::list(self, cluster_id)
245    }
246
247    /// Create a cluster nodegroup.
248    pub fn create_nodegroup(
249        &self,
250        cluster_id: &str,
251        opts: &nodegroup::schemas::CreateOpts,
252    ) -> Result<(), Error> {
253        nodegroup::api::create(self, cluster_id, opts)
254    }
255
256    /// Delete a cluster nodegroup.
257    pub fn delete_nodegroup(&self, cluster_id: &str, nodegroup_id: &str) -> Result<(), Error> {
258        nodegroup::api::delete(self, cluster_id, nodegroup_id)
259    }
260
261    /// Resize a cluster nodegroup.
262    pub fn resize_nodegroup(
263        &self,
264        cluster_id: &str,
265        nodegroup_id: &str,
266        opts: &nodegroup::schemas::ResizeOpts,
267    ) -> Result<(), Error> {
268        nodegroup::api::resize(self, cluster_id, nodegroup_id, opts)
269    }
270
271    /// Update a cluster nodegroup.
272    pub fn update_nodegroup(
273        &self,
274        cluster_id: &str,
275        nodegroup_id: &str,
276        opts: &nodegroup::schemas::UpdateOpts,
277    ) -> Result<(), Error> {
278        nodegroup::api::update(self, cluster_id, nodegroup_id, opts)
279    }
280}
281
282/// Methods to work with tasks.
283impl Client {
284    /// Get a task.
285    pub fn get_task(&self, cluster_id: &str, task_id: &str) -> Result<task::schemas::Task, Error> {
286        task::api::get(self, cluster_id, task_id)
287    }
288
289    /// List tasks.
290    pub fn list_tasks(&self, cluster_id: &str) -> Result<Vec<task::schemas::Task>, Error> {
291        task::api::list(self, cluster_id)
292    }
293}
294
295/// Builder for `Client`.
296pub struct Builder {
297    /// Hyper client to use for the connection.
298    client: Option<hyper::Client<HttpsConnector>>,
299
300    /// Request timeout.
301    timeout: Duration,
302}
303
304// Default timeout for requests.
305const DEFAULT_TIMEOUT: u64 = 30;
306
307impl Default for Builder {
308    fn default() -> Self {
309        Self {
310            client: None,
311            timeout: Duration::from_secs(DEFAULT_TIMEOUT),
312        }
313    }
314}
315
316impl Builder {
317    /// Set Hyper client.
318    ///
319    /// By default this library will instantiate a new HttpsConnector.
320    /// It will use hyper_rustls or hyper_tls depending on selected library features.
321    pub fn with_client(mut self, client: hyper::Client<HttpsConnector>) -> Self {
322        self.client = Some(client);
323        self
324    }
325
326    /// Set request timeout.
327    ///
328    /// Default is 30 seconds.
329    pub fn with_timeout(mut self, timeout: Duration) -> Self {
330        self.timeout = timeout;
331        self
332    }
333
334    /// Create `Client` with the configuration in this builder.
335    pub fn build(self, base_endpoint: &str, token: &str) -> Result<Client, Error> {
336        Client::with_builder(base_endpoint, token, self)
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn new_client_default_builder() {
346        let client = Client::new("https://example.org", "token_a").unwrap();
347
348        assert_eq!(
349            client.base_endpoint,
350            Url::parse("https://example.org").unwrap()
351        );
352        assert_eq!(client.token, String::from("token_a"));
353        assert_eq!(client.user_agent, format!("{}/{}", PKG_NAME, PKG_VERSION));
354        assert_eq!(client.timeout, Duration::from_secs(DEFAULT_TIMEOUT));
355    }
356
357    #[test]
358    fn new_client_with_builder() {
359        let client = Client::builder()
360            .with_timeout(Duration::from_secs(10))
361            .build("https://example.com", "token_b")
362            .unwrap();
363
364        assert_eq!(
365            client.base_endpoint,
366            Url::parse("https://example.com").unwrap()
367        );
368        assert_eq!(client.token, String::from("token_b"));
369        assert_eq!(client.user_agent, format!("{}/{}", PKG_NAME, PKG_VERSION));
370        assert_eq!(client.timeout, Duration::from_secs(10));
371    }
372}