mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
#![allow(dead_code)]

//! Docker service for container and Docker Compose operations
//!
//! This service provides a centralized interface for all Docker operations,
//! including installation checks, daemon status, image building, and
//! container lifecycle management.

mod compose;
mod containers;
mod installation;
mod types;

pub use types::{ContainerInfo, DockerInfo};

use anyhow::Result;
use std::path::Path;
use std::time::Duration;

/// Docker service for managing containers and Docker Compose
///
/// # Examples
///
/// ```rust,ignore
/// use mecha10_cli::services::DockerService;
///
/// # async fn example() -> anyhow::Result<()> {
/// let service = DockerService::new();
///
/// // Check if Docker is available
/// service.check_installation()?;
///
/// // Start services with Docker Compose
/// service.compose_up(true).await?;
///
/// // Check container status
/// let running = service.is_container_running("mecha10-redis").await?;
/// # Ok(())
/// # }
/// ```
pub struct DockerService {
    /// Path to docker-compose.yml file
    compose_file: Option<String>,
}

impl DockerService {
    /// Create a new Docker service
    pub fn new() -> Self {
        Self { compose_file: None }
    }

    /// Create a Docker service with a custom compose file path
    ///
    /// # Arguments
    ///
    /// * `compose_file` - Path to docker-compose.yml
    pub fn with_compose_file(compose_file: impl Into<String>) -> Self {
        Self {
            compose_file: Some(compose_file.into()),
        }
    }

    /// Check if Docker is installed and accessible
    ///
    /// # Errors
    ///
    /// Returns an error if Docker is not found or not executable.
    pub fn check_installation(&self) -> Result<DockerInfo> {
        installation::check_installation()
    }

    /// Check if Docker daemon is running
    ///
    /// # Errors
    ///
    /// Returns an error if the daemon is not running or unreachable.
    pub fn check_daemon(&self) -> Result<()> {
        installation::check_daemon()
    }

    /// Build Docker images using Docker Compose
    ///
    /// # Arguments
    ///
    /// * `service` - Optional service name to build (builds all if None)
    pub fn compose_build(&self, service: Option<&str>) -> Result<()> {
        compose::compose_build(self.compose_file.as_deref(), service)
    }

    /// Start services using Docker Compose
    ///
    /// # Arguments
    ///
    /// * `detach` - Run in detached mode
    pub async fn compose_up(&self, detach: bool) -> Result<()> {
        compose::compose_up(self.compose_file.as_deref(), detach).await
    }

    /// Start a specific service using Docker Compose
    ///
    /// # Arguments
    ///
    /// * `service` - Service name to start
    /// * `detach` - Run in detached mode
    pub async fn compose_up_service(&self, service: &str, detach: bool) -> Result<()> {
        compose::compose_up_service(self.compose_file.as_deref(), service, detach).await
    }

    /// Run a one-off command in a service container
    ///
    /// Uses `docker compose run` to start a service with custom command arguments.
    ///
    /// # Arguments
    ///
    /// * `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(
        &self,
        service: &str,
        detach: bool,
        service_ports: bool,
        command_args: &[String],
    ) -> Result<()> {
        compose::compose_run_service(
            self.compose_file.as_deref(),
            service,
            detach,
            service_ports,
            command_args,
        )
        .await
    }

    /// Stop services using Docker Compose
    ///
    /// # Arguments
    ///
    /// * `service` - Optional service name to stop (stops all if None)
    pub async fn compose_stop(&self, service: Option<&str>) -> Result<()> {
        compose::compose_stop(self.compose_file.as_deref(), service).await
    }

    /// Stop and remove services using Docker Compose
    pub async fn compose_down(&self) -> Result<()> {
        compose::compose_down(self.compose_file.as_deref()).await
    }

    /// Restart services using Docker Compose
    ///
    /// # Arguments
    ///
    /// * `service` - Optional service name to restart (restarts all if None)
    pub async fn compose_restart(&self, service: Option<&str>) -> Result<()> {
        compose::compose_restart(self.compose_file.as_deref(), service).await
    }

    /// Get logs from Docker Compose services
    ///
    /// # Arguments
    ///
    /// * `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(&self, service: Option<&str>, follow: bool, tail: Option<usize>) -> Result<()> {
        compose::compose_logs(self.compose_file.as_deref(), service, follow, tail)
    }

    /// Check if a container is running
    ///
    /// # Arguments
    ///
    /// * `container_name` - Name or ID of the container
    pub async fn is_container_running(&self, container_name: &str) -> Result<bool> {
        containers::is_container_running(container_name).await
    }

    /// List running containers
    pub async fn list_containers(&self) -> Result<Vec<ContainerInfo>> {
        containers::list_containers().await
    }

    /// Check if docker-compose.yml exists
    ///
    /// # Arguments
    ///
    /// * `path` - Path to check (defaults to current directory)
    pub fn compose_file_exists(&self, path: Option<&Path>) -> bool {
        containers::compose_file_exists(self.compose_file.as_deref(), path)
    }

    /// Wait for a service to be healthy
    ///
    /// # Arguments
    ///
    /// * `container_name` - Name of the container
    /// * `timeout` - Maximum time to wait
    pub async fn wait_for_healthy(&self, container_name: &str, timeout: Duration) -> Result<()> {
        containers::wait_for_healthy(container_name, timeout).await
    }

    /// Start project services for development mode
    ///
    /// This is a convenience method that:
    /// 1. Checks if services are already running
    /// 2. Starts only the services that aren't running
    /// 3. Waits for them to be healthy
    ///
    /// # Arguments
    ///
    /// * `services` - List of service names to start
    /// * `project_name` - Project name for container naming
    ///
    /// # Returns
    ///
    /// Returns Ok(true) if services were started, Ok(false) if already running
    pub async fn start_project_services(&self, services: &[String], project_name: &str) -> Result<bool> {
        if services.is_empty() {
            return Ok(false);
        }

        // Check if services are already running
        let mut all_running = true;
        for service in services {
            let container_name = format!("{}-{}", project_name, service);
            if !self.is_container_running(&container_name).await.unwrap_or(false) {
                all_running = false;
                break;
            }
        }

        if all_running {
            return Ok(false); // Already running
        }

        // Start services
        for service in services {
            self.compose_up_service(service, true).await?;
        }

        // Wait for services to be healthy
        tokio::time::sleep(Duration::from_secs(2)).await;

        Ok(true) // Started services
    }
}

impl Default for DockerService {
    fn default() -> Self {
        Self::new()
    }
}