feat: configurable stack paths and local installer support

- Add BOTSERVER_INSTALLERS_PATH env var to use local installers without downloading
- Replace hardcoded ./botserver-stack paths with configurable stack_path
- Add stack_dir() and vault_bin() helper methods in BootstrapManager
- Add Port: 8300 to Zitadel config to fix port binding issue
- Start Directory service before setup_directory() call
- Add SKIP_LLM_SERVER env var to skip local LLM in tests
- Update template loading to check ../bottemplates and botserver-templates paths
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-14 15:58:54 -03:00
parent 7029e49f80
commit 17618f692c
4 changed files with 220 additions and 96 deletions

View file

@ -1,7 +1,7 @@
{
"base_url": "http://localhost:8080",
"base_url": "http://localhost:8300",
"default_org": {
"id": "350493764617240590",
"id": "351049887434932238",
"name": "default",
"domain": "default.localhost"
},
@ -13,8 +13,8 @@
"first_name": "Admin",
"last_name": "User"
},
"admin_token": "PQV0tJlkYiGwiXCbaGDszHZZEJ5zNeCppXq4C8Ryi4A_astZuM_aYZUEq1PnNbn6g7Mcc_A",
"admin_token": "yR_pDxClepmQw-7neHEcRa6lEMyFB2ECoMEVfBCZGZW7F-TdUvG2W-dWGhEhGYqGDYApbCM",
"project_id": "",
"client_id": "350493765221285902",
"client_secret": "KHKXlgoRPdVLhwjnjoy0DvCSREdGv5ukRA2ZhTeSyCjRZrSkZKEc6aQ2pG351nlM"
"client_id": "351049888072531982",
"client_secret": "LojyVztS8EpcnM6qyhCfjtSkeohUy2rO0oi36lKZmtyF5OpNUX88bruNdgqOQWEQ"
}

View file

@ -24,15 +24,35 @@ pub struct ComponentInfo {
pub struct BootstrapManager {
pub install_mode: InstallMode,
pub tenant: Option<String>,
pub stack_path: PathBuf,
}
impl BootstrapManager {
pub async fn new(mode: InstallMode, tenant: Option<String>) -> Self {
// Get stack path from env var or use default
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./botserver-stack"));
Self {
install_mode: mode,
tenant,
stack_path,
}
}
/// Get a path relative to the stack directory
fn stack_dir(&self, subpath: &str) -> PathBuf {
self.stack_path.join(subpath)
}
/// Get the vault binary path as a string for shell commands
fn vault_bin(&self) -> String {
self.stack_dir("bin/vault/vault")
.to_str()
.unwrap_or("./botserver-stack/bin/vault/vault")
.to_string()
}
/// Kill all processes running from the botserver-stack directory
/// This ensures a clean startup when bootstrapping fresh
pub fn kill_stack_processes() {
@ -105,7 +125,9 @@ impl BootstrapManager {
/// Check if another botserver process is already running on this stack
pub fn check_single_instance() -> Result<bool> {
let lock_file = PathBuf::from("./botserver-stack/.lock");
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
.unwrap_or_else(|_| "./botserver-stack".to_string());
let lock_file = PathBuf::from(&stack_path).join(".lock");
if lock_file.exists() {
// Check if the PID in the lock file is still running
if let Ok(pid_str) = fs::read_to_string(&lock_file) {
@ -131,7 +153,9 @@ impl BootstrapManager {
/// Release the instance lock on shutdown
pub fn release_instance_lock() {
let lock_file = PathBuf::from("./botserver-stack/.lock");
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
.unwrap_or_else(|_| "./botserver-stack".to_string());
let lock_file = PathBuf::from(&stack_path).join(".lock");
if lock_file.exists() {
fs::remove_file(&lock_file).ok();
}
@ -140,19 +164,21 @@ impl BootstrapManager {
/// Check if botserver-stack has installed components (indicating a working installation)
/// This is used to prevent accidental re-initialization of existing installations
fn has_installed_stack() -> bool {
let stack_dir = PathBuf::from("./botserver-stack");
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
.unwrap_or_else(|_| "./botserver-stack".to_string());
let stack_dir = PathBuf::from(&stack_path);
if !stack_dir.exists() {
return false;
}
// Check for key indicators of an installed stack
let indicators = vec![
"./botserver-stack/bin/vault/vault",
"./botserver-stack/data/vault",
"./botserver-stack/conf/vault/config.hcl",
stack_dir.join("bin/vault/vault"),
stack_dir.join("data/vault"),
stack_dir.join("conf/vault/config.hcl"),
];
indicators.iter().any(|path| PathBuf::from(path).exists())
indicators.iter().any(|path| path.exists())
}
/// Reset only Vault credentials (when re-initialization is needed)
@ -168,7 +194,9 @@ impl BootstrapManager {
));
}
let vault_init = PathBuf::from("./botserver-stack/conf/vault/init.json");
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
.unwrap_or_else(|_| "./botserver-stack".to_string());
let vault_init = PathBuf::from(&stack_path).join("conf/vault/init.json");
let env_file = PathBuf::from("./.env");
// Only remove vault init.json and .env - NEVER touch data/
@ -343,8 +371,7 @@ impl BootstrapManager {
Err(e) => {
debug!(
"Component {} might already be running: {}",
component.name,
e
component.name, e
);
}
}
@ -377,7 +404,7 @@ impl BootstrapManager {
// Check if we need to bootstrap first
let vault_installed = installer.is_installed("vault");
let vault_initialized = PathBuf::from("./botserver-stack/conf/vault/init.json").exists();
let vault_initialized = self.stack_dir("conf/vault/init.json").exists();
if !vault_installed || !vault_initialized {
info!("Stack not fully bootstrapped, running bootstrap first...");
@ -553,7 +580,7 @@ impl BootstrapManager {
/// Ensure Vault is unsealed (required after restart)
/// Returns Ok(()) if Vault is ready, Err if it needs re-initialization
async fn ensure_vault_unsealed(&self) -> Result<()> {
let vault_init_path = PathBuf::from("./botserver-stack/conf/vault/init.json");
let vault_init_path = self.stack_dir("conf/vault/init.json");
let vault_addr = "http://localhost:8200";
if !vault_init_path.exists() {
@ -582,11 +609,12 @@ impl BootstrapManager {
}
// First check if Vault is initialized (not just running)
let vault_bin = self.vault_bin();
let status_output = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"VAULT_ADDR={} ./botserver-stack/bin/vault/vault status -format=json 2>/dev/null",
vault_addr
"VAULT_ADDR={} {} status -format=json 2>/dev/null",
vault_addr, vault_bin
))
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
@ -613,8 +641,8 @@ impl BootstrapManager {
let unseal_output = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"VAULT_ADDR={} ./botserver-stack/bin/vault/vault operator unseal {} >/dev/null 2>&1",
vault_addr, unseal_key
"VAULT_ADDR={} {} operator unseal {} >/dev/null 2>&1",
vault_addr, vault_bin, unseal_key
))
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
@ -630,8 +658,8 @@ impl BootstrapManager {
let verify_output = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"VAULT_ADDR={} ./botserver-stack/bin/vault/vault status -format=json 2>/dev/null",
vault_addr
"VAULT_ADDR={} {} status -format=json 2>/dev/null",
vault_addr, vault_bin
))
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
@ -661,15 +689,21 @@ impl BootstrapManager {
// Also set mTLS cert paths
std::env::set_var(
"VAULT_CACERT",
"./botserver-stack/conf/system/certificates/ca/ca.crt",
self.stack_dir("conf/system/certificates/ca/ca.crt")
.to_str()
.unwrap_or(""),
);
std::env::set_var(
"VAULT_CLIENT_CERT",
"./botserver-stack/conf/system/certificates/botserver/client.crt",
self.stack_dir("conf/system/certificates/botserver/client.crt")
.to_str()
.unwrap_or(""),
);
std::env::set_var(
"VAULT_CLIENT_KEY",
"./botserver-stack/conf/system/certificates/botserver/client.key",
self.stack_dir("conf/system/certificates/botserver/client.key")
.to_str()
.unwrap_or(""),
);
info!("Vault environment configured");
@ -720,7 +754,7 @@ impl BootstrapManager {
];
// Special check: Vault needs setup even if binary exists but not initialized
let vault_needs_setup = !PathBuf::from("./botserver-stack/conf/vault/init.json").exists();
let vault_needs_setup = !self.stack_dir("conf/vault/init.json").exists();
for component in required_components {
// For vault, also check if it needs initialization
@ -809,6 +843,18 @@ impl BootstrapManager {
// Directory configuration - setup happens after install starts Zitadel
if component == "directory" {
info!("Starting Directory (Zitadel) service...");
match pm.start("directory") {
Ok(_) => {
info!("Directory service started successfully");
// Give Zitadel time to initialize before health checks
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
Err(e) => {
warn!("Failed to start Directory service: {}", e);
}
}
info!("Waiting for Directory to be ready...");
if let Err(e) = self.setup_directory().await {
// Don't fail completely - Zitadel may still be usable with first instance setup
@ -821,7 +867,7 @@ impl BootstrapManager {
info!("Setting up Vault secrets service...");
// Verify vault binary exists and is executable
let vault_bin = PathBuf::from("./botserver-stack/bin/vault/vault");
let vault_bin = self.stack_dir("bin/vault/vault");
if !vault_bin.exists() {
error!("Vault binary not found at {:?}", vault_bin);
return Err(anyhow::anyhow!("Vault binary not found after installation"));
@ -829,7 +875,7 @@ impl BootstrapManager {
info!("Vault binary verified at {:?}", vault_bin);
// Ensure logs directory exists
let vault_log_path = PathBuf::from("./botserver-stack/logs/vault/vault.log");
let vault_log_path = self.stack_dir("logs/vault/vault.log");
if let Some(parent) = vault_log_path.parent() {
if let Err(e) = fs::create_dir_all(parent) {
error!("Failed to create vault logs directory: {}", e);
@ -837,7 +883,7 @@ impl BootstrapManager {
}
// Ensure data directory exists
let vault_data_path = PathBuf::from("./botserver-stack/data/vault");
let vault_data_path = self.stack_dir("data/vault");
if let Err(e) = fs::create_dir_all(&vault_data_path) {
error!("Failed to create vault data directory: {}", e);
}
@ -845,9 +891,14 @@ impl BootstrapManager {
info!("Starting Vault server...");
// Try starting vault directly first
let vault_bin_dir = self.stack_dir("bin/vault");
let vault_start_cmd = format!(
"cd {} && nohup ./vault server -config=../../conf/vault/config.hcl > ../../logs/vault/vault.log 2>&1 &",
vault_bin_dir.display()
);
let _ = std::process::Command::new("sh")
.arg("-c")
.arg("cd ./botserver-stack/bin/vault && nohup ./vault server -config=../../conf/vault/config.hcl > ../../logs/vault/vault.log 2>&1 &")
.arg(&vault_start_cmd)
.status();
std::thread::sleep(std::time::Duration::from_secs(2));
@ -968,11 +1019,14 @@ impl BootstrapManager {
async fn configure_services_in_directory(&self, db_password: &str) -> Result<()> {
info!("Creating Zitadel configuration files...");
let zitadel_config_path = PathBuf::from("./botserver-stack/conf/directory/zitadel.yaml");
let steps_config_path = PathBuf::from("./botserver-stack/conf/directory/steps.yaml");
let zitadel_config_path = self.stack_dir("conf/directory/zitadel.yaml");
let steps_config_path = self.stack_dir("conf/directory/steps.yaml");
// Use absolute path for PAT file since zitadel runs from bin/directory/
let pat_path =
std::env::current_dir()?.join("botserver-stack/conf/directory/admin-pat.txt");
let pat_path = if self.stack_path.is_absolute() {
self.stack_dir("conf/directory/admin-pat.txt")
} else {
std::env::current_dir()?.join(self.stack_dir("conf/directory/admin-pat.txt"))
};
fs::create_dir_all(zitadel_config_path.parent().unwrap())?;
@ -987,6 +1041,8 @@ impl BootstrapManager {
Formatter:
Format: text
Port: 8300
Database:
postgres:
Host: localhost
@ -1098,7 +1154,7 @@ DefaultInstance:
/// Setup Caddy as reverse proxy for all services
async fn setup_caddy_proxy(&self) -> Result<()> {
let caddy_config = PathBuf::from("./botserver-stack/conf/proxy/Caddyfile");
let caddy_config = self.stack_dir("conf/proxy/Caddyfile");
fs::create_dir_all(caddy_config.parent().unwrap())?;
let config = format!(
@ -1151,10 +1207,10 @@ meet.botserver.local {{
/// Setup CoreDNS for dynamic DNS service
async fn setup_coredns(&self) -> Result<()> {
let dns_config = PathBuf::from("./botserver-stack/conf/dns/Corefile");
let dns_config = self.stack_dir("conf/dns/Corefile");
fs::create_dir_all(dns_config.parent().unwrap())?;
let zone_file = PathBuf::from("./botserver-stack/conf/dns/botserver.local.zone");
let zone_file = self.stack_dir("conf/dns/botserver.local.zone");
// Create Corefile
let corefile = r#"botserver.local:53 {
@ -1212,7 +1268,7 @@ meet IN A 127.0.0.1
/// Setup Directory (Zitadel) with default organization and user
async fn setup_directory(&self) -> Result<()> {
let config_path = PathBuf::from("./config/directory_config.json");
let pat_path = PathBuf::from("./botserver-stack/conf/directory/admin-pat.txt");
let pat_path = self.stack_dir("conf/directory/admin-pat.txt");
// Ensure config directory exists
tokio::fs::create_dir_all("./config").await?;
@ -1364,7 +1420,7 @@ meet IN A 127.0.0.1
drive_secret: &str,
cache_password: &str,
) -> Result<()> {
let vault_conf_path = PathBuf::from("./botserver-stack/conf/vault");
let vault_conf_path = self.stack_dir("conf/vault");
let vault_init_path = vault_conf_path.join("init.json");
let env_file_path = PathBuf::from("./.env");
@ -1385,7 +1441,7 @@ meet IN A 127.0.0.1
if ps_result.contains("NOT_RUNNING") {
warn!("Vault process is not running (attempt {})", attempts + 1);
// Check vault.log for crash info
let vault_log_path = PathBuf::from("./botserver-stack/logs/vault/vault.log");
let vault_log_path = self.stack_dir("logs/vault/vault.log");
if vault_log_path.exists() {
if let Ok(log_content) = fs::read_to_string(&vault_log_path) {
let last_lines: Vec<&str> =
@ -1428,7 +1484,7 @@ meet IN A 127.0.0.1
max_attempts
);
// Final check of vault.log
let vault_log_path = PathBuf::from("./botserver-stack/logs/vault/vault.log");
let vault_log_path = self.stack_dir("logs/vault/vault.log");
if vault_log_path.exists() {
if let Ok(log_content) = fs::read_to_string(&vault_log_path) {
let last_lines: Vec<&str> = log_content.lines().rev().take(20).collect();
@ -1471,7 +1527,8 @@ meet IN A 127.0.0.1
// Check if .env exists with VAULT_TOKEN - try to recover from that
let env_token = if env_file_path.exists() {
if let Ok(env_content) = fs::read_to_string(&env_file_path) {
env_content.lines()
env_content
.lines()
.find(|line| line.starts_with("VAULT_TOKEN="))
.map(|line| line.trim_start_matches("VAULT_TOKEN=").to_string())
} else {
@ -1483,12 +1540,13 @@ meet IN A 127.0.0.1
// Initialize Vault if not already done
info!("Initializing Vault...");
let vault_bin = self.vault_bin();
// Clear any mTLS env vars that might interfere with CLI
let init_output = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} ./botserver-stack/bin/vault/vault operator init -key-shares=1 -key-threshold=1 -format=json",
vault_addr
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} {} operator init -key-shares=1 -key-threshold=1 -format=json",
vault_addr, vault_bin
))
.output()?;
@ -1506,14 +1564,16 @@ meet IN A 127.0.0.1
let status_check = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} ./botserver-stack/bin/vault/vault status -format=json 2>/dev/null",
vault_addr
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} {} status -format=json 2>/dev/null",
vault_addr, vault_bin
))
.output();
if let Ok(status_output) = status_check {
let status_str = String::from_utf8_lossy(&status_output.stdout);
if let Ok(status) = serde_json::from_str::<serde_json::Value>(&status_str) {
if let Ok(status) =
serde_json::from_str::<serde_json::Value>(&status_str)
{
let sealed = status["sealed"].as_bool().unwrap_or(true);
if !sealed {
// Vault is unsealed! We can continue with the token from .env
@ -1528,7 +1588,9 @@ meet IN A 127.0.0.1
error!("Vault is sealed and unseal key is lost (init.json missing)");
error!("Options:");
error!(" 1. If you have a backup of init.json, restore it to ./botserver-stack/conf/vault/init.json");
error!(" 2. To start fresh, delete ./botserver-stack/data/vault/ and restart");
error!(
" 2. To start fresh, delete ./botserver-stack/data/vault/ and restart"
);
return Err(anyhow::anyhow!(
"Vault is sealed but unseal key is lost. See error messages above for recovery options."
));
@ -1568,12 +1630,13 @@ meet IN A 127.0.0.1
// Unseal Vault
info!("Unsealing Vault...");
let vault_bin = self.vault_bin();
// Clear any mTLS env vars that might interfere with CLI
let unseal_output = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} ./botserver-stack/bin/vault/vault operator unseal {}",
vault_addr, unseal_key
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} {} operator unseal {}",
vault_addr, vault_bin, unseal_key
))
.output()?;
@ -1628,8 +1691,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault secrets enable -path=secret kv-v2 2>&1 || true",
vault_addr, root_token
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} secrets enable -path=secret kv-v2 2>&1 || true",
vault_addr, root_token, vault_bin
))
.output();
@ -1638,12 +1701,13 @@ VAULT_CACHE_TTL=300
info!("Storing secrets in Vault (only if not existing)...");
// Helper to check if a secret path exists
let vault_bin_clone = vault_bin.clone();
let secret_exists = |path: &str| -> bool {
let output = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv get {} 2>/dev/null",
vault_addr, root_token, path
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv get {} 2>/dev/null",
vault_addr, root_token, vault_bin_clone, path
))
.output();
output.map(|o| o.status.success()).unwrap_or(false)
@ -1654,8 +1718,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/tables host=localhost port=5432 database=botserver username=gbuser password='{}'",
vault_addr, root_token, db_password
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv put secret/gbo/tables host=localhost port=5432 database=botserver username=gbuser password='{}'",
vault_addr, root_token, vault_bin, db_password
))
.output()?;
info!(" Stored database credentials");
@ -1668,8 +1732,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/drive accesskey='{}' secret='{}'",
vault_addr, root_token, drive_accesskey, drive_secret
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv put secret/gbo/drive accesskey='{}' secret='{}'",
vault_addr, root_token, vault_bin, drive_accesskey, drive_secret
))
.output()?;
info!(" Stored drive credentials");
@ -1682,8 +1746,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/cache password='{}'",
vault_addr, root_token, cache_password
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv put secret/gbo/cache password='{}'",
vault_addr, root_token, vault_bin, cache_password
))
.output()?;
info!(" Stored cache credentials");
@ -1696,8 +1760,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/directory url=https://localhost:8300 project_id= client_id= client_secret=",
vault_addr, root_token
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv put secret/gbo/directory url=https://localhost:8300 project_id= client_id= client_secret=",
vault_addr, root_token, vault_bin
))
.output()?;
info!(" Created directory placeholder");
@ -1710,8 +1774,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/llm openai_key= anthropic_key= groq_key=",
vault_addr, root_token
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv put secret/gbo/llm openai_key= anthropic_key= groq_key=",
vault_addr, root_token, vault_bin
))
.output()?;
info!(" Created LLM placeholder");
@ -1724,8 +1788,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/email username= password=",
vault_addr, root_token
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv put secret/gbo/email username= password=",
vault_addr, root_token, vault_bin
))
.output()?;
info!(" Created email placeholder");
@ -1739,8 +1803,8 @@ VAULT_CACHE_TTL=300
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/encryption master_key='{}'",
vault_addr, root_token, encryption_key
"unset VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_CACERT; VAULT_ADDR={} VAULT_TOKEN={} {} kv put secret/gbo/encryption master_key='{}'",
vault_addr, root_token, vault_bin, encryption_key
))
.output()?;
info!(" Generated and stored encryption key");
@ -1844,10 +1908,26 @@ VAULT_CACHE_TTL=300
}
pub async fn upload_templates_to_drive(&self, _config: &AppConfig) -> Result<()> {
let templates_dir = Path::new("templates");
if !templates_dir.exists() {
// Check multiple possible template locations
let possible_paths = [
"../bottemplates", // Development: sibling directory
"bottemplates", // In current directory
"botserver-templates", // Installation bundle subdirectory
"templates", // Legacy path
];
let templates_dir = possible_paths.iter().map(Path::new).find(|p| p.exists());
let templates_dir = match templates_dir {
Some(dir) => {
info!("Using templates from: {:?}", dir);
dir
}
None => {
info!("No templates directory found, skipping template upload");
return Ok(());
}
};
let client = Self::get_drive_client(_config).await;
let mut read_dir = tokio::fs::read_dir(templates_dir).await?;
while let Some(entry) = read_dir.next_entry().await? {
@ -1881,11 +1961,32 @@ VAULT_CACHE_TTL=300
use crate::shared::models::schema::bots;
use diesel::prelude::*;
let templates_dir = Path::new("templates");
if !templates_dir.exists() {
warn!("Templates directory does not exist");
// Check multiple possible template locations
let possible_paths = [
"../bottemplates", // Development: sibling directory
"bottemplates", // In current directory
"botserver-templates", // Installation bundle subdirectory
"templates", // Legacy path
];
let templates_dir = possible_paths
.iter()
.map(|p| PathBuf::from(p))
.find(|p| p.exists());
let templates_dir = match templates_dir {
Some(dir) => {
info!("Loading templates from: {:?}", dir);
dir
}
None => {
warn!(
"Templates directory does not exist (checked: {:?})",
possible_paths
);
return Ok(());
}
};
// Get the default bot (created by migrations) - we'll sync all template configs to it
let default_bot: Option<(uuid::Uuid, String)> = bots::table
@ -1909,6 +2010,7 @@ VAULT_CACHE_TTL=300
// Only sync the default.gbai template config (main config for the system)
let default_template = templates_dir.join("default.gbai");
info!("Looking for default template at: {:?}", default_template);
if default_template.exists() {
let config_path = default_template.join("default.gbot").join("config.csv");
@ -2063,7 +2165,7 @@ VAULT_CACHE_TTL=300
/// Create Vault configuration with mTLS settings
async fn create_vault_config(&self) -> Result<()> {
let vault_conf_dir = PathBuf::from("./botserver-stack/conf/vault");
let vault_conf_dir = self.stack_dir("conf/vault");
let config_path = vault_conf_dir.join("config.hcl");
fs::create_dir_all(&vault_conf_dir)?;
@ -2109,7 +2211,7 @@ log_level = "info"
fs::write(&config_path, config)?;
// Create data directory for Vault storage
fs::create_dir_all("./botserver-stack/data/vault")?;
fs::create_dir_all(self.stack_dir("data/vault"))?;
info!(
"Created Vault config with mTLS at {}",
@ -2120,7 +2222,7 @@ log_level = "info"
/// Generate TLS certificates for all services
async fn generate_certificates(&self) -> Result<()> {
let cert_dir = PathBuf::from("./botserver-stack/conf/system/certificates");
let cert_dir = self.stack_dir("conf/system/certificates");
// Create certificate directory structure
fs::create_dir_all(&cert_dir)?;

View file

@ -75,10 +75,26 @@ impl DownloadCache {
///
/// # Arguments
/// * `base_path` - Base path for botserver (typically current directory or botserver root)
///
/// # Environment Variables
/// * `BOTSERVER_INSTALLERS_PATH` - Override path to pre-downloaded installers directory
pub fn new(base_path: impl AsRef<Path>) -> Result<Self> {
let base_path = base_path.as_ref().to_path_buf();
let config = Self::load_config(&base_path)?;
let cache_dir = base_path.join(&config.cache_settings.cache_dir);
// Check for BOTSERVER_INSTALLERS_PATH env var first (for testing/offline installs)
let cache_dir = if let Ok(installers_path) = std::env::var("BOTSERVER_INSTALLERS_PATH") {
let path = PathBuf::from(&installers_path);
if path.exists() {
info!("Using installers from BOTSERVER_INSTALLERS_PATH: {:?}", path);
path
} else {
warn!("BOTSERVER_INSTALLERS_PATH set but path doesn't exist: {:?}", path);
base_path.join(&config.cache_settings.cache_dir)
}
} else {
base_path.join(&config.cache_settings.cache_dir)
};
// Ensure cache directory exists
if !cache_dir.exists() {

View file

@ -10,6 +10,12 @@ use tokio;
pub async fn ensure_llama_servers_running(
app_state: Arc<AppState>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Skip LLM server startup if SKIP_LLM_SERVER is set (for testing with mock LLM)
if std::env::var("SKIP_LLM_SERVER").is_ok() {
info!("SKIP_LLM_SERVER set - skipping local LLM server startup (using mock/external LLM)");
return Ok(());
}
let config_values = {
let conn_arc = app_state.conn.clone();
let default_bot_id = tokio::task::spawn_blocking(move || {