botserver/src/bootstrap/mod.rs

202 lines
7.8 KiB
Rust
Raw Normal View History

2025-10-18 19:08:00 -03:00
use crate::config::AppConfig;
use crate::package_manager::{InstallMode, PackageManager};
use anyhow::Result;
use diesel::connection::SimpleConnection;
use diesel::Connection;
use dotenvy::dotenv;
use log::{info, trace};
use rand::distr::Alphanumeric;
use sha2::{Digest, Sha256};
2025-10-18 19:08:00 -03:00
pub struct BootstrapManager {
pub install_mode: InstallMode,
pub tenant: Option<String>,
2025-10-18 19:08:00 -03:00
}
impl BootstrapManager {
pub fn new(install_mode: InstallMode, tenant: Option<String>) -> Self {
trace!(
"Initializing BootstrapManager with mode {:?} and tenant {:?}",
install_mode,
tenant
);
2025-10-18 19:08:00 -03:00
Self {
install_mode,
2025-10-18 19:08:00 -03:00
tenant,
}
}
pub fn start_all(&mut self) -> Result<()> {
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
let components = vec![
"tables",
"cache",
"drive",
"llm",
"email",
"proxy",
"directory",
"alm",
"alm_ci",
"dns",
"webmail",
"meeting",
"table_editor",
"doc_editor",
"desktop",
"devtools",
"bot",
"system",
"vector_db",
"host",
];
for component in components {
if pm.is_installed(component) {
trace!("Starting component: {}", component);
pm.start(component)?;
} else {
trace!("Component {} not installed, skipping start", component);
}
}
Ok(())
}
2025-10-18 19:08:00 -03:00
pub fn bootstrap(&mut self) -> Result<AppConfig> {
// Check for legacy mode - if TABLES_SERVER is present, skip bootstrap
if let Ok(tables_server) = std::env::var("TABLES_SERVER") {
if !tables_server.is_empty() {
trace!(
"Legacy mode detected (TABLES_SERVER present), skipping bootstrap installation"
);
info!("Running in legacy mode with existing database configuration");
// Try to connect to the database and load config
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
let username =
std::env::var("TABLES_USERNAME").unwrap_or_else(|_| "postgres".to_string());
let password =
std::env::var("TABLES_PASSWORD").unwrap_or_else(|_| "postgres".to_string());
let server =
std::env::var("TABLES_SERVER").unwrap_or_else(|_| "localhost".to_string());
let port = std::env::var("TABLES_PORT").unwrap_or_else(|_| "5432".to_string());
let database =
std::env::var("TABLES_DATABASE").unwrap_or_else(|_| "gbserver".to_string());
format!(
"postgres://{}:{}@{}:{}/{}",
username, password, server, port, database
)
});
match diesel::PgConnection::establish(&database_url) {
Ok(mut conn) => {
info!("Successfully connected to legacy database, loading configuration");
return Ok(AppConfig::from_database(&mut conn));
}
Err(e) => {
log::warn!("Failed to connect to legacy database: {}", e);
info!("Using environment variables as fallback");
return Ok(AppConfig::from_env());
}
}
}
}
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
let required_components = vec!["tables", "drive", "cache", "llm"];
let mut config = AppConfig::from_env();
for component in required_components {
if !pm.is_installed(component) {
if component == "tables" {
let db_password = self.generate_secure_password(16);
let farm_password = self.generate_secure_password(32);
let env_contents = format!(
"FARM_PASSWORD={}\nDATABASE_URL=postgres://gbuser:{}@localhost:5432/botserver",
farm_password, db_password
);
std::fs::write(".env", &env_contents)
.map_err(|e| anyhow::anyhow!("Failed to write .env file: {}", e))?;
dotenv().ok();
trace!("Generated database credentials and wrote to .env file");
}
trace!("Installing required component: {}", component);
futures::executor::block_on(pm.install(component))?;
if component == "tables" {
trace!("Component {} installed successfully", component);
let database_url = std::env::var("DATABASE_URL").unwrap();
let mut conn = diesel::PgConnection::establish(&database_url)
.map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?;
let migration_dir = include_dir::include_dir!("./migrations");
let mut migration_files: Vec<_> = migration_dir
.files()
.filter_map(|file| {
let path = file.path();
trace!("Found file: {:?}", path);
if path.extension()? == "sql" {
trace!(" -> SQL file included");
Some(file)
} else {
trace!(" -> Not a SQL file, skipping");
None
}
})
.collect();
trace!("Total migration files found: {}", migration_files.len());
migration_files.sort_by_key(|f| f.path());
for migration_file in migration_files {
let migration = migration_file
.contents_utf8()
.ok_or_else(|| anyhow::anyhow!("Migration file is not valid UTF-8"))?;
trace!("Executing migration: {}", migration_file.path().display());
// Use batch_execute to handle multiple statements including those with dollar-quoted strings
if let Err(e) = conn.batch_execute(migration) {
log::error!(
"Failed to execute migration {}: {}",
migration_file.path().display(),
e
);
return Err(e.into());
}
trace!(
"Successfully executed migration: {}",
migration_file.path().display()
);
}
config = AppConfig::from_database(&mut conn);
info!("Database migrations completed and configuration loaded");
}
2025-10-18 19:08:00 -03:00
}
}
Ok(config)
2025-10-18 19:08:00 -03:00
}
fn generate_secure_password(&self, length: usize) -> String {
// Ensure the Rng trait is in scope for `sample`
use rand::Rng;
let mut rng = rand::rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric) as char)
.take(length)
.collect()
}
fn encrypt_password(&self, password: &str, key: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(key.as_bytes());
hasher.update(password.as_bytes());
format!("{:x}", hasher.finalize())
}
2025-10-18 19:08:00 -03:00
}