mecha10_cli/services/docker/
mod.rs

1#![allow(dead_code)]
2
3//! Docker service for container and Docker Compose operations
4//!
5//! This service provides a centralized interface for all Docker operations,
6//! including installation checks, daemon status, image building, and
7//! container lifecycle management.
8
9mod compose;
10mod containers;
11mod installation;
12mod types;
13
14pub use types::{ContainerInfo, DockerInfo};
15
16use anyhow::Result;
17use std::path::Path;
18use std::time::Duration;
19
20/// Docker service for managing containers and Docker Compose
21///
22/// # Examples
23///
24/// ```rust,ignore
25/// use mecha10_cli::services::DockerService;
26///
27/// # async fn example() -> anyhow::Result<()> {
28/// let service = DockerService::new();
29///
30/// // Check if Docker is available
31/// service.check_installation()?;
32///
33/// // Start services with Docker Compose
34/// service.compose_up(true).await?;
35///
36/// // Check container status
37/// let running = service.is_container_running("mecha10-redis").await?;
38/// # Ok(())
39/// # }
40/// ```
41pub struct DockerService {
42    /// Path to docker-compose.yml file
43    compose_file: Option<String>,
44}
45
46impl DockerService {
47    /// Create a new Docker service
48    pub fn new() -> Self {
49        Self { compose_file: None }
50    }
51
52    /// Create a Docker service with a custom compose file path
53    ///
54    /// # Arguments
55    ///
56    /// * `compose_file` - Path to docker-compose.yml
57    pub fn with_compose_file(compose_file: impl Into<String>) -> Self {
58        Self {
59            compose_file: Some(compose_file.into()),
60        }
61    }
62
63    /// Check if Docker is installed and accessible
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if Docker is not found or not executable.
68    pub fn check_installation(&self) -> Result<DockerInfo> {
69        installation::check_installation()
70    }
71
72    /// Check if Docker daemon is running
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the daemon is not running or unreachable.
77    pub fn check_daemon(&self) -> Result<()> {
78        installation::check_daemon()
79    }
80
81    /// Build Docker images using Docker Compose
82    ///
83    /// # Arguments
84    ///
85    /// * `service` - Optional service name to build (builds all if None)
86    pub fn compose_build(&self, service: Option<&str>) -> Result<()> {
87        compose::compose_build(self.compose_file.as_deref(), service)
88    }
89
90    /// Start services using Docker Compose
91    ///
92    /// # Arguments
93    ///
94    /// * `detach` - Run in detached mode
95    pub async fn compose_up(&self, detach: bool) -> Result<()> {
96        compose::compose_up(self.compose_file.as_deref(), detach).await
97    }
98
99    /// Start a specific service using Docker Compose
100    ///
101    /// # Arguments
102    ///
103    /// * `service` - Service name to start
104    /// * `detach` - Run in detached mode
105    pub async fn compose_up_service(&self, service: &str, detach: bool) -> Result<()> {
106        compose::compose_up_service(self.compose_file.as_deref(), service, detach).await
107    }
108
109    /// Run a one-off command in a service container
110    ///
111    /// Uses `docker compose run` to start a service with custom command arguments.
112    ///
113    /// # Arguments
114    ///
115    /// * `service` - Service name to run
116    /// * `detach` - Run in detached mode
117    /// * `service_ports` - Publish service ports (useful for detached mode)
118    /// * `command_args` - Additional arguments to pass to the container's entrypoint
119    pub async fn compose_run_service(
120        &self,
121        service: &str,
122        detach: bool,
123        service_ports: bool,
124        command_args: &[String],
125    ) -> Result<()> {
126        compose::compose_run_service(
127            self.compose_file.as_deref(),
128            service,
129            detach,
130            service_ports,
131            command_args,
132        )
133        .await
134    }
135
136    /// Stop services using Docker Compose
137    ///
138    /// # Arguments
139    ///
140    /// * `service` - Optional service name to stop (stops all if None)
141    pub async fn compose_stop(&self, service: Option<&str>) -> Result<()> {
142        compose::compose_stop(self.compose_file.as_deref(), service).await
143    }
144
145    /// Stop and remove services using Docker Compose
146    pub async fn compose_down(&self) -> Result<()> {
147        compose::compose_down(self.compose_file.as_deref()).await
148    }
149
150    /// Restart services using Docker Compose
151    ///
152    /// # Arguments
153    ///
154    /// * `service` - Optional service name to restart (restarts all if None)
155    pub async fn compose_restart(&self, service: Option<&str>) -> Result<()> {
156        compose::compose_restart(self.compose_file.as_deref(), service).await
157    }
158
159    /// Get logs from Docker Compose services
160    ///
161    /// # Arguments
162    ///
163    /// * `service` - Optional service name (all services if None)
164    /// * `follow` - Follow log output
165    /// * `tail` - Number of lines to show from the end
166    pub fn compose_logs(&self, service: Option<&str>, follow: bool, tail: Option<usize>) -> Result<()> {
167        compose::compose_logs(self.compose_file.as_deref(), service, follow, tail)
168    }
169
170    /// Check if a container is running
171    ///
172    /// # Arguments
173    ///
174    /// * `container_name` - Name or ID of the container
175    pub async fn is_container_running(&self, container_name: &str) -> Result<bool> {
176        containers::is_container_running(container_name).await
177    }
178
179    /// List running containers
180    pub async fn list_containers(&self) -> Result<Vec<ContainerInfo>> {
181        containers::list_containers().await
182    }
183
184    /// Check if docker-compose.yml exists
185    ///
186    /// # Arguments
187    ///
188    /// * `path` - Path to check (defaults to current directory)
189    pub fn compose_file_exists(&self, path: Option<&Path>) -> bool {
190        containers::compose_file_exists(self.compose_file.as_deref(), path)
191    }
192
193    /// Wait for a service to be healthy
194    ///
195    /// # Arguments
196    ///
197    /// * `container_name` - Name of the container
198    /// * `timeout` - Maximum time to wait
199    pub async fn wait_for_healthy(&self, container_name: &str, timeout: Duration) -> Result<()> {
200        containers::wait_for_healthy(container_name, timeout).await
201    }
202
203    /// Start project services for development mode
204    ///
205    /// This is a convenience method that:
206    /// 1. Checks if services are already running
207    /// 2. Starts only the services that aren't running
208    /// 3. Waits for them to be healthy
209    ///
210    /// # Arguments
211    ///
212    /// * `services` - List of service names to start
213    /// * `project_name` - Project name for container naming
214    ///
215    /// # Returns
216    ///
217    /// Returns Ok(true) if services were started, Ok(false) if already running
218    pub async fn start_project_services(&self, services: &[String], project_name: &str) -> Result<bool> {
219        if services.is_empty() {
220            return Ok(false);
221        }
222
223        // Check if services are already running
224        let mut all_running = true;
225        for service in services {
226            let container_name = format!("{}-{}", project_name, service);
227            if !self.is_container_running(&container_name).await.unwrap_or(false) {
228                all_running = false;
229                break;
230            }
231        }
232
233        if all_running {
234            return Ok(false); // Already running
235        }
236
237        // Start services
238        for service in services {
239            self.compose_up_service(service, true).await?;
240        }
241
242        // Wait for services to be healthy
243        tokio::time::sleep(Duration::from_secs(2)).await;
244
245        Ok(true) // Started services
246    }
247}
248
249impl Default for DockerService {
250    fn default() -> Self {
251        Self::new()
252    }
253}