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,88 @@
//! `VHost` — un host virtual de proxy inverso.
use serde::{Deserialize, Serialize};
/// El destino al que un `VHost` reenvía el tráfico.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Upstream {
/// Una dirección `host:puerto` literal.
Address(String),
/// Un contenedor del inventario, por nombre y puerto interno.
Container { name: String, port: u16 },
}
/// Un host virtual: un dominio que se reenvía a un upstream. Clave
/// única: `domain`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VHost {
pub domain: String,
pub upstream: Upstream,
/// Si se sirve sobre HTTPS.
pub tls: bool,
/// Dominios alternativos que resuelven al mismo upstream.
pub aliases: Vec<String>,
}
impl VHost {
/// VHost que apunta a una dirección literal.
pub fn to_address(domain: impl Into<String>, address: impl Into<String>) -> Self {
Self {
domain: domain.into(),
upstream: Upstream::Address(address.into()),
tls: false,
aliases: Vec::new(),
}
}
/// VHost que apunta a un contenedor del inventario.
pub fn to_container(
domain: impl Into<String>,
container: impl Into<String>,
port: u16,
) -> Self {
Self {
domain: domain.into(),
upstream: Upstream::Container { name: container.into(), port },
tls: false,
aliases: Vec::new(),
}
}
/// Activa TLS (encadenable).
pub fn with_tls(mut self) -> Self {
self.tls = true;
self
}
/// Añade un alias de dominio (encadenable).
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.aliases.push(alias.into());
self
}
/// Nombre del contenedor del que depende, si el upstream es uno.
pub fn depends_on_container(&self) -> Option<&str> {
match &self.upstream {
Upstream::Container { name, .. } => Some(name),
Upstream::Address(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn container_upstream_reports_its_dependency() {
let v = VHost::to_container("app.example.com", "web", 8080).with_tls();
assert_eq!(v.depends_on_container(), Some("web"));
assert!(v.tls);
}
#[test]
fn address_upstream_has_no_container_dependency() {
let v = VHost::to_address("static.example.com", "10.0.0.9:80");
assert_eq!(v.depends_on_container(), None);
}
}