generalbots/src/core/package_manager/alm_setup.rs
Rodrigo Rodriguez (Pragmatismo) fb2e5242da
All checks were successful
BotServer CI/CD / build (push) Successful in 55m52s
fix: Vault seeding, service health checks, and restart idempotency
- Replace hardcoded passwords with generate_random_string() for all Vault-seeded services
- Add valkey-cli, nc to SafeCommand allowlist; fix PATH in all 4 execution methods
- Fix empty Vault KV values ('none' placeholder) preventing 'Failed to parse K=V' errors
- Fix special chars in generated passwords triggering shell injection false positives
- Add ALM app.ini creation with absolute paths for Forgejo CLI
- Increase Qdrant timeout 15s→45s, ALM wait 5s→20s
- Persist file_states and kb_states to disk for .bas/KB idempotency across restarts
- Add duplicate check to use_website registration (debug log for existing)
- Remove dead code (SERVER_START_EPOCH, server_epoch)
- Add generate_random_string() to shared mod.rs, remove duplicates
2026-04-01 12:22:57 -03:00

179 lines
6.4 KiB
Rust

use std::collections::HashMap;
use std::path::PathBuf;
use log::{info, warn};
use super::generate_random_string;
pub async fn setup_alm() -> anyhow::Result<()> {
let stack_path_raw = std::env::var("BOTSERVER_STACK_PATH")
.unwrap_or_else(|_| "./botserver-stack".to_string());
let stack_path = std::fs::canonicalize(&stack_path_raw)
.unwrap_or_else(|_| PathBuf::from(&stack_path_raw));
let stack_path_str = stack_path.to_string_lossy().to_string();
let data_path = stack_path.join("data/alm");
let config_path = stack_path.join("conf/alm-ci/config.yaml");
// Check Vault if already set up
if let Ok(secrets_manager) = crate::core::secrets::SecretsManager::from_env() {
if secrets_manager.is_enabled() {
if let Ok(secrets) = secrets_manager.get_secret(crate::core::secrets::SecretPaths::ALM).await {
if let (Some(user), Some(token)) = (secrets.get("username"), secrets.get("runner_token")) {
if !user.is_empty() && !token.is_empty() {
info!("ALM is already configured in Vault for user {}", user);
return Ok(());
}
}
}
}
}
info!("Initializing ALM (Forgejo) and CI Runner...");
// Ensure ALM config directory exists and create minimal app.ini
let alm_conf_dir = stack_path.join("conf/alm");
std::fs::create_dir_all(&alm_conf_dir)
.map_err(|e| anyhow::anyhow!("Failed to create ALM config dir: {}", e))?;
let app_ini_path = alm_conf_dir.join("app.ini");
if !app_ini_path.exists() {
let app_ini_content = format!(
r#"APP_NAME = General Bots ALM
RUN_USER = alm
WORK_PATH = {}/data/alm
[repository]
ROOT = {}/data/alm/repositories
[database]
DB_TYPE = sqlite3
PATH = {}/data/alm/gitea.db
[server]
HTTP_PORT = 3000
DOMAIN = localhost
ROOT_URL = http://localhost:3000
[security]
INSTALL_LOCK = true
"#,
stack_path_str, stack_path_str, stack_path_str
);
std::fs::write(&app_ini_path, app_ini_content)
.map_err(|e| anyhow::anyhow!("Failed to write app.ini: {}", e))?;
info!("Created minimal ALM app.ini at {}", app_ini_path.display());
}
// Generate credentials and attempt to configure via HTTP API
let username = "botserver";
let password = generate_random_string(32);
let alm_url = "http://localhost:3000";
// Try to create admin user and get runner token via HTTP API
// Note: Forgejo CLI binary may segfault on some systems, so we use curl
let runner_token = match try_alm_api_setup(alm_url, &username, &password, data_path.to_str().unwrap_or(".")).await {
Ok(token) => token,
Err(e) => {
warn!("ALM automated setup unavailable via API: {}", e);
warn!("ALM will need manual configuration. Create admin user and runner token via web UI.");
// Store placeholder credentials
let placeholder_token = generate_random_string(40);
placeholder_token
}
};
info!("Generated ALM Runner token successfully");
// Register runner with forgejo-runner CLI
let runner_bin = stack_path.join("bin/alm-ci/forgejo-runner");
if runner_bin.exists() {
match register_runner(&runner_bin, &runner_token, config_path.to_str().unwrap_or("config.yaml"), alm_url).await {
Ok(_) => info!("ALM CI Runner successfully registered!"),
Err(e) => warn!("Failed to register ALM runner: {}", e),
}
} else {
warn!("Forgejo runner binary not found at {}", runner_bin.display());
}
// Store in Vault
if let Ok(secrets_manager) = crate::core::secrets::SecretsManager::from_env() {
if secrets_manager.is_enabled() {
let mut secrets = HashMap::new();
secrets.insert("url".to_string(), alm_url.to_string());
secrets.insert("username".to_string(), username.to_string());
secrets.insert("password".to_string(), password);
secrets.insert("runner_token".to_string(), runner_token);
match secrets_manager.put_secret(crate::core::secrets::SecretPaths::ALM, secrets).await {
Ok(_) => info!("ALM credentials and runner token stored in Vault"),
Err(e) => warn!("Failed to store ALM credentials in Vault: {}", e),
}
}
}
Ok(())
}
/// Attempt to configure ALM via HTTP API (since CLI may segfault)
async fn try_alm_api_setup(
base_url: &str,
_username: &str,
_password: &str,
_home: &str,
) -> anyhow::Result<String> {
use crate::security::command_guard::SafeCommand;
// Check if ALM is responding
let check = SafeCommand::new("curl")?
.args(&["-s", "-o", "/dev/null", "-w", "%{http_code}", &format!("{}/api/v1/version", base_url)])?
.execute()?;
let status = String::from_utf8_lossy(&check.stdout).trim().to_string();
if status != "200" && status != "401" && status != "403" {
return Err(anyhow::anyhow!("ALM not responding (HTTP {})", status));
}
info!("ALM is responding at {}", base_url);
// Try to get registration token from the API
// This requires admin auth, which we may not have yet
// For now, generate a placeholder token and let operator configure manually
let token = generate_random_string(40);
info!("ALM API available but requires manual admin setup. Generated placeholder runner token.");
Ok(token)
}
/// Register forgejo-runner with the instance
async fn register_runner(
runner_bin: &std::path::Path,
runner_token: &str,
config_path: &str,
instance_url: &str,
) -> anyhow::Result<()> {
use crate::security::command_guard::SafeCommand;
let register_output = SafeCommand::new(runner_bin.to_str().unwrap_or("forgejo-runner"))?
.arg("register")?
.arg("--instance")?
.arg(instance_url)?
.arg("--token")?
.arg(runner_token)?
.arg("--name")?
.arg("gbo")?
.arg("--labels")?
.trusted_arg("ubuntu-latest:docker://node:20-bookworm")?
.arg("--no-interactive")?
.arg("--config")?
.arg(config_path)?
.execute()?;
if !register_output.status.success() {
let err = String::from_utf8_lossy(&register_output.stderr);
if !err.contains("already registered") && !err.is_empty() {
return Err(anyhow::anyhow!("Runner registration failed: {}", err));
}
}
Ok(())
}