bollard_compose/lib/src/container.rs

216 lines
6.7 KiB
Rust
Raw Normal View History

use crate::configuration::compose_types::DockerCompose;
use crate::helpers::dir::parent_dir_name;
use anyhow::anyhow;
use bollard::container::{Config, CreateContainerOptions, RemoveContainerOptions};
use bollard::models::{HostConfig, PortBinding, PortMap, RestartPolicy, RestartPolicyNameEnum};
use bollard::Docker;
use std::collections::HashMap;
pub(crate) async fn create_containers(
compose: &DockerCompose,
docker: &Docker,
detach: bool,
) -> anyhow::Result<Vec<String>> {
let mut container_ids = Vec::new();
let parent_dir = parent_dir_name()?;
for (name, service) in &compose.services {
let env = create_env_vec(&service.environment);
let conf: Config<String> = Config {
image: service.image.clone(),
attach_stdout: Some(!detach),
attach_stderr: Some(!detach),
open_stdin: Some(false),
env,
host_config: Some(HostConfig {
auto_remove: service.auto_remove,
restart_policy: parse_restart_policy(&service.restart),
port_bindings: create_port_map(&service.ports),
binds: parse_volume_mounts(compose, &service.volumes)?,
..Default::default()
}),
..Default::default()
};
let container_name = service
.container_name
.clone()
.unwrap_or_else(|| format!("{}_{}_1", parent_dir, name));
let create_info = docker
.create_container::<String, String>(
Some(CreateContainerOptions {
name: container_name,
platform: None,
}),
conf,
)
.await?;
container_ids.push(create_info.id);
}
Ok(container_ids)
}
pub(crate) async fn start_containers(docker: &Docker, ids: Vec<String>) -> anyhow::Result<()> {
for id in ids {
docker.start_container::<String>(&id, None).await?;
}
Ok(())
}
pub async fn stop_containers(compose: &DockerCompose, docker: &Docker) -> anyhow::Result<()> {
let parent_dir = parent_dir_name()?;
for (name, service) in &compose.services {
let container_name = service
.container_name
.clone()
.unwrap_or_else(|| format!("{}_{}_1", parent_dir, name));
docker.stop_container(&container_name, None).await?;
}
Ok(())
}
pub async fn remove_containers(compose: &DockerCompose, docker: &Docker) -> anyhow::Result<()> {
let parent_dir = parent_dir_name()?;
for (name, service) in &compose.services {
let container_name = service
.container_name
.clone()
.unwrap_or_else(|| format!("{}_{}_1", parent_dir, name));
docker
.remove_container(
&container_name,
Some(RemoveContainerOptions {
force: true,
..Default::default()
}),
)
.await?;
}
Ok(())
}
pub(crate) async fn stop_containers_by_ids(
docker: &Docker,
ids: Vec<String>,
) -> anyhow::Result<()> {
for id in ids {
docker.stop_container(&id, None).await?;
}
Ok(())
}
fn create_env_vec(env: &Option<HashMap<String, String>>) -> Option<Vec<String>> {
match env {
Some(env) => {
let list = env
.iter()
.map(|(key, value)| format!("{}={}", key, value))
.collect();
Some(list)
}
None => None,
}
}
fn create_port_map(ports: &Option<Vec<String>>) -> Option<PortMap> {
ports.as_ref().map(|ports| {
ports
.iter()
.map(|port| {
let parts: Vec<&str> = port.split(':').collect();
(
parts[1].to_string(),
Some(vec![PortBinding {
host_ip: None,
host_port: Some(parts[0].to_string()),
}]),
)
})
.collect()
})
}
fn parse_restart_policy(restart: &Option<String>) -> Option<RestartPolicy> {
match restart {
None => None,
Some(restart) => match restart.as_str() {
"no" => Some(RestartPolicy {
name: Some(RestartPolicyNameEnum::NO),
..Default::default()
}),
"always" => Some(RestartPolicy {
name: Some(RestartPolicyNameEnum::ALWAYS),
..Default::default()
}),
"unless-stopped" => Some(RestartPolicy {
name: Some(RestartPolicyNameEnum::UNLESS_STOPPED),
..Default::default()
}),
"on-failure" => Some(RestartPolicy {
name: Some(RestartPolicyNameEnum::NO),
..Default::default()
}),
v => {
// handle special case when retry count is specified
if v.starts_with("on-failure:") {
let parts: Vec<&str> = v.split(':').collect();
Some(RestartPolicy {
name: Some(RestartPolicyNameEnum::ON_FAILURE),
maximum_retry_count: Some(parts[1].parse().unwrap()),
})
} else {
None
}
}
},
}
}
fn parse_volume_mount(compose: &DockerCompose, volume: String) -> anyhow::Result<String> {
let parts: Vec<&str> = volume.split(':').collect();
if parts.len() != 2 {
return Err(anyhow!("Invalid volume mount: {}", volume));
}
if parts[0].starts_with('/') {
return Ok(volume);
}
let parent_dir = parent_dir_name()?;
let field_missing = format!("volume field for {} missing", volume);
// volumes map has to contain the volume defined here
if compose
.volumes
.as_ref()
.ok_or(anyhow!(field_missing.clone()))?
.contains_key(parts[0])
{
Ok(format!("{}_{}:{}", parent_dir, parts[0], parts[1]))
} else {
Err(anyhow!(field_missing))
}
}
fn parse_volume_mounts(
compose: &DockerCompose,
volumes: &Option<Vec<String>>,
) -> anyhow::Result<Option<Vec<String>>> {
match volumes {
None => Ok(None),
Some(volumes) => {
let mounts: Vec<anyhow::Result<String>> = volumes
.iter()
.map(|volume| parse_volume_mount(compose, volume.clone()))
.collect();
let mounts = mounts
.into_iter()
.collect::<anyhow::Result<Vec<String>>>()?;
if mounts.is_empty() {
Ok(None)
} else {
Ok(Some(mounts))
}
}
}
}