feat(bootstrap): add secure password generation and env setup

Added functionality to generate secure passwords for database and drive server credentials during bootstrap. Removed the PostgreSQL running check and auto-start logic as it's no longer needed. Renamed `create_s3_operator` to more descriptive `get_drive_client`. The bootstrap process now automatically sets up required environment variables in .env file including database URL and drive server credentials.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-11 11:12:54 -03:00
parent fd73d207cc
commit b52e4b2737
5 changed files with 100 additions and 102 deletions

View file

@ -6,6 +6,7 @@ use aws_config::BehaviorVersion;
use aws_sdk_s3::Client;
use diesel::connection::SimpleConnection;
use log::{error, info, trace};
use rand::distr::Alphanumeric;
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
@ -17,35 +18,12 @@ pub struct BootstrapManager {
pub tenant: Option<String>,
}
impl BootstrapManager {
fn is_postgres_running() -> bool {
match Command::new("pg_isready").arg("-q").status() {
Ok(status) => status.success(),
Err(_) => Command::new("pgrep")
.arg("postgres")
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false),
}
}
pub async fn new(install_mode: InstallMode, tenant: Option<String>) -> Self {
trace!(
"Initializing BootstrapManager with mode {:?} and tenant {:?}",
install_mode,
tenant
);
if !Self::is_postgres_running() {
let pm = PackageManager::new(install_mode.clone(), tenant.clone())
.expect("Failed to initialize PackageManager");
if let Err(e) = pm.start("tables") {
error!(
"Failed to start Tables server component automatically: {}",
e
);
panic!("Database not available and auto-start failed.");
} else {
trace!("Tables server started successfully");
}
}
Self {
install_mode,
tenant,
@ -84,6 +62,18 @@ impl BootstrapManager {
}
Ok(())
}
fn generate_secure_password(&self, length: usize) -> String {
let mut rng = rand::rng();
(0..length)
.map(|_| {
let byte = rand::Rng::sample(&mut rng, Alphanumeric);
char::from(byte)
})
.collect()
}
pub async fn bootstrap(&mut self) {
if let Ok(tables_server) = std::env::var("TABLES_SERVER") {
if !tables_server.is_empty() {
@ -116,7 +106,30 @@ impl BootstrapManager {
}
}
}
} else {
let db_env_path = std::env::current_dir().unwrap().join(".env");
let db_password = self.generate_secure_password(32);
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
format!("postgres://gbuser:{}@localhost:5432/botserver", db_password)
});
let db_line = format!("DATABASE_URL={}\n", database_url);
let drive_password = self.generate_secure_password(16);
let drive_user = "gbdriveuser".to_string();
let env_path = std::env::current_dir().unwrap().join(".env");
let env_content = format!(
"\nDRIVE_SERVER=http://localhost:9000\nDRIVE_ACCESSKEY={}\nDRIVE_SECRET={}\n",
drive_user, drive_password
);
let _ = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&env_path)
.and_then(|mut file| std::io::Write::write_all(&mut file, env_content.as_bytes()));
let _ = std::fs::write(&db_env_path, db_line);
}
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone()).unwrap();
let required_components = vec!["tables", "drive", "cache", "llm"];
for component in required_components {
@ -190,8 +203,8 @@ impl BootstrapManager {
}
}
}
async fn create_s3_operator(config: &AppConfig) -> Client {
async fn get_drive_client(config: &AppConfig) -> Client {
let endpoint = if !config.drive.server.ends_with('/') {
format!("{}/", config.drive.server)
} else {
@ -222,7 +235,7 @@ impl BootstrapManager {
if !templates_dir.exists() {
return Ok(());
}
let client = Self::create_s3_operator(_config).await;
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? {
let path = entry.path();

View file

@ -140,7 +140,7 @@ impl AppConfig {
pub fn from_env() -> Result<Self, anyhow::Error> {
let database_url = std::env::var("DATABASE_URL").unwrap();
let (db_username, db_password, db_server, db_port, db_name) =
parse_database_url(&database_url);
crate::shared::utils::parse_database_url(&database_url);
let database = DatabaseConfig {
username: db_username,
password: db_password,
@ -174,34 +174,8 @@ impl AppConfig {
})
}
}
fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
if let Some(stripped) = url.strip_prefix("postgres://") {
let parts: Vec<&str> = stripped.split('@').collect();
if parts.len() == 2 {
let user_pass: Vec<&str> = parts[0].split(':').collect();
let host_db: Vec<&str> = parts[1].split('/').collect();
if user_pass.len() >= 2 && host_db.len() >= 2 {
let username = user_pass[0].to_string();
let password = user_pass[1].to_string();
let host_port: Vec<&str> = host_db[0].split(':').collect();
let server = host_port[0].to_string();
let port = host_port
.get(1)
.and_then(|p| p.parse().ok())
.unwrap_or(5432);
let database = host_db[1].to_string();
return (username, password, server, port, database);
}
}
}
(
"gbuser".to_string(),
"".to_string(),
"localhost".to_string(),
5432,
"botserver".to_string(),
)
}
pub struct ConfigManager {
conn: DbPool,
}

View file

@ -149,6 +149,15 @@ async fn main() -> std::io::Result<()> {
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
let env_path = std::env::current_dir().unwrap().join(".env");
let cfg = if env_path.exists() {
progress_tx_clone
.send(BootstrapProgress::StartingComponent(
"all services".to_string(),
))
.ok();
bootstrap.start_all().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
progress_tx_clone
.send(BootstrapProgress::ConnectingDatabase)
.ok();
@ -167,25 +176,21 @@ async fn main() -> std::io::Result<()> {
}
} else {
bootstrap.bootstrap().await;
progress_tx_clone
.send(BootstrapProgress::StartingComponent(
"all services".to_string(),
))
.ok();
bootstrap.start_all().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
match create_conn() {
Ok(pool) => AppConfig::from_database(&pool)
.unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")),
Err(_) => AppConfig::from_env().expect("Failed to load config from env"),
}
};
progress_tx_clone
.send(BootstrapProgress::StartingComponent(
"all services".to_string(),
))
.ok();
if let Err(e) = bootstrap.start_all() {
progress_tx_clone
.send(BootstrapProgress::BootstrapError(format!(
"Failed to start services: {}",
e
)))
.ok();
}
progress_tx_clone
.send(BootstrapProgress::UploadingTemplates)
.ok();

View file

@ -1,9 +1,9 @@
use crate::package_manager::component::ComponentConfig;
use crate::package_manager::os::detect_os;
use crate::package_manager::{InstallMode, OsType};
use crate::shared::utils::parse_database_url;
use anyhow::Result;
use log::trace;
use rand::distr::Alphanumeric;
use std::collections::HashMap;
use std::path::PathBuf;
@ -61,19 +61,8 @@ impl PackageManager {
fn register_drive(&mut self) {
let drive_password = self.generate_secure_password(16);
let drive_user = "gbdriveuser".to_string();
let env_path = std::env::current_dir().unwrap().join(".env");
let env_content = format!(
"\nDRIVE_ACCESSKEY={}\nDRIVE_SECRET={}\nDRIVE_SERVER=http://localhost:9000\n",
drive_user, drive_password
);
let _ = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&env_path)
.and_then(|mut file| std::io::Write::write_all(&mut file, env_content.as_bytes()));
let drive_user = std::env::var("DRIVE_ACCESSKEY").unwrap();
let drive_password = std::env::var("DRIVE_SECRET").unwrap();
self.components.insert(
"drive".to_string(),
@ -95,8 +84,8 @@ impl PackageManager {
pre_install_cmds_windows: vec![],
post_install_cmds_windows: vec![],
env_vars: HashMap::from([
("DRIVE_ROOT_USER".to_string(), drive_user.clone()),
("DRIVE_ROOT_PASSWORD".to_string(), drive_password.clone()),
("MINIO_ROOT_USER".to_string(), drive_user.clone()),
("MINIO_ROOT_PASSWORD".to_string(), drive_password.clone()),
]),
data_download_list: Vec::new(),
exec_cmd: "nohup {{BIN_PATH}}/minio server {{DATA_PATH}} --address :9000 --console-address :9001 > {{LOGS_PATH}}/minio.log 2>&1 &".to_string(),
@ -110,14 +99,9 @@ impl PackageManager {
fn register_tables(&mut self) {
let db_env_path = std::env::current_dir().unwrap().join(".env");
let db_password = self.generate_secure_password(32);
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| format!("postgres://gbuser:{}@localhost:5432/botserver", db_password));
let db_line = format!("DATABASE_URL={}\n", database_url);
let _ = std::fs::write(&db_env_path, db_line);
let database_url = std::env::var("DATABASE_URL").unwrap();
let (_db_username, db_password, _db_server, _db_port, _db_name) =
parse_database_url(&database_url);
self.components.insert(
"tables".to_string(),
@ -756,10 +740,12 @@ impl PackageManager {
rendered_cmd
);
let child = std::process::Command::new("sh")
.current_dir(&bin_path)
.arg("-c")
.arg(&rendered_cmd)
.envs(&component.env_vars)
.spawn();
std::thread::sleep(std::time::Duration::from_secs(2));
@ -787,14 +773,4 @@ impl PackageManager {
}
}
fn generate_secure_password(&self, length: usize) -> String {
let mut rng = rand::rng();
(0..length)
.map(|_| {
let byte = rand::Rng::sample(&mut rng, Alphanumeric);
char::from(byte)
})
.collect()
}
}

View file

@ -151,3 +151,33 @@ pub fn create_conn() -> Result<DbPool, r2d2::Error> {
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::builder().build(manager)
}
pub fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
if let Some(stripped) = url.strip_prefix("postgres://") {
let parts: Vec<&str> = stripped.split('@').collect();
if parts.len() == 2 {
let user_pass: Vec<&str> = parts[0].split(':').collect();
let host_db: Vec<&str> = parts[1].split('/').collect();
if user_pass.len() >= 2 && host_db.len() >= 2 {
let username = user_pass[0].to_string();
let password = user_pass[1].to_string();
let host_port: Vec<&str> = host_db[0].split(':').collect();
let server = host_port[0].to_string();
let port = host_port
.get(1)
.and_then(|p| p.parse().ok())
.unwrap_or(5432);
let database = host_db[1].to_string();
return (username, password, server, port, database);
}
}
}
(
"gbuser".to_string(),
"".to_string(),
"localhost".to_string(),
5432,
"botserver".to_string(),
)
}