test: validate isolated Docker container lifecycle
This commit is contained in:
Generated
+1
@@ -257,6 +257,7 @@ dependencies = [
|
||||
"bollard",
|
||||
"chrono",
|
||||
"enuxia-aio-protocol",
|
||||
"futures-util",
|
||||
"rusqlite",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 l’image 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 n’a retourné aucun résultat d’attente.".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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user