feat(matilda): administración de servidores — core + config + plan

matilda-core: modelo declarativo (Host, Container, VHost, Inventory).
matilda-config: renderiza Container→docker-compose/docker run y
VHost→bloque server nginx (con TLS + redirección :80→:443).
matilda-plan: reconciliación pura actual→deseado con acciones
ordenadas por dependencia (contenedores antes que vhosts, removes
en orden inverso). Demo CLI en apps/matilda.

29 tests. Funciones puras, cero Docker/SSH/disco.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 17:06:36 +00:00
parent 71f6cf1306
commit 3f8a3ea4b6
18 changed files with 1190 additions and 0 deletions
@@ -0,0 +1,46 @@
//! `Host` — un servidor administrado.
use serde::{Deserialize, Serialize};
/// Un servidor bajo administración. La clave única es `name`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Host {
/// Nombre lógico — clave de inventario, no necesariamente el hostname.
pub name: String,
/// IP o nombre DNS por el que se alcanza.
pub address: String,
/// Etiquetas libres — `"prod"`, `"db"`, `"edge"`.
pub tags: Vec<String>,
}
impl Host {
pub fn new(name: impl Into<String>, address: impl Into<String>) -> Self {
Self { name: name.into(), address: address.into(), tags: Vec::new() }
}
/// Añade una etiqueta (encadenable). No duplica.
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
let tag = tag.into();
if !self.tags.contains(&tag) {
self.tags.push(tag);
}
self
}
/// `true` si el host lleva la etiqueta `tag`.
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.iter().any(|t| t == tag)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_tag_dedups() {
let h = Host::new("edge-1", "10.0.0.1").with_tag("prod").with_tag("prod");
assert_eq!(h.tags.len(), 1);
assert!(h.has_tag("prod"));
}
}