lmrc-docker 0.3.16

Docker client library for the LMRC Stack - ergonomic fluent APIs for containers, images, networks, volumes, and registry management
Documentation
//! Container management operations.

use crate::DockerClient;
use crate::error::{DockerError, Result};
use bollard::container::*;
use bollard::exec::CreateExecOptions;
use bollard::models::*;
use futures_util::StreamExt;
use std::collections::HashMap;
use tracing::{debug, info};

mod builder;
pub use builder::*;

/// Container operations manager.
pub struct Containers<'a> {
    client: &'a DockerClient,
}

impl<'a> Containers<'a> {
    pub(crate) fn new(client: &'a DockerClient) -> Self {
        Self { client }
    }

    /// Create a new container builder.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///
    ///     let container = client.containers()
    ///         .create("nginx:latest")
    ///         .name("web-server")
    ///         .port(8080, 80, "tcp")
    ///         .env("ENV", "production")
    ///         .build()
    ///         .await?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn create(&self, image: impl Into<String>) -> ContainerBuilder<'a> {
        ContainerBuilder::new(self.client, image)
    }

    /// Get a reference to a specific container.
    pub fn get(&self, name_or_id: impl Into<String>) -> ContainerRef<'a> {
        ContainerRef::new(self.client, name_or_id.into())
    }

    /// List all containers.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     let containers = client.containers().list(true).await?;
    ///     for container in containers {
    ///         println!("{:?}", container.names);
    ///     }
    ///     Ok(())
    /// }
    /// ```
    pub async fn list(&self, all: bool) -> Result<Vec<ContainerSummary>> {
        let mut filters = HashMap::new();
        if all {
            filters.insert("all", vec!["true"]);
        }

        let options: Option<ListContainersOptions<String>> = Some(ListContainersOptions {
            all,
            ..Default::default()
        });

        self.client
            .docker
            .list_containers(options)
            .await
            .map_err(|e| DockerError::Other(format!("Failed to list containers: {}", e)))
    }

    /// Prune stopped containers.
    pub async fn prune(&self) -> Result<ContainerPruneResponse> {
        info!("Pruning stopped containers...");
        self.client
            .docker
            .prune_containers(Some(
                bollard::query_parameters::PruneContainersOptions::default(),
            ))
            .await
            .map_err(|e| DockerError::Other(format!("Failed to prune containers: {}", e)))
    }
}

/// Reference to a specific container.
pub struct ContainerRef<'a> {
    client: &'a DockerClient,
    id: String,
}

impl<'a> ContainerRef<'a> {
    pub(crate) fn new(client: &'a DockerClient, id: String) -> Self {
        Self { client, id }
    }

    /// Get the container ID.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Start the container.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     client.containers().get("my-container").start().await?;
    ///     Ok(())
    /// }
    /// ```
    pub async fn start(&self) -> Result<()> {
        info!("Starting container: {}", self.id);
        self.client
            .docker
            .start_container(
                &self.id,
                Some(bollard::query_parameters::StartContainerOptions::default()),
            )
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to start: {}", e)))
    }

    /// Stop the container.
    ///
    /// # Arguments
    ///
    /// * `timeout` - Optional timeout in seconds before killing the container
    pub async fn stop(&self, timeout: Option<i64>) -> Result<()> {
        info!("Stopping container: {}", self.id);
        let options = timeout.map(|t| StopContainerOptions { t });
        self.client
            .docker
            .stop_container(&self.id, options)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to stop: {}", e)))
    }

    /// Restart the container.
    pub async fn restart(&self, timeout: Option<i64>) -> Result<()> {
        info!("Restarting container: {}", self.id);
        let options = timeout.map(|t| RestartContainerOptions { t: t as isize });
        self.client
            .docker
            .restart_container(&self.id, options)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to restart: {}", e)))
    }

    /// Kill the container with optional signal.
    pub async fn kill(&self, signal: Option<&str>) -> Result<()> {
        info!("Killing container: {}", self.id);
        let options = signal.map(|s| KillContainerOptions { signal: s });
        self.client
            .docker
            .kill_container(&self.id, options)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to kill: {}", e)))
    }

    /// Pause the container.
    pub async fn pause(&self) -> Result<()> {
        info!("Pausing container: {}", self.id);
        self.client
            .docker
            .pause_container(&self.id)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to pause: {}", e)))
    }

    /// Unpause the container.
    pub async fn unpause(&self) -> Result<()> {
        info!("Unpausing container: {}", self.id);
        self.client
            .docker
            .unpause_container(&self.id)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to unpause: {}", e)))
    }

    /// Remove the container.
    ///
    /// # Arguments
    ///
    /// * `force` - Force remove even if running
    /// * `remove_volumes` - Remove associated volumes
    pub async fn remove(&self, force: bool, remove_volumes: bool) -> Result<()> {
        info!("Removing container: {}", self.id);
        let options = Some(RemoveContainerOptions {
            force,
            v: remove_volumes,
            ..Default::default()
        });
        self.client
            .docker
            .remove_container(&self.id, options)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to remove: {}", e)))
    }

    /// Inspect the container to get detailed information.
    pub async fn inspect(&self) -> Result<ContainerInspectResponse> {
        debug!("Inspecting container: {}", self.id);
        self.client
            .docker
            .inspect_container(
                &self.id,
                Some(bollard::query_parameters::InspectContainerOptions::default()),
            )
            .await
            .map_err(|e| DockerError::ContainerNotFound(format!("{}: {}", self.id, e)))
    }

    /// Get container logs.
    ///
    /// # Arguments
    ///
    /// * `follow` - Follow log output
    /// * `stdout` - Show stdout
    /// * `stderr` - Show stderr
    /// * `tail` - Number of lines to show from end (default: all)
    pub async fn logs(
        &self,
        follow: bool,
        stdout: bool,
        stderr: bool,
        tail: Option<&str>,
    ) -> Result<Vec<String>> {
        debug!("Getting logs for container: {}", self.id);
        let options: Option<LogsOptions<String>> = Some(LogsOptions {
            follow,
            stdout,
            stderr,
            tail: tail.unwrap_or("all").to_string(),
            ..Default::default()
        });

        let mut stream = self.client.docker.logs(&self.id, options);
        let mut logs = Vec::new();

        while let Some(chunk) = stream.next().await {
            match chunk {
                Ok(output) => {
                    logs.push(output.to_string());
                }
                Err(e) => {
                    return Err(DockerError::ContainerOperationFailed(format!(
                        "Failed to get logs: {}",
                        e
                    )));
                }
            }
        }

        Ok(logs)
    }

    // Note: stats() method removed - use bollard directly via client.inner() for advanced stats

    /// Rename the container.
    pub async fn rename(&self, new_name: impl Into<String>) -> Result<()> {
        let new_name = new_name.into();
        info!("Renaming container {} to {}", self.id, new_name);
        let options = RenameContainerOptions { name: new_name };
        self.client
            .docker
            .rename_container(&self.id, options)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to rename: {}", e)))
    }

    /// Execute a command in the container and return the exec ID.
    pub async fn exec(&self, cmd: Vec<String>, tty: bool) -> Result<String> {
        debug!("Executing command in container {}: {:?}", self.id, cmd);
        let config = CreateExecOptions {
            cmd: Some(cmd),
            attach_stdout: Some(true),
            attach_stderr: Some(true),
            tty: Some(tty),
            ..Default::default()
        };

        let response = self
            .client
            .docker
            .create_exec(&self.id, config)
            .await
            .map_err(|e| DockerError::ContainerOperationFailed(format!("Failed to exec: {}", e)))?;

        Ok(response.id)
    }

    // Note: top() method removed - use bollard directly via client.inner() for process listing
}

impl DockerClient {
    /// Access container operations.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     let containers = client.containers().list(true).await?;
    ///     Ok(())
    /// }
    /// ```
    pub fn containers(&self) -> Containers<'_> {
        Containers::new(self)
    }
}