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"]
|
||||
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
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