nomad_client_rs/
lib.rs

1extern crate core;
2extern crate serde;
3extern crate serde_json;
4extern crate url;
5
6use std::env;
7use std::fs::File;
8use std::io::{Error, Read};
9
10use reqwest::{Client, Identity, Method, RequestBuilder};
11use serde::de::DeserializeOwned;
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use crate::api::path_combine;
16
17pub mod api;
18mod extensions;
19pub mod models;
20
21static ENV_NOMAD_BASE_URL: &str = "NOMAD_BASE_URL";
22static ENV_NOMAD_PORT: &str = "NOMAD_PORT";
23static ENV_NOMAD_API_VERSION: &str = "NOMAD_API_VERSION";
24static ENV_NOMAD_SECRET_TOKEN: &str = "NOMAD_SECRET_TOKEN";
25static ENV_NOMAD_TLS_ALLOW_INSECURE: &str = "NOMAD_TLS_ALLOW_INSECURE";
26static ENV_NOMAD_MTLS_CERT_PATH: &str = "NOMAD_MTLS_CERT_PATH";
27static ENV_NOMAD_MTLS_KEY_PATH: &str = "NOMAD_MTLS_KEY_PATH";
28
29#[derive(Clone, Debug)]
30pub struct NomadClient {
31    client: Client,
32    config: Config,
33}
34
35impl NomadClient {
36    pub fn new(config: Config) -> Self {
37        let builder = Client::builder()
38            .use_rustls_tls()
39            .danger_accept_invalid_certs(config.allow_insecure_certs);
40
41        let client = match &config.mtls {
42            Some(mtls_config) => {
43                let certs = mtls_config.load_certs().expect("Certs should be readable");
44                let pkcs12 = Identity::from_pem(&certs).expect("Certs should be parseable");
45
46                builder
47                    .identity(pkcs12)
48                    .build()
49                    .expect("Http client should be buildable")
50            }
51            None => builder.build().expect("Http client should be buildable"),
52        };
53
54        NomadClient { client, config }
55    }
56
57    pub fn config(&self) -> &Config {
58        &self.config
59    }
60
61    pub fn config_mut(&mut self) -> &mut Config {
62        &mut self.config
63    }
64
65    pub fn get_base_url(&self) -> String {
66        format!(
67            "{}:{}/{}",
68            self.config.base_url, self.config.port, self.config.api_version
69        )
70    }
71
72    pub fn get_endpoint(&self, endpoint: &str) -> String {
73        path_combine(self.get_base_url().as_str(), endpoint)
74    }
75
76    pub fn request(&self, method: Method, endpoint: &str) -> RequestBuilder {
77        self.client.request(method, self.get_endpoint(endpoint))
78    }
79
80    async fn send_plain(&self, mut req: RequestBuilder) -> Result<String, ClientError> {
81        if let Some(token) = self.config.token.as_deref() {
82            req = req.header::<String, String>("X-Nomad-Token".into(), token.to_string());
83        }
84
85        let req_result = req.build();
86        if let Err(error) = req_result {
87            return Err(ClientError::RequestError(error.to_string()));
88        }
89
90        let req = req_result.unwrap();
91
92        match self.client.execute(req).await {
93            Ok(response) => {
94                let status = response.status();
95
96                match response.text().await {
97                    Ok(body) => {
98                        if status.is_success() {
99                            Ok(body)
100                        } else {
101                            Err(ClientError::ServerError(status.as_u16(), body))
102                        }
103                    }
104                    Err(err) => Err(ClientError::NetworkError(err.to_string())),
105                }
106            }
107            Err(err) => Err(ClientError::NetworkError(err.to_string())),
108        }
109    }
110
111    async fn send<TResponse: DeserializeOwned>(
112        &self,
113        mut req: RequestBuilder,
114    ) -> Result<TResponse, ClientError> {
115        if let Some(token) = self.config.token.as_deref() {
116            req = req.header::<String, String>("X-Nomad-Token".into(), token.to_string());
117        }
118
119        let req_result = req.build();
120        if let Err(error) = req_result {
121            return Err(ClientError::RequestError(error.to_string()));
122        }
123
124        let req = req_result.unwrap();
125
126        match self.client.execute(req).await {
127            Ok(response) => {
128                let status = response.status();
129                if status.is_success() {
130                    match response.json::<TResponse>().await {
131                        Ok(body) => Ok(body),
132                        Err(err) => Err(ClientError::DeserializationError(err.to_string())),
133                    }
134                } else {
135                    match response.text().await {
136                        Ok(body) => Err(ClientError::ServerError(status.as_u16(), body)),
137                        Err(err) => Err(ClientError::NetworkError(err.to_string())),
138                    }
139                }
140            }
141            Err(err) => Err(ClientError::NetworkError(err.to_string())),
142        }
143    }
144}
145
146impl Default for NomadClient {
147    fn default() -> Self {
148        NomadClient::new(Config::from_env())
149    }
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize)]
153pub struct Config {
154    pub base_url: String,
155    pub port: u16,
156    pub api_version: String,
157    pub token: Option<String>,
158    pub allow_insecure_certs: bool,
159    pub mtls: Option<MTLSConfig>,
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize)]
163pub struct MTLSConfig {
164    pub cert_path: String,
165    pub key_path: String,
166}
167
168impl MTLSConfig {
169    pub fn new(cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
170        MTLSConfig {
171            cert_path: cert_path.into(),
172            key_path: key_path.into(),
173        }
174    }
175
176    pub fn load_certs(&self) -> Result<Vec<u8>, Error> {
177        let mut buffer = Vec::new();
178        File::open(&self.cert_path)?.read_to_end(&mut buffer)?;
179        File::open(&self.key_path)?.read_to_end(&mut buffer)?;
180        Ok(buffer)
181    }
182}
183
184impl Config {
185    pub fn from_env() -> Config {
186        let mut default = Config::default();
187        default.base_url = env::var(ENV_NOMAD_BASE_URL).unwrap_or(default.base_url);
188        default.port = env::var(ENV_NOMAD_PORT).map_or(default.port, |port| {
189            port.parse::<u16>().unwrap_or(default.port)
190        });
191        default.api_version = env::var(ENV_NOMAD_API_VERSION).unwrap_or(default.api_version);
192        default.token = env::var(ENV_NOMAD_SECRET_TOKEN).map_or(default.token, Some);
193        default.allow_insecure_certs =
194            env::var(ENV_NOMAD_TLS_ALLOW_INSECURE).map_or(default.allow_insecure_certs, |value| {
195                value
196                    .parse::<bool>()
197                    .unwrap_or(default.allow_insecure_certs)
198            });
199
200        // MTLS
201        if let (Some(cert_path), Some(key_path)) = (
202            env::var(ENV_NOMAD_MTLS_CERT_PATH).ok(),
203            env::var(ENV_NOMAD_MTLS_KEY_PATH).ok(),
204        ) {
205            default.mtls = Some(MTLSConfig::new(cert_path, key_path));
206        }
207
208        default
209    }
210}
211
212impl Default for Config {
213    fn default() -> Self {
214        Config {
215            base_url: "http://localhost".into(),
216            port: 4646,
217            api_version: "v1".into(),
218            token: None,
219            mtls: None,
220            allow_insecure_certs: false,
221        }
222    }
223}
224
225#[derive(Error, Debug)]
226pub enum ClientError {
227    #[error("Error building the request: {0}")]
228    RequestError(String),
229    #[error("Response could not be deserialized: {0}")]
230    DeserializationError(String),
231    #[error("The api has returned an error: [{0}] '{1}'")]
232    ServerError(u16, String),
233    #[error("A network related error occurred: {0}")]
234    NetworkError(String),
235}