buildkit-frontend 0.1.0

Foundation for BuildKit frontends implemented in Rust
Documentation
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

use failure::{bail, format_err, Error, ResultExt};
use futures::compat::*;
use futures::lock::Mutex;
use log::*;

use tower_grpc::{BoxBody, Request};
use tower_hyper::client::Connection;

use buildkit_proto::google::rpc::Status;
use buildkit_proto::moby::buildkit::v1::frontend::{
    client, result::Result as RefResult, ReadFileRequest, ResolveImageConfigRequest,
    Result as Output, ReturnRequest, SolveRequest,
};

pub use buildkit_llb::ops::source::{ImageSource, ResolveMode};
pub use buildkit_llb::ops::Terminal;
pub use buildkit_proto::moby::buildkit::v1::frontend::FileRange;

use crate::error::ErrorCode;
use crate::oci::ImageSpecification;
use crate::utils::OutputRef;

type BridgeConnection = tower_request_modifier::RequestModifier<Connection<BoxBody>, BoxBody>;

#[derive(Clone)]
pub struct Bridge {
    client: Arc<Mutex<client::LlbBridge<BridgeConnection>>>,
}

impl Bridge {
    pub(crate) fn new(client: BridgeConnection) -> Self {
        Self {
            client: Arc::new(Mutex::new(client::LlbBridge::new(client))),
        }
    }

    pub async fn resolve_image_config(
        &self,
        image: &ImageSource,
        log: Option<&str>,
    ) -> Result<(String, ImageSpecification), Error> {
        let request = ResolveImageConfigRequest {
            r#ref: image.canonical_name().into(),
            platform: None,
            resolve_mode: image.resolve_mode().unwrap_or_default().to_string(),
            log_name: log.unwrap_or_default().into(),
        };

        debug!("requesting to resolve an image: {:?}", request);
        let response = {
            self.client
                .lock()
                .await
                .resolve_image_config(Request::new(request))
                .compat()
                .await
                .unwrap()
                .into_inner()
        };

        Ok((
            response.digest,
            serde_json::from_slice(&response.config)
                .context("Unable to parse image specification")?,
        ))
    }

    pub async fn solve<'a, 'b: 'a>(&'a self, graph: Terminal<'b>) -> Result<OutputRef, Error> {
        debug!("serializing a graph to request");
        let request = SolveRequest {
            definition: Some(graph.into_definition()),
            exporter_attr: vec![],
            allow_result_return: true,

            ..Default::default()
        };

        debug!("requesting to solve a graph");
        let response = {
            self.client
                .lock()
                .await
                .solve(Request::new(request))
                .compat()
                .await
                .context("Unable to solve the graph")?
                .into_inner()
                .result
                .ok_or_else(|| format_err!("Unable to extract solve result"))?
        };

        debug!("got response: {:#?}", response);

        let inner = {
            response
                .result
                .ok_or_else(|| format_err!("Unable to extract solve result"))?
        };

        match inner {
            RefResult::Ref(inner) => Ok(OutputRef(inner)),
            other => bail!("Unexpected solve response: {:?}", other),
        }
    }

    pub async fn read_file<'a, 'b: 'a, P>(
        &'a self,
        layer: &'b OutputRef,
        path: P,
        range: Option<FileRange>,
    ) -> Result<Vec<u8>, Error>
    where
        P: Into<PathBuf>,
    {
        let file_path = path.into().display().to_string();
        debug!("requesting a file contents: {:#?}", file_path);

        let request = ReadFileRequest {
            r#ref: layer.0.clone(),
            file_path,
            range,
        };

        let response = {
            self.client
                .lock()
                .await
                .read_file(Request::new(request))
                .compat()
                .await
                .context("Unable to read the file")?
                .into_inner()
                .data
        };

        Ok(response)
    }

    pub(crate) async fn finish_with_success(
        self,
        output: OutputRef,
        config: Option<ImageSpecification>,
    ) -> Result<(), Error> {
        let mut metadata = HashMap::new();

        if let Some(config) = config {
            metadata.insert("containerimage.config".into(), serde_json::to_vec(&config)?);
        }

        let request = ReturnRequest {
            error: None,
            result: Some(Output {
                result: Some(RefResult::Ref(output.0)),
                metadata,
            }),
        };

        self.client
            .lock()
            .await
            .r#return(Request::new(request))
            .compat()
            .await?;

        // TODO: gracefully shutdown the HTTP/2 connection

        Ok(())
    }

    pub(crate) async fn finish_with_error<S>(self, code: ErrorCode, message: S) -> Result<(), Error>
    where
        S: Into<String>,
    {
        let request = ReturnRequest {
            result: None,
            error: Some(Status {
                code: code as i32,
                message: message.into(),
                details: vec![],
            }),
        };

        debug!("sending an error result: {:#?}", request);
        self.client
            .lock()
            .await
            .r#return(Request::new(request))
            .compat()
            .await?;

        // TODO: gracefully shutdown the HTTP/2 connection

        Ok(())
    }
}