diff --git a/src/core/package_manager/cli.rs b/src/core/package_manager/cli.rs index c0f934e4..67b59dd1 100644 --- a/src/core/package_manager/cli.rs +++ b/src/core/package_manager/cli.rs @@ -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 = env::args().collect(); if args.len() < 2 { print_usage(); return Ok(()); } - use tracing::info; - fn print_usage() { - info!("usage: botserver [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 [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 [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 [options]"); + println!(); + println!("Commands:"); + println!(" install Install a component"); + println!(" remove Remove a component"); + println!(" list List all components"); + println!(" status Check component status"); + println!(" start Start all installed components"); + println!(" stop Stop all components"); + println!(" restart Restart all components"); + println!(" vault Manage Vault secrets"); + println!(" --help, -h Show this help"); + println!(); + println!("Options:"); + println!(" --container Use container mode (LXC)"); + println!(" --tenant Specify tenant name"); + println!(); + println!("Vault subcommands:"); + println!(" vault migrate [.env] Migrate .env secrets to Vault"); + println!(" vault put k=v Store secrets in Vault"); + println!(" vault get 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 [options]"); + println!(); + println!("Subcommands:"); + println!(" migrate [file] Migrate secrets from .env to Vault"); + println!(" Default file: .env"); + println!(); + println!(" put k=v... Store key-value pairs at path"); + println!(" Example: botserver vault put gbo/email user=x pass=y"); + println!(); + println!(" get [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 = 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 = 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 = 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 = 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 = 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 = 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 = 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="); + println!(" 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 = 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::()) + } 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(()) +}