use crate::vault::VaultError;
use anyhow::Result;
use reqwest::Response;
use reqwest::{Client, StatusCode};
#[cfg(any(test, feature = "full-api"))]
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
use tokio::time::sleep;
use tracing::{info, warn};
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultStatusInfo {
pub initialized: bool,
pub sealed: bool,
pub standby: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultStatus {
#[serde(rename = "type")]
pub type_field: String,
pub initialized: bool,
pub sealed: bool,
pub t: u8,
pub n: u8,
pub progress: u8,
pub nonce: String,
pub version: String,
#[serde(rename = "build_date")]
pub build_date: String,
pub migration: bool,
#[serde(rename = "recovery_seal")]
pub recovery_seal: bool,
#[serde(rename = "storage_type")]
pub storage_type: String,
#[serde(
rename = "cluster_name",
default,
skip_serializing_if = "Option::is_none"
)]
pub cluster_name: Option<String>,
#[serde(
rename = "cluster_id",
default,
skip_serializing_if = "Option::is_none"
)]
pub cluster_id: Option<String>,
#[serde(default)]
pub standby: bool,
}
#[cfg(any(test, feature = "full-api"))]
pub type StatusResult = VaultStatusInfo;
pub async fn check_response(resp: Response) -> Result<Value, VaultError> {
let status = resp.status();
if status.is_success() {
if status == StatusCode::NO_CONTENT {
Ok(serde_json::json!({}))
} else {
Ok(resp.json().await?)
}
} else {
let body = resp.text().await.unwrap_or_default();
if let Ok(val) = serde_json::from_str::<Value>(&body) {
if let Some(errors) = val.get("errors").and_then(|v| v.as_array()) {
if !errors.is_empty() {
if let Some(msg) = errors[0].as_str() {
return Err(VaultError::Api(msg.to_string()));
}
}
}
}
Err(VaultError::HttpStatus(status.as_u16(), body))
}
}
#[cfg(any(test, feature = "full-api"))]
pub async fn process_response<T: DeserializeOwned>(response: Response) -> Result<T, VaultError> {
let status = response.status();
if status.is_success() {
if status == StatusCode::NO_CONTENT {
return Err(VaultError::Api("No content received".to_string()));
}
let response_body = response
.json::<T>()
.await
.map_err(|e| VaultError::Api(format!("JSON error: {}", e)))?;
Ok(response_body)
} else {
let body = response.text().await.unwrap_or_default();
if let Ok(val) = serde_json::from_str::<Value>(&body) {
if let Some(errors) = val.get("errors").and_then(|v| v.as_array()) {
if !errors.is_empty() {
if let Some(msg) = errors[0].as_str() {
return Err(VaultError::Api(msg.to_string()));
}
}
}
}
Err(VaultError::HttpStatus(status.as_u16(), body))
}
}
#[cfg(any(test, feature = "full-api"))]
pub fn check_success_response(response: &Response) -> Result<(), VaultError> {
let status = response.status();
if !status.is_success() {
return Err(VaultError::Api(format!("API error: {}", status)));
}
Ok(())
}
pub async fn auth_post(
client: &Client,
token: &str,
url: &str,
json_payload: Value,
) -> Result<reqwest::Response, reqwest::Error> {
client
.post(url)
.bearer_auth(token)
.json(&json_payload)
.send()
.await
}
pub async fn check_vault_status(vault_addr: &str) -> Result<VaultStatus> {
let client = reqwest::Client::new();
let url = format!("{}/v1/sys/seal-status", vault_addr);
let response = client.get(&url).send().await?;
if response.status().is_success() {
let status: VaultStatus = response.json().await?;
Ok(status)
} else if response.status() == reqwest::StatusCode::BAD_REQUEST {
let error_text = response.text().await?;
if error_text.contains("Vault is not initialized")
|| error_text.contains("not initialized")
|| error_text.contains("security barrier not initialized")
{
Ok(VaultStatus {
type_field: "shamir".to_string(),
initialized: false,
sealed: true,
t: 0,
n: 0,
progress: 0,
nonce: "".to_string(),
version: "".to_string(),
build_date: "".to_string(),
migration: false,
recovery_seal: false,
storage_type: "".to_string(),
cluster_name: None,
cluster_id: None,
standby: false,
})
} else {
Err(anyhow::anyhow!(
"Failed to get Vault status: {}",
error_text
))
}
} else {
let status = response.status();
let error_text = response.text().await?;
Err(anyhow::anyhow!(
"Failed to get Vault status: {} - {}",
status,
error_text
))
}
}
pub async fn wait_for_vault_unseal(addr: &str, timeout: Duration) -> Result<()> {
let start = std::time::Instant::now();
loop {
match check_vault_status(addr).await {
Ok(status) if status.initialized && !status.sealed => {
info!("Vault at {} is unsealed.", addr);
return Ok(());
}
Ok(status) => {
info!(
"Waiting for Vault at {} (initialized: {}, sealed: {})",
addr, status.initialized, status.sealed
);
}
Err(e) => warn!("Error checking Vault status at {}: {}", addr, e),
}
if start.elapsed() > timeout {
return Err(anyhow::anyhow!(
"Timed out waiting for Vault at {} to unseal",
addr
));
}
sleep(Duration::from_secs(2)).await;
}
}
pub async fn wait_for_vault_availability(addr: &str, timeout: Duration) -> Result<()> {
let start = std::time::Instant::now();
info!("Waiting for Vault at {} to become available...", addr);
loop {
let response = reqwest::get(format!("{}/v1/sys/health", addr)).await;
match response {
Ok(response) => {
match response.text().await {
Ok(body) => {
if body.contains("initialized") {
info!("Vault at {} is available (responding to API calls).", addr);
return Ok(());
}
info!("Vault responded but with unexpected content, continuing to wait...");
}
Err(_) => {
info!("Vault responded but couldn't read body, continuing to wait...");
}
}
}
Err(_) => {
info!("Waiting for Vault at {} to become available...", addr);
}
}
if start.elapsed() > timeout {
return Err(anyhow::anyhow!(
"Timed out waiting for Vault at {} to become available",
addr
));
}
sleep(Duration::from_secs(1)).await;
}
}