testcontainers 0.27.3

A library for integration-testing against docker containers from within Rust.
use crate::{
    compose::{
        client::{ComposeInterface, DownCommand, UpCommand},
        error::Result,
        ContainerisedComposeOptions,
    },
    core::{CmdWaitFor, ExecCommand},
    images::docker_cli::DockerCli,
    runners::AsyncRunner,
    ContainerAsync, ContainerRequest, ImageExt,
};

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

impl ContainerisedComposeCli {
    pub(super) async fn new(options: ContainerisedComposeOptions) -> Result<Self> {
        let (compose_files, project_directory) = options.into_parts();
        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();
        for (path, file_name) in compose_files
            .into_iter()
            .zip(compose_files_in_container.iter())
        {
            image = image.with_copy_to(file_name, path);
        }

        let container = image.start().await?;
        let project_directory = project_directory.map(|path| path.to_string_lossy().into_owned());

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

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

        if let Some(project_directory) = &self.project_directory {
            cmd_parts.push("--project-directory".to_string());
            cmd_parts.push(project_directory.clone());
        }

        cmd_parts.push("--project-name".to_string());
        cmd_parts.push(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());
        }

        if command.wait {
            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))
            .with_env_vars(command.env_vars);
        self.container.exec(exec).await?;

        Ok(())
    }

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

        if let Some(project_directory) = &self.project_directory {
            cmd.push("--project-directory".to_string());
            cmd.push(project_directory.clone());
        }

        cmd.extend([
            "--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(())
    }
}