testcontainers 0.26.0

A library for integration-testing against docker containers from within Rust.
Documentation
use std::path::PathBuf;

use crate::{
    compose::{
        client::{ComposeInterface, DownCommand, UpCommand},
        error::Result,
    },
    core::{CmdWaitFor, ExecCommand, Mount},
    images::docker_cli::DockerCli,
    runners::AsyncRunner,
    ContainerAsync, ContainerRequest, ImageExt,
};

pub(crate) struct ContainerisedComposeCli {
    container: ContainerAsync<DockerCli>,
    compose_files_in_container: Vec<String>,
}

impl ContainerisedComposeCli {
    pub(super) async fn new(compose_files: Vec<PathBuf>) -> Result<Self> {
        let mut image = ContainerRequest::from(DockerCli::new("/var/run/docker.sock"));

        let compose_files_in_container: Vec<String> = compose_files
            .iter()
            .enumerate()
            .map(|(i, _)| format!("/docker-compose-{i}.yml"))
            .collect();
        let mounts: Vec<_> = compose_files
            .iter()
            .zip(compose_files_in_container.iter())
            .filter_map(|(path, file_name)| path.to_str().map(|p| Mount::bind_mount(p, file_name)))
            .collect();

        for mount in mounts {
            image = image.with_mount(mount);
        }

        let container = image.start().await?;

        Ok(Self {
            container,
            compose_files_in_container,
        })
    }
}

impl ComposeInterface for ContainerisedComposeCli {
    async fn up(&self, command: UpCommand) -> Result<()> {
        let mut cmd_parts = vec![];

        for (key, value) in &command.env_vars {
            cmd_parts.push(format!("{}={}", key, value));
        }

        cmd_parts.extend([
            "docker".to_string(),
            "compose".to_string(),
            "--project-name".to_string(),
            command.project_name.clone(),
        ]);

        for file in &self.compose_files_in_container {
            cmd_parts.push("-f".to_string());
            cmd_parts.push(file.to_string());
        }

        cmd_parts.push("up".to_string());
        cmd_parts.push("-d".to_string());

        if command.build {
            cmd_parts.push("--build".to_string());
        }

        if command.pull {
            cmd_parts.push("--pull".to_string());
            cmd_parts.push("always".to_string());
        }

        cmd_parts.push("--wait".to_string());
        cmd_parts.push("--wait-timeout".to_string());
        cmd_parts.push(command.wait_timeout.as_secs().to_string());

        let exec = ExecCommand::new(cmd_parts).with_cmd_ready_condition(CmdWaitFor::exit_code(0));
        self.container.exec(exec).await?;

        Ok(())
    }

    async fn down(&self, command: DownCommand) -> Result<()> {
        let mut cmd = vec![
            "docker".to_string(),
            "compose".to_string(),
            "--project-name".to_string(),
            command.project_name.clone(),
            "down".to_string(),
        ];

        if command.volumes {
            cmd.push("--volumes".to_string());
        }
        if command.rmi {
            cmd.push("--rmi".to_string());
        }

        let exec = ExecCommand::new(cmd).with_cmd_ready_condition(CmdWaitFor::exit_code(0));
        self.container.exec(exec).await?;
        Ok(())
    }
}