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,63 @@
//! `matilda-config` — del modelo declarativo a archivos de configuración.
//!
//! Funciones puras: toman un tipo de `matilda-core` y devuelven el texto
//! de configuración listo para escribir en el servidor. No tocan disco
//! ni Docker — sólo construyen strings, así que cada salida es testeable
//! y determinista.
//!
//! - [`docker`] — `Container` → `docker run` / servicio docker-compose.
//! - [`nginx`] — `VHost` → bloque `server` de nginx.
#![forbid(unsafe_code)]
pub mod docker;
pub mod nginx;
pub use docker::{compose_service, docker_run_command};
pub use nginx::nginx_server_block;
use matilda_core::Inventory;
/// Renderiza el `docker-compose.yml` completo de un inventario.
pub fn compose_file(inv: &Inventory) -> String {
let mut out = String::from("services:\n");
for c in inv.containers() {
out.push_str(&compose_service(c));
}
out
}
/// Renderiza el archivo de sites de nginx — un bloque `server` por
/// vhost, separados por una línea en blanco.
pub fn nginx_sites(inv: &Inventory) -> String {
inv.vhosts()
.map(nginx_server_block)
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use matilda_core::{Container, VHost};
#[test]
fn compose_file_lists_every_container() {
let mut inv = Inventory::new();
inv.add_container(Container::new("web", "nginx"));
inv.add_container(Container::new("db", "postgres:16"));
let yaml = compose_file(&inv);
assert!(yaml.starts_with("services:\n"));
assert!(yaml.contains(" web:\n") && yaml.contains(" db:\n"));
}
#[test]
fn nginx_sites_renders_every_vhost() {
let mut inv = Inventory::new();
inv.add_vhost(VHost::to_container("a.com", "web", 80));
inv.add_vhost(VHost::to_container("b.com", "web", 80));
let conf = nginx_sites(&inv);
assert!(conf.contains("server_name a.com;"));
assert!(conf.contains("server_name b.com;"));
}
}