use anyhow::Context;
use reqwest::header::{HeaderMap, HeaderValue};
use super::wire::format_request_as_curl;
#[derive(Debug)]
pub struct AuthServerUnreachable {
pub url: String,
}
impl std::fmt::Display for AuthServerUnreachable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"cannot reach manta server at {} for authentication. \
Is the server running, and is `manta_server_url` in your \
config correct?",
self.url,
)
}
}
impl std::error::Error for AuthServerUnreachable {}
pub trait OpenApiResultExt<T> {
fn into_anyhow(self) -> anyhow::Result<T>;
}
impl<T, E> OpenApiResultExt<T>
for Result<progenitor_client::ResponseValue<T>, progenitor_client::Error<E>>
where
E: std::fmt::Debug + serde::Serialize,
{
fn into_anyhow(self) -> anyhow::Result<T> {
match self {
Ok(rv) => Ok(rv.into_inner()),
Err(progenitor_client::Error::ErrorResponse(rv)) => {
let status = rv.status();
let inner = rv.into_inner();
let msg = serde_json::to_value(&inner)
.ok()
.and_then(|v| {
v.get("error").and_then(|e| e.as_str()).map(String::from)
})
.unwrap_or_else(|| format!("{inner:?}"));
Err(anyhow::anyhow!("HTTP {}: {msg}", status.as_u16()))
}
Err(e) => Err(anyhow::anyhow!("{}", e)),
}
}
}
#[derive(Debug)]
pub struct MantaClient {
pub openapi: crate::openapi_client::Client,
pub site_name: String,
pub raw: reqwest::Client,
pub token: Option<String>,
pub base_url: String,
}
impl MantaClient {
pub fn new(server_url: &str, site_name: &str) -> anyhow::Result<Self> {
Self::new_with_timeout(server_url, site_name, None, None)
}
pub fn from_app_ctx(
ctx: &crate::common::app_context::AppContext<'_>,
token: Option<&str>,
) -> anyhow::Result<Self> {
Self::new_with_timeout(
ctx.manta_server_url,
ctx.site_name,
ctx.request_timeout_secs,
token,
)
}
pub fn new_with_timeout(
server_url: &str,
site_name: &str,
timeout_secs: Option<u64>,
token: Option<&str>,
) -> anyhow::Result<Self> {
let normalized = if server_url.starts_with("http://")
|| server_url.starts_with("https://")
{
server_url.to_owned()
} else {
format!("http://{server_url}")
};
let base_url = format!("{}/api/v1", normalized.trim_end_matches('/'));
let mut default_headers = HeaderMap::new();
if let Some(t) = token {
let mut bearer = HeaderValue::from_str(&format!("Bearer {t}"))
.context("token contained non-ASCII characters; cannot build Authorization header")?;
bearer.set_sensitive(true);
default_headers.insert(reqwest::header::AUTHORIZATION, bearer);
}
let mut builder = reqwest::Client::builder().default_headers(default_headers);
if let Some(secs) = timeout_secs {
builder = builder.timeout(std::time::Duration::from_secs(secs));
}
let raw = builder.build().context("Failed to build HTTP client")?;
let openapi =
crate::openapi_client::Client::new_with_client(&base_url, raw.clone());
Ok(Self {
openapi,
site_name: site_name.to_owned(),
raw,
token: token.map(str::to_owned),
base_url,
})
}
pub fn site_name(&self) -> &str {
&self.site_name
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub(super) fn log_request_as_curl(builder: &reqwest::RequestBuilder) {
if !tracing::enabled!(tracing::Level::DEBUG) {
return;
}
let Some(cloned) = builder.try_clone() else {
return;
};
let Ok(req) = cloned.build() else {
return;
};
tracing::debug!(
"curl equivalent (secrets replaced with <REDACTED>):\n{}",
format_request_as_curl(&req)
);
}
pub(super) fn unreachable_server_msg(&self) -> String {
let server_url = self.base_url.trim_end_matches("/api/v1");
format!(
"cannot reach manta server at {server_url}. Is the server \
running, and is `manta_server_url` in your config correct?"
)
}
}
pub(super) fn unwrap_error_body(body: &str) -> String {
#[derive(serde::Deserialize)]
struct ServerErrorBody {
error: String,
}
serde_json::from_str::<ServerErrorBody>(body)
.map(|e| e.error)
.unwrap_or_else(|_| body.to_string())
}
#[cfg(test)]
mod tests {
use super::unwrap_error_body;
#[test]
fn unwrap_error_body_extracts_error_field_from_standard_body() {
let body = r#"{"error":"Can't access HSM group 'compute-2'."}"#;
assert_eq!(
unwrap_error_body(body),
"Can't access HSM group 'compute-2'."
);
}
#[test]
fn unwrap_error_body_falls_back_to_raw_for_non_json() {
let body = "Method Not Allowed";
assert_eq!(unwrap_error_body(body), "Method Not Allowed");
}
#[test]
fn unwrap_error_body_falls_back_to_raw_for_json_without_error_field() {
let body = r#"{"detail":"something"}"#;
assert_eq!(unwrap_error_body(body), body);
}
#[test]
fn unwrap_error_body_falls_back_to_raw_for_empty_body() {
assert_eq!(unwrap_error_body(""), "");
}
}