Skip to main content

devrig/docker/
mod.rs

1pub mod container;
2pub mod exec;
3pub mod image;
4pub mod network;
5pub mod ready;
6pub mod volume;
7
8use anyhow::{Context, Result};
9use bollard::Docker;
10use std::collections::HashSet;
11
12use crate::config::model::{DockerConfig, Port};
13use crate::docker::container::PortMap;
14use crate::docker::network::resource_labels;
15use crate::orchestrator::ports::resolve_port;
16use crate::orchestrator::state::DockerState;
17
18/// Manages Docker infrastructure containers for a devrig project.
19pub struct DockerManager {
20    docker: Docker,
21    slug: String,
22}
23
24impl DockerManager {
25    /// Create a new DockerManager, verifying Docker daemon connectivity.
26    pub async fn new(slug: String) -> Result<Self> {
27        let docker =
28            Docker::connect_with_local_defaults().context("connecting to Docker daemon")?;
29        docker
30            .ping()
31            .await
32            .context("Cannot connect to Docker daemon. Is Docker running?")?;
33        Ok(Self { docker, slug })
34    }
35
36    /// Get a reference to the Docker client.
37    pub fn docker(&self) -> &Docker {
38        &self.docker
39    }
40
41    /// Get the project slug.
42    pub fn slug(&self) -> &str {
43        &self.slug
44    }
45
46    /// Get the project network name.
47    pub fn network_name(&self) -> String {
48        format!("devrig-{}-net", self.slug)
49    }
50
51    /// Ensure the project Docker network exists.
52    pub async fn ensure_network(&self) -> Result<()> {
53        let network_name = self.network_name();
54        let labels = resource_labels(&self.slug, "network");
55        network::ensure_network(&self.docker, &network_name, labels).await
56    }
57
58    /// Start a single docker service: pull image, create volumes, create and
59    /// start container, run ready check, run init scripts if needed.
60    pub async fn start_service(
61        &self,
62        name: &str,
63        config: &DockerConfig,
64        prev_state: Option<&DockerState>,
65        allocated_ports: &mut HashSet<u16>,
66    ) -> Result<DockerState> {
67        // Pull image if needed
68        if !image::check_image_exists(&self.docker, &config.image).await {
69            image::pull_image(&self.docker, &config.image).await?;
70        }
71
72        // Resolve ports
73        let mut port: Option<u16> = None;
74        let mut port_auto = false;
75        let mut named_ports = std::collections::BTreeMap::new();
76
77        if let Some(port_config) = &config.port {
78            let prev_port = prev_state.and_then(|s| s.port);
79            let prev_auto = prev_state.map(|s| s.port_auto).unwrap_or(false);
80            let resolved = resolve_port(
81                &format!("docker:{}", name),
82                port_config,
83                prev_port,
84                prev_auto,
85                allocated_ports,
86            );
87            port = Some(resolved);
88            port_auto = port_config.is_auto();
89        }
90
91        for (port_name, port_config) in &config.ports {
92            let prev_port = prev_state
93                .and_then(|s| s.named_ports.get(port_name))
94                .copied();
95            let prev_auto = port_config.is_auto();
96            let resolved = resolve_port(
97                &format!("docker:{}:{}", name, port_name),
98                port_config,
99                prev_port,
100                prev_auto,
101                allocated_ports,
102            );
103            named_ports.insert(port_name.clone(), resolved);
104        }
105
106        // Create volumes
107        let mut volume_binds = Vec::new();
108        for vol_spec in &config.volumes {
109            if let Some((vol_name, container_path)) =
110                volume::parse_volume_spec(vol_spec, &self.slug)
111            {
112                let labels = resource_labels(&self.slug, name);
113                volume::ensure_volume(&self.docker, &vol_name, labels).await?;
114                volume_binds.push((vol_name, container_path));
115            }
116        }
117
118        // Build port mappings
119        let mut port_maps = Vec::new();
120        if let Some(host_port) = port {
121            // Use the container's default port (from image) — we need to know it.
122            // For single-port services, the container port is the same as the default
123            // port for the service type, or we use the host port.
124            let container_port = match &config.port {
125                Some(Port::Fixed(p)) => *p,
126                _ => host_port,
127            };
128            port_maps.push(PortMap {
129                container_port,
130                host_port,
131            });
132        }
133        for (port_name, port_config) in &config.ports {
134            if let Some(&host_port) = named_ports.get(port_name) {
135                let container_port = match port_config {
136                    Port::Fixed(p) => *p,
137                    Port::Auto => host_port,
138                };
139                port_maps.push(PortMap {
140                    container_port,
141                    host_port,
142                });
143            }
144        }
145
146        // Build env vars
147        let env_vars: Vec<(String, String)> = config
148            .env
149            .iter()
150            .map(|(k, v)| (k.clone(), v.clone()))
151            .collect();
152
153        let network_name = self.network_name();
154
155        // Create and start container
156        let container_name = format!("devrig-{}-{}", self.slug, name);
157        let container_id = container::create_container(
158            &self.docker,
159            &self.slug,
160            name,
161            &config.image,
162            &env_vars,
163            &port_maps,
164            &volume_binds,
165            &network_name,
166        )
167        .await?;
168
169        container::start_container(&self.docker, &container_id).await?;
170        tracing::info!(docker = %name, container = %container_name, "container started");
171
172        // Run ready check
173        if let Some(check) = &config.ready_check {
174            tracing::info!(docker = %name, "waiting for ready check");
175            ready::run_ready_check(&self.docker, &container_id, check, port, name).await?;
176            tracing::info!(docker = %name, "ready");
177        }
178
179        // Run init scripts (only if not already completed)
180        let already_init = prev_state.map(|s| s.init_completed).unwrap_or(false);
181        let mut init_completed = already_init;
182        let mut init_completed_at = prev_state.and_then(|s| s.init_completed_at);
183
184        if !already_init && !config.init.is_empty() {
185            exec::run_init_scripts(&self.docker, &container_id, name, config).await?;
186            init_completed = true;
187            init_completed_at = Some(chrono::Utc::now());
188            tracing::info!(docker = %name, "init scripts completed");
189        }
190
191        Ok(DockerState {
192            container_id,
193            container_name,
194            port,
195            port_auto,
196            named_ports,
197            init_completed,
198            init_completed_at,
199        })
200    }
201
202    /// Stop a single docker service container.
203    pub async fn stop_service(&self, state: &DockerState) -> Result<()> {
204        container::stop_container(&self.docker, &state.container_id, 10).await?;
205        tracing::info!(container = %state.container_name, "container stopped");
206        Ok(())
207    }
208
209    /// Stop and remove a single docker service container.
210    pub async fn delete_service(&self, state: &DockerState) -> Result<()> {
211        container::stop_container(&self.docker, &state.container_id, 10).await?;
212        container::remove_container(&self.docker, &state.container_id, true).await?;
213        tracing::info!(container = %state.container_name, "container removed");
214        Ok(())
215    }
216
217    /// Remove all Docker resources (containers, volumes, networks) for this project.
218    pub async fn cleanup_all(&self) -> Result<()> {
219        // Remove containers by label
220        let containers = container::list_project_containers(&self.docker, &self.slug).await?;
221        for c in &containers {
222            if let Some(id) = &c.id {
223                container::stop_container(&self.docker, id, 5).await?;
224                container::remove_container(&self.docker, id, true).await?;
225            }
226        }
227
228        // Remove volumes by label
229        volume::remove_project_volumes(&self.docker, &self.slug).await?;
230
231        // Remove network
232        let network_name = self.network_name();
233        network::remove_network(&self.docker, &network_name).await?;
234
235        Ok(())
236    }
237
238    /// Check if Docker daemon is available and reachable.
239    pub async fn ensure_docker_available(&self) -> Result<()> {
240        self.docker
241            .ping()
242            .await
243            .context("Cannot connect to Docker daemon. Is Docker running?")?;
244        Ok(())
245    }
246}