mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Docker Compose operations

use anyhow::{Context, Result};
use std::process::Command;

/// Build Docker images using Docker Compose
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
/// * `service` - Optional service name to build (builds all if None)
pub fn compose_build(compose_file: Option<&str>, service: Option<&str>) -> Result<()> {
    let mut args = vec!["compose"];

    if let Some(file) = compose_file {
        args.push("-f");
        args.push(file);
    }

    args.push("build");

    if let Some(svc) = service {
        args.push(svc);
    }

    let status = Command::new("docker")
        .args(&args)
        .status()
        .context("Failed to execute 'docker compose build'")?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to build Docker images"));
    }

    Ok(())
}

/// Start services using Docker Compose
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
/// * `detach` - Run in detached mode
pub async fn compose_up(compose_file: Option<&str>, detach: bool) -> Result<()> {
    let mut args = vec!["compose"];

    if let Some(file) = compose_file {
        args.push("-f");
        args.push(file);
    }

    args.push("up");

    if detach {
        args.push("-d");
    }

    let status = Command::new("docker")
        .args(&args)
        .status()
        .context("Failed to execute 'docker compose up'")?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to start services"));
    }

    Ok(())
}

/// Start a specific service using Docker Compose
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
/// * `service` - Service name to start
/// * `detach` - Run in detached mode
pub async fn compose_up_service(compose_file: Option<&str>, service: &str, detach: bool) -> Result<()> {
    let mut args = vec!["compose"];

    if let Some(file) = compose_file {
        args.push("-f");
        args.push(file);
    }

    args.push("up");

    if detach {
        args.push("-d");
    }

    args.push(service);

    let status = Command::new("docker")
        .args(&args)
        .status()
        .context(format!("Failed to start service: {}", service))?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to start service: {}", service));
    }

    Ok(())
}

/// Run a one-off command in a service container
///
/// Uses `docker compose run` to start a service with custom command arguments.
/// This is useful for passing runtime arguments that aren't in docker-compose.yml.
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
/// * `service` - Service name to run
/// * `detach` - Run in detached mode
/// * `service_ports` - Publish service ports (useful for detached mode)
/// * `command_args` - Additional arguments to pass to the container's entrypoint
pub async fn compose_run_service(
    compose_file: Option<&str>,
    service: &str,
    detach: bool,
    service_ports: bool,
    command_args: &[String],
) -> Result<()> {
    let mut args = vec!["compose"];

    if let Some(file) = compose_file {
        args.push("-f");
        args.push(file);
    }

    args.push("run");

    if detach {
        args.push("-d");
    }

    if service_ports {
        args.push("--service-ports");
    }

    // Only remove container if not running in detached mode
    // Detached containers should persist for inspection/logs
    if !detach {
        args.push("--rm");
    }

    args.push(service);

    // Add command arguments
    let cmd_args: Vec<&str> = command_args.iter().map(|s| s.as_str()).collect();
    args.extend(cmd_args);

    let status = Command::new("docker")
        .args(&args)
        .status()
        .context(format!("Failed to run service: {}", service))?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to run service: {}", service));
    }

    Ok(())
}

/// Stop services using Docker Compose
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
/// * `service` - Optional service name to stop (stops all if None)
pub async fn compose_stop(compose_file: Option<&str>, service: Option<&str>) -> Result<()> {
    let mut args = vec!["compose"];

    if let Some(file) = compose_file {
        args.push("-f");
        args.push(file);
    }

    args.push("stop");

    if let Some(svc) = service {
        args.push(svc);
    }

    let status = Command::new("docker")
        .args(&args)
        .status()
        .context("Failed to execute 'docker compose stop'")?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to stop services"));
    }

    Ok(())
}

/// Stop and remove services using Docker Compose
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
pub async fn compose_down(compose_file: Option<&str>) -> Result<()> {
    let mut args = vec!["compose"];

    if let Some(file) = compose_file {
        args.push("-f");
        args.push(file);
    }

    args.push("down");

    let status = Command::new("docker")
        .args(&args)
        .status()
        .context("Failed to execute 'docker compose down'")?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to stop and remove services"));
    }

    Ok(())
}

/// Restart services using Docker Compose
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
/// * `service` - Optional service name to restart (restarts all if None)
pub async fn compose_restart(compose_file: Option<&str>, service: Option<&str>) -> Result<()> {
    let mut args = vec!["compose"];

    if let Some(file) = compose_file {
        args.push("-f");
        args.push(file);
    }

    args.push("restart");

    if let Some(svc) = service {
        args.push(svc);
    }

    let status = Command::new("docker")
        .args(&args)
        .status()
        .context("Failed to execute 'docker compose restart'")?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to restart services"));
    }

    Ok(())
}

/// Get logs from Docker Compose services
///
/// # Arguments
///
/// * `compose_file` - Optional path to docker-compose.yml
/// * `service` - Optional service name (all services if None)
/// * `follow` - Follow log output
/// * `tail` - Number of lines to show from the end
pub fn compose_logs(
    compose_file: Option<&str>,
    service: Option<&str>,
    follow: bool,
    tail: Option<usize>,
) -> Result<()> {
    let mut cmd = Command::new("docker");

    cmd.arg("compose");

    if let Some(file) = compose_file {
        cmd.arg("-f").arg(file);
    }

    cmd.arg("logs");

    if follow {
        cmd.arg("-f");
    }

    if let Some(n) = tail {
        cmd.arg("--tail").arg(n.to_string());
    }

    if let Some(svc) = service {
        cmd.arg(svc);
    }

    let status = cmd.status().context("Failed to execute 'docker compose logs'")?;

    if !status.success() {
        return Err(anyhow::anyhow!("Failed to get logs"));
    }

    Ok(())
}