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
18pub struct DockerManager {
20 docker: Docker,
21 slug: String,
22}
23
24impl DockerManager {
25 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 pub fn docker(&self) -> &Docker {
38 &self.docker
39 }
40
41 pub fn slug(&self) -> &str {
43 &self.slug
44 }
45
46 pub fn network_name(&self) -> String {
48 format!("devrig-{}-net", self.slug)
49 }
50
51 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 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 if !image::check_image_exists(&self.docker, &config.image).await {
69 image::pull_image(&self.docker, &config.image).await?;
70 }
71
72 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 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 let mut port_maps = Vec::new();
120 if let Some(host_port) = port {
121 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 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 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 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 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 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 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 pub async fn cleanup_all(&self) -> Result<()> {
219 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 volume::remove_project_volumes(&self.docker, &self.slug).await?;
230
231 let network_name = self.network_name();
233 network::remove_network(&self.docker, &network_name).await?;
234
235 Ok(())
236 }
237
238 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}