feat: add workshop-vault
Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
[submodule "rust-tongsuo"]
|
[submodule "rust-tongsuo"]
|
||||||
path = rust-tongsuo
|
path = rust-tongsuo
|
||||||
url = https://github.com/Tongsuo-Project/rust-tongsuo.git
|
url = https://github.com/Tongsuo-Project/rust-tongsuo.git
|
||||||
|
[submodule "RustyVault"]
|
||||||
|
path = RustyVault
|
||||||
|
url = https://github.com/Tongsuo-Project/RustyVault.git
|
||||||
|
|||||||
Submodule
+1
Submodule RustyVault added at 4efe033ce7
Generated
+4003
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "workshop-vault"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.100"
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
log = "0.4.28"
|
||||||
|
rusty_vault = "0.2.1"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.11.8"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use workshop_vault::*;
|
||||||
|
use env_logger;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
// 从环境变量创建客户端
|
||||||
|
let client = create_vault_client_from_env()?;
|
||||||
|
|
||||||
|
// 检查是否初始化 Vault
|
||||||
|
let initialized = client.is_initialized().await?;
|
||||||
|
info!("Vault initialized: {}", initialized);
|
||||||
|
|
||||||
|
// 创建 CI/CD 密钥管理器
|
||||||
|
let client = create_vault_client_from_env()?;
|
||||||
|
let secret_manager = WorkshopSecretManager::new(client, "my-ci-pipeline".to_string());
|
||||||
|
info!("Vault initialized: {}", initialized);
|
||||||
|
|
||||||
|
// 存储 CI/CD 变量
|
||||||
|
secret_manager.store_cicd_variable("DATABASE_URL", "postgres://user:pass@localhost/db").await?;
|
||||||
|
info!("Vault initialized: {}", initialized);
|
||||||
|
secret_manager.store_cicd_variable("API_SECRET", "super-secret-key").await?;
|
||||||
|
|
||||||
|
// 读取变量
|
||||||
|
let db_url = secret_manager.get_cicd_variable("DATABASE_URL").await?;
|
||||||
|
println!("Database URL: {}", db_url);
|
||||||
|
|
||||||
|
// 存储密钥文件
|
||||||
|
secret_manager.store_secret_file("id_rsa", "-----BEGIN PRIVATE KEY-----\n...").await?;
|
||||||
|
|
||||||
|
// 列出所有变量
|
||||||
|
let variables = secret_manager.list_cicd_variables().await?;
|
||||||
|
println!("Available variables: {:?}", variables);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use crate::models::*;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait VaultClient: Send + Sync {
|
||||||
|
/// 检查 Vault 是否已初始化
|
||||||
|
async fn is_initialized(&self) -> Result<bool>;
|
||||||
|
|
||||||
|
/// 获取健康状态
|
||||||
|
async fn health(&self) -> Result<HealthStatus>;
|
||||||
|
|
||||||
|
/// 写入密钥
|
||||||
|
async fn write_secret(&self, path: &str, data: HashMap<String, String>) -> Result<()>;
|
||||||
|
|
||||||
|
/// 读取密钥
|
||||||
|
async fn read_secret(&self, path: &str) -> Result<SecretData>;
|
||||||
|
|
||||||
|
/// 列出路径下的所有密钥
|
||||||
|
async fn list_secrets(&self, path: &str) -> Result<Vec<String>>;
|
||||||
|
|
||||||
|
/// 删除密钥
|
||||||
|
async fn delete_secret(&self, path: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 检查密钥是否存在
|
||||||
|
async fn secret_exists(&self, path: &str) -> Result<bool>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum VaultError {
|
||||||
|
#[error("Vault not initialized")]
|
||||||
|
NotInitialized,
|
||||||
|
|
||||||
|
#[error("Vault operation failed: {0}")]
|
||||||
|
OperationFailed(String),
|
||||||
|
|
||||||
|
#[error("Authentication failed: {0}")]
|
||||||
|
AuthFailed(String),
|
||||||
|
|
||||||
|
#[error("Network error: {0}")]
|
||||||
|
NetworkError(String),
|
||||||
|
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
SerializationError(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Vault error: {0}")]
|
||||||
|
RvError(#[from] rusty_vault::errors::RvError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, VaultError>;
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
pub mod error;
|
||||||
|
pub mod models;
|
||||||
|
pub mod client;
|
||||||
|
pub mod rusty_vault_client;
|
||||||
|
|
||||||
|
pub use error::{VaultError, Result};
|
||||||
|
pub use models::*;
|
||||||
|
pub use client::VaultClient;
|
||||||
|
pub use rusty_vault_client::RustyVaultClient;
|
||||||
|
|
||||||
|
use rusty_vault::api::client::TLSConfig;
|
||||||
|
|
||||||
|
/// 创建默认的 Vault 客户端
|
||||||
|
pub fn create_vault_client(
|
||||||
|
address: Option<String>,
|
||||||
|
token: Option<String>,
|
||||||
|
secret_mount: Option<String>,
|
||||||
|
) -> Result<RustyVaultClient> {
|
||||||
|
let address = address.unwrap_or_else(|| "https://127.0.0.1:8200".to_string());
|
||||||
|
let token = token.ok_or_else(|| VaultError::AuthFailed("Vault token is required".to_string()))?;
|
||||||
|
|
||||||
|
Ok(RustyVaultClient::new(address, token, secret_mount, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从环境变量创建 Vault 客户端
|
||||||
|
pub fn create_vault_client_from_env() -> Result<RustyVaultClient> {
|
||||||
|
RustyVaultClient::from_env()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建带 TLS 的 Vault 客户端
|
||||||
|
pub fn create_vault_client_with_tls(
|
||||||
|
address: String,
|
||||||
|
token: String,
|
||||||
|
secret_mount: Option<String>,
|
||||||
|
tls_config: TLSConfig,
|
||||||
|
) -> RustyVaultClient {
|
||||||
|
RustyVaultClient::new(address, token, secret_mount, Some(tls_config))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CI/CD 密钥管理器的便捷接口
|
||||||
|
pub struct WorkshopSecretManager<T: VaultClient> {
|
||||||
|
client: T,
|
||||||
|
namespace: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: VaultClient> WorkshopSecretManager<T> {
|
||||||
|
pub fn new(client: T, namespace: String) -> Self {
|
||||||
|
Self { client, namespace }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 存储 CI/CD 变量
|
||||||
|
pub async fn store_cicd_variable(&self, key: &str, value: &str) -> Result<()> {
|
||||||
|
let path = format!("{}/variables/{}", self.namespace, key);
|
||||||
|
let mut data = std::collections::HashMap::new();
|
||||||
|
data.insert("value".to_string(), value.to_string());
|
||||||
|
|
||||||
|
self.client.write_secret(&path, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取 CI/CD 变量
|
||||||
|
pub async fn get_cicd_variable(&self, key: &str) -> Result<String> {
|
||||||
|
let path = format!("{}/variables/{}", self.namespace, key);
|
||||||
|
let secret = self.client.read_secret(&path).await?;
|
||||||
|
|
||||||
|
secret.data.get("value")
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| VaultError::OperationFailed("Variable not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 存储密钥文件内容
|
||||||
|
pub async fn store_secret_file(&self, filename: &str, content: &str) -> Result<()> {
|
||||||
|
let path = format!("{}/files/{}", self.namespace, filename);
|
||||||
|
let mut data = std::collections::HashMap::new();
|
||||||
|
data.insert("content".to_string(), content.to_string());
|
||||||
|
|
||||||
|
self.client.write_secret(&path, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取密钥文件内容
|
||||||
|
pub async fn get_secret_file(&self, filename: &str) -> Result<String> {
|
||||||
|
let path = format!("{}/files/{}", self.namespace, filename);
|
||||||
|
let secret = self.client.read_secret(&path).await?;
|
||||||
|
|
||||||
|
secret.data.get("content")
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| VaultError::OperationFailed("File not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出所有 CI/CD 变量
|
||||||
|
pub async fn list_cicd_variables(&self) -> Result<Vec<String>> {
|
||||||
|
let path = format!("{}/variables", self.namespace);
|
||||||
|
self.client.list_secrets(&path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除 CI/CD 变量
|
||||||
|
pub async fn delete_cicd_variable(&self, key: &str) -> Result<()> {
|
||||||
|
let path = format!("{}/variables/{}", self.namespace, key);
|
||||||
|
self.client.delete_secret(&path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// 注意:这些测试需要实际的 Vault 实例,因此标记为忽略
|
||||||
|
// 在实际使用中应该使用模拟的测试
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_vault_operations() {
|
||||||
|
let client = create_vault_client_from_env().unwrap();
|
||||||
|
let manager = WorkshopSecretManager::new(client, "test-ci".to_string());
|
||||||
|
|
||||||
|
// 测试存储和读取变量
|
||||||
|
manager.store_cicd_variable("API_KEY", "test-value").await.unwrap();
|
||||||
|
let value = manager.get_cicd_variable("API_KEY").await.unwrap();
|
||||||
|
assert_eq!(value, "test-value");
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
manager.delete_cicd_variable("API_KEY").await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Vault 初始化配置
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VaultInitConfig {
|
||||||
|
pub secret_shares: usize,
|
||||||
|
pub secret_threshold: usize,
|
||||||
|
pub pgp_keys: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vault 初始化响应
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VaultInitResponse {
|
||||||
|
pub keys: Vec<String>,
|
||||||
|
pub keys_base64: Vec<String>,
|
||||||
|
pub root_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 密钥数据
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SecretData {
|
||||||
|
pub data: HashMap<String, String>,
|
||||||
|
pub metadata: Option<SecretMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 密钥元数据
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SecretMetadata {
|
||||||
|
pub created_time: Option<String>,
|
||||||
|
pub deletion_time: Option<String>,
|
||||||
|
pub destroyed: Option<bool>,
|
||||||
|
pub version: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出密钥的响应
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ListSecretsResponse {
|
||||||
|
pub keys: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入密钥的请求
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WriteSecretRequest {
|
||||||
|
pub data: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 健康状态
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct HealthStatus {
|
||||||
|
pub initialized: bool,
|
||||||
|
pub sealed: bool,
|
||||||
|
pub standby: bool,
|
||||||
|
pub performance_standby: bool,
|
||||||
|
pub replication_performance_mode: String,
|
||||||
|
pub replication_dr_mode: String,
|
||||||
|
pub server_time_utc: u64,
|
||||||
|
pub version: String,
|
||||||
|
pub cluster_name: String,
|
||||||
|
pub cluster_id: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use rusty_vault::api::client::{Client, TLSConfig, TLSConfigBuilder};
|
||||||
|
use rusty_vault::api::HttpResponse;
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::client::VaultClient;
|
||||||
|
use crate::error::{Result, VaultError};
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
/// RustyVault 客户端实现
|
||||||
|
pub struct RustyVaultClient {
|
||||||
|
client: Client,
|
||||||
|
secret_mount: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustyVaultClient {
|
||||||
|
pub fn new(
|
||||||
|
address: String,
|
||||||
|
token: String,
|
||||||
|
secret_mount: Option<String>,
|
||||||
|
tls_config: Option<TLSConfig>,
|
||||||
|
) -> Self {
|
||||||
|
let mut client = Client::new()
|
||||||
|
.with_addr(&address)
|
||||||
|
.with_token(&token);
|
||||||
|
|
||||||
|
if let Some(tls_config) = tls_config {
|
||||||
|
client = client.with_tls_config(tls_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = client.build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
secret_mount: secret_mount.unwrap_or_else(|| "secret".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从环境变量创建客户端
|
||||||
|
pub fn from_env() -> Result<Self> {
|
||||||
|
let address = std::env::var("VAULT_ADDR")
|
||||||
|
.unwrap_or_else(|_| "https://127.0.0.1:8200".to_string());
|
||||||
|
|
||||||
|
let token = std::env::var("VAULT_TOKEN")
|
||||||
|
.map_err(|_| VaultError::AuthFailed("VAULT_TOKEN environment variable not set".to_string()))?;
|
||||||
|
|
||||||
|
let secret_mount = std::env::var("VAULT_SECRET_MOUNT").ok();
|
||||||
|
|
||||||
|
Ok(Self::new(address, token, secret_mount, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建 TLS 配置
|
||||||
|
pub fn build_tls_config(
|
||||||
|
ca_cert_path: Option<String>,
|
||||||
|
client_cert_path: Option<String>,
|
||||||
|
client_key_path: Option<String>,
|
||||||
|
insecure: bool,
|
||||||
|
) -> Result<TLSConfig> {
|
||||||
|
let mut builder = TLSConfigBuilder::new().with_insecure(insecure);
|
||||||
|
|
||||||
|
if let Some(ca_path) = ca_cert_path {
|
||||||
|
builder = builder.with_server_ca_path(&std::path::PathBuf::from(ca_path))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(cert_path), Some(key_path)) = (client_cert_path, client_key_path) {
|
||||||
|
builder = builder.with_client_cert_path(
|
||||||
|
&std::path::PathBuf::from(cert_path),
|
||||||
|
&std::path::PathBuf::from(key_path),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build().map_err(|e| VaultError::OperationFailed(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_secret_path(&self, path: &str) -> String {
|
||||||
|
format!("/v1/{}/data/{}", self.secret_mount, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_secret_metadata_path(&self, path: &str) -> String {
|
||||||
|
format!("/v1/{}/metadata/{}", self.secret_mount, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response<T: serde::de::DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
response: HttpResponse,
|
||||||
|
) -> Result<T> {
|
||||||
|
if response.response_status >= 400 {
|
||||||
|
let error_msg = if let Some(data) = response.response_data {
|
||||||
|
serde_json::to_string(&data).unwrap_or_else(|_| "Unknown error".to_string())
|
||||||
|
} else {
|
||||||
|
format!("HTTP {}: {}", response.response_status, response.url)
|
||||||
|
};
|
||||||
|
return Err(VaultError::OperationFailed(error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
match response.response_data {
|
||||||
|
Some(data) => {
|
||||||
|
serde_json::from_value(data).map_err(VaultError::SerializationError)
|
||||||
|
}
|
||||||
|
None => Err(VaultError::OperationFailed("No response data".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl VaultClient for RustyVaultClient {
|
||||||
|
async fn is_initialized(&self) -> Result<bool> {
|
||||||
|
let response = self.client.request_get("/v1/sys/init")?;
|
||||||
|
let data: Map<String, Value> = self.handle_response(response)?;
|
||||||
|
|
||||||
|
data.get("initialized")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.ok_or_else(|| VaultError::OperationFailed("Invalid response format".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health(&self) -> Result<HealthStatus> {
|
||||||
|
let response = self.client.request_get("/v1/sys/health")?;
|
||||||
|
self.handle_response(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_secret(&self, path: &str, data: HashMap<String, String>) -> Result<()> {
|
||||||
|
let secret_path = self.build_secret_path(path);
|
||||||
|
|
||||||
|
let mut request_data = Map::new();
|
||||||
|
let mut data_map = Map::new();
|
||||||
|
|
||||||
|
for (key, value) in data {
|
||||||
|
data_map.insert(key, Value::from(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
request_data.insert("data".to_string(), Value::from(data_map));
|
||||||
|
|
||||||
|
let response = self.client.request_put(&secret_path, Some(request_data))?;
|
||||||
|
|
||||||
|
if response.response_status >= 400 {
|
||||||
|
let error_msg = if let Some(data) = response.response_data {
|
||||||
|
serde_json::to_string(&data).unwrap_or_else(|_| "Unknown error".to_string())
|
||||||
|
} else {
|
||||||
|
format!("HTTP {}: {}", response.response_status, response.url)
|
||||||
|
};
|
||||||
|
return Err(VaultError::OperationFailed(error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_secret(&self, path: &str) -> Result<SecretData> {
|
||||||
|
let secret_path = self.build_secret_path(path);
|
||||||
|
let response = self.client.request_get(&secret_path)?;
|
||||||
|
|
||||||
|
let data: Map<String, Value> = self.handle_response(response)?;
|
||||||
|
|
||||||
|
// 提取 data 字段
|
||||||
|
let secret_data = data.get("data")
|
||||||
|
.and_then(|v| v.get("data"))
|
||||||
|
.ok_or_else(|| VaultError::OperationFailed("Invalid secret data format".to_string()))?;
|
||||||
|
|
||||||
|
let metadata = data.get("data")
|
||||||
|
.and_then(|v| v.get("metadata"))
|
||||||
|
.and_then(|v| serde_json::from_value(v.clone()).ok());
|
||||||
|
|
||||||
|
let data_map: HashMap<String, String> = serde_json::from_value(secret_data.clone())?;
|
||||||
|
|
||||||
|
Ok(SecretData {
|
||||||
|
data: data_map,
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_secrets(&self, path: &str) -> Result<Vec<String>> {
|
||||||
|
let list_path = self.build_secret_metadata_path(path);
|
||||||
|
let response = self.client.request_list(&list_path)?;
|
||||||
|
|
||||||
|
let data: Map<String, Value> = self.handle_response(response)?;
|
||||||
|
|
||||||
|
data.get("data")
|
||||||
|
.and_then(|v| v.get("keys"))
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.ok_or_else(|| VaultError::OperationFailed("Invalid list response format".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_secret(&self, path: &str) -> Result<()> {
|
||||||
|
let secret_path = self.build_secret_metadata_path(path);
|
||||||
|
let response = self.client.request_delete(&secret_path, None)?;
|
||||||
|
|
||||||
|
if response.response_status >= 400 {
|
||||||
|
let error_msg = if let Some(data) = response.response_data {
|
||||||
|
serde_json::to_string(&data).unwrap_or_else(|_| "Unknown error".to_string())
|
||||||
|
} else {
|
||||||
|
format!("HTTP {}: {}", response.response_status, response.url)
|
||||||
|
};
|
||||||
|
return Err(VaultError::OperationFailed(error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn secret_exists(&self, path: &str) -> Result<bool> {
|
||||||
|
match self.read_secret(path).await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(VaultError::OperationFailed(msg)) if msg.contains("404") => Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user