test: validate isolated Docker container lifecycle

This commit is contained in:
Julien Denizot
2026-06-24 11:29:28 +02:00
parent 15fdc98595
commit d16353a72a
3 changed files with 217 additions and 0 deletions
Generated
+1
View File
@@ -257,6 +257,7 @@ dependencies = [
"bollard",
"chrono",
"enuxia-aio-protocol",
"futures-util",
"rusqlite",
"serde_json",
"tempfile",
+1
View File
@@ -9,6 +9,7 @@ chrono = "0.4.45"
enuxia-aio-protocol = {
path = "../protocol"
}
futures-util = "0.3.32"
rusqlite = { version = "0.40.1", features = ["bundled"] }
serde_json = "1"
+215
View File
@@ -101,3 +101,218 @@ mod tests {
assert!(error.contains("sockets Unix"),);
}
}
#[cfg(test)]
mod docker_smoke_tests {
use std::collections::HashMap;
use bollard::{
container::LogOutput,
models::{ContainerCreateBody, HostConfig},
query_parameters::{
CreateContainerOptionsBuilder, CreateImageOptionsBuilder, LogsOptionsBuilder,
RemoveContainerOptionsBuilder, WaitContainerOptionsBuilder,
},
};
use futures_util::StreamExt;
use uuid::Uuid;
use super::DockerApiClient;
const TEST_IMAGE: &str = "alpine:3.20";
const EXPECTED_OUTPUT: &str = "ENUXIA_DOCKER_SMOKE=ok";
#[tokio::test]
#[ignore = "nécessite un véritable daemon Docker"]
async fn real_docker_lifecycle() {
run_real_docker_lifecycle()
.await
.expect("le cycle Docker réel doit réussir");
}
async fn run_real_docker_lifecycle() -> Result<(), String> {
let client = DockerApiClient::connect_from_env().await?;
let docker = client.docker();
println!("Docker utilisé : {}", client.host());
println!("Image de test : {TEST_IMAGE}");
pull_image(docker).await?;
let container_name = format!("enuxia-aio-smoke-{}", Uuid::now_v7(),);
let labels = HashMap::from([
("fr.enuxia.aio.kind".to_owned(), "smoke-test".to_owned()),
("fr.enuxia.aio.managed".to_owned(), "true".to_owned()),
]);
let create_options = CreateContainerOptionsBuilder::default()
.name(&container_name)
.build();
let config = ContainerCreateBody {
image: Some(TEST_IMAGE.to_owned()),
cmd: Some(vec![
"/bin/sh".to_owned(),
"-c".to_owned(),
format!("printf '{}\\n'", EXPECTED_OUTPUT,),
]),
user: Some("65534:65534".to_owned()),
attach_stdout: Some(true),
attach_stderr: Some(true),
network_disabled: Some(true),
labels: Some(labels),
host_config: Some(HostConfig {
auto_remove: Some(false),
network_mode: Some("none".to_owned()),
readonly_rootfs: Some(true),
cap_drop: Some(vec!["ALL".to_owned()]),
..Default::default()
}),
..Default::default()
};
let created = docker
.create_container(Some(create_options), config)
.await
.map_err(|error| format!("Création du conteneur impossible : {error}"))?;
let container_id = created.id;
println!("Conteneur créé : {container_name}");
println!("Identifiant : {container_id}");
let execution_result = execute_container(docker, &container_id).await;
let cleanup_result = docker
.remove_container(
&container_id,
Some(RemoveContainerOptionsBuilder::default().force(true).build()),
)
.await
.map_err(|error| format!("Suppression du conteneur impossible : {error}"));
match (execution_result, cleanup_result) {
(Ok((exit_code, stdout, stderr)), Ok(())) => {
println!("Code de sortie : {exit_code}");
println!("stdout : {}", stdout.trim());
if !stderr.trim().is_empty() {
println!("stderr : {}", stderr.trim());
}
if exit_code != 0 {
return Err(format!(
"Le conteneur a terminé avec le code \
{exit_code}. stderr : {}",
stderr.trim(),
));
}
if stdout.trim() != EXPECTED_OUTPUT {
return Err(format!("Sortie inattendue : {:?}", stdout.trim(),));
}
println!("Conteneur supprimé : {container_name}");
Ok(())
}
(Err(execution_error), Ok(())) => Err(execution_error),
(Ok(_), Err(cleanup_error)) => Err(cleanup_error),
(Err(execution_error), Err(cleanup_error)) => Err(format!(
"{execution_error} ; nettoyage également \
en échec : {cleanup_error}"
)),
}
}
async fn pull_image(docker: &bollard::Docker) -> Result<(), String> {
let options = CreateImageOptionsBuilder::default()
.from_image(TEST_IMAGE)
.build();
let mut stream = docker.create_image(Some(options), None, None);
while let Some(item) = stream.next().await {
item.map_err(|error| {
format!(
"Téléchargement de limage impossible : \
{error}"
)
})?;
}
println!("Image disponible : {TEST_IMAGE}");
Ok(())
}
async fn execute_container(
docker: &bollard::Docker,
container_id: &str,
) -> Result<(i64, String, String), String> {
docker
.start_container(container_id, None)
.await
.map_err(|error| {
format!(
"Démarrage du conteneur impossible : \
{error}"
)
})?;
let wait_options = WaitContainerOptionsBuilder::default()
.condition("not-running")
.build();
let mut wait_stream = docker.wait_container(container_id, Some(wait_options));
let wait_response = wait_stream
.next()
.await
.ok_or_else(|| "Docker na retourné aucun résultat dattente.".to_owned())?
.map_err(|error| format!("Attente du conteneur impossible : {error}"))?;
let logs_options = LogsOptionsBuilder::default()
.stdout(true)
.stderr(true)
.build();
let mut logs_stream = docker.logs(container_id, Some(logs_options));
let mut stdout = Vec::new();
let mut stderr = Vec::new();
while let Some(item) = logs_stream.next().await {
match item.map_err(|error| format!("Lecture des journaux impossible : {error}"))? {
LogOutput::StdOut { message } | LogOutput::Console { message } => {
stdout.extend_from_slice(message.as_ref());
}
LogOutput::StdErr { message } => {
stderr.extend_from_slice(message.as_ref());
}
LogOutput::StdIn { .. } => {}
}
}
Ok((
wait_response.status_code,
String::from_utf8_lossy(&stdout).into_owned(),
String::from_utf8_lossy(&stderr).into_owned(),
))
}
}