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:
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "matilda"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "matilda — demostración de administración de servidores: inventario declarativo, renderizado de docker-compose y nginx, y plan de reconciliación."
|
||||
|
||||
[[bin]]
|
||||
name = "matilda"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
matilda-core = { path = "../../modules/matilda/matilda-core" }
|
||||
matilda-config = { path = "../../modules/matilda/matilda-config" }
|
||||
matilda-plan = { path = "../../modules/matilda/matilda-plan" }
|
||||
@@ -0,0 +1,98 @@
|
||||
//! `matilda` — demostración de administración de servidores.
|
||||
//!
|
||||
//! Declara un inventario *deseado*, renderiza su `docker-compose.yml` y
|
||||
//! su configuración nginx, y luego calcula el *plan* que lleva un
|
||||
//! servidor desde un estado actual distinto hasta el deseado.
|
||||
//!
|
||||
//! Smoke test legible del módulo: `cargo run -p matilda`.
|
||||
|
||||
use matilda_core::{Container, Host, Inventory, RestartPolicy, VHost};
|
||||
use matilda_config::{compose_file, nginx_sites};
|
||||
use matilda_plan::plan;
|
||||
|
||||
/// El inventario que queremos tener en el servidor.
|
||||
fn desired() -> Inventory {
|
||||
let mut inv = Inventory::new();
|
||||
inv.add_host(Host::new("edge-1", "10.0.0.1").with_tag("prod"));
|
||||
|
||||
inv.add_container(
|
||||
Container::new("web", "nginx:1.27")
|
||||
.with_port(8080, 80)
|
||||
.with_volume("/srv/site", "/usr/share/nginx/html")
|
||||
.with_restart(RestartPolicy::Always),
|
||||
);
|
||||
inv.add_container(
|
||||
Container::new("api", "ghcr.io/jls/api:2.4")
|
||||
.with_port(9000, 9000)
|
||||
.with_env("DATABASE_URL", "postgres://db/app")
|
||||
.with_restart(RestartPolicy::UnlessStopped),
|
||||
);
|
||||
inv.add_container(
|
||||
Container::new("db", "postgres:16")
|
||||
.with_env("POSTGRES_DB", "app")
|
||||
.with_volume("/srv/pgdata", "/var/lib/postgresql/data")
|
||||
.with_restart(RestartPolicy::Always),
|
||||
);
|
||||
|
||||
inv.add_vhost(VHost::to_container("jlsoltech.com", "web", 80).with_alias("www.jlsoltech.com").with_tls());
|
||||
inv.add_vhost(VHost::to_container("api.jlsoltech.com", "api", 9000).with_tls());
|
||||
inv
|
||||
}
|
||||
|
||||
/// El estado en que está el servidor hoy: `web` con imagen vieja, sin
|
||||
/// `api`, y un contenedor `legacy` que ya no se quiere.
|
||||
fn current() -> Inventory {
|
||||
let mut inv = Inventory::new();
|
||||
inv.add_host(Host::new("edge-1", "10.0.0.1").with_tag("prod"));
|
||||
inv.add_container(Container::new("web", "nginx:1.25").with_port(8080, 80));
|
||||
inv.add_container(Container::new("db", "postgres:16")
|
||||
.with_env("POSTGRES_DB", "app")
|
||||
.with_volume("/srv/pgdata", "/var/lib/postgresql/data")
|
||||
.with_restart(RestartPolicy::Always));
|
||||
inv.add_container(Container::new("legacy", "old/cgi:1"));
|
||||
inv.add_vhost(VHost::to_container("jlsoltech.com", "web", 80));
|
||||
inv
|
||||
}
|
||||
|
||||
fn rule(title: &str) {
|
||||
println!("\n── {title} {}", "─".repeat(56usize.saturating_sub(title.len())));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let desired = desired();
|
||||
|
||||
rule("docker-compose.yml (deseado)");
|
||||
print!("{}", compose_file(&desired));
|
||||
|
||||
rule("nginx — sites (deseado)");
|
||||
print!("{}", nginx_sites(&desired));
|
||||
|
||||
rule("plan de reconciliación (actual → deseado)");
|
||||
let current = current();
|
||||
let p = plan(¤t, &desired);
|
||||
if p.is_empty() {
|
||||
println!(" sin cambios: el servidor ya está al día.");
|
||||
} else {
|
||||
for (i, action) in p.actions.iter().enumerate() {
|
||||
println!(" {:>2}. {}", i + 1, action.describe());
|
||||
}
|
||||
println!(
|
||||
"\n {} acciones — {} crear, {} actualizar, {} eliminar.",
|
||||
p.len(),
|
||||
p.count(matilda_plan::Op::Create),
|
||||
p.count(matilda_plan::Op::Update),
|
||||
p.count(matilda_plan::Op::Remove),
|
||||
);
|
||||
}
|
||||
|
||||
let broken = desired.broken_vhosts();
|
||||
rule("consistencia");
|
||||
if broken.is_empty() {
|
||||
println!(" todos los vhosts apuntan a contenedores existentes. ✔");
|
||||
} else {
|
||||
for v in broken {
|
||||
println!(" ✘ vhost «{}» apunta a un contenedor inexistente", v.domain);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
Reference in New Issue
Block a user