Add vault CLI commands for secret migration
Some checks failed
GBCI Bundle / build-bundle (push) Has been skipped
GBCI / build (push) Failing after 24m51s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-18 09:40:16 -03:00
parent 55faf55e08
commit f8ac224d00

View file

@ -1,18 +1,17 @@
use crate::core::secrets::{SecretPaths, SecretsManager};
use crate::package_manager::{get_all_components, InstallMode, PackageManager};
use anyhow::Result;
use std::collections::HashMap;
use std::env;
use std::process::Command;
pub async fn run() -> Result<()> {
// Logger is already initialized in main.rs, don't initialize again
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
print_usage();
return Ok(());
}
use tracing::info;
fn print_usage() {
info!("usage: botserver <command> [options]")
}
let command = &args[1];
match command.as_str() {
"start" => {
@ -164,6 +163,47 @@ pub async fn run() -> Result<()> {
println!("x Component '{}' is not installed", component);
}
}
"vault" => {
if args.len() < 3 {
print_vault_usage();
return Ok(());
}
let subcommand = &args[2];
match subcommand.as_str() {
"migrate" => {
let env_file = args.get(3).map(|s| s.as_str()).unwrap_or(".env");
vault_migrate(env_file).await?;
}
"put" => {
if args.len() < 5 {
eprintln!("Usage: botserver vault put <path> <key=value> [key=value...]");
return Ok(());
}
let path = &args[3];
let kvs: Vec<&str> = args[4..].iter().map(|s| s.as_str()).collect();
vault_put(path, &kvs).await?;
}
"get" => {
if args.len() < 4 {
eprintln!("Usage: botserver vault get <path> [key]");
return Ok(());
}
let path = &args[3];
let key = args.get(4).map(|s| s.as_str());
vault_get(path, key).await?;
}
"list" => {
vault_list().await?;
}
"health" => {
vault_health().await?;
}
_ => {
eprintln!("Unknown vault command: {}", subcommand);
print_vault_usage();
}
}
}
"--help" | "-h" => {
print_usage();
}
@ -174,3 +214,362 @@ pub async fn run() -> Result<()> {
}
Ok(())
}
fn print_usage() {
println!("BotServer CLI");
println!();
println!("Usage: botserver <command> [options]");
println!();
println!("Commands:");
println!(" install <component> Install a component");
println!(" remove <component> Remove a component");
println!(" list List all components");
println!(" status <component> Check component status");
println!(" start Start all installed components");
println!(" stop Stop all components");
println!(" restart Restart all components");
println!(" vault <subcommand> Manage Vault secrets");
println!(" --help, -h Show this help");
println!();
println!("Options:");
println!(" --container Use container mode (LXC)");
println!(" --tenant <name> Specify tenant name");
println!();
println!("Vault subcommands:");
println!(" vault migrate [.env] Migrate .env secrets to Vault");
println!(" vault put <path> k=v Store secrets in Vault");
println!(" vault get <path> Get secrets from Vault");
println!(" vault list List all secret paths");
println!(" vault health Check Vault health");
}
fn print_vault_usage() {
println!("Vault Secret Management");
println!();
println!("Usage: botserver vault <subcommand> [options]");
println!();
println!("Subcommands:");
println!(" migrate [file] Migrate secrets from .env to Vault");
println!(" Default file: .env");
println!();
println!(" put <path> k=v... Store key-value pairs at path");
println!(" Example: botserver vault put gbo/email user=x pass=y");
println!();
println!(" get <path> [key] Get all secrets or specific key from path");
println!(" Example: botserver vault get gbo/tables password");
println!();
println!(" list List all configured secret paths");
println!();
println!(" health Check Vault connection status");
println!();
println!("Secret paths:");
println!(" gbo/tables Database credentials");
println!(" gbo/drive S3/MinIO credentials");
println!(" gbo/email SMTP credentials");
println!(" gbo/cache Redis credentials");
println!(" gbo/directory Zitadel credentials");
println!(" gbo/llm AI API keys");
println!(" gbo/encryption Encryption keys");
println!(" gbo/stripe Payment credentials");
println!(" gbo/vectordb Qdrant credentials");
println!();
println!("Environment:");
println!(" VAULT_ADDR Vault server address");
println!(" VAULT_TOKEN Vault authentication token");
}
async fn vault_migrate(env_file: &str) -> Result<()> {
println!("Migrating secrets from {} to Vault...", env_file);
// Load .env file
let content = std::fs::read_to_string(env_file)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", env_file, e))?;
// Parse env vars
let mut env_vars: HashMap<String, String> = HashMap::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
env_vars.insert(key.trim().to_string(), value.trim().to_string());
}
}
// Create SecretsManager
let manager = SecretsManager::from_env()?;
if !manager.is_enabled() {
return Err(anyhow::anyhow!(
"Vault not configured. Set VAULT_ADDR and VAULT_TOKEN"
));
}
// Migrate tables/database
let mut tables: HashMap<String, String> = HashMap::new();
if let Some(v) = env_vars.get("TABLES_SERVER") {
tables.insert("host".into(), v.clone());
}
if let Some(v) = env_vars.get("TABLES_PORT") {
tables.insert("port".into(), v.clone());
}
if let Some(v) = env_vars.get("TABLES_DATABASE") {
tables.insert("database".into(), v.clone());
}
if let Some(v) = env_vars.get("TABLES_USERNAME") {
tables.insert("username".into(), v.clone());
}
if let Some(v) = env_vars.get("TABLES_PASSWORD") {
tables.insert("password".into(), v.clone());
}
if !tables.is_empty() {
manager.put_secret(SecretPaths::TABLES, tables).await?;
println!(" * Migrated tables credentials");
}
// Migrate custom database
let mut custom: HashMap<String, String> = HashMap::new();
if let Some(v) = env_vars.get("CUSTOM_SERVER") {
custom.insert("host".into(), v.clone());
}
if let Some(v) = env_vars.get("CUSTOM_PORT") {
custom.insert("port".into(), v.clone());
}
if let Some(v) = env_vars.get("CUSTOM_DATABASE") {
custom.insert("database".into(), v.clone());
}
if let Some(v) = env_vars.get("CUSTOM_USERNAME") {
custom.insert("username".into(), v.clone());
}
if let Some(v) = env_vars.get("CUSTOM_PASSWORD") {
custom.insert("password".into(), v.clone());
}
if !custom.is_empty() {
manager.put_secret("gbo/custom", custom).await?;
println!(" * Migrated custom database credentials");
}
// Migrate drive/S3
let mut drive: HashMap<String, String> = HashMap::new();
if let Some(v) = env_vars.get("DRIVE_SERVER") {
drive.insert("server".into(), v.clone());
}
if let Some(v) = env_vars.get("DRIVE_PORT") {
drive.insert("port".into(), v.clone());
}
if let Some(v) = env_vars.get("DRIVE_USE_SSL") {
drive.insert("use_ssl".into(), v.clone());
}
if let Some(v) = env_vars.get("DRIVE_ACCESSKEY") {
drive.insert("accesskey".into(), v.clone());
}
if let Some(v) = env_vars.get("DRIVE_SECRET") {
drive.insert("secret".into(), v.clone());
}
if let Some(v) = env_vars.get("DRIVE_ORG_PREFIX") {
drive.insert("org_prefix".into(), v.clone());
}
if !drive.is_empty() {
manager.put_secret(SecretPaths::DRIVE, drive).await?;
println!(" * Migrated drive credentials");
}
// Migrate email
let mut email: HashMap<String, String> = HashMap::new();
if let Some(v) = env_vars.get("EMAIL_FROM") {
email.insert("from".into(), v.clone());
}
if let Some(v) = env_vars.get("EMAIL_SERVER") {
email.insert("server".into(), v.clone());
}
if let Some(v) = env_vars.get("EMAIL_PORT") {
email.insert("port".into(), v.clone());
}
if let Some(v) = env_vars.get("EMAIL_USER") {
email.insert("username".into(), v.clone());
}
if let Some(v) = env_vars.get("EMAIL_PASS") {
email.insert("password".into(), v.clone());
}
if let Some(v) = env_vars.get("EMAIL_REJECT_UNAUTHORIZED") {
email.insert("reject_unauthorized".into(), v.clone());
}
if !email.is_empty() {
manager.put_secret(SecretPaths::EMAIL, email).await?;
println!(" * Migrated email credentials");
}
// Migrate Stripe
let mut stripe: HashMap<String, String> = HashMap::new();
if let Some(v) = env_vars.get("STRIPE_SECRET_KEY") {
stripe.insert("secret_key".into(), v.clone());
}
if let Some(v) = env_vars.get("STRIPE_PROFESSIONAL_PLAN_PRICE_ID") {
stripe.insert("professional_plan_price_id".into(), v.clone());
}
if let Some(v) = env_vars.get("STRIPE_PERSONAL_PLAN_PRICE_ID") {
stripe.insert("personal_plan_price_id".into(), v.clone());
}
if !stripe.is_empty() {
manager.put_secret("gbo/stripe", stripe).await?;
println!(" * Migrated stripe credentials");
}
// Migrate AI/LLM
let mut llm: HashMap<String, String> = HashMap::new();
if let Some(v) = env_vars.get("AI_KEY") {
llm.insert("api_key".into(), v.clone());
}
if let Some(v) = env_vars.get("AI_LLM_MODEL") {
llm.insert("model".into(), v.clone());
}
if let Some(v) = env_vars.get("AI_ENDPOINT") {
llm.insert("endpoint".into(), v.clone());
}
if let Some(v) = env_vars.get("AI_EMBEDDING_MODEL") {
llm.insert("embedding_model".into(), v.clone());
}
if let Some(v) = env_vars.get("AI_IMAGE_MODEL") {
llm.insert("image_model".into(), v.clone());
}
if let Some(v) = env_vars.get("LLM_LOCAL") {
llm.insert("local".into(), v.clone());
}
if let Some(v) = env_vars.get("LLM_CPP_PATH") {
llm.insert("cpp_path".into(), v.clone());
}
if let Some(v) = env_vars.get("LLM_URL") {
llm.insert("url".into(), v.clone());
}
if let Some(v) = env_vars.get("LLM_MODEL_PATH") {
llm.insert("model_path".into(), v.clone());
}
if let Some(v) = env_vars.get("EMBEDDING_MODEL_PATH") {
llm.insert("embedding_model_path".into(), v.clone());
}
if let Some(v) = env_vars.get("EMBEDDING_URL") {
llm.insert("embedding_url".into(), v.clone());
}
if !llm.is_empty() {
manager.put_secret(SecretPaths::LLM, llm).await?;
println!(" * Migrated LLM credentials");
}
println!();
println!("Migration complete!");
println!();
println!(
"You can now remove secrets from {} and keep only:",
env_file
);
println!(" RUST_LOG=info");
println!(" VAULT_ADDR=<vault-address>");
println!(" VAULT_TOKEN=<vault-token>");
println!(" SERVER_HOST=0.0.0.0");
println!(" SERVER_PORT=5858");
Ok(())
}
async fn vault_put(path: &str, kvs: &[&str]) -> Result<()> {
let manager = SecretsManager::from_env()?;
if !manager.is_enabled() {
return Err(anyhow::anyhow!("Vault not configured"));
}
let mut data: HashMap<String, String> = HashMap::new();
for kv in kvs {
if let Some((k, v)) = kv.split_once('=') {
data.insert(k.to_string(), v.to_string());
} else {
eprintln!("Invalid key=value pair: {}", kv);
}
}
if data.is_empty() {
return Err(anyhow::anyhow!("No valid key=value pairs provided"));
}
manager.put_secret(path, data).await?;
println!("* Stored {} key(s) at {}", kvs.len(), path);
Ok(())
}
async fn vault_get(path: &str, key: Option<&str>) -> Result<()> {
let manager = SecretsManager::from_env()?;
if !manager.is_enabled() {
return Err(anyhow::anyhow!("Vault not configured"));
}
let secrets = manager.get_secret(path).await?;
if let Some(k) = key {
if let Some(v) = secrets.get(k) {
println!("{}", v);
} else {
eprintln!("Key '{}' not found at {}", k, path);
}
} else {
println!("Secrets at {}:", path);
for (k, v) in &secrets {
// Mask sensitive values
let masked = if k.contains("password")
|| k.contains("secret")
|| k.contains("key")
|| k.contains("token")
{
format!("{}...", &v.chars().take(4).collect::<String>())
} else {
v.clone()
};
println!(" {}={}", k, masked);
}
}
Ok(())
}
async fn vault_list() -> Result<()> {
println!("Configured secret paths:");
println!(" {} - Database credentials", SecretPaths::TABLES);
println!(" {} - S3/MinIO credentials", SecretPaths::DRIVE);
println!(" {} - Redis credentials", SecretPaths::CACHE);
println!(" {} - SMTP credentials", SecretPaths::EMAIL);
println!(" {} - Zitadel credentials", SecretPaths::DIRECTORY);
println!(" {} - AI API keys", SecretPaths::LLM);
println!(" {} - Encryption keys", SecretPaths::ENCRYPTION);
println!(" {} - LiveKit credentials", SecretPaths::MEET);
println!(" {} - Forgejo credentials", SecretPaths::ALM);
println!(" {} - Qdrant credentials", SecretPaths::VECTORDB);
println!(" {} - InfluxDB credentials", SecretPaths::OBSERVABILITY);
println!(" gbo/stripe - Payment credentials");
println!(" gbo/custom - Custom database");
Ok(())
}
async fn vault_health() -> Result<()> {
let manager = SecretsManager::from_env()?;
if !manager.is_enabled() {
println!("x Vault not configured");
println!(" Set VAULT_ADDR and VAULT_TOKEN environment variables");
return Ok(());
}
match manager.health_check().await {
Ok(true) => {
println!("* Vault is healthy");
println!(" Address: {}", env::var("VAULT_ADDR").unwrap_or_default());
}
Ok(false) => {
println!("x Vault health check failed");
}
Err(e) => {
println!("x Vault error: {}", e);
}
}
Ok(())
}