diff --git a/Cargo.lock b/Cargo.lock index f91b806f..411d83ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -451,6 +451,7 @@ dependencies = [ "blue-build-recipe", "blue-build-utils", "bon", + "chrono", "colored", "log", "rinja", diff --git a/process/drivers.rs b/process/drivers.rs index d1948ba1..25253e38 100644 --- a/process/drivers.rs +++ b/process/drivers.rs @@ -20,7 +20,7 @@ use clap::Args; use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; use log::{info, trace, warn}; -use miette::{miette, IntoDiagnostic, Report, Result}; +use miette::{miette, IntoDiagnostic, Result}; use oci_distribution::Reference; use once_cell::sync::Lazy; use opts::{GenerateImageNameOpts, GenerateTagsOpts}; @@ -202,6 +202,8 @@ impl Driver { #[builder(default)] platform: Platform, ) -> Result { + trace!("Driver::get_os_version({oci_ref:#?})"); + #[cfg(test)] { let _ = oci_ref; // silence lint @@ -211,8 +213,33 @@ impl Driver { } } - trace!("Driver::get_os_version({oci_ref:#?})"); - get_version(oci_ref, platform) + info!("Retrieving OS version from {oci_ref}"); + + let inspect_opts = GetMetadataOpts::builder() + .image(format!( + "{}/{}", + oci_ref.resolve_registry(), + oci_ref.repository() + )) + .tag(oci_ref.tag().unwrap_or("latest")) + .platform(platform) + .build(); + + let os_version = Self::get_metadata(&inspect_opts) + .and_then(|inspection| { + inspection.get_version().ok_or_else(|| { + miette!( + "Failed to parse version from metadata for {}", + oci_ref.to_string().bold() + ) + }) + }) + .or_else(|err| { + warn!("Unable to get version via image inspection due to error:\n{err:?}"); + get_version_run_image(oci_ref) + })?; + trace!("os_version: {os_version}"); + Ok(os_version) } fn get_build_driver() -> BuildDriverType { @@ -239,74 +266,46 @@ impl Driver { #[cached( result = true, key = "String", - convert = r#"{ format!("{oci_ref}-{platform}") }"#, + convert = r#"{ oci_ref.to_string() }"#, sync_writes = true )] -fn get_version(oci_ref: &Reference, platform: Platform) -> Result { - info!("Retrieving OS version from {oci_ref}. This might take a bit"); - let inspect_opts = GetMetadataOpts::builder() - .image(format!( - "{}/{}", - oci_ref.resolve_registry(), - oci_ref.repository() - )) - .tag(oci_ref.tag().unwrap_or("latest")) - .platform(platform) - .build(); - let os_version = Driver::get_metadata(&inspect_opts) - .and_then(|inspection| { - inspection.get_version().ok_or_else(|| { - miette!( - "Failed to parse version from metadata for {}", - oci_ref.to_string().bold() - ) - }) - }) - .or_else(get_version_run_image(oci_ref))?; - trace!("os_version: {os_version}"); - Ok(os_version) -} - -fn get_version_run_image(oci_ref: &Reference) -> impl FnOnce(Report) -> Result + '_ { - |err: Report| -> Result { - warn!("Unable to get version via image inspection due to error:\n{err:?}"); - warn!(concat!( - "Pulling and running the image to retrieve the version. ", - "This will take a while..." - )); - - let progress = Logger::multi_progress().add( - ProgressBar::new_spinner() - .with_style(ProgressStyle::default_spinner()) - .with_message(format!( - "Pulling image {} to get version", - oci_ref.to_string().bold() - )), - ); - progress.enable_steady_tick(Duration::from_millis(100)); - - let output = Driver::run_output( - &RunOpts::builder() - .image(oci_ref.to_string()) - .args(bon::vec![ - "/bin/bash", - "-c", - "grep -Po '(?<=VERSION_ID=)\\d+' /usr/lib/os-release", - ]) - .pull(true) - .remove(true) - .build(), - ) - .into_diagnostic()?; - - progress.finish_and_clear(); - Logger::multi_progress().remove(&progress); - - String::from_utf8_lossy(&output.stdout) - .trim() - .parse() - .into_diagnostic() - } +fn get_version_run_image(oci_ref: &Reference) -> Result { + warn!(concat!( + "Pulling and running the image to retrieve the version. ", + "This will take a while..." + )); + + let progress = Logger::multi_progress().add( + ProgressBar::new_spinner() + .with_style(ProgressStyle::default_spinner()) + .with_message(format!( + "Pulling image {} to get version", + oci_ref.to_string().bold() + )), + ); + progress.enable_steady_tick(Duration::from_millis(100)); + + let output = Driver::run_output( + &RunOpts::builder() + .image(oci_ref.to_string()) + .args(bon::vec![ + "/bin/bash", + "-c", + "grep -Po '(?<=VERSION_ID=)\\d+' /usr/lib/os-release", + ]) + .pull(true) + .remove(true) + .build(), + ) + .into_diagnostic()?; + + progress.finish_and_clear(); + Logger::multi_progress().remove(&progress); + + String::from_utf8_lossy(&output.stdout) + .trim() + .parse() + .into_diagnostic() } macro_rules! impl_build_driver { diff --git a/process/drivers/docker_driver.rs b/process/drivers/docker_driver.rs index 94e78636..0bbba130 100644 --- a/process/drivers/docker_driver.rs +++ b/process/drivers/docker_driver.rs @@ -13,6 +13,7 @@ use blue_build_utils::{ credentials::Credentials, string_vec, }; +use cached::proc_macro::cached; use log::{debug, info, trace, warn}; use miette::{bail, IntoDiagnostic, Result}; use once_cell::sync::Lazy; @@ -346,44 +347,54 @@ impl BuildDriver for DockerDriver { impl InspectDriver for DockerDriver { fn get_metadata(opts: &GetMetadataOpts) -> Result { - trace!("DockerDriver::get_metadata({opts:#?})"); - - let url = opts.tag.as_ref().map_or_else( - || format!("{}", opts.image), - |tag| format!("{}:{tag}", opts.image), - ); + get_metadata_cache(opts) + } +} - let mut command = cmd!( - "docker", - "buildx", - |command|? { - if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) { - Self::setup()?; - cmd!(command, "--builder=bluebuild"); - } - }, - "imagetools", - "inspect", - "--format", - "{{json .}}", - &url - ); - trace!("{command:?}"); +#[cached( + result = true, + key = "String", + convert = r#"{ format!("{}-{:?}-{}", &*opts.image, opts.tag.as_ref(), opts.platform)}"#, + sync_writes = true +)] +fn get_metadata_cache(opts: &GetMetadataOpts) -> Result { + trace!("DockerDriver::get_metadata({opts:#?})"); + + let url = opts.tag.as_ref().map_or_else( + || format!("{}", opts.image), + |tag| format!("{}:{tag}", opts.image), + ); - let output = command.output().into_diagnostic()?; + let mut command = cmd!( + "docker", + "buildx", + |command|? { + if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) { + DockerDriver::setup()?; + cmd!(command, "--builder=bluebuild"); + } + }, + "imagetools", + "inspect", + "--format", + "{{json .}}", + &url + ); + trace!("{command:?}"); - if output.status.success() { - info!("Successfully inspected image {url}!"); - } else { - bail!("Failed to inspect image {url}") - } + let output = command.output().into_diagnostic()?; - serde_json::from_slice::(&output.stdout) - .into_diagnostic() - .inspect(|metadata| trace!("{metadata:#?}")) - .map(ImageMetadata::from) - .inspect(|metadata| trace!("{metadata:#?}")) + if output.status.success() { + info!("Successfully inspected image {url}!"); + } else { + bail!("Failed to inspect image {url}") } + + serde_json::from_slice::(&output.stdout) + .into_diagnostic() + .inspect(|metadata| trace!("{metadata:#?}")) + .map(ImageMetadata::from) + .inspect(|metadata| trace!("{metadata:#?}")) } impl RunDriver for DockerDriver { diff --git a/process/drivers/opts/inspect.rs b/process/drivers/opts/inspect.rs index 169c2aa6..de22b735 100644 --- a/process/drivers/opts/inspect.rs +++ b/process/drivers/opts/inspect.rs @@ -4,7 +4,7 @@ use bon::Builder; use crate::drivers::types::Platform; -#[derive(Debug, Clone, Builder)] +#[derive(Debug, Clone, Builder, Hash)] #[builder(derive(Clone))] pub struct GetMetadataOpts<'scope> { #[builder(into)] diff --git a/process/drivers/podman_driver.rs b/process/drivers/podman_driver.rs index 5ba338b1..c8d2259f 100644 --- a/process/drivers/podman_driver.rs +++ b/process/drivers/podman_driver.rs @@ -7,6 +7,7 @@ use std::{ }; use blue_build_utils::{cmd, credentials::Credentials}; +use cached::proc_macro::cached; use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, error, info, trace, warn}; @@ -251,59 +252,69 @@ impl BuildDriver for PodmanDriver { impl InspectDriver for PodmanDriver { fn get_metadata(opts: &GetMetadataOpts) -> Result { - trace!("PodmanDriver::get_metadata({opts:#?})"); - - let url = opts.tag.as_deref().map_or_else( - || format!("{}", opts.image), - |tag| format!("{}:{tag}", opts.image), - ); + get_metadata_cache(opts) + } +} - let progress = Logger::multi_progress().add( - ProgressBar::new_spinner() - .with_style(ProgressStyle::default_spinner()) - .with_message(format!( - "Inspecting metadata for {}, pulling image...", - url.bold() - )), - ); - progress.enable_steady_tick(Duration::from_millis(100)); +#[cached( + result = true, + key = "String", + convert = r#"{ format!("{}-{:?}-{}", &*opts.image, opts.tag.as_ref(), opts.platform)}"#, + sync_writes = true +)] +fn get_metadata_cache(opts: &GetMetadataOpts) -> Result { + trace!("PodmanDriver::get_metadata({opts:#?})"); + + let url = opts.tag.as_deref().map_or_else( + || format!("{}", opts.image), + |tag| format!("{}:{tag}", opts.image), + ); - let mut command = cmd!( - "podman", - "pull", - if !matches!(opts.platform, Platform::Native) => [ - "--platform", - opts.platform.to_string(), - ], - &url, - ); - trace!("{command:?}"); + let progress = Logger::multi_progress().add( + ProgressBar::new_spinner() + .with_style(ProgressStyle::default_spinner()) + .with_message(format!( + "Inspecting metadata for {}, pulling image...", + url.bold() + )), + ); + progress.enable_steady_tick(Duration::from_millis(100)); + + let mut command = cmd!( + "podman", + "pull", + if !matches!(opts.platform, Platform::Native) => [ + "--platform", + opts.platform.to_string(), + ], + &url, + ); + trace!("{command:?}"); - let output = command.output().into_diagnostic()?; + let output = command.output().into_diagnostic()?; - if !output.status.success() { - bail!("Failed to pull {} for inspection!", url.bold()); - } + if !output.status.success() { + bail!("Failed to pull {} for inspection!", url.bold()); + } - let mut command = cmd!("podman", "image", "inspect", "--format=json", &url); - trace!("{command:?}"); + let mut command = cmd!("podman", "image", "inspect", "--format=json", &url); + trace!("{command:?}"); - let output = command.output().into_diagnostic()?; + let output = command.output().into_diagnostic()?; - progress.finish_and_clear(); - Logger::multi_progress().remove(&progress); + progress.finish_and_clear(); + Logger::multi_progress().remove(&progress); - if output.status.success() { - debug!("Successfully inspected image {url}!"); - } else { - bail!("Failed to inspect image {url}"); - } - serde_json::from_slice::>(&output.stdout) - .into_diagnostic() - .inspect(|metadata| trace!("{metadata:#?}")) - .and_then(TryFrom::try_from) - .inspect(|metadata| trace!("{metadata:#?}")) + if output.status.success() { + debug!("Successfully inspected image {url}!"); + } else { + bail!("Failed to inspect image {url}"); } + serde_json::from_slice::>(&output.stdout) + .into_diagnostic() + .inspect(|metadata| trace!("{metadata:#?}")) + .and_then(TryFrom::try_from) + .inspect(|metadata| trace!("{metadata:#?}")) } impl RunDriver for PodmanDriver { diff --git a/process/drivers/skopeo_driver.rs b/process/drivers/skopeo_driver.rs index af68d17f..b9e169ad 100644 --- a/process/drivers/skopeo_driver.rs +++ b/process/drivers/skopeo_driver.rs @@ -1,6 +1,7 @@ use std::{process::Stdio, time::Duration}; use blue_build_utils::cmd; +use cached::proc_macro::cached; use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, trace}; @@ -15,42 +16,52 @@ pub struct SkopeoDriver; impl InspectDriver for SkopeoDriver { fn get_metadata(opts: &GetMetadataOpts) -> Result { - trace!("SkopeoDriver::get_metadata({opts:#?})"); - - let url = opts.tag.as_ref().map_or_else( - || format!("docker://{}", opts.image), - |tag| format!("docker://{}:{tag}", opts.image), - ); - - let progress = Logger::multi_progress().add( - ProgressBar::new_spinner() - .with_style(ProgressStyle::default_spinner()) - .with_message(format!("Inspecting metadata for {}", url.bold())), - ); - progress.enable_steady_tick(Duration::from_millis(100)); - - let mut command = cmd!( - "skopeo", - if !matches!(opts.platform, Platform::Native) => [ - "--override-arch", - opts.platform.arch(), - ], - "inspect", - &url, - stderr = Stdio::inherit(), - ); - trace!("{command:?}"); - - let output = command.output().into_diagnostic()?; - - progress.finish_and_clear(); - Logger::multi_progress().remove(&progress); - - if output.status.success() { - debug!("Successfully inspected image {url}!"); - } else { - bail!("Failed to inspect image {url}") - } - serde_json::from_slice(&output.stdout).into_diagnostic() + get_metadata_cache(opts) } } + +#[cached( + result = true, + key = "String", + convert = r#"{ format!("{}-{:?}-{}", &*opts.image, opts.tag.as_ref(), opts.platform)}"#, + sync_writes = true +)] +fn get_metadata_cache(opts: &GetMetadataOpts) -> Result { + trace!("SkopeoDriver::get_metadata({opts:#?})"); + + let url = opts.tag.as_ref().map_or_else( + || format!("docker://{}", opts.image), + |tag| format!("docker://{}:{tag}", opts.image), + ); + + let progress = Logger::multi_progress().add( + ProgressBar::new_spinner() + .with_style(ProgressStyle::default_spinner()) + .with_message(format!("Inspecting metadata for {}", url.bold())), + ); + progress.enable_steady_tick(Duration::from_millis(100)); + + let mut command = cmd!( + "skopeo", + if !matches!(opts.platform, Platform::Native) => [ + "--override-arch", + opts.platform.arch(), + ], + "inspect", + &url, + stderr = Stdio::inherit(), + ); + trace!("{command:?}"); + + let output = command.output().into_diagnostic()?; + + progress.finish_and_clear(); + Logger::multi_progress().remove(&progress); + + if output.status.success() { + debug!("Successfully inspected image {url}!"); + } else { + bail!("Failed to inspect image {url}") + } + serde_json::from_slice(&output.stdout).into_diagnostic() +} diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 16bb6753..246cf7b4 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -145,6 +145,16 @@ impl GenerateCommand { .registry(registry) .repo(Driver::get_repo_url()?) .build_scripts_image(determine_scripts_tag(self.platform)?) + .base_digest( + Driver::get_metadata( + &GetMetadataOpts::builder() + .image(&*recipe.base_image) + .tag(&*recipe.image_version) + .platform(self.platform) + .build(), + )? + .digest, + ) .build(); let output_str = template.render().into_diagnostic()?; diff --git a/template/Cargo.toml b/template/Cargo.toml index d436918d..cdb96642 100644 --- a/template/Cargo.toml +++ b/template/Cargo.toml @@ -13,6 +13,7 @@ rinja = { version = "0.3", features = ["serde_json"] } blue-build-recipe = { version = "=0.8.20", path = "../recipe" } blue-build-utils = { version = "=0.8.20", path = "../utils" } +chrono.workspace = true log.workspace = true colored.workspace = true bon.workspace = true diff --git a/template/src/lib.rs b/template/src/lib.rs index d9f9eb15..56dab690 100644 --- a/template/src/lib.rs +++ b/template/src/lib.rs @@ -5,6 +5,7 @@ use blue_build_utils::constants::{ CONFIG_PATH, CONTAINERFILES_PATH, CONTAINER_FILE, COSIGN_PUB_PATH, FILES_PATH, }; use bon::Builder; +use chrono::Utc; use colored::control::ShouldColorize; use log::{debug, error, trace, warn}; use uuid::Uuid; @@ -27,6 +28,7 @@ pub struct ContainerFileTemplate<'a> { registry: Cow<'a, str>, build_scripts_image: Cow<'a, str>, repo: Cow<'a, str>, + base_digest: Cow<'a, str>, } #[derive(Debug, Clone, Template, Builder)] @@ -111,6 +113,10 @@ fn config_dir_exists() -> bool { exists } +fn current_timestamp() -> String { + Utc::now().to_rfc3339() +} + fn should_color() -> bool { ShouldColorize::from_env().should_colorize() } diff --git a/template/templates/Containerfile.j2 b/template/templates/Containerfile.j2 index 10a52422..bdcf0b9d 100644 --- a/template/templates/Containerfile.j2 +++ b/template/templates/Containerfile.j2 @@ -2,7 +2,7 @@ {%- include "stages.j2" %} # Main image -FROM {{ recipe.base_image }}:{{ recipe.image_version }} AS {{ recipe.name|replace('/', "-") }} +FROM {{ recipe.base_image }}@{{ base_digest }} AS {{ recipe.name|replace('/', "-") }} ARG RECIPE={{ recipe_path.display() }} ARG IMAGE_REGISTRY={{ registry }} @@ -46,4 +46,7 @@ LABEL {{ blue_build_utils::constants::BUILD_ID_LABEL }}="{{ build_id }}" LABEL org.opencontainers.image.title="{{ recipe.name }}" LABEL org.opencontainers.image.description="{{ recipe.description }}" LABEL org.opencontainers.image.source="{{ repo }}" +LABEL org.opencontainers.image.base.digest="{{ base_digest }}" +LABEL org.opencontainers.image.base.name="{{ recipe.base_image }}:{{ recipe.image_version }}" +LABEL org.opencontainers.image.created="{{ self::current_timestamp() }}" LABEL io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md