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}