docker-client-async 0.1.0

A modern async/await Docker client written in Rust.
Documentation
/*
 * Copyright 2020 Damian Peckett <damian@pecke.tt>.
 * Copyright 2013-2018 Docker, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Docker volume api implementation.

use crate::error::*;
use crate::types::client::{VolumeCreateBody, VolumeListOKBody, VolumesPruneReport};
use crate::types::filters::Args;
use crate::types::volume::Volume;
use crate::{read_response_body, version, DockerEngineClient};
use hyper::client::connect::Connect;
use hyper::{Body, Method, Request};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use snafu::{ensure, ResultExt};
use std::collections::HashMap;
use tokio::time::timeout;

impl<C: Connect + Clone + Send + Sync + 'static> DockerEngineClient<C> {
    /// volume_list returns the volumes configured in the docker host.
    pub async fn volume_list(&self, filter: Option<Args>) -> Result<VolumeListOKBody, Error> {
        let mut query_params: HashMap<String, String> = HashMap::new();
        if let Some(filter) = filter {
            query_params.insert(
                "filters".into(),
                serde_json::to_string(&filter.fields).context(JsonSerializationError {})?,
            );
        }
        let query_params = if !query_params.is_empty() {
            Some(query_params)
        } else {
            None
        };

        let request = Request::builder()
            .method(Method::GET)
            .uri(self.request_uri("/volumes", query_params)?)
            .header("Accept", "application/json")
            .body(Body::empty())
            .context(HttpClientRequestBuilderError {})?;

        let client = self.client.as_ref().unwrap();
        let response = timeout(self.timeout, client.request(request))
            .await
            .context(HttpClientTimeoutError {})?
            .context(HttpClientError {})?;
        ensure!(
            response.status().is_success(),
            HttpClientResponseError {
                status: response.status().as_u16()
            }
        );
        let response_body = read_response_body(response, self.timeout).await?;
        Ok(serde_json::from_str(&response_body).context(JsonDeserializationError {})?)
    }

    /// volume_create creates a volume in the docker host.
    pub async fn volume_create(&self, options: VolumeCreateBody) -> Result<Volume, Error> {
        let request = Request::builder()
            .method(Method::POST)
            .uri(self.request_uri("/volumes/create", None)?)
            .header("Content-Type", "application/json")
            .header("Accept", "application/json")
            .body(Body::from(
                serde_json::to_string(&options).context(JsonSerializationError {})?,
            ))
            .context(HttpClientRequestBuilderError {})?;

        let client = self.client.as_ref().unwrap();
        let response = timeout(self.timeout, client.request(request))
            .await
            .context(HttpClientTimeoutError {})?
            .context(HttpClientError {})?;
        ensure!(
            response.status().is_success(),
            HttpClientResponseError {
                status: response.status().as_u16()
            }
        );
        let response_body = read_response_body(response, self.timeout).await?;
        Ok(serde_json::from_str(&response_body).context(JsonDeserializationError {})?)
    }

    /// volume_inspect returns the information about a specific volume in the docker host.
    pub async fn volume_inspect(&self, volume_id: &str) -> Result<Volume, Error> {
        let request = Request::builder()
            .method(Method::GET)
            .uri(self.request_uri(
                &format!(
                    "/volumes/{}",
                    utf8_percent_encode(volume_id, NON_ALPHANUMERIC).to_string()
                ),
                None,
            )?)
            .header("Accept", "application/json")
            .body(Body::empty())
            .context(HttpClientRequestBuilderError {})?;

        let client = self.client.as_ref().unwrap();
        let response = timeout(self.timeout, client.request(request))
            .await
            .context(HttpClientTimeoutError {})?
            .context(HttpClientError {})?;
        ensure!(
            response.status().is_success(),
            HttpClientResponseError {
                status: response.status().as_u16()
            }
        );
        let response_body = read_response_body(response, self.timeout).await?;
        Ok(serde_json::from_str(&response_body).context(JsonDeserializationError {})?)
    }

    /// volume_remove removes a volume from the docker host.
    pub async fn volume_remove(&self, volume_id: &str, force: bool) -> Result<(), Error> {
        let mut query_params: HashMap<String, String> = HashMap::new();
        if version::greater_than_or_equal_to(&self.version, "1.25") && force {
            query_params.insert("force".into(), "1".into());
        }
        let query_params = if !query_params.is_empty() {
            Some(query_params)
        } else {
            None
        };

        let request = Request::builder()
            .method(Method::DELETE)
            .uri(self.request_uri(
                &format!(
                    "/volumes/{}",
                    utf8_percent_encode(volume_id, NON_ALPHANUMERIC).to_string()
                ),
                query_params,
            )?)
            .header("Accept", "application/json")
            .body(Body::empty())
            .context(HttpClientRequestBuilderError {})?;

        let client = self.client.as_ref().unwrap();
        let response = timeout(self.timeout, client.request(request))
            .await
            .context(HttpClientTimeoutError {})?
            .context(HttpClientError {})?;
        ensure!(
            response.status().is_success(),
            HttpClientResponseError {
                status: response.status().as_u16()
            }
        );

        Ok(())
    }

    /// volumes_prune requests the daemon to delete unused data.
    pub async fn volumes_prune(
        &self,
        prune_filters: Option<Args>,
    ) -> Result<VolumesPruneReport, Error> {
        let mut query_params: HashMap<String, String> = HashMap::new();
        if let Some(prune_filters) = prune_filters {
            query_params.insert(
                "filters".into(),
                serde_json::to_string(&prune_filters.fields).context(JsonSerializationError {})?,
            );
        }
        let query_params = if !query_params.is_empty() {
            Some(query_params)
        } else {
            None
        };

        let request = Request::builder()
            .method(Method::POST)
            .uri(self.request_uri("/volumes/prune", query_params)?)
            .header("Accept", "application/json")
            .body(Body::empty())
            .context(HttpClientRequestBuilderError {})?;

        let client = self.client.as_ref().unwrap();
        let response = timeout(self.timeout, client.request(request))
            .await
            .context(HttpClientTimeoutError {})?
            .context(HttpClientError {})?;
        ensure!(
            response.status().is_success(),
            HttpClientResponseError {
                status: response.status().as_u16()
            }
        );
        let response_body = read_response_body(response, self.timeout).await?;
        Ok(serde_json::from_str(&response_body).context(JsonDeserializationError {})?)
    }
}

#[cfg(test)]
mod tests {
    use crate::types::client::*;
    use crate::types::filters::*;
    use crate::{opts, LocalDockerEngineClient};
    use maplit::hashmap;

    #[tokio::test]
    async fn test_volume_api() {
        let docker_client =
            LocalDockerEngineClient::new_client_with_opts(Some(vec![Box::new(opts::from_env)]))
                .unwrap();

        // Create a temporary named volume.
        let name = "test_volume_api_volume";
        let volume_create_response = docker_client
            .volume_create(
                VolumeCreateBodyBuilder::default()
                    .name(Some(name.into()))
                    .build()
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(volume_create_response.name.as_ref().unwrap(), name);
        assert_eq!(volume_create_response.driver.as_ref().unwrap(), "local");

        // Attempt to list the volume using a name based filter.
        let volume_list_response = docker_client
            .volume_list(Some(
                ArgsBuilder::default()
                    .fields(hashmap! {
                        "name".into() => vec![name.into()],
                    })
                    .build()
                    .unwrap(),
            ))
            .await
            .unwrap();

        let volumes = volume_list_response.volumes.unwrap();

        assert_eq!(volumes.len(), 1);
        assert_eq!(volumes[0].name.as_ref().unwrap(), name);

        // Inspect the temporary volume directly.
        let volume_inspect_response = docker_client.volume_inspect(name).await.unwrap();

        assert_eq!(volume_inspect_response.name.as_ref().unwrap(), name);

        // Remove and cleanup the temporary volume.
        docker_client.volume_remove(name, true).await.unwrap();

        // The temporary volume should no longer exist.
        let response = docker_client.volume_inspect(name).await;

        assert!(response.is_err());
    }
}