- Agregados campos deleted, deleted_at, deleted_reason a MonitoredApp - Implementado soft_delete_app() y restore_app() en ConfigManager - Modificado get_apps() para filtrar apps eliminadas por defecto - Agregados métodos get_all_apps() y get_deleted_apps() - Actualizado unregister_app() para usar soft delete en lugar de hard delete - Creados endpoints: * GET /api/apps/deleted - Ver historial de apps eliminadas * POST /api/apps/:name/restore - Restaurar app eliminada - Agregada sección de Historial en index.html con UI completa - Botón de restaurar para cada app eliminada - El servicio systemd se elimina físicamente, solo el JSON mantiene historial - Permite auditoría y recuperación de apps eliminadas accidentalmente
317 lines
10 KiB
Rust
317 lines
10 KiB
Rust
use serde::{Serialize, Deserialize};
|
|
use std::fs::{self, create_dir_all};
|
|
use std::path::Path;
|
|
use std::sync::{Arc, RwLock, OnceLock};
|
|
use crate::logger::get_logger;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MonitoredApp {
|
|
/// Nombre de la aplicación
|
|
pub name: String,
|
|
|
|
/// Nombre del servicio systemd (ej: siax-app-TAREAS.service)
|
|
#[serde(default)]
|
|
pub service_name: String,
|
|
|
|
/// Ruta completa al directorio de la aplicación (WorkingDirectory)
|
|
#[serde(default)]
|
|
pub path: String,
|
|
|
|
/// Puerto donde escucha la aplicación
|
|
pub port: i32,
|
|
|
|
/// Archivo de entrada (ej: server.js, app.js)
|
|
#[serde(default)]
|
|
pub entry_point: String,
|
|
|
|
/// Ruta completa al binario de node/python
|
|
#[serde(default)]
|
|
pub node_bin: String,
|
|
|
|
/// Modo de ejecución (production, development, test)
|
|
#[serde(default = "default_mode")]
|
|
pub mode: String,
|
|
|
|
/// Ruta completa al archivo .service de systemd
|
|
#[serde(default)]
|
|
pub service_file_path: String,
|
|
|
|
/// Fecha de registro (ISO 8601)
|
|
#[serde(default, skip_serializing_if = "String::is_empty", rename = "reg")]
|
|
pub registered_at: String,
|
|
|
|
// --- SOFT DELETE FIELDS ---
|
|
/// Indica si la app fue eliminada (soft delete)
|
|
#[serde(default)]
|
|
pub deleted: bool,
|
|
|
|
/// Fecha de eliminación (ISO 8601)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub deleted_at: Option<String>,
|
|
|
|
/// Razón de eliminación (opcional)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub deleted_reason: Option<String>,
|
|
|
|
// DEPRECATED: Mantener por compatibilidad con versiones antiguas
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub systemd_service: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub created_at: Option<String>,
|
|
}
|
|
|
|
fn default_mode() -> String {
|
|
"production".to_string()
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AppConfig {
|
|
pub apps: Vec<MonitoredApp>,
|
|
}
|
|
|
|
impl Default for AppConfig {
|
|
fn default() -> Self {
|
|
AppConfig {
|
|
apps: vec![]
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ConfigManager {
|
|
config_path: String,
|
|
config: Arc<RwLock<AppConfig>>,
|
|
}
|
|
|
|
impl ConfigManager {
|
|
pub fn new(config_path: &str) -> Self {
|
|
let logger = get_logger();
|
|
|
|
// Crear directorio config si no existe
|
|
if let Some(parent) = Path::new(config_path).parent() {
|
|
if !parent.exists() {
|
|
logger.info("Config", &format!("Creando directorio: {}", parent.display()));
|
|
match create_dir_all(parent) {
|
|
Ok(_) => logger.info("Config", &format!("✅ Directorio creado: {}", parent.display())),
|
|
Err(e) => logger.error("Config", "Error creando directorio", Some(&e.to_string())),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cargar o crear configuración
|
|
let config = Self::load_config(config_path);
|
|
|
|
ConfigManager {
|
|
config_path: config_path.to_string(),
|
|
config: Arc::new(RwLock::new(config)),
|
|
}
|
|
}
|
|
|
|
fn load_config(path: &str) -> AppConfig {
|
|
let logger = get_logger();
|
|
|
|
match fs::read_to_string(path) {
|
|
Ok(content) => {
|
|
match serde_json::from_str(&content) {
|
|
Ok(config) => {
|
|
let app_count = if let AppConfig { apps } = &config { apps.len() } else { 0 };
|
|
logger.info("Config", &format!("✅ Configuración cargada: {} apps desde {}", app_count, path));
|
|
config
|
|
}
|
|
Err(e) => {
|
|
logger.error("Config", "Error parseando JSON, creando vacío", Some(&e.to_string()));
|
|
let default_config = AppConfig::default();
|
|
let _ = Self::save_config_to_file(path, &default_config);
|
|
default_config
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
logger.warning("Config", &format!("Archivo no encontrado ({}), creando vacío en: {}", e.kind(), path), None);
|
|
let default_config = AppConfig::default();
|
|
match Self::save_config_to_file(path, &default_config) {
|
|
Ok(_) => logger.info("Config", &format!("✅ Archivo de configuración creado: {}", path)),
|
|
Err(save_err) => logger.error("Config", "Error al crear archivo", Some(&save_err.to_string())),
|
|
}
|
|
default_config
|
|
}
|
|
}
|
|
}
|
|
|
|
fn save_config_to_file(path: &str, config: &AppConfig) -> std::io::Result<()> {
|
|
let json = serde_json::to_string_pretty(config)?;
|
|
fs::write(path, json)?;
|
|
println!("💾 Configuración guardada en: {}", path);
|
|
Ok(())
|
|
}
|
|
|
|
/// Obtiene las apps activas (no eliminadas)
|
|
pub fn get_apps(&self) -> Vec<MonitoredApp> {
|
|
let config = self.config.read().unwrap();
|
|
config.apps.iter()
|
|
.filter(|app| !app.deleted)
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
/// Obtiene TODAS las apps, incluyendo las eliminadas
|
|
pub fn get_all_apps(&self) -> Vec<MonitoredApp> {
|
|
let config = self.config.read().unwrap();
|
|
config.apps.clone()
|
|
}
|
|
|
|
/// Obtiene solo las apps eliminadas
|
|
pub fn get_deleted_apps(&self) -> Vec<MonitoredApp> {
|
|
let config = self.config.read().unwrap();
|
|
config.apps.iter()
|
|
.filter(|app| app.deleted)
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
/// Agrega una app con información completa
|
|
pub fn add_app_full(&self, app: MonitoredApp) -> Result<(), String> {
|
|
let mut config = self.config.write().unwrap();
|
|
|
|
// Verificar si ya existe
|
|
if config.apps.iter().any(|a| a.name == app.name) {
|
|
return Err(format!("La app '{}' ya está siendo monitoreada", app.name));
|
|
}
|
|
|
|
config.apps.push(app);
|
|
|
|
// Guardar en disco
|
|
match Self::save_config_to_file(&self.config_path, &config) {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => Err(format!("Error al guardar configuración: {}", e))
|
|
}
|
|
}
|
|
|
|
/// Método simplificado para compatibilidad (DEPRECATED)
|
|
#[deprecated(note = "Usar add_app_full() con MonitoredApp completo")]
|
|
pub fn add_app(&self, name: String, port: i32) -> Result<(), String> {
|
|
let service_name = format!("siax-app-{}.service", name);
|
|
let registered_at = chrono::Local::now().to_rfc3339();
|
|
|
|
let app = MonitoredApp {
|
|
name,
|
|
service_name,
|
|
path: String::new(),
|
|
port,
|
|
entry_point: String::new(),
|
|
node_bin: String::new(),
|
|
mode: "production".to_string(),
|
|
service_file_path: String::new(),
|
|
registered_at,
|
|
deleted: false,
|
|
deleted_at: None,
|
|
deleted_reason: None,
|
|
systemd_service: None,
|
|
created_at: None,
|
|
};
|
|
|
|
self.add_app_full(app)
|
|
}
|
|
|
|
/// Realiza un soft delete: marca la app como eliminada pero mantiene el registro
|
|
pub fn soft_delete_app(&self, name: &str, reason: Option<String>) -> Result<(), String> {
|
|
let mut config = self.config.write().unwrap();
|
|
|
|
// Buscar la app
|
|
let app = config.apps.iter_mut().find(|a| a.name == name && !a.deleted);
|
|
|
|
match app {
|
|
Some(app) => {
|
|
// Marcar como eliminada
|
|
app.deleted = true;
|
|
app.deleted_at = Some(chrono::Local::now().to_rfc3339());
|
|
app.deleted_reason = reason;
|
|
|
|
// Guardar en disco
|
|
match Self::save_config_to_file(&self.config_path, &config) {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => Err(format!("Error al guardar configuración: {}", e))
|
|
}
|
|
}
|
|
None => Err(format!("La app '{}' no se encontró o ya está eliminada", name))
|
|
}
|
|
}
|
|
|
|
/// Restaura una app previamente eliminada (soft delete)
|
|
pub fn restore_app(&self, name: &str) -> Result<(), String> {
|
|
let mut config = self.config.write().unwrap();
|
|
|
|
// Buscar la app eliminada
|
|
let app = config.apps.iter_mut().find(|a| a.name == name && a.deleted);
|
|
|
|
match app {
|
|
Some(app) => {
|
|
// Restaurar
|
|
app.deleted = false;
|
|
app.deleted_at = None;
|
|
app.deleted_reason = None;
|
|
|
|
// Guardar en disco
|
|
match Self::save_config_to_file(&self.config_path, &config) {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => Err(format!("Error al guardar configuración: {}", e))
|
|
}
|
|
}
|
|
None => Err(format!("La app '{}' no se encontró en apps eliminadas", name))
|
|
}
|
|
}
|
|
|
|
/// HARD DELETE: Elimina permanentemente una app del JSON (usar con precaución)
|
|
pub fn remove_app(&self, name: &str) -> Result<(), String> {
|
|
let mut config = self.config.write().unwrap();
|
|
|
|
let original_len = config.apps.len();
|
|
config.apps.retain(|app| app.name != name);
|
|
|
|
if config.apps.len() == original_len {
|
|
return Err(format!("La app '{}' no se encontró", name));
|
|
}
|
|
|
|
// Guardar en disco
|
|
match Self::save_config_to_file(&self.config_path, &config) {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => Err(format!("Error al guardar configuración: {}", e))
|
|
}
|
|
}
|
|
|
|
pub fn get_arc(&self) -> Arc<RwLock<AppConfig>> {
|
|
Arc::clone(&self.config)
|
|
}
|
|
}
|
|
|
|
// Singleton global del ConfigManager
|
|
static CONFIG_MANAGER: OnceLock<ConfigManager> = OnceLock::new();
|
|
|
|
/// Determina la ruta del archivo de configuración
|
|
fn get_config_path() -> String {
|
|
// Prioridad de rutas:
|
|
// 1. Variable de entorno SIAX_CONFIG_PATH
|
|
// 2. /opt/siax-agent/config/monitored_apps.json (producción)
|
|
// 3. ./config/monitored_apps.json (desarrollo)
|
|
|
|
if let Ok(env_path) = std::env::var("SIAX_CONFIG_PATH") {
|
|
return env_path;
|
|
}
|
|
|
|
let prod_path = "/opt/siax-agent/config/monitored_apps.json";
|
|
if Path::new("/opt/siax-agent").exists() {
|
|
return prod_path.to_string();
|
|
}
|
|
|
|
"config/monitored_apps.json".to_string()
|
|
}
|
|
|
|
// ⚠️ IMPORTANTE: Esta función DEBE ser pública
|
|
pub fn get_config_manager() -> &'static ConfigManager {
|
|
CONFIG_MANAGER.get_or_init(|| {
|
|
let config_path = get_config_path();
|
|
let logger = get_logger();
|
|
logger.info("Config", &format!("Usando archivo de configuración: {}", config_path));
|
|
ConfigManager::new(&config_path)
|
|
})
|
|
}
|