use crate::DockerClient;
use crate::containers::ContainerRef;
use crate::error::{DockerError, Result};
use bollard::container::*;
use bollard::models::{
ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig, PortBinding,
};
use std::collections::HashMap;
use tracing::info;
pub struct ContainerBuilder<'a> {
client: &'a DockerClient,
image: String,
name: Option<String>,
config: ContainerCreateBody,
host_config: HostConfig,
}
impl<'a> ContainerBuilder<'a> {
pub(crate) fn new(client: &'a DockerClient, image: impl Into<String>) -> Self {
let image = image.into();
Self {
client,
config: ContainerCreateBody {
image: Some(image.clone()),
..Default::default()
},
host_config: HostConfig::default(),
name: None,
image,
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
let env_var = format!("{}={}", key.into(), value.into());
if let Some(ref mut env) = self.config.env {
env.push(env_var);
} else {
self.config.env = Some(vec![env_var]);
}
self
}
pub fn port(mut self, host_port: u16, container_port: u16, protocol: &str) -> Self {
let port_key = format!("{}/{}", container_port, protocol);
if let Some(ref mut exposed_ports) = self.config.exposed_ports {
exposed_ports.insert(port_key.clone(), HashMap::new());
} else {
let mut exposed = HashMap::new();
exposed.insert(port_key.clone(), HashMap::new());
self.config.exposed_ports = Some(exposed);
}
let binding = vec![PortBinding {
host_ip: Some("0.0.0.0".to_string()),
host_port: Some(host_port.to_string()),
}];
if let Some(ref mut port_bindings) = self.host_config.port_bindings {
port_bindings.insert(port_key, Some(binding));
} else {
let mut bindings = HashMap::new();
bindings.insert(port_key, Some(binding));
self.host_config.port_bindings = Some(bindings);
}
self
}
pub fn volume(
mut self,
host_path: impl Into<String>,
container_path: impl Into<String>,
) -> Self {
let binding = format!("{}:{}", host_path.into(), container_path.into());
if let Some(ref mut binds) = self.host_config.binds {
binds.push(binding);
} else {
self.host_config.binds = Some(vec![binding]);
}
self
}
pub fn network(mut self, network: impl Into<String>) -> Self {
let network = network.into();
let endpoint_config = EndpointSettings::default();
let mut endpoints = HashMap::new();
endpoints.insert(network, endpoint_config);
if let Some(ref mut networking_config) = self.config.networking_config {
if let Some(ref mut endpoints_config) = networking_config.endpoints_config {
endpoints_config.extend(endpoints);
} else {
networking_config.endpoints_config = Some(endpoints);
}
} else {
self.config.networking_config = Some(NetworkingConfig {
endpoints_config: Some(endpoints),
});
}
self
}
pub fn cmd(mut self, cmd: Vec<impl Into<String>>) -> Self {
self.config.cmd = Some(cmd.into_iter().map(|s| s.into()).collect());
self
}
pub fn entrypoint(mut self, entrypoint: Vec<impl Into<String>>) -> Self {
self.config.entrypoint = Some(entrypoint.into_iter().map(|s| s.into()).collect());
self
}
pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
self.config.working_dir = Some(dir.into());
self
}
pub fn restart_always(mut self) -> Self {
self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
name: Some(bollard::models::RestartPolicyNameEnum::ALWAYS),
maximum_retry_count: None,
});
self
}
pub fn restart_unless_stopped(mut self) -> Self {
self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
name: Some(bollard::models::RestartPolicyNameEnum::UNLESS_STOPPED),
maximum_retry_count: None,
});
self
}
pub fn restart_on_failure(mut self, max_retries: Option<i64>) -> Self {
self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
name: Some(bollard::models::RestartPolicyNameEnum::ON_FAILURE),
maximum_retry_count: max_retries,
});
self
}
pub fn memory(mut self, bytes: i64) -> Self {
self.host_config.memory = Some(bytes);
self
}
pub fn cpu_shares(mut self, shares: i64) -> Self {
self.host_config.cpu_shares = Some(shares);
self
}
pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
if self.config.labels.is_none() {
self.config.labels = Some(HashMap::new());
}
if let Some(ref mut labels) = self.config.labels {
labels.insert(key.into(), value.into());
}
self
}
pub fn auto_remove(mut self, enable: bool) -> Self {
self.host_config.auto_remove = Some(enable);
self
}
pub fn privileged(mut self, enable: bool) -> Self {
self.host_config.privileged = Some(enable);
self
}
pub async fn build(self) -> Result<ContainerRef<'a>> {
info!("Creating container from image: {}", self.image);
let options = CreateContainerOptions {
name: self.name.as_deref().unwrap_or(""),
platform: None,
};
let config = ContainerCreateBody {
host_config: Some(self.host_config),
..self.config
};
let response = self
.client
.docker
.create_container(Some(options), config)
.await
.map_err(|e| {
DockerError::ContainerOperationFailed(format!("Failed to create: {}", e))
})?;
info!("Container created with ID: {}", response.id);
Ok(ContainerRef::new(self.client, response.id))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_env() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest")
.env("KEY1", "value1")
.env("KEY2", "value2");
assert_eq!(builder.config.env.as_ref().unwrap().len(), 2);
assert!(
builder
.config
.env
.as_ref()
.unwrap()
.contains(&"KEY1=value1".to_string())
);
assert!(
builder
.config
.env
.as_ref()
.unwrap()
.contains(&"KEY2=value2".to_string())
);
}
#[test]
fn test_builder_name() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest").name("test-container");
assert_eq!(builder.name, Some("test-container".to_string()));
}
#[test]
fn test_builder_port() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "nginx:latest").port(8080, 80, "tcp");
assert!(builder.config.exposed_ports.is_some());
assert!(builder.host_config.port_bindings.is_some());
let bindings = builder.host_config.port_bindings.as_ref().unwrap();
assert!(bindings.contains_key("80/tcp"));
}
#[test]
fn test_builder_volume() {
let client = DockerClient::new().unwrap();
let builder =
ContainerBuilder::new(&client, "alpine:latest").volume("/host/path", "/container/path");
assert!(builder.host_config.binds.is_some());
let binds = builder.host_config.binds.as_ref().unwrap();
assert_eq!(binds.len(), 1);
assert_eq!(binds[0], "/host/path:/container/path");
}
#[test]
fn test_builder_cmd() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest").cmd(vec!["echo", "hello"]);
assert!(builder.config.cmd.is_some());
let cmd = builder.config.cmd.as_ref().unwrap();
assert_eq!(cmd.len(), 2);
assert_eq!(cmd[0], "echo");
assert_eq!(cmd[1], "hello");
}
#[test]
fn test_builder_working_dir() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest").working_dir("/app");
assert_eq!(builder.config.working_dir, Some("/app".to_string()));
}
#[test]
fn test_builder_restart_policies() {
let client = DockerClient::new().unwrap();
let builder_always = ContainerBuilder::new(&client, "alpine:latest").restart_always();
assert!(builder_always.host_config.restart_policy.is_some());
let builder_unless =
ContainerBuilder::new(&client, "alpine:latest").restart_unless_stopped();
assert!(builder_unless.host_config.restart_policy.is_some());
let builder_failure =
ContainerBuilder::new(&client, "alpine:latest").restart_on_failure(Some(5));
assert!(builder_failure.host_config.restart_policy.is_some());
assert_eq!(
builder_failure
.host_config
.restart_policy
.as_ref()
.unwrap()
.maximum_retry_count,
Some(5)
);
}
#[test]
fn test_builder_memory() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest").memory(512 * 1024 * 1024);
assert_eq!(builder.host_config.memory, Some(512 * 1024 * 1024));
}
#[test]
fn test_builder_cpu_shares() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest").cpu_shares(512);
assert_eq!(builder.host_config.cpu_shares, Some(512));
}
#[test]
fn test_builder_labels() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest")
.label("env", "test")
.label("version", "1.0");
assert!(builder.config.labels.is_some());
let labels = builder.config.labels.as_ref().unwrap();
assert_eq!(labels.get("env"), Some(&"test".to_string()));
assert_eq!(labels.get("version"), Some(&"1.0".to_string()));
}
#[test]
fn test_builder_auto_remove() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest").auto_remove(true);
assert_eq!(builder.host_config.auto_remove, Some(true));
}
#[test]
fn test_builder_privileged() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "alpine:latest").privileged(true);
assert_eq!(builder.host_config.privileged, Some(true));
}
#[test]
fn test_builder_chaining() {
let client = DockerClient::new().unwrap();
let builder = ContainerBuilder::new(&client, "nginx:latest")
.name("web-server")
.env("ENV", "production")
.port(8080, 80, "tcp")
.volume("/data", "/app/data")
.memory(512 * 1024 * 1024)
.restart_always()
.label("app", "web");
assert_eq!(builder.name, Some("web-server".to_string()));
assert!(builder.config.env.is_some());
assert!(builder.config.exposed_ports.is_some());
assert!(builder.host_config.binds.is_some());
assert!(builder.host_config.memory.is_some());
assert!(builder.host_config.restart_policy.is_some());
assert!(builder.config.labels.is_some());
}
}