feat: Include base image information in labels

This commit is contained in:
Gerald Pinder 2024-11-13 23:55:23 -05:00
parent 3674f83fd2
commit e3b246ef91
10 changed files with 246 additions and 193 deletions

1
Cargo.lock generated
View file

@ -451,6 +451,7 @@ dependencies = [
"blue-build-recipe", "blue-build-recipe",
"blue-build-utils", "blue-build-utils",
"bon", "bon",
"chrono",
"colored", "colored",
"log", "log",
"rinja", "rinja",

View file

@ -20,7 +20,7 @@ use clap::Args;
use colored::Colorize; use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use log::{info, trace, warn}; use log::{info, trace, warn};
use miette::{miette, IntoDiagnostic, Report, Result}; use miette::{miette, IntoDiagnostic, Result};
use oci_distribution::Reference; use oci_distribution::Reference;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use opts::{GenerateImageNameOpts, GenerateTagsOpts}; use opts::{GenerateImageNameOpts, GenerateTagsOpts};
@ -202,6 +202,8 @@ impl Driver {
#[builder(default)] #[builder(default)]
platform: Platform, platform: Platform,
) -> Result<u64> { ) -> Result<u64> {
trace!("Driver::get_os_version({oci_ref:#?})");
#[cfg(test)] #[cfg(test)]
{ {
let _ = oci_ref; // silence lint let _ = oci_ref; // silence lint
@ -211,8 +213,33 @@ impl Driver {
} }
} }
trace!("Driver::get_os_version({oci_ref:#?})"); info!("Retrieving OS version from {oci_ref}");
get_version(oci_ref, platform)
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 { fn get_build_driver() -> BuildDriverType {
@ -239,74 +266,46 @@ impl Driver {
#[cached( #[cached(
result = true, result = true,
key = "String", key = "String",
convert = r#"{ format!("{oci_ref}-{platform}") }"#, convert = r#"{ oci_ref.to_string() }"#,
sync_writes = true sync_writes = true
)] )]
fn get_version(oci_ref: &Reference, platform: Platform) -> Result<u64> { fn get_version_run_image(oci_ref: &Reference) -> Result<u64> {
info!("Retrieving OS version from {oci_ref}. This might take a bit"); warn!(concat!(
let inspect_opts = GetMetadataOpts::builder() "Pulling and running the image to retrieve the version. ",
.image(format!( "This will take a while..."
"{}/{}", ));
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<u64> + '_ { let progress = Logger::multi_progress().add(
|err: Report| -> Result<u64> { ProgressBar::new_spinner()
warn!("Unable to get version via image inspection due to error:\n{err:?}"); .with_style(ProgressStyle::default_spinner())
warn!(concat!( .with_message(format!(
"Pulling and running the image to retrieve the version. ", "Pulling image {} to get version",
"This will take a while..." oci_ref.to_string().bold()
)); )),
);
progress.enable_steady_tick(Duration::from_millis(100));
let progress = Logger::multi_progress().add( let output = Driver::run_output(
ProgressBar::new_spinner() &RunOpts::builder()
.with_style(ProgressStyle::default_spinner()) .image(oci_ref.to_string())
.with_message(format!( .args(bon::vec![
"Pulling image {} to get version", "/bin/bash",
oci_ref.to_string().bold() "-c",
)), "grep -Po '(?<=VERSION_ID=)\\d+' /usr/lib/os-release",
); ])
progress.enable_steady_tick(Duration::from_millis(100)); .pull(true)
.remove(true)
.build(),
)
.into_diagnostic()?;
let output = Driver::run_output( progress.finish_and_clear();
&RunOpts::builder() Logger::multi_progress().remove(&progress);
.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(); String::from_utf8_lossy(&output.stdout)
Logger::multi_progress().remove(&progress); .trim()
.parse()
String::from_utf8_lossy(&output.stdout) .into_diagnostic()
.trim()
.parse()
.into_diagnostic()
}
} }
macro_rules! impl_build_driver { macro_rules! impl_build_driver {

View file

@ -13,6 +13,7 @@ use blue_build_utils::{
credentials::Credentials, credentials::Credentials,
string_vec, string_vec,
}; };
use cached::proc_macro::cached;
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use miette::{bail, IntoDiagnostic, Result}; use miette::{bail, IntoDiagnostic, Result};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -346,46 +347,56 @@ impl BuildDriver for DockerDriver {
impl InspectDriver for DockerDriver { impl InspectDriver for DockerDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> { fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("DockerDriver::get_metadata({opts:#?})"); get_metadata_cache(opts)
let url = opts.tag.as_ref().map_or_else(
|| format!("{}", opts.image),
|tag| format!("{}:{tag}", opts.image),
);
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:?}");
let output = command.output().into_diagnostic()?;
if output.status.success() {
info!("Successfully inspected image {url}!");
} else {
bail!("Failed to inspect image {url}")
}
serde_json::from_slice::<DockerImageMetadata>(&output.stdout)
.into_diagnostic()
.inspect(|metadata| trace!("{metadata:#?}"))
.map(ImageMetadata::from)
.inspect(|metadata| trace!("{metadata:#?}"))
} }
} }
#[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<ImageMetadata> {
trace!("DockerDriver::get_metadata({opts:#?})");
let url = opts.tag.as_ref().map_or_else(
|| format!("{}", opts.image),
|tag| format!("{}:{tag}", opts.image),
);
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:?}");
let output = command.output().into_diagnostic()?;
if output.status.success() {
info!("Successfully inspected image {url}!");
} else {
bail!("Failed to inspect image {url}")
}
serde_json::from_slice::<DockerImageMetadata>(&output.stdout)
.into_diagnostic()
.inspect(|metadata| trace!("{metadata:#?}"))
.map(ImageMetadata::from)
.inspect(|metadata| trace!("{metadata:#?}"))
}
impl RunDriver for DockerDriver { impl RunDriver for DockerDriver {
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> { fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
let cid_path = TempDir::new()?; let cid_path = TempDir::new()?;

View file

@ -4,7 +4,7 @@ use bon::Builder;
use crate::drivers::types::Platform; use crate::drivers::types::Platform;
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Builder, Hash)]
#[builder(derive(Clone))] #[builder(derive(Clone))]
pub struct GetMetadataOpts<'scope> { pub struct GetMetadataOpts<'scope> {
#[builder(into)] #[builder(into)]

View file

@ -7,6 +7,7 @@ use std::{
}; };
use blue_build_utils::{cmd, credentials::Credentials}; use blue_build_utils::{cmd, credentials::Credentials};
use cached::proc_macro::cached;
use colored::Colorize; use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
@ -251,61 +252,71 @@ impl BuildDriver for PodmanDriver {
impl InspectDriver for PodmanDriver { impl InspectDriver for PodmanDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> { fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("PodmanDriver::get_metadata({opts:#?})"); get_metadata_cache(opts)
let url = opts.tag.as_deref().map_or_else(
|| format!("{}", opts.image),
|tag| format!("{}:{tag}", opts.image),
);
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()?;
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 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::<Vec<PodmanImageMetadata>>(&output.stdout)
.into_diagnostic()
.inspect(|metadata| trace!("{metadata:#?}"))
.and_then(TryFrom::try_from)
.inspect(|metadata| trace!("{metadata:#?}"))
} }
} }
#[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<ImageMetadata> {
trace!("PodmanDriver::get_metadata({opts:#?})");
let url = opts.tag.as_deref().map_or_else(
|| format!("{}", opts.image),
|tag| format!("{}:{tag}", opts.image),
);
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()?;
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 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::<Vec<PodmanImageMetadata>>(&output.stdout)
.into_diagnostic()
.inspect(|metadata| trace!("{metadata:#?}"))
.and_then(TryFrom::try_from)
.inspect(|metadata| trace!("{metadata:#?}"))
}
impl RunDriver for PodmanDriver { impl RunDriver for PodmanDriver {
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> { fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
trace!("PodmanDriver::run({opts:#?})"); trace!("PodmanDriver::run({opts:#?})");

View file

@ -1,6 +1,7 @@
use std::{process::Stdio, time::Duration}; use std::{process::Stdio, time::Duration};
use blue_build_utils::cmd; use blue_build_utils::cmd;
use cached::proc_macro::cached;
use colored::Colorize; use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, trace}; use log::{debug, trace};
@ -15,42 +16,52 @@ pub struct SkopeoDriver;
impl InspectDriver for SkopeoDriver { impl InspectDriver for SkopeoDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> { fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("SkopeoDriver::get_metadata({opts:#?})"); get_metadata_cache(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()
} }
} }
#[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<ImageMetadata> {
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()
}

View file

@ -145,6 +145,16 @@ impl GenerateCommand {
.registry(registry) .registry(registry)
.repo(Driver::get_repo_url()?) .repo(Driver::get_repo_url()?)
.build_scripts_image(determine_scripts_tag(self.platform)?) .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(); .build();
let output_str = template.render().into_diagnostic()?; let output_str = template.render().into_diagnostic()?;

View file

@ -13,6 +13,7 @@ rinja = { version = "0.3", features = ["serde_json"] }
blue-build-recipe = { version = "=0.8.20", path = "../recipe" } blue-build-recipe = { version = "=0.8.20", path = "../recipe" }
blue-build-utils = { version = "=0.8.20", path = "../utils" } blue-build-utils = { version = "=0.8.20", path = "../utils" }
chrono.workspace = true
log.workspace = true log.workspace = true
colored.workspace = true colored.workspace = true
bon.workspace = true bon.workspace = true

View file

@ -5,6 +5,7 @@ use blue_build_utils::constants::{
CONFIG_PATH, CONTAINERFILES_PATH, CONTAINER_FILE, COSIGN_PUB_PATH, FILES_PATH, CONFIG_PATH, CONTAINERFILES_PATH, CONTAINER_FILE, COSIGN_PUB_PATH, FILES_PATH,
}; };
use bon::Builder; use bon::Builder;
use chrono::Utc;
use colored::control::ShouldColorize; use colored::control::ShouldColorize;
use log::{debug, error, trace, warn}; use log::{debug, error, trace, warn};
use uuid::Uuid; use uuid::Uuid;
@ -27,6 +28,7 @@ pub struct ContainerFileTemplate<'a> {
registry: Cow<'a, str>, registry: Cow<'a, str>,
build_scripts_image: Cow<'a, str>, build_scripts_image: Cow<'a, str>,
repo: Cow<'a, str>, repo: Cow<'a, str>,
base_digest: Cow<'a, str>,
} }
#[derive(Debug, Clone, Template, Builder)] #[derive(Debug, Clone, Template, Builder)]
@ -111,6 +113,10 @@ fn config_dir_exists() -> bool {
exists exists
} }
fn current_timestamp() -> String {
Utc::now().to_rfc3339()
}
fn should_color() -> bool { fn should_color() -> bool {
ShouldColorize::from_env().should_colorize() ShouldColorize::from_env().should_colorize()
} }

View file

@ -2,7 +2,7 @@
{%- include "stages.j2" %} {%- include "stages.j2" %}
# Main image # 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 RECIPE={{ recipe_path.display() }}
ARG IMAGE_REGISTRY={{ registry }} 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.title="{{ recipe.name }}"
LABEL org.opencontainers.image.description="{{ recipe.description }}" LABEL org.opencontainers.image.description="{{ recipe.description }}"
LABEL org.opencontainers.image.source="{{ repo }}" 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 LABEL io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md