dbctl-core 0.1.0

A CLI tool to manage Dockerized databases like PostgreSQL, Redis, and MariaDB
Documentation
use bollard::Docker;
use bollard::container::Config as ContainerConfig;
use bollard::container::CreateContainerOptions;
use bollard::container::StartContainerOptions;
use bollard::container::Stats;
use bollard::image::CreateImageOptions;
use bollard::models::*;
use futures_util::stream::TryStreamExt;
use std::collections::HashMap;
use std::default::Default;

use crate::db::Database;

pub struct DockerEngine {
    pub docker: Docker,
}

impl DockerEngine {
    pub async fn new() -> Self {
        let docker = Docker::connect_with_local_defaults().unwrap();
        DockerEngine { docker }
    }

    pub async fn start_container<D: Database>(&self, db: D) -> anyhow::Result<String> {
        self.docker
            .create_image(
                Some(CreateImageOptions {
                    from_image: db.image(),
                    ..Default::default()
                }),
                None,
                None,
            )
            .try_collect::<Vec<_>>()
            .await?;

        let env_vars: Vec<String> = db
            .env_vars()
            .into_iter()
            .map(|(k, v)| format!("{}={}", k, v))
            .collect();
        let env: Vec<&str> = env_vars.iter().map(|s| s.as_str()).collect();

        let port_str = format!("{}/tcp", db.port());

        let config = ContainerConfig {
            image: Some(db.image()),
            env: Some(env),
            exposed_ports: Some({
                let mut map = HashMap::new();
                map.insert(port_str.as_str(), HashMap::new());
                map
            }),
            host_config: Some(HostConfig {
                port_bindings: Some({
                    let mut pb = HashMap::new();
                    pb.insert(
                        port_str.clone(),
                        Some(vec![PortBinding {
                            host_ip: Some("0.0.0.0".to_string()),
                            host_port: Some(db.port().to_string()),
                        }]),
                    );
                    pb
                }),
                ..Default::default()
            }),
            ..Default::default()
        };

        let container_name = format!("dbctl-{}", db.name());

        let created = self
            .docker
            .create_container(
                Some(CreateContainerOptions::<&str> {
                    name: &container_name,

                    platform: None,
                }),
                config,
            )
            .await?;

        self.docker
            .start_container(&created.id, None::<StartContainerOptions<String>>)
            .await?;

        Ok(created.id)
    }

    pub async fn stop_container(&self, container_id: &str) -> anyhow::Result<()> {
        self.docker.stop_container(container_id, None).await?;

        self.docker.remove_container(container_id, None).await?;

        Ok(())
    }

    pub async fn container_logs(&self, container_id: &str) -> anyhow::Result<Vec<String>> {
        let logs = self
            .docker
            .logs(
                container_id,
                Some(bollard::container::LogsOptions {
                    stdout: true,
                    stderr: true,
                    follow: false,
                    tail: "100".to_string(),
                    ..Default::default()
                }),
            )
            .try_collect::<Vec<_>>()
            .await?;

        let log_lines = logs.iter().map(|log| log.to_string()).collect();

        Ok(log_lines)
    }

    pub async fn container_stats(
        &self,
        container_id: &str,
    ) -> anyhow::Result<Stats> {
        let stats = self
            .docker
            .stats(
                container_id,
                Some(bollard::container::StatsOptions {
                    stream: false,
                    one_shot: true,
                }),
            )
            .try_collect::<Vec<_>>()
            .await?;

        if let Some(container_stats) = stats.first() {
            Ok(container_stats.clone())
        } else {
            anyhow::bail!("No stats available for container {}", container_id)
        }
    }

    pub async fn container_info(
        &self,
        container_id: &str,
    ) -> anyhow::Result<ContainerInspectResponse> {
        let info = self.docker.inspect_container(container_id, None).await?;
        Ok(info)
    }
}