mod cliente;
use std::time::Duration;
use thiserror::Error;
pub use cliente::Cliente;
pub struct Orden {
pub cmd: Vec<String>,
pub env: Vec<String>,
}
impl std::fmt::Debug for Orden {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let claves: Vec<&str> = self
.env
.iter()
.map(|variable| variable.split('=').next().unwrap_or(variable))
.collect();
f.debug_struct("Orden")
.field("cmd", &self.cmd)
.field("env", &claves)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Canal {
Salida,
Error,
}
#[derive(Debug)]
pub struct Trozo {
pub canal: Canal,
pub bytes: Vec<u8>,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("el socket del motor de contenedores no existe: {socket}")]
SocketInexistente {
socket: String,
},
#[error("no se pudo hablar con el motor de contenedores por {socket}: {causa}")]
MotorNoDisponible {
socket: String,
#[source]
causa: bollard::errors::Error,
},
#[error(
"el motor de contenedores contestó {status_code} sobre el contenedor \
`{contenedor}`: {message}"
)]
MotorContesta {
contenedor: String,
status_code: u16,
message: String,
},
#[error(
"no se pudo hablar con el motor de contenedores sobre el contenedor \
`{contenedor}`: {causa}"
)]
MotorNoContesta {
contenedor: String,
#[source]
causa: bollard::errors::Error,
},
#[error("el motor de contenedores no dejó leer la salida de la orden en `{contenedor}`")]
SinSalida {
contenedor: String,
},
#[error(
"el motor de contenedores no informó un código de salida interpretable para la \
orden en `{contenedor}`"
)]
SinCodigoDeSalida {
contenedor: String,
},
#[error("la orden en `{contenedor}` no produjo salida en {inactividad:?}: puede estar colgada")]
ExtraccionEstancada {
contenedor: String,
inactividad: Duration,
},
}
impl Error {
fn del_contenedor(contenedor: &str, causa: bollard::errors::Error) -> Self {
match causa {
bollard::errors::Error::DockerResponseServerError {
status_code,
message,
} => Self::MotorContesta {
contenedor: contenedor.to_owned(),
status_code,
message,
},
causa => Self::MotorNoContesta {
contenedor: contenedor.to_owned(),
causa,
},
}
}
fn del_socket(socket: &str, causa: bollard::errors::Error) -> Self {
match causa {
bollard::errors::Error::SocketNotFoundError(_) => Self::SocketInexistente {
socket: socket.to_owned(),
},
causa => Self::MotorNoDisponible {
socket: socket.to_owned(),
causa,
},
}
}
}
#[cfg(test)]
mod tests {
use bollard::errors::Error as ErrorDelMotor;
use super::{Canal, Error, Orden};
const MENSAJE_DEL_404: &str =
"no container with name or ID \"mi-contenedor\" found: no such container";
const MENSAJE_DEL_500: &str = "must provide at least one stream to attach to: invalid argument";
#[test]
fn un_404_del_motor_nombra_el_contenedor() {
let medida = ErrorDelMotor::DockerResponseServerError {
status_code: 404,
message: MENSAJE_DEL_404.to_owned(),
};
let display_de_la_biblioteca = format!("{medida}");
let error = Error::del_contenedor("mi-contenedor", medida);
let motivo = error.to_string();
let debug = format!("{error:?}");
assert!(
motivo.contains("mi-contenedor"),
"el motivo no nombra el contenedor: {motivo}"
);
assert!(
motivo.contains("404"),
"el motivo no lleva el código: {motivo}"
);
assert!(
motivo.contains(MENSAJE_DEL_404),
"el motivo no conserva el mensaje del motor: {motivo}"
);
assert_ne!(
motivo, display_de_la_biblioteca,
"el motivo es el `Display` de bollard tal cual"
);
assert!(
debug.contains("mi-contenedor") && debug.contains("404"),
"el Debug del error no lleva lo que hace falta para diagnosticarlo: {debug}"
);
}
#[test]
fn el_motivo_nombra_el_contenedor_aunque_el_motor_no_lo_haga() {
let medida = ErrorDelMotor::DockerResponseServerError {
status_code: 404,
message: "no such container".to_owned(),
};
let motivo = Error::del_contenedor("mi-contenedor", medida).to_string();
assert!(
motivo.contains("mi-contenedor"),
"el motivo se conformó con el texto del motor y no nombró el contenedor: {motivo}"
);
}
#[test]
fn un_500_del_motor_conserva_su_codigo_y_su_mensaje() {
let medida = ErrorDelMotor::DockerResponseServerError {
status_code: 500,
message: MENSAJE_DEL_500.to_owned(),
};
let error = Error::del_contenedor("mi-contenedor", medida);
let motivo = error.to_string();
assert!(
matches!(
&error,
Error::MotorContesta {
status_code: 500,
message,
..
} if message == MENSAJE_DEL_500
),
"el 500 no llegó como respuesta del motor con su mensaje: {error:?}"
);
assert!(
motivo.contains(MENSAJE_DEL_500),
"el motivo no conserva el mensaje del motor: {motivo}"
);
}
#[test]
fn un_fallo_que_no_es_del_servidor_se_conserva_como_causa() {
let error = Error::del_contenedor("mi-contenedor", ErrorDelMotor::RequestTimeoutError);
assert!(
matches!(&error, Error::MotorNoContesta { contenedor, .. } if contenedor == "mi-contenedor"),
"un fallo de transporte no puede llegar como respuesta del motor: {error:?}"
);
assert!(
std::error::Error::source(&error).is_some(),
"la causa del fallo de transporte se perdió: {error:?}"
);
}
#[test]
fn un_socket_inexistente_se_reconoce_por_su_ruta() {
let error = Error::del_socket(
"unix:///no/existe/kesto.sock",
ErrorDelMotor::SocketNotFoundError("/no/existe/kesto.sock".to_owned()),
);
assert!(
matches!(
&error,
Error::SocketInexistente { socket } if socket == "unix:///no/existe/kesto.sock"
),
"el socket no se reconoció con la ruta que se pidió: {error:?}"
);
assert!(
error.to_string().contains("unix:///no/existe/kesto.sock"),
"el motivo no nombra el socket: {error}"
);
}
#[test]
fn el_debug_de_una_orden_no_publica_el_entorno() {
let orden = Orden {
cmd: vec!["mariadb-dump".to_owned(), "--all-databases".to_owned()],
env: vec!["MYSQL_PWD=secreto-de-prueba".to_owned()],
};
let debug = format!("{orden:?}");
assert!(
debug.contains("mariadb-dump"),
"el Debug no publica el comando: {debug}"
);
assert!(
debug.contains("MYSQL_PWD"),
"el Debug no publica el nombre de la variable: {debug}"
);
assert!(
!debug.contains("secreto-de-prueba"),
"el Debug publicó el valor del entorno: {debug}"
);
}
#[test]
fn los_dos_canales_son_distintos_y_se_leen() {
assert_ne!(Canal::Salida, Canal::Error);
assert_eq!(format!("{:?}", Canal::Salida), "Salida");
assert_eq!(format!("{:?}", Canal::Error), "Error");
}
}