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 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}