Add vault CLI commands for secret migration
This commit is contained in:
parent
55faf55e08
commit
f8ac224d00
1 changed files with 404 additions and 5 deletions
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue