1use crate::DockerClient;
4use crate::containers::ContainerRef;
5use crate::error::{DockerError, Result};
6use bollard::container::*;
7use bollard::models::{
8 ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig, PortBinding,
9};
10use std::collections::HashMap;
11use tracing::info;
12
13pub struct ContainerBuilder<'a> {
15 client: &'a DockerClient,
16 image: String,
17 name: Option<String>,
18 config: ContainerCreateBody,
19 host_config: HostConfig,
20}
21
22impl<'a> ContainerBuilder<'a> {
23 pub(crate) fn new(client: &'a DockerClient, image: impl Into<String>) -> Self {
24 let image = image.into();
25 Self {
26 client,
27 config: ContainerCreateBody {
28 image: Some(image.clone()),
29 ..Default::default()
30 },
31 host_config: HostConfig::default(),
32 name: None,
33 image,
34 }
35 }
36
37 pub fn name(mut self, name: impl Into<String>) -> Self {
39 self.name = Some(name.into());
40 self
41 }
42
43 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
60 let env_var = format!("{}={}", key.into(), value.into());
61 if let Some(ref mut env) = self.config.env {
62 env.push(env_var);
63 } else {
64 self.config.env = Some(vec![env_var]);
65 }
66 self
67 }
68
69 pub fn port(mut self, host_port: u16, container_port: u16, protocol: &str) -> Self {
92 let port_key = format!("{}/{}", container_port, protocol);
94 if let Some(ref mut exposed_ports) = self.config.exposed_ports {
95 exposed_ports.insert(port_key.clone(), HashMap::new());
96 } else {
97 let mut exposed = HashMap::new();
98 exposed.insert(port_key.clone(), HashMap::new());
99 self.config.exposed_ports = Some(exposed);
100 }
101
102 let binding = vec![PortBinding {
104 host_ip: Some("0.0.0.0".to_string()),
105 host_port: Some(host_port.to_string()),
106 }];
107
108 if let Some(ref mut port_bindings) = self.host_config.port_bindings {
109 port_bindings.insert(port_key, Some(binding));
110 } else {
111 let mut bindings = HashMap::new();
112 bindings.insert(port_key, Some(binding));
113 self.host_config.port_bindings = Some(bindings);
114 }
115
116 self
117 }
118
119 pub fn volume(
140 mut self,
141 host_path: impl Into<String>,
142 container_path: impl Into<String>,
143 ) -> Self {
144 let binding = format!("{}:{}", host_path.into(), container_path.into());
145 if let Some(ref mut binds) = self.host_config.binds {
146 binds.push(binding);
147 } else {
148 self.host_config.binds = Some(vec![binding]);
149 }
150 self
151 }
152
153 pub fn network(mut self, network: impl Into<String>) -> Self {
155 let network = network.into();
156 let endpoint_config = EndpointSettings::default();
157
158 let mut endpoints = HashMap::new();
159 endpoints.insert(network, endpoint_config);
160
161 if let Some(ref mut networking_config) = self.config.networking_config {
162 if let Some(ref mut endpoints_config) = networking_config.endpoints_config {
163 endpoints_config.extend(endpoints);
164 } else {
165 networking_config.endpoints_config = Some(endpoints);
166 }
167 } else {
168 self.config.networking_config = Some(NetworkingConfig {
169 endpoints_config: Some(endpoints),
170 });
171 }
172
173 self
174 }
175
176 pub fn cmd(mut self, cmd: Vec<impl Into<String>>) -> Self {
192 self.config.cmd = Some(cmd.into_iter().map(|s| s.into()).collect());
193 self
194 }
195
196 pub fn entrypoint(mut self, entrypoint: Vec<impl Into<String>>) -> Self {
198 self.config.entrypoint = Some(entrypoint.into_iter().map(|s| s.into()).collect());
199 self
200 }
201
202 pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
204 self.config.working_dir = Some(dir.into());
205 self
206 }
207
208 pub fn restart_always(mut self) -> Self {
210 self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
211 name: Some(bollard::models::RestartPolicyNameEnum::ALWAYS),
212 maximum_retry_count: None,
213 });
214 self
215 }
216
217 pub fn restart_unless_stopped(mut self) -> Self {
219 self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
220 name: Some(bollard::models::RestartPolicyNameEnum::UNLESS_STOPPED),
221 maximum_retry_count: None,
222 });
223 self
224 }
225
226 pub fn restart_on_failure(mut self, max_retries: Option<i64>) -> Self {
228 self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
229 name: Some(bollard::models::RestartPolicyNameEnum::ON_FAILURE),
230 maximum_retry_count: max_retries,
231 });
232 self
233 }
234
235 pub fn memory(mut self, bytes: i64) -> Self {
237 self.host_config.memory = Some(bytes);
238 self
239 }
240
241 pub fn cpu_shares(mut self, shares: i64) -> Self {
243 self.host_config.cpu_shares = Some(shares);
244 self
245 }
246
247 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
249 if self.config.labels.is_none() {
250 self.config.labels = Some(HashMap::new());
251 }
252 if let Some(ref mut labels) = self.config.labels {
253 labels.insert(key.into(), value.into());
254 }
255 self
256 }
257
258 pub fn auto_remove(mut self, enable: bool) -> Self {
260 self.host_config.auto_remove = Some(enable);
261 self
262 }
263
264 pub fn privileged(mut self, enable: bool) -> Self {
266 self.host_config.privileged = Some(enable);
267 self
268 }
269
270 pub async fn build(self) -> Result<ContainerRef<'a>> {
274 info!("Creating container from image: {}", self.image);
275
276 let options = CreateContainerOptions {
277 name: self.name.as_deref().unwrap_or(""),
278 platform: None,
279 };
280
281 let config = ContainerCreateBody {
282 host_config: Some(self.host_config),
283 ..self.config
284 };
285
286 let response = self
287 .client
288 .docker
289 .create_container(Some(options), config)
290 .await
291 .map_err(|e| {
292 DockerError::ContainerOperationFailed(format!("Failed to create: {}", e))
293 })?;
294
295 info!("Container created with ID: {}", response.id);
296
297 Ok(ContainerRef::new(self.client, response.id))
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_builder_env() {
307 let client = DockerClient::new().unwrap();
308 let builder = ContainerBuilder::new(&client, "alpine:latest")
309 .env("KEY1", "value1")
310 .env("KEY2", "value2");
311
312 assert_eq!(builder.config.env.as_ref().unwrap().len(), 2);
313 assert!(
314 builder
315 .config
316 .env
317 .as_ref()
318 .unwrap()
319 .contains(&"KEY1=value1".to_string())
320 );
321 assert!(
322 builder
323 .config
324 .env
325 .as_ref()
326 .unwrap()
327 .contains(&"KEY2=value2".to_string())
328 );
329 }
330
331 #[test]
332 fn test_builder_name() {
333 let client = DockerClient::new().unwrap();
334 let builder = ContainerBuilder::new(&client, "alpine:latest").name("test-container");
335
336 assert_eq!(builder.name, Some("test-container".to_string()));
337 }
338
339 #[test]
340 fn test_builder_port() {
341 let client = DockerClient::new().unwrap();
342 let builder = ContainerBuilder::new(&client, "nginx:latest").port(8080, 80, "tcp");
343
344 assert!(builder.config.exposed_ports.is_some());
345 assert!(builder.host_config.port_bindings.is_some());
346
347 let bindings = builder.host_config.port_bindings.as_ref().unwrap();
348 assert!(bindings.contains_key("80/tcp"));
349 }
350
351 #[test]
352 fn test_builder_volume() {
353 let client = DockerClient::new().unwrap();
354 let builder =
355 ContainerBuilder::new(&client, "alpine:latest").volume("/host/path", "/container/path");
356
357 assert!(builder.host_config.binds.is_some());
358 let binds = builder.host_config.binds.as_ref().unwrap();
359 assert_eq!(binds.len(), 1);
360 assert_eq!(binds[0], "/host/path:/container/path");
361 }
362
363 #[test]
364 fn test_builder_cmd() {
365 let client = DockerClient::new().unwrap();
366 let builder = ContainerBuilder::new(&client, "alpine:latest").cmd(vec!["echo", "hello"]);
367
368 assert!(builder.config.cmd.is_some());
369 let cmd = builder.config.cmd.as_ref().unwrap();
370 assert_eq!(cmd.len(), 2);
371 assert_eq!(cmd[0], "echo");
372 assert_eq!(cmd[1], "hello");
373 }
374
375 #[test]
376 fn test_builder_working_dir() {
377 let client = DockerClient::new().unwrap();
378 let builder = ContainerBuilder::new(&client, "alpine:latest").working_dir("/app");
379
380 assert_eq!(builder.config.working_dir, Some("/app".to_string()));
381 }
382
383 #[test]
384 fn test_builder_restart_policies() {
385 let client = DockerClient::new().unwrap();
386
387 let builder_always = ContainerBuilder::new(&client, "alpine:latest").restart_always();
388 assert!(builder_always.host_config.restart_policy.is_some());
389
390 let builder_unless =
391 ContainerBuilder::new(&client, "alpine:latest").restart_unless_stopped();
392 assert!(builder_unless.host_config.restart_policy.is_some());
393
394 let builder_failure =
395 ContainerBuilder::new(&client, "alpine:latest").restart_on_failure(Some(5));
396 assert!(builder_failure.host_config.restart_policy.is_some());
397 assert_eq!(
398 builder_failure
399 .host_config
400 .restart_policy
401 .as_ref()
402 .unwrap()
403 .maximum_retry_count,
404 Some(5)
405 );
406 }
407
408 #[test]
409 fn test_builder_memory() {
410 let client = DockerClient::new().unwrap();
411 let builder = ContainerBuilder::new(&client, "alpine:latest").memory(512 * 1024 * 1024);
412
413 assert_eq!(builder.host_config.memory, Some(512 * 1024 * 1024));
414 }
415
416 #[test]
417 fn test_builder_cpu_shares() {
418 let client = DockerClient::new().unwrap();
419 let builder = ContainerBuilder::new(&client, "alpine:latest").cpu_shares(512);
420
421 assert_eq!(builder.host_config.cpu_shares, Some(512));
422 }
423
424 #[test]
425 fn test_builder_labels() {
426 let client = DockerClient::new().unwrap();
427 let builder = ContainerBuilder::new(&client, "alpine:latest")
428 .label("env", "test")
429 .label("version", "1.0");
430
431 assert!(builder.config.labels.is_some());
432 let labels = builder.config.labels.as_ref().unwrap();
433 assert_eq!(labels.get("env"), Some(&"test".to_string()));
434 assert_eq!(labels.get("version"), Some(&"1.0".to_string()));
435 }
436
437 #[test]
438 fn test_builder_auto_remove() {
439 let client = DockerClient::new().unwrap();
440 let builder = ContainerBuilder::new(&client, "alpine:latest").auto_remove(true);
441
442 assert_eq!(builder.host_config.auto_remove, Some(true));
443 }
444
445 #[test]
446 fn test_builder_privileged() {
447 let client = DockerClient::new().unwrap();
448 let builder = ContainerBuilder::new(&client, "alpine:latest").privileged(true);
449
450 assert_eq!(builder.host_config.privileged, Some(true));
451 }
452
453 #[test]
454 fn test_builder_chaining() {
455 let client = DockerClient::new().unwrap();
456 let builder = ContainerBuilder::new(&client, "nginx:latest")
457 .name("web-server")
458 .env("ENV", "production")
459 .port(8080, 80, "tcp")
460 .volume("/data", "/app/data")
461 .memory(512 * 1024 * 1024)
462 .restart_always()
463 .label("app", "web");
464
465 assert_eq!(builder.name, Some("web-server".to_string()));
466 assert!(builder.config.env.is_some());
467 assert!(builder.config.exposed_ports.is_some());
468 assert!(builder.host_config.binds.is_some());
469 assert!(builder.host_config.memory.is_some());
470 assert!(builder.host_config.restart_policy.is_some());
471 assert!(builder.config.labels.is_some());
472 }
473}