pub mod container;
pub mod exec;
pub mod image;
pub mod log_stream;
pub mod network;
pub mod ready;
pub mod volume;
use anyhow::{Context, Result};
use bollard::Docker;
use std::collections::HashSet;
use crate::config::model::{DockerConfig, Port};
use crate::docker::container::{ContainerCmdOptions, PortMap};
use crate::docker::network::resource_labels;
use crate::orchestrator::ports::resolve_port;
use crate::orchestrator::state::DockerState;
pub struct DockerManager {
docker: Docker,
slug: String,
}
impl DockerManager {
pub async fn new(slug: String) -> Result<Self> {
let docker =
Docker::connect_with_local_defaults().context("connecting to Docker daemon")?;
docker
.ping()
.await
.context("Cannot connect to Docker daemon. Is Docker running?")?;
Ok(Self { docker, slug })
}
pub fn docker(&self) -> &Docker {
&self.docker
}
pub fn slug(&self) -> &str {
&self.slug
}
pub fn network_name(&self) -> String {
format!("devrig-{}-net", self.slug)
}
pub async fn ensure_network(&self) -> Result<()> {
let network_name = self.network_name();
let labels = resource_labels(&self.slug, "network");
network::ensure_network(&self.docker, &network_name, labels).await
}
pub async fn start_service(
&self,
name: &str,
config: &DockerConfig,
prev_state: Option<&DockerState>,
allocated_ports: &mut HashSet<u16>,
config_dir: &std::path::Path,
) -> Result<DockerState> {
if !image::check_image_exists(&self.docker, &config.image).await {
image::pull_image_with_auth(&self.docker, &config.image, config.registry_auth.as_ref())
.await?;
}
let mut port: Option<u16> = None;
let mut port_auto = false;
let mut named_ports = std::collections::BTreeMap::new();
if let Some(port_config) = &config.port {
let prev_port = prev_state.and_then(|s| s.port);
let prev_auto = prev_state.map(|s| s.port_auto).unwrap_or(false);
let resolved = resolve_port(
&format!("docker:{}", name),
port_config,
prev_port,
prev_auto,
allocated_ports,
);
port = Some(resolved);
port_auto = port_config.is_auto();
}
for (port_name, port_config) in &config.ports {
let prev_port = prev_state
.and_then(|s| s.named_ports.get(port_name))
.copied();
let prev_auto = port_config.is_auto();
let resolved = resolve_port(
&format!("docker:{}:{}", name, port_name),
port_config,
prev_port,
prev_auto,
allocated_ports,
);
named_ports.insert(port_name.clone(), resolved);
}
let mut volume_binds = Vec::new();
for vol_spec in &config.volumes {
match volume::parse_volume_spec(vol_spec, &self.slug) {
Some(volume::VolumeSpec::Named {
volume_name,
container_path,
}) => {
let labels = resource_labels(&self.slug, name);
volume::ensure_volume(&self.docker, &volume_name, labels).await?;
volume_binds.push((volume_name, container_path));
}
Some(volume::VolumeSpec::Bind {
host_path,
container_path,
}) => {
let resolved = if host_path.starts_with('/') {
host_path
} else {
config_dir
.join(&host_path)
.canonicalize()
.with_context(|| {
format!(
"resolving bind mount path '{}' relative to '{}'",
host_path,
config_dir.display()
)
})?
.to_string_lossy()
.into_owned()
};
volume_binds.push((resolved, container_path));
}
None => {}
}
}
let mut port_maps = Vec::new();
if let Some(host_port) = port {
let container_port = config.container_port.unwrap_or(match &config.port {
Some(Port::Fixed(p)) => *p,
_ => host_port,
});
port_maps.push(PortMap {
container_port,
host_port,
});
}
for (port_name, port_config) in &config.ports {
if let Some(&host_port) = named_ports.get(port_name) {
let container_port = match port_config {
Port::Fixed(p) => *p,
Port::Auto => host_port,
};
port_maps.push(PortMap {
container_port,
host_port,
});
}
}
let env_vars: Vec<(String, String)> = config
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let network_name = self.network_name();
let cmd_options = ContainerCmdOptions {
cmd: config.command.as_ref().map(|s| s.as_slice().to_vec()),
entrypoint: config.entrypoint.as_ref().map(|s| s.as_slice().to_vec()),
};
let container_name = format!("devrig-{}-{}", self.slug, name);
let container_id = container::create_container(
&self.docker,
&self.slug,
name,
&config.image,
&env_vars,
&port_maps,
&volume_binds,
&network_name,
&cmd_options,
)
.await?;
container::start_container(&self.docker, &container_id).await?;
tracing::debug!(docker = %name, container = %container_name, "container started");
if let Some(check) = &config.ready_check {
tracing::debug!(docker = %name, "waiting for ready check");
ready::run_ready_check(&self.docker, &container_id, check, port, name).await?;
tracing::debug!(docker = %name, "ready");
}
let already_init = prev_state.map(|s| s.init_completed).unwrap_or(false);
let mut init_completed = already_init;
let mut init_completed_at = prev_state.and_then(|s| s.init_completed_at);
if !already_init && !config.init.is_empty() {
exec::run_init_scripts(&self.docker, &container_id, name, config).await?;
init_completed = true;
init_completed_at = Some(chrono::Utc::now());
tracing::debug!(docker = %name, "init scripts completed");
}
Ok(DockerState {
container_id,
container_name,
port,
port_auto,
protocol: config.protocol.clone(),
named_ports,
init_completed,
init_completed_at,
})
}
pub async fn stop_service(&self, state: &DockerState) -> Result<()> {
container::stop_container(&self.docker, &state.container_id, 10).await?;
tracing::debug!(container = %state.container_name, "container stopped");
Ok(())
}
pub async fn delete_service(&self, state: &DockerState) -> Result<()> {
container::stop_container(&self.docker, &state.container_id, 10).await?;
container::remove_container(&self.docker, &state.container_id, true).await?;
tracing::debug!(container = %state.container_name, "container removed");
Ok(())
}
pub async fn cleanup_all(&self) -> Result<()> {
let containers = container::list_project_containers(&self.docker, &self.slug).await?;
for c in &containers {
if let Some(id) = &c.id {
container::stop_container(&self.docker, id, 5).await?;
container::remove_container(&self.docker, id, true).await?;
}
}
volume::remove_project_volumes(&self.docker, &self.slug).await?;
let network_name = self.network_name();
network::remove_network(&self.docker, &network_name).await?;
Ok(())
}
pub async fn ensure_docker_available(&self) -> Result<()> {
self.docker
.ping()
.await
.context("Cannot connect to Docker daemon. Is Docker running?")?;
Ok(())
}
}