extern crate core;
extern crate serde;
extern crate serde_json;
extern crate url;
use std::env;
use std::fs::File;
use std::io::{Error, Read};
use reqwest::{Client, Identity, Method, RequestBuilder};
use serde::de::DeserializeOwned;
use thiserror::Error;
use crate::api::path_combine;
pub mod api;
mod extensions;
pub mod models;
static ENV_NOMAD_BASE_URL: &str = "NOMAD_BASE_URL";
static ENV_NOMAD_PORT: &str = "NOMAD_PORT";
static ENV_NOMAD_API_VERSION: &str = "NOMAD_API_VERSION";
static ENV_NOMAD_SECRET_TOKEN: &str = "NOMAD_SECRET_TOKEN";
static ENV_NOMAD_TLS_ALLOW_INSECURE: &str = "NOMAD_TLS_ALLOW_INSECURE";
static ENV_NOMAD_MTLS_CERT_PATH: &str = "NOMAD_MTLS_CERT_PATH";
static ENV_NOMAD_MTLS_KEY_PATH: &str = "NOMAD_MTLS_KEY_PATH";
#[derive(Clone, Debug)]
pub struct NomadClient {
client: Client,
config: Config,
}
impl NomadClient {
pub fn new(config: Config) -> Self {
let builder = Client::builder()
.use_rustls_tls()
.danger_accept_invalid_certs(config.allow_insecure_certs);
let client = match &config.mtls {
Some(mtls_config) => {
let certs = mtls_config.load_certs().expect("Certs should be readable");
let pkcs12 = Identity::from_pem(&certs).expect("Certs should be parseable");
builder
.identity(pkcs12)
.build()
.expect("Http client should be buildable")
}
None => builder.build().expect("Http client should be buildable"),
};
NomadClient { client, config }
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn config_mut(&mut self) -> &mut Config {
&mut self.config
}
pub fn get_base_url(&self) -> String {
format!(
"{}:{}/{}",
self.config.base_url, self.config.port, self.config.api_version
)
}
pub fn get_endpoint(&self, endpoint: &str) -> String {
path_combine(self.get_base_url().as_str(), endpoint)
}
pub fn request(&self, method: Method, endpoint: &str) -> RequestBuilder {
self.client.request(method, self.get_endpoint(endpoint))
}
async fn send_plain(&self, mut req: RequestBuilder) -> Result<String, ClientError> {
if let Some(token) = self.config.token.as_deref() {
req = req.header::<String, String>("X-Nomad-Token".into(), token.to_string());
}
let req_result = req.build();
if let Err(error) = req_result {
return Err(ClientError::RequestError(error.to_string()));
}
let req = req_result.unwrap();
match self.client.execute(req).await {
Ok(response) => {
let status = response.status();
match response.text().await {
Ok(body) => {
if status.is_success() {
Ok(body)
} else {
Err(ClientError::ServerError(status.as_u16(), body))
}
}
Err(err) => Err(ClientError::NetworkError(err.to_string())),
}
}
Err(err) => Err(ClientError::NetworkError(err.to_string())),
}
}
async fn send<TResponse: DeserializeOwned>(
&self,
mut req: RequestBuilder,
) -> Result<TResponse, ClientError> {
if let Some(token) = self.config.token.as_deref() {
req = req.header::<String, String>("X-Nomad-Token".into(), token.to_string());
}
let req_result = req.build();
if let Err(error) = req_result {
return Err(ClientError::RequestError(error.to_string()));
}
let req = req_result.unwrap();
match self.client.execute(req).await {
Ok(response) => {
let status = response.status();
if status.is_success() {
match response.json::<TResponse>().await {
Ok(body) => Ok(body),
Err(err) => Err(ClientError::DeserializationError(err.to_string())),
}
} else {
match response.text().await {
Ok(body) => Err(ClientError::ServerError(status.as_u16(), body)),
Err(err) => Err(ClientError::NetworkError(err.to_string())),
}
}
}
Err(err) => Err(ClientError::NetworkError(err.to_string())),
}
}
}
impl Default for NomadClient {
fn default() -> Self {
NomadClient::new(Config::from_env())
}
}
#[derive(Clone, Debug)]
pub struct Config {
pub base_url: String,
pub port: u16,
pub api_version: String,
pub token: Option<String>,
pub allow_insecure_certs: bool,
pub mtls: Option<MTLSConfig>,
}
#[derive(Clone, Debug)]
pub struct MTLSConfig {
pub cert_path: String,
pub key_path: String,
}
impl MTLSConfig {
pub fn new(cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
MTLSConfig {
cert_path: cert_path.into(),
key_path: key_path.into(),
}
}
pub fn load_certs(&self) -> Result<Vec<u8>, Error> {
let mut buffer = Vec::new();
File::open(&self.cert_path)?.read_to_end(&mut buffer)?;
File::open(&self.key_path)?.read_to_end(&mut buffer)?;
Ok(buffer)
}
}
impl Config {
pub fn from_env() -> Config {
let mut default = Config::default();
default.base_url = env::var(ENV_NOMAD_BASE_URL).unwrap_or(default.base_url);
default.port = env::var(ENV_NOMAD_PORT).map_or(default.port, |port| {
port.parse::<u16>().unwrap_or(default.port)
});
default.api_version = env::var(ENV_NOMAD_API_VERSION).unwrap_or(default.api_version);
default.token = env::var(ENV_NOMAD_SECRET_TOKEN).map_or(default.token, Some);
default.allow_insecure_certs =
env::var(ENV_NOMAD_TLS_ALLOW_INSECURE).map_or(default.allow_insecure_certs, |value| {
value
.parse::<bool>()
.unwrap_or(default.allow_insecure_certs)
});
if let (Some(cert_path), Some(key_path)) = (
env::var(ENV_NOMAD_MTLS_CERT_PATH).ok(),
env::var(ENV_NOMAD_MTLS_KEY_PATH).ok(),
) {
default.mtls = Some(MTLSConfig::new(cert_path, key_path));
}
default
}
}
impl Default for Config {
fn default() -> Self {
Config {
base_url: "http://localhost".into(),
port: 4646,
api_version: "v1".into(),
token: None,
mtls: None,
allow_insecure_certs: false,
}
}
}
#[derive(Error, Debug)]
pub enum ClientError {
#[error("Error building the request: {0}")]
RequestError(String),
#[error("Response could not be deserialized: {0}")]
DeserializationError(String),
#[error("The api has returned an error: [{0}] '{1}'")]
ServerError(u16, String),
#[error("A network related error occurred: {0}")]
NetworkError(String),
}