zlayer-builder 0.14.0

Dockerfile parsing and buildah-based container image building
Documentation
//! Thin tonic-client wrappers for the sidecar's post-build RPCs
//! (`push` / `tag` / `manifest_create` / `manifest_add` /
//! `manifest_push`).
//!
//! These mirror the `BuildBackend` trait's same-named methods and
//! delegate to the gRPC channel managed by
//! [`super::SidecarLifecycle`]. The translation is intentionally
//! minimal — the wire protocol is the contract; the sidecar handles
//! all buildah-level work.

use super::proto;
use super::BuildahSidecarBackend;
use crate::builder::RegistryAuth;
use crate::error::{BuildError, Result};

impl BuildahSidecarBackend {
    /// gRPC equivalent of `buildah push`.
    pub(super) async fn push_image_impl(
        &self,
        tag: &str,
        auth: Option<&RegistryAuth>,
    ) -> Result<()> {
        let live = self.lifecycle.ensure().await?;
        let mut client = live.client();

        // Convention: source image = supplied tag. Destination defaults
        // to the same tag canonicalised to docker:// when no transport
        // scheme is present. This matches BuildahBackend's CLI surface.
        let destination = if tag.contains("://") {
            tag.to_string()
        } else {
            format!("docker://{tag}")
        };

        let req = proto::PushRequest {
            image: tag.to_string(),
            destination,
            format: String::new(),
            remove_signatures: false,
            auth: resolve_push_auth(tag, auth),
        };
        client
            .push(tonic::Request::new(req))
            .await
            .map_err(|s| grpc_err(&s))?;
        Ok(())
    }

    /// gRPC equivalent of `buildah tag`.
    pub(super) async fn tag_image_impl(&self, image: &str, new_tag: &str) -> Result<()> {
        let live = self.lifecycle.ensure().await?;
        let mut client = live.client();
        let req = proto::TagRequest {
            image: image.into(),
            new_tag: new_tag.into(),
        };
        client
            .tag(tonic::Request::new(req))
            .await
            .map_err(|s| grpc_err(&s))?;
        Ok(())
    }

    /// gRPC equivalent of `buildah manifest create`.
    pub(super) async fn manifest_create_impl(&self, name: &str) -> Result<()> {
        let live = self.lifecycle.ensure().await?;
        let mut client = live.client();
        let req = proto::ManifestCreateRequest { name: name.into() };
        client
            .manifest_create(tonic::Request::new(req))
            .await
            .map_err(|s| grpc_err(&s))?;
        Ok(())
    }

    /// gRPC equivalent of `buildah manifest add`.
    pub(super) async fn manifest_add_impl(&self, manifest: &str, image: &str) -> Result<()> {
        let live = self.lifecycle.ensure().await?;
        let mut client = live.client();
        let req = proto::ManifestAddRequest {
            manifest: manifest.into(),
            image: image.into(),
        };
        client
            .manifest_add(tonic::Request::new(req))
            .await
            .map_err(|s| grpc_err(&s))?;
        Ok(())
    }

    /// gRPC equivalent of `buildah manifest push --all`.
    pub(super) async fn manifest_push_impl(
        &self,
        name: &str,
        destination: &str,
        auth: Option<&RegistryAuth>,
    ) -> Result<()> {
        let live = self.lifecycle.ensure().await?;
        let mut client = live.client();
        let req = proto::ManifestPushRequest {
            name: name.into(),
            destination: destination.into(),
            all: true,
            auth: auth.map(push_auth_from),
        };
        client
            .manifest_push(tonic::Request::new(req))
            .await
            .map_err(|s| grpc_err(&s))?;
        Ok(())
    }
}

/// Translate `RegistryAuth` (username + password) into the wire
/// `PushAuth`. The sidecar treats empty identity/registry tokens as
/// "not set" so we leave them blank.
fn push_auth_from(auth: &RegistryAuth) -> proto::PushAuth {
    proto::PushAuth {
        username: auth.username.clone(),
        password: auth.password.clone(),
        identity_token: String::new(),
        registry_token: String::new(),
    }
}

/// Resolve the wire auth for a `buildah push`.
///
/// Explicit `auth` always wins. When it is absent, fall back to the
/// host's `~/.docker/config.json` credentials for the tag's registry.
/// The VZ-Linux guest that actually runs `buildah push` has none of the
/// host's `docker login` state, so forwarding a bare `None` would push
/// anonymously and 401 against a private forge registry. This mirrors
/// `sandbox::sandbox_push::resolve_auth`.
///
/// The Docker-config fallback lives behind the `cache` feature because
/// that is the only build in which `zlayer_core` (which owns
/// `DockerConfigAuth`) is compiled into this crate. With the feature
/// off we preserve the original verbatim behaviour.
#[cfg(feature = "cache")]
fn resolve_push_auth(tag: &str, auth: Option<&RegistryAuth>) -> Option<proto::PushAuth> {
    // Explicitly provided credentials take precedence.
    if let Some(auth) = auth {
        return Some(push_auth_from(auth));
    }

    // Fall back to the host Docker config for the tag's registry.
    let docker_config = zlayer_core::auth::DockerConfigAuth::load().ok()?;
    let registry = extract_registry(tag);
    let (username, password) = docker_config.get_credentials(&registry)?;
    Some(proto::PushAuth {
        username,
        password,
        identity_token: String::new(),
        registry_token: String::new(),
    })
}

/// Feature-off variant: no `DockerConfigAuth` available, so preserve the
/// original behaviour of forwarding the caller's auth verbatim.
#[cfg(not(feature = "cache"))]
fn resolve_push_auth(_tag: &str, auth: Option<&RegistryAuth>) -> Option<proto::PushAuth> {
    auth.map(push_auth_from)
}

/// Extract the registry hostname from an image reference.
///
/// e.g. `ghcr.io/foo/bar:latest` → `ghcr.io`. Replicated locally
/// (rather than importing `sandbox`'s private helper) so this module
/// stays self-contained; only the Docker-config fallback needs it.
#[cfg(feature = "cache")]
fn extract_registry(tag: &str) -> String {
    // Strip tag/digest.
    let without_tag = tag.split(':').next().unwrap_or(tag);
    let without_digest = without_tag.split('@').next().unwrap_or(without_tag);

    // First path component is the registry if it contains a dot or colon.
    if let Some(first) = without_digest.split('/').next() {
        if first.contains('.') || first.contains(':') {
            return first.to_string();
        }
    }

    // Default to Docker Hub.
    "docker.io".to_string()
}

/// Map a tonic `Status` into the closest available `BuildError`.
/// `BuildahExecution` is the only variant carrying enough room for both
/// a gRPC code and a human message, and matches the rest of the
/// builder crate's error taxonomy where sidecar-side errors are
/// classified as "buildah failed."
fn grpc_err(status: &tonic::Status) -> BuildError {
    BuildError::BuildahExecution {
        command: format!("zlayer-buildd:{:?}", status.code()),
        exit_code: status.code() as i32,
        stderr: status.message().to_string(),
    }
}