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 { 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(®ister_output.stderr); if !err.contains("already registered") && !err.is_empty() { return Err(anyhow::anyhow!("Runner registration failed: {}", err)); } } Ok(()) }