feat(matilda): matilda-discover — estado actual del servidor

Observa qué contenedores y vhosts existen (docker ps + sitios de
nginx) y reconstruye un Inventory "actual" que matilda-plan diferencia
contra el deseado: detecta correctamente qué crear y qué eliminar
(huérfanos). Parseo puro y testeable; sólo discover_local toca el
sistema. 6 tests.

La CLI gana el flag --discover en plan/script/apply: reconcilia
contra el estado real de la máquina en vez de partir de vacío.

matilda: 7 crates + CLI, ~48 tests. Pendiente: matilda-app (GPUI) y
la inspección detallada (docker inspect) para detectar drift de
configuración, no sólo presencia.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 20:31:26 +00:00
parent df8f92fbb0
commit 93e003be0d
6 changed files with 210 additions and 11 deletions
+1
View File
@@ -19,6 +19,7 @@ matilda-plan = { path = "../../modules/matilda/matilda-plan" }
matilda-apply = { path = "../../modules/matilda/matilda-apply" }
matilda-ghost = { path = "../../modules/matilda/matilda-ghost" }
matilda-linker = { path = "../../modules/matilda/matilda-linker" }
matilda-discover = { path = "../../modules/matilda/matilda-discover" }
clap = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
+30 -11
View File
@@ -40,18 +40,26 @@ enum Cmd {
/// Estado actual del servidor (por defecto: vacío).
#[arg(long)]
current: Option<PathBuf>,
/// Descubre el estado actual de esta máquina (docker + nginx).
#[arg(long)]
discover: bool,
},
/// Emite el script de shell que aplicaría el plan.
Script {
inventory: PathBuf,
#[arg(long)]
current: Option<PathBuf>,
#[arg(long)]
discover: bool,
},
/// Aplica el plan: local, en seco, o remoto por SSH.
Apply {
inventory: PathBuf,
#[arg(long)]
current: Option<PathBuf>,
/// Descubre el estado actual de esta máquina antes de reconciliar.
#[arg(long)]
discover: bool,
/// Simula sin tocar nada.
#[arg(long)]
dry_run: bool,
@@ -71,11 +79,22 @@ fn load(path: &PathBuf) -> Result<Inventory, String> {
serde_json::from_str(&text).map_err(|e| format!("JSON inválido en {}: {e}", path.display()))
}
/// Carga el inventario actual, o uno vacío si no se especificó.
fn load_current(current: &Option<PathBuf>) -> Result<Inventory, String> {
match current {
Some(p) => load(p),
None => Ok(Inventory::new()),
/// Resuelve el inventario "actual" contra el que reconciliar:
/// `--discover` observa esta máquina; `--current` lee un archivo; si no,
/// se parte de un inventario vacío (todo es creación).
fn current_inventory(
discover: bool,
current: &Option<PathBuf>,
desired: &Inventory,
) -> Result<Inventory, String> {
if discover {
let state = matilda_discover::discover_local();
Ok(matilda_discover::observed_inventory(&state, desired))
} else {
match current {
Some(p) => load(p),
None => Ok(Inventory::new()),
}
}
}
@@ -155,9 +174,9 @@ fn run() -> Result<(), String> {
println!("{json}");
}
Cmd::Plan { inventory, current } => {
Cmd::Plan { inventory, current, discover } => {
let desired = load(&inventory)?;
let p = plan(&load_current(&current)?, &desired);
let p = plan(&current_inventory(discover, &current, &desired)?, &desired);
if p.is_empty() {
println!("Sin cambios: el servidor ya está al día.");
} else {
@@ -174,15 +193,15 @@ fn run() -> Result<(), String> {
}
}
Cmd::Script { inventory, current } => {
Cmd::Script { inventory, current, discover } => {
let desired = load(&inventory)?;
let p = plan(&load_current(&current)?, &desired);
let p = plan(&current_inventory(discover, &current, &desired)?, &desired);
print!("{}", steps_to_script(&plan_to_steps(&p, &desired)));
}
Cmd::Apply { inventory, current, dry_run, host, password } => {
Cmd::Apply { inventory, current, discover, dry_run, host, password } => {
let desired = load(&inventory)?;
let p = plan(&load_current(&current)?, &desired);
let p = plan(&current_inventory(discover, &current, &desired)?, &desired);
let steps = plan_to_steps(&p, &desired);
if steps.is_empty() {
println!("Sin cambios: nada que aplicar.");