⎇ 3f8a3ea4b6d3de529e42ae418d486bc81cca308e
crates/modules/matilda/matilda-core/src/container.rs
Historia
Quién tocó qué
crudo
feat(matilda): administración de servidores — core + config + plan
1//! `Container` — la especificación declarativa de un contenedor Docker.
2//!
3//! Es sólo el *deseo*: qué imagen, qué puertos, qué entorno. Ejecutar
4//! Docker es trabajo de capas superiores; aquí el contenedor es un dato
5//! comparable (`PartialEq`) para que el plan detecte cambios.
6
7use serde::{Deserialize, Serialize};
8
9/// Política de reinicio del contenedor.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
11#[serde(rename_all = "kebab-case")]
12pub enum RestartPolicy {
13 /// Nunca reiniciar.
14 #[default]
15 No,
16 /// Reiniciar sólo si salió con error.
17 OnFailure,
18 /// Reiniciar siempre.
19 Always,
20 /// Reiniciar salvo que se haya detenido a mano.
21 UnlessStopped,
22}
23
24impl RestartPolicy {
25 /// Valor tal como lo espera el flag `--restart` de Docker.
26 pub fn docker_flag(self) -> &'static str {
27 match self {
28 RestartPolicy::No => "no",
29 RestartPolicy::OnFailure => "on-failure",
30 RestartPolicy::Always => "always",
31 RestartPolicy::UnlessStopped => "unless-stopped",
32 }
33 }
34}
35
36/// Un mapeo de puerto `host → contenedor`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub struct PortMap {
39 pub host: u16,
40 pub container: u16,
41}
42
43impl PortMap {
44 pub fn new(host: u16, container: u16) -> Self {
45 Self { host, container }
46 }
47}
48
49/// La especificación declarativa de un contenedor. Clave única: `name`.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct Container {
52 pub name: String,
53 /// Imagen con etiqueta — `"nginx:1.27"`, `"postgres:16"`.
54 pub image: String,
55 pub ports: Vec<PortMap>,
56 /// Variables de entorno, ordenadas por clave para comparación estable.
57 pub env: Vec<(String, String)>,
58 /// Volúmenes `ruta_host → ruta_contenedor`.
59 pub volumes: Vec<(String, String)>,
60 pub restart: RestartPolicy,
61}
62
63impl Container {
64 /// Contenedor mínimo: nombre + imagen.
65 pub fn new(name: impl Into<String>, image: impl Into<String>) -> Self {
66 Self {
67 name: name.into(),
68 image: image.into(),
69 ports: Vec::new(),
70 env: Vec::new(),
71 volumes: Vec::new(),
72 restart: RestartPolicy::default(),
73 }
74 }
75
76 /// Publica un puerto (encadenable).
77 pub fn with_port(mut self, host: u16, container: u16) -> Self {
78 self.ports.push(PortMap::new(host, container));
79 self
80 }
81
82 /// Define una variable de entorno (encadenable). El vector se
83 /// mantiene ordenado por clave para que dos contenedores con el
84 /// mismo entorno comparen iguales sin importar el orden de llamada.
85 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
86 let key = key.into();
87 self.env.retain(|(k, _)| k != &key);
88 self.env.push((key, value.into()));
89 self.env.sort_by(|a, b| a.0.cmp(&b.0));
90 self
91 }
92
93 /// Monta un volumen (encadenable).
94 pub fn with_volume(
95 mut self,
96 host_path: impl Into<String>,
97 container_path: impl Into<String>,
98 ) -> Self {
99 self.volumes.push((host_path.into(), container_path.into()));
100 self
101 }
102
103 /// Fija la política de reinicio (encadenable).
104 pub fn with_restart(mut self, restart: RestartPolicy) -> Self {
105 self.restart = restart;
106 self
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn env_order_does_not_affect_equality() {
116 let a = Container::new("c", "img").with_env("B", "2").with_env("A", "1");
117 let b = Container::new("c", "img").with_env("A", "1").with_env("B", "2");
118 assert_eq!(a, b);
119 }
120
121 #[test]
122 fn with_env_overwrites_same_key() {
123 let c = Container::new("c", "img").with_env("K", "old").with_env("K", "new");
124 assert_eq!(c.env, vec![("K".to_string(), "new".to_string())]);
125 }
126
127 #[test]
128 fn restart_flags_match_docker() {
129 assert_eq!(RestartPolicy::UnlessStopped.docker_flag(), "unless-stopped");
130 assert_eq!(RestartPolicy::default(), RestartPolicy::No);
131 }
132}