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 client configuration and options.

use crate::error::*;
use crate::{parse_host_url, DockerEngineClient, UnixConnector};
use hyper::client::connect::Connect;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_tls::HttpsConnector;
use native_tls::{Certificate, Identity};
use openssl::pkcs12::Pkcs12;
use openssl::pkey::PKey;
use openssl::x509::X509;
use snafu::ResultExt;
use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
use std::{env, fs};
use tokio_tls::TlsConnector;

/// DockerEngineClientOption is a configuration option to initialize a client.
pub type DockerEngineClientOption<C> = Box<dyn Fn(&mut DockerEngineClient<C>) -> Result<(), Error>>;

/// from_env_remote_tls configures a remote Docker client with values from environment variables.
/// Supported environment variables:
/// DOCKER_HOST to set the url to the docker server.
/// DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
/// DOCKER_CERT_PATH to load the TLS certificates from.
/// DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
pub fn from_env_remote_tls(
    client: &mut DockerEngineClient<HttpsConnector<HttpConnector>>,
) -> Result<(), Error> {
    if let Ok(docker_cert_path) = env::var("DOCKER_CERT_PATH") {
        let insecure_skip_verify = if let Ok(tls_verify) = env::var("DOCKER_TLS_VERIFY") {
            tls_verify.is_empty()
        } else {
            false
        };

        let docker_cert_path = Path::new(&docker_cert_path);
        let ca_certificate_pem =
            fs::read_to_string(docker_cert_path.join("ca.pem")).context(IoError {})?;
        let client_certificate_pem =
            fs::read_to_string(docker_cert_path.join("cert.pem")).context(IoError {})?;
        let client_key_pem =
            fs::read_to_string(docker_cert_path.join("key.pem")).context(IoError {})?;

        let pcks12_encoded_client_key = pem_to_pkcs12(&client_certificate_pem, &client_key_pem)?;

        let ca_certificate = Certificate::from_pem(ca_certificate_pem.as_bytes())
            .context(CertificateParseError {})?;
        let client_identity = Identity::from_pkcs12(&pcks12_encoded_client_key, "")
            .context(CertificateParseError {})?;

        let mut http_connector = HttpConnector::new();
        http_connector.enforce_http(false);
        let tls_connector = TlsConnector::from(
            native_tls::TlsConnector::builder()
                .identity(client_identity)
                .add_root_certificate(ca_certificate)
                .danger_accept_invalid_hostnames(insecure_skip_verify)
                .build()
                .unwrap(),
        );
        let https_connector = HttpsConnector::from((http_connector, tls_connector));
        client.client = Some(Client::builder().build(https_connector));
    }
    if let Ok(host) = env::var("DOCKER_HOST") {
        with_host(host)(client)?;
    }
    if let Ok(version) = env::var("DOCKER_API_VERSION") {
        with_version(version)(client)?;
    }
    Ok(())
}

/// from_env_remote configures a remote Docker client with values from environment variables.
/// Supported environment variables:
/// DOCKER_HOST to set the url to the docker server.
/// DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
pub fn from_env_remote(client: &mut DockerEngineClient<HttpConnector>) -> Result<(), Error> {
    let mut http_connector = HttpConnector::new();
    http_connector.enforce_http(false);
    client.client = Some(Client::builder().build(http_connector));

    if let Ok(host) = env::var("DOCKER_HOST") {
        with_host(host)(client)?;
    }
    if let Ok(version) = env::var("DOCKER_API_VERSION") {
        with_version(version)(client)?;
    }
    Ok(())
}

/// from_env configures a local client with values from environment variables.
/// Supported environment variables:
/// DOCKER_HOST to set the url to the docker server.
/// DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
pub fn from_env(client: &mut DockerEngineClient<UnixConnector>) -> Result<(), Error> {
    let unix_connector = UnixConnector;
    client.client = Some(Client::builder().build(unix_connector));

    if let Ok(host) = env::var("DOCKER_HOST") {
        with_host(host)(client)?;
    }
    if let Ok(version) = env::var("DOCKER_API_VERSION") {
        with_version(version)(client)?;
    }
    Ok(())
}

/// with_host overrides the client host with the specified one.
pub fn with_host<C: Connect + Clone + Send + Sync + 'static>(
    host: String,
) -> DockerEngineClientOption<C> {
    Box::new(
        move |client: &mut DockerEngineClient<C>| -> Result<(), Error> {
            let host_url = parse_host_url(&host)?;
            client.host = host_url.host().map(String::from);
            client.proto = host_url.scheme_str().map(String::from);
            client.addr = host_url.host().map(String::from);

            let path = host_url.path();
            client.base_path = if !path.is_empty() {
                Some(path.into())
            } else {
                None
            };
            Ok(())
        },
    )
}

/// with_timeout configures the time limit for requests made by the HTTP client.
pub fn with_timeout<C: Connect + Clone + Send + Sync + 'static>(
    timeout: Duration,
) -> DockerEngineClientOption<C> {
    Box::new(
        move |client: &mut DockerEngineClient<C>| -> Result<(), Error> {
            client.timeout = timeout;
            Ok(())
        },
    )
}

/// with_http_headers overrides the client default http headers.
#[allow(clippy::implicit_hasher)]
pub fn with_http_headers<C: Connect + Clone + Send + Sync + 'static>(
    headers: HashMap<String, String>,
) -> DockerEngineClientOption<C> {
    Box::new(
        move |client: &mut DockerEngineClient<C>| -> Result<(), Error> {
            if !headers.is_empty() {
                client.custom_http_headers = Some(headers.clone());
            }
            Ok(())
        },
    )
}

/// with_scheme overrides the client scheme with the specified one.
pub fn with_scheme<C: Connect + Clone + Send + Sync + 'static>(
    scheme: String,
) -> DockerEngineClientOption<C> {
    Box::new(
        move |client: &mut DockerEngineClient<C>| -> Result<(), Error> {
            client.scheme = Some(scheme.clone());
            Ok(())
        },
    )
}

/// with_tls_client_config applies a tls config to the client transport.
pub fn with_tls_client_config(
    cacert_path: String,
    cert_path: String,
    key_path: String,
) -> DockerEngineClientOption<HttpsConnector<HttpConnector>> {
    Box::new(
        move |client: &mut DockerEngineClient<HttpsConnector<HttpConnector>>| -> Result<(), Error> {
            let ca_certificate_pem = fs::read_to_string(&cacert_path).context(IoError {})?;
            let client_certificate_pem = fs::read_to_string(&cert_path).context(IoError {})?;
            let client_key_pem = fs::read_to_string(&key_path).context(IoError {})?;

            let pcks12_encoded_client_key =
                pem_to_pkcs12(&client_certificate_pem, &client_key_pem)?;

            let ca_certificate = Certificate::from_pem(ca_certificate_pem.as_bytes())
                .context(CertificateParseError {})?;
            let client_identity = Identity::from_pkcs12(&pcks12_encoded_client_key, "")
                .context(CertificateParseError {})?;

            let mut http_connector = HttpConnector::new();
            http_connector.enforce_http(false);
            let tls_connector = TlsConnector::from(
                native_tls::TlsConnector::builder()
                    .identity(client_identity)
                    .add_root_certificate(ca_certificate)
                    .build()
                    .unwrap(),
            );
            let https_connector = HttpsConnector::from((http_connector, tls_connector));
            client.client = Some(Client::builder().build(https_connector));
            Ok(())
        },
    )
}

/// with_version overrides the client version with the specified one. If an empty
/// version is specified, the value will be ignored to allow version negotiation.
pub fn with_version<C: Connect + Clone + Send + Sync + 'static>(
    version: String,
) -> DockerEngineClientOption<C> {
    Box::new(
        move |client: &mut DockerEngineClient<C>| -> Result<(), Error> {
            if !version.is_empty() {
                client.version = version.clone();
                client.manual_override = true;
            }
            Ok(())
        },
    )
}

/// with_api_version_negotiation enables automatic API version negotiation for the client.
/// With this option enabled, the client automatically negotiates the API version
/// to use when making requests. API version negotiation is performed on the first
/// request; subsequent requests will not re-negotiate.
pub fn with_api_version_negotiation<C: Connect + Clone + Send + Sync + 'static>(
) -> DockerEngineClientOption<C> {
    Box::new(
        move |client: &mut DockerEngineClient<C>| -> Result<(), Error> {
            client.negotiate_version = true;
            Ok(())
        },
    )
}

/// rust-native-tls currently does not support PEM based mutual authentication out of the box.
/// to support this, this function converts a PEM key pair into a pkcs#12 archive.
fn pem_to_pkcs12(client_certificate_pem: &str, client_key_pem: &str) -> Result<Vec<u8>, Error> {
    let client_certificate =
        X509::from_pem(client_certificate_pem.as_bytes()).context(CryptoError {})?;
    let client_private_key =
        PKey::private_key_from_pem(client_key_pem.as_bytes()).context(CryptoError {})?;
    Pkcs12::builder()
        .build("", "", &client_private_key, &client_certificate)
        .context(CryptoError {})?
        .to_der()
        .context(CryptoError {})
}