diff --git a/Cargo.lock b/Cargo.lock index 4ee7ce9..9cfee21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,7 @@ dependencies = [ "podman-api", "requestty", "rusty-hook", + "semver", "serde", "serde_json", "serde_yaml 0.9.32", @@ -2936,9 +2937,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] [[package]] name = "serde" diff --git a/Cargo.toml b/Cargo.toml index 20ccd13..8b17c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,9 +57,11 @@ clap_complete_nushell = "4" colorized = "1" env_logger = "0.11" fuzzy-matcher = "0.3" +once_cell = "1.19.0" open = "5" os_info = "3.7" # update os module config and tests when upgrading os_info requestty = { version = "0.5", features = ["macros", "termion"] } +semver = { version = "1.0.22", features = ["serde"] } shadow-rs = { version = "0.26" } urlencoding = "2.1.3" users = "0.11.0" @@ -82,7 +84,6 @@ serde_json.workspace = true serde_yaml.workspace = true typed-builder.workspace = true uuid.workspace = true -once_cell = "1.19.0" [features] default = [] diff --git a/README.md b/README.md index e2ee6ed..2cc89c1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ BlueBuild's command line program that builds Containerfiles and custom images based on your recipe.yml. +## Requirements + +The `bluebuild` tool takes advantage of newer build features. Specifically bind, cache, and tmpfs mounts on the `RUN` instructions. We support using the following tools and their versions: + +- Docker - v23 and above +- Podman - v4 and above +- Buildah - v1.24 and above + ## Installation ### Distrobox diff --git a/integration-tests/mock-scripts/buildah b/integration-tests/mock-scripts/buildah index fc8d2ee..cf1b83e 100755 --- a/integration-tests/mock-scripts/buildah +++ b/integration-tests/mock-scripts/buildah @@ -1,3 +1,16 @@ #!/bin/sh -echo 'Running buildah' +print_version_json() { + local version="1.24.0" + printf '{"version": "%s"}\n' "$version" +} + +main() { + if [[ "$1" == "version" && "$2" == "--json" ]]; then + print_version_json + else + echo 'Running buildah' + fi +} + +main "$@" diff --git a/integration-tests/mock-scripts/podman b/integration-tests/mock-scripts/podman index 92ef8f3..211e2e1 100755 --- a/integration-tests/mock-scripts/podman +++ b/integration-tests/mock-scripts/podman @@ -1,3 +1,16 @@ #!/bin/sh -echo 'Running podman' +print_version_json() { + local version="4.0.0" + printf '{"Client":{"Version": "%s"}}\n' "$version" +} + +main() { + if [[ "$1" == "version" && "$2" == "-f" && "$3" == "json" ]]; then + print_version_json + else + echo 'Running podman' + fi +} + +main "$@" diff --git a/src/drivers.rs b/src/drivers.rs index 5762fff..c953bc0 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -20,6 +20,7 @@ use blue_build_utils::constants::{ }; use log::{debug, error, info, trace}; use once_cell::sync::Lazy; +use semver::{Version, VersionReq}; use typed_builder::TypedBuilder; use uuid::Uuid; @@ -92,6 +93,26 @@ static BUILD_ID: Lazy = Lazy::new(Uuid::new_v4); /// The cached os versions static OS_VERSION: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +/// Trait for retrieving version of a driver. +pub trait DriverVersion { + /// The version req string slice that follows + /// the semver standard . + const VERSION_REQ: &'static str; + + /// Returns the version of the driver. + /// + /// # Errors + /// Will error if it can't retrieve the version. + fn version() -> Result; + + #[must_use] + fn is_supported_version() -> bool { + Self::version().is_ok_and(|version| { + VersionReq::parse(Self::VERSION_REQ).is_ok_and(|req| req.matches(&version)) + }) + } +} + /// Allows agnostic building, tagging /// pushing, and login. pub trait BuildDriver: Sync + Send { @@ -221,86 +242,92 @@ impl Driver<'_> { fn determine_inspect_driver() -> Result> { trace!("Strategy::determine_inspect_strategy()"); - Ok( - match ( - blue_build_utils::check_command_exists("skopeo"), - blue_build_utils::check_command_exists("docker"), - blue_build_utils::check_command_exists("podman"), - ) { - (Ok(_skopeo), _, _) => Arc::new(SkopeoDriver), - (_, Ok(_docker), _) => Arc::new(DockerDriver), - (_, _, Ok(_podman)) => Arc::new(PodmanDriver), - _ => bail!("Could not determine inspection strategy. You need either skopeo, docker, or podman"), - } - ) + let driver: Arc = match ( + blue_build_utils::check_command_exists("skopeo"), + blue_build_utils::check_command_exists("docker"), + blue_build_utils::check_command_exists("podman"), + ) { + (Ok(_skopeo), _, _) => Arc::new(SkopeoDriver), + (_, Ok(_docker), _) => Arc::new(DockerDriver), + (_, _, Ok(_podman)) => Arc::new(PodmanDriver), + _ => bail!("Could not determine inspection strategy. You need either skopeo, docker, or podman"), + }; + + Ok(driver) } fn determine_build_driver() -> Result> { trace!("Strategy::determine_build_strategy()"); - Ok( - match ( - env::var(XDG_RUNTIME_DIR), - PathBuf::from(RUN_PODMAN_SOCK), - PathBuf::from(VAR_RUN_PODMAN_PODMAN_SOCK), - PathBuf::from(VAR_RUN_PODMAN_SOCK), - blue_build_utils::check_command_exists("docker"), - blue_build_utils::check_command_exists("podman"), - blue_build_utils::check_command_exists("buildah"), - ) { - #[cfg(feature = "builtin-podman")] - (Ok(xdg_runtime), _, _, _, _, _, _) - if PathBuf::from(format!("{xdg_runtime}/podman/podman.sock")).exists() => - { - Arc::new( - PodmanApiDriver::builder() - .client( - Podman::unix(PathBuf::from(format!( - "{xdg_runtime}/podman/podman.sock" - ))) - .into(), - ) - .rt(Runtime::new()?) - .build(), - ) - } - #[cfg(feature = "builtin-podman")] - (_, run_podman_podman_sock, _, _, _, _, _) if run_podman_podman_sock.exists() => { - Arc::new( - PodmanApiDriver::builder() - .client(Podman::unix(run_podman_podman_sock).into()) - .rt(Runtime::new()?) - .build(), - ) - } - #[cfg(feature = "builtin-podman")] - (_, _, var_run_podman_podman_sock, _, _, _, _) - if var_run_podman_podman_sock.exists() => - { - Arc::new( - PodmanApiDriver::builder() - .client(Podman::unix(var_run_podman_podman_sock).into()) - .rt(Runtime::new()?) - .build(), - ) - } - #[cfg(feature = "builtin-podman")] - (_, _, _, var_run_podman_sock, _, _, _) if var_run_podman_sock.exists() => { - Arc::new( - PodmanApiDriver::builder() - .client(Podman::unix(var_run_podman_sock).into()) - .rt(Runtime::new()?) - .build(), - ) - } - // (_, _, _, _, Ok(_docker), _, _) if !oci_required => { - (_, _, _, _, Ok(_docker), _, _) => Arc::new(DockerDriver), - (_, _, _, _, _, Ok(_podman), _) => Arc::new(PodmanDriver), - (_, _, _, _, _, _, Ok(_buildah)) => Arc::new(BuildahDriver), - _ => bail!( - "Could not determine strategy, need either docker, podman, or buildah to continue" - ), - }, - ) + let driver: Arc = match ( + env::var(XDG_RUNTIME_DIR), + PathBuf::from(RUN_PODMAN_SOCK), + PathBuf::from(VAR_RUN_PODMAN_PODMAN_SOCK), + PathBuf::from(VAR_RUN_PODMAN_SOCK), + blue_build_utils::check_command_exists("docker"), + blue_build_utils::check_command_exists("podman"), + blue_build_utils::check_command_exists("buildah"), + ) { + #[cfg(feature = "builtin-podman")] + (Ok(xdg_runtime), _, _, _, _, _, _) + if PathBuf::from(format!("{xdg_runtime}/podman/podman.sock")).exists() => + { + Arc::new( + PodmanApiDriver::builder() + .client( + Podman::unix(PathBuf::from(format!( + "{xdg_runtime}/podman/podman.sock" + ))) + .into(), + ) + .rt(Runtime::new()?) + .build(), + ) + } + #[cfg(feature = "builtin-podman")] + (_, run_podman_podman_sock, _, _, _, _, _) if run_podman_podman_sock.exists() => { + Arc::new( + PodmanApiDriver::builder() + .client(Podman::unix(run_podman_podman_sock).into()) + .rt(Runtime::new()?) + .build(), + ) + } + #[cfg(feature = "builtin-podman")] + (_, _, var_run_podman_podman_sock, _, _, _, _) + if var_run_podman_podman_sock.exists() => + { + Arc::new( + PodmanApiDriver::builder() + .client(Podman::unix(var_run_podman_podman_sock).into()) + .rt(Runtime::new()?) + .build(), + ) + } + #[cfg(feature = "builtin-podman")] + (_, _, _, var_run_podman_sock, _, _, _) if var_run_podman_sock.exists() => Arc::new( + PodmanApiDriver::builder() + .client(Podman::unix(var_run_podman_sock).into()) + .rt(Runtime::new()?) + .build(), + ), + (_, _, _, _, Ok(_docker), _, _) if DockerDriver::is_supported_version() => { + Arc::new(DockerDriver) + } + (_, _, _, _, _, Ok(_podman), _) if PodmanDriver::is_supported_version() => { + Arc::new(PodmanDriver) + } + (_, _, _, _, _, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => { + Arc::new(BuildahDriver) + } + _ => bail!( + "Could not determine strategy, need either docker version {}, podman version {}, or buildah version {} to continue", + DockerDriver::VERSION_REQ, + PodmanDriver::VERSION_REQ, + BuildahDriver::VERSION_REQ, + ), + }; + + Ok(driver) } } diff --git a/src/drivers/buildah_driver.rs b/src/drivers/buildah_driver.rs index d398b1c..d3379cd 100644 --- a/src/drivers/buildah_driver.rs +++ b/src/drivers/buildah_driver.rs @@ -2,14 +2,38 @@ use std::process::Command; use anyhow::{bail, Result}; use log::{info, trace}; +use semver::Version; +use serde::Deserialize; use crate::credentials; -use super::BuildDriver; +use super::{BuildDriver, DriverVersion}; + +#[derive(Debug, Deserialize)] +struct BuildahVersionJson { + pub version: Version, +} #[derive(Debug)] pub struct BuildahDriver; +impl DriverVersion for BuildahDriver { + // RUN mounts for bind, cache, and tmpfs first supported in 1.24.0 + // https://buildah.io/releases/#changes-for-v1240 + const VERSION_REQ: &'static str = ">=1.24"; + + fn version() -> Result { + let output = Command::new("buildah") + .arg("version") + .arg("--json") + .output()?; + + let version_json: BuildahVersionJson = serde_json::from_slice(&output.stdout)?; + + Ok(version_json.version) + } +} + impl BuildDriver for BuildahDriver { fn build(&self, image: &str) -> Result<()> { trace!("buildah build -t {image}"); diff --git a/src/drivers/docker_driver.rs b/src/drivers/docker_driver.rs index 8061d07..b403c6f 100644 --- a/src/drivers/docker_driver.rs +++ b/src/drivers/docker_driver.rs @@ -6,14 +6,46 @@ use std::{ use anyhow::{bail, Result}; use blue_build_utils::constants::{BB_BUILDKIT_CACHE_GHA, SKOPEO_IMAGE}; use log::{info, trace}; +use semver::Version; +use serde::Deserialize; use crate::image_inspection::ImageInspection; -use super::{credentials, BuildDriver, InspectDriver}; +use super::{credentials, BuildDriver, DriverVersion, InspectDriver}; + +#[derive(Debug, Deserialize)] +struct DockerVerisonJsonClient { + #[serde(alias = "Version")] + pub version: Version, +} + +#[derive(Debug, Deserialize)] +struct DockerVersionJson { + #[serde(alias = "Client")] + pub client: DockerVerisonJsonClient, +} #[derive(Debug)] pub struct DockerDriver; +impl DriverVersion for DockerDriver { + // First docker verison to use buildkit + // https://docs.docker.com/build/buildkit/ + const VERSION_REQ: &'static str = ">=23"; + + fn version() -> Result { + let output = Command::new("docker") + .arg("version") + .arg("-f") + .arg("json") + .output()?; + + let version_json: DockerVersionJson = serde_json::from_slice(&output.stdout)?; + + Ok(version_json.client.version) + } +} + impl BuildDriver for DockerDriver { fn build(&self, image: &str) -> Result<()> { trace!("docker"); diff --git a/src/drivers/podman_driver.rs b/src/drivers/podman_driver.rs index 8938af7..5f05ff3 100644 --- a/src/drivers/podman_driver.rs +++ b/src/drivers/podman_driver.rs @@ -3,14 +3,46 @@ use std::process::{Command, Stdio}; use anyhow::{bail, Result}; use blue_build_utils::constants::SKOPEO_IMAGE; use log::{debug, info, trace}; +use semver::Version; +use serde::Deserialize; use crate::image_inspection::ImageInspection; -use super::{credentials, BuildDriver, InspectDriver}; +use super::{credentials, BuildDriver, DriverVersion, InspectDriver}; + +#[derive(Debug, Deserialize)] +struct PodmanVersionJsonClient { + #[serde(alias = "Version")] + pub version: Version, +} + +#[derive(Debug, Deserialize)] +struct PodmanVersionJson { + #[serde(alias = "Client")] + pub client: PodmanVersionJsonClient, +} #[derive(Debug)] pub struct PodmanDriver; +impl DriverVersion for PodmanDriver { + // First podman version to use buildah v1.24 + // https://github.com/containers/podman/blob/main/RELEASE_NOTES.md#400 + const VERSION_REQ: &'static str = ">=4"; + + fn version() -> Result { + let output = Command::new("podman") + .arg("version") + .arg("-f") + .arg("json") + .output()?; + + let version_json: PodmanVersionJson = serde_json::from_slice(&output.stdout)?; + + Ok(version_json.client.version) + } +} + impl BuildDriver for PodmanDriver { fn build(&self, image: &str) -> Result<()> { trace!("podman build . -t {image}");