feat: add workshop-vault

Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
This commit is contained in:
Catty Steve
2025-11-09 22:55:18 +08:00
parent 004df79e66
commit 49aa1dc36c
10 changed files with 4508 additions and 0 deletions
+3
View File
@@ -1,3 +1,6 @@
[submodule "rust-tongsuo"]
path = rust-tongsuo
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
+4003
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -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"
+37
View File
@@ -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(())
}
+28
View File
@@ -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>;
}
+24
View File
@@ -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>;
+123
View File
@@ -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();
}
}
+61
View File
@@ -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,
}
+211
View File
@@ -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),
}
}
}