Files
brahman/crates/modules/matilda/matilda-config/src/docker.rs
T
sergio 3f8a3ea4b6 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>
2026-05-20 17:06:36 +00:00

106 lines
3.3 KiB
Rust

//! Renderizado de un [`Container`] a Docker — `docker run` y compose.
use matilda_core::Container;
/// Comando `docker run` de un contenedor, en una sola línea. El orden de
/// los flags es fijo (determinista): `-d --name --restart -p -e -v img`.
pub fn docker_run_command(c: &Container) -> String {
let mut parts: Vec<String> = vec![
"docker".into(),
"run".into(),
"-d".into(),
"--name".into(),
c.name.clone(),
"--restart".into(),
c.restart.docker_flag().into(),
];
for p in &c.ports {
parts.push("-p".into());
parts.push(format!("{}:{}", p.host, p.container));
}
for (k, v) in &c.env {
parts.push("-e".into());
parts.push(format!("{k}={v}"));
}
for (host, container) in &c.volumes {
parts.push("-v".into());
parts.push(format!("{host}:{container}"));
}
parts.push(c.image.clone());
parts.join(" ")
}
/// Bloque de servicio para un `docker-compose.yml`. Viene indentado para
/// colocarse tal cual bajo la clave `services:`.
pub fn compose_service(c: &Container) -> String {
let mut out = String::new();
out.push_str(&format!(" {}:\n", c.name));
out.push_str(&format!(" image: {}\n", c.image));
out.push_str(&format!(" restart: {}\n", c.restart.docker_flag()));
if !c.ports.is_empty() {
out.push_str(" ports:\n");
for p in &c.ports {
out.push_str(&format!(" - \"{}:{}\"\n", p.host, p.container));
}
}
if !c.env.is_empty() {
out.push_str(" environment:\n");
for (k, v) in &c.env {
out.push_str(&format!(" - {k}={v}\n"));
}
}
if !c.volumes.is_empty() {
out.push_str(" volumes:\n");
for (host, container) in &c.volumes {
out.push_str(&format!(" - {host}:{container}\n"));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use matilda_core::RestartPolicy;
fn sample() -> Container {
Container::new("web", "nginx:1.27")
.with_port(8080, 80)
.with_env("TZ", "America/Caracas")
.with_volume("/srv/web", "/usr/share/nginx/html")
.with_restart(RestartPolicy::Always)
}
#[test]
fn run_command_has_all_flags() {
let cmd = docker_run_command(&sample());
assert!(cmd.starts_with("docker run -d --name web --restart always"));
assert!(cmd.contains("-p 8080:80"));
assert!(cmd.contains("-e TZ=America/Caracas"));
assert!(cmd.contains("-v /srv/web:/usr/share/nginx/html"));
assert!(cmd.ends_with("nginx:1.27"));
}
#[test]
fn run_command_is_deterministic() {
assert_eq!(docker_run_command(&sample()), docker_run_command(&sample()));
}
#[test]
fn compose_service_indents_under_services() {
let yaml = compose_service(&sample());
assert!(yaml.contains(" web:\n"));
assert!(yaml.contains(" image: nginx:1.27\n"));
assert!(yaml.contains(" restart: always\n"));
assert!(yaml.contains(" - \"8080:80\"\n"));
}
#[test]
fn minimal_container_omits_empty_sections() {
let yaml = compose_service(&Container::new("bare", "alpine"));
assert!(!yaml.contains("ports:"));
assert!(!yaml.contains("environment:"));
assert!(!yaml.contains("volumes:"));
}
}