use std::{
collections::{hash_map::Entry, HashMap},
sync::{Arc, Mutex},
};
use anyhow::{anyhow, bail, Result};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::IMAGE_VERSION_LABEL;
use log::{debug, info, trace};
use once_cell::sync::Lazy;
use semver::{Version, VersionReq};
use typed_builder::TypedBuilder;
use uuid::Uuid;
use crate::{credentials, image_metadata::ImageMetadata};
use self::{
buildah_driver::BuildahDriver,
docker_driver::DockerDriver,
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, TagOpts},
podman_driver::PodmanDriver,
skopeo_driver::SkopeoDriver,
types::{BuildDriverType, InspectDriverType},
};
mod buildah_driver;
mod docker_driver;
pub mod opts;
mod podman_driver;
mod skopeo_driver;
pub mod types;
static INIT: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
static SELECTED_BUILD_DRIVER: Lazy<Mutex<Option<BuildDriverType>>> = Lazy::new(|| Mutex::new(None));
static SELECTED_INSPECT_DRIVER: Lazy<Mutex<Option<InspectDriverType>>> =
Lazy::new(|| Mutex::new(None));
static BUILD_DRIVER: Lazy<Arc<dyn BuildDriver>> = Lazy::new(|| {
let driver = SELECTED_BUILD_DRIVER.lock().unwrap();
driver.map_or_else(
|| panic!("Driver needs to be initialized"),
|driver| -> Arc<dyn BuildDriver> {
match driver {
BuildDriverType::Buildah => Arc::new(BuildahDriver),
BuildDriverType::Podman => Arc::new(PodmanDriver),
BuildDriverType::Docker => Arc::new(DockerDriver),
}
},
)
});
static INSPECT_DRIVER: Lazy<Arc<dyn InspectDriver>> = Lazy::new(|| {
let driver = SELECTED_INSPECT_DRIVER.lock().unwrap();
driver.map_or_else(
|| panic!("Driver needs to be initialized"),
|driver| -> Arc<dyn InspectDriver> {
match driver {
InspectDriverType::Skopeo => Arc::new(SkopeoDriver),
InspectDriverType::Podman => Arc::new(PodmanDriver),
InspectDriverType::Docker => Arc::new(DockerDriver),
}
},
)
});
static BUILD_ID: Lazy<Uuid> = Lazy::new(Uuid::new_v4);
static OS_VERSION: Lazy<Mutex<HashMap<String, u64>>> = Lazy::new(|| Mutex::new(HashMap::new()));
pub trait DriverVersion {
const VERSION_REQ: &'static str;
fn version() -> Result<Version>;
#[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))
})
}
}
pub trait BuildDriver: Sync + Send {
fn build(&self, opts: &BuildOpts) -> Result<()>;
fn tag(&self, opts: &TagOpts) -> Result<()>;
fn push(&self, opts: &PushOpts) -> Result<()>;
fn login(&self) -> Result<()>;
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
trace!("BuildDriver::build_tag_push({opts:#?})");
let full_image = match (opts.archive_path.as_ref(), opts.image.as_ref()) {
(Some(archive_path), None) => {
format!("oci-archive:{archive_path}")
}
(None, Some(image)) => opts
.tags
.first()
.map_or_else(|| image.to_string(), |tag| format!("{image}:{tag}")),
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
(None, None) => bail!("Need either the image or archive path set"),
};
let build_opts = BuildOpts::builder()
.image(&full_image)
.squash(opts.squash)
.build();
info!("Building image {full_image}");
self.build(&build_opts)?;
if !opts.tags.is_empty() && opts.archive_path.is_none() {
let image = opts
.image
.as_ref()
.ok_or_else(|| anyhow!("Image is required in order to tag"))?;
debug!("Tagging all images");
for tag in opts.tags.as_ref() {
debug!("Tagging {} with {tag}", &full_image);
let tag_opts = TagOpts::builder()
.src_image(&full_image)
.dest_image(format!("{image}:{tag}"))
.build();
self.tag(&tag_opts)?;
if opts.push {
let retry_count = if opts.no_retry_push {
0
} else {
opts.retry_count
};
debug!("Pushing all images");
blue_build_utils::retry(retry_count, 1000, || {
let tag_image = format!("{image}:{tag}");
debug!("Pushing image {tag_image}");
let push_opts = PushOpts::builder()
.image(&tag_image)
.compression_type(opts.compression)
.build();
self.push(&push_opts)
})?;
}
}
}
Ok(())
}
}
pub trait InspectDriver: Sync + Send {
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata>;
}
#[derive(Debug, TypedBuilder)]
pub struct Driver<'a> {
#[builder(default)]
username: Option<&'a String>,
#[builder(default)]
password: Option<&'a String>,
#[builder(default)]
registry: Option<&'a String>,
#[builder(default)]
build_driver: Option<BuildDriverType>,
#[builder(default)]
inspect_driver: Option<InspectDriverType>,
}
impl Driver<'_> {
pub fn init(self) -> Result<()> {
trace!("Driver::init()");
let init = INIT.lock().map_err(|e| anyhow!("{e}"))?;
credentials::set_user_creds(self.username, self.password, self.registry)?;
let mut build_driver = SELECTED_BUILD_DRIVER.lock().map_err(|e| anyhow!("{e}"))?;
let mut inspect_driver = SELECTED_INSPECT_DRIVER.lock().map_err(|e| anyhow!("{e}"))?;
*build_driver = Some(match self.build_driver {
None => Self::determine_build_driver()?,
Some(driver) => driver,
});
trace!("Build driver set to {:?}", *build_driver);
drop(build_driver);
let _ = Self::get_build_driver();
*inspect_driver = Some(match self.inspect_driver {
None => Self::determine_inspect_driver()?,
Some(driver) => driver,
});
trace!("Inspect driver set to {:?}", *inspect_driver);
drop(inspect_driver);
let _ = Self::get_inspection_driver();
drop(init);
Ok(())
}
#[must_use]
pub fn get_build_id() -> Uuid {
trace!("Driver::get_build_id()");
*BUILD_ID
}
pub fn get_build_driver() -> Arc<dyn BuildDriver> {
trace!("Driver::get_build_driver()");
BUILD_DRIVER.clone()
}
pub fn get_inspection_driver() -> Arc<dyn InspectDriver> {
trace!("Driver::get_inspection_driver()");
INSPECT_DRIVER.clone()
}
pub fn get_os_version(recipe: &Recipe) -> Result<u64> {
trace!("Driver::get_os_version({recipe:#?})");
let image = format!("{}:{}", &recipe.base_image, &recipe.image_version);
let mut os_version_lock = OS_VERSION
.lock()
.map_err(|e| anyhow!("Unable set OS_VERSION {e}"))?;
let entry = os_version_lock.get(&image);
let os_version = match entry {
None => {
info!("Retrieving OS version from {image}. This might take a bit");
let inspect_opts = GetMetadataOpts::builder()
.image(recipe.base_image.as_ref())
.tag(recipe.image_version.as_ref())
.build();
let inspection = INSPECT_DRIVER.get_metadata(&inspect_opts)?;
let os_version = inspection.get_version().ok_or_else(|| {
anyhow!(
"Unable to get the OS version from the labels. Please check with the image author about using '{IMAGE_VERSION_LABEL}' to report the os version."
)
})?;
trace!("os_version: {os_version}");
os_version
}
Some(os_version) => {
debug!("Found cached {os_version} for {image}");
*os_version
}
};
if let Entry::Vacant(entry) = os_version_lock.entry(image.clone()) {
trace!("Caching version {os_version} for {image}");
entry.insert(os_version);
}
drop(os_version_lock);
Ok(os_version)
}
fn determine_inspect_driver() -> Result<InspectDriverType> {
trace!("Driver::determine_inspect_driver()");
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), _, _) => InspectDriverType::Skopeo,
(_, Ok(_docker), _) => InspectDriverType::Docker,
(_, _, Ok(_podman)) => InspectDriverType::Podman,
_ => bail!("Could not determine inspection strategy. You need either skopeo, docker, or podman"),
})
}
fn determine_build_driver() -> Result<BuildDriverType> {
trace!("Driver::determine_build_driver()");
Ok(match (
blue_build_utils::check_command_exists("docker"),
blue_build_utils::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
) {
(Ok(_docker), _, _) if DockerDriver::is_supported_version() => {
BuildDriverType::Docker
}
(_, Ok(_podman), _) if PodmanDriver::is_supported_version() => {
BuildDriverType::Podman
}
(_, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => {
BuildDriverType::Buildah
}
_ => 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,
),
})
}
}