use std::{
collections::BTreeMap,
ops::Not,
path::{Path, PathBuf},
};
use crate::{BuildScripts, DriverTemplate, commands::validate::ValidateCommand};
use blue_build_process_management::drivers::{
CiDriver, Driver, DriverArgs, InspectDriver, opts::GetMetadataOpts,
};
use blue_build_recipe::Recipe;
use blue_build_template::{ContainerFileTemplate, Template};
use blue_build_utils::{
constants::{BB_SKIP_VALIDATION, CONFIG_PATH, RECIPE_FILE, RECIPE_PATH},
current_timestamp,
platform::Platform,
syntax_highlighting::{self, DefaultThemes},
};
use bon::Builder;
use cached::proc_macro::cached;
use clap::Args;
use colored::Colorize;
use log::{debug, info, trace, warn};
use miette::{Context, IntoDiagnostic, Result};
use oci_distribution::Reference;
use super::BlueBuildCommand;
#[derive(Debug, Clone, Args, Builder)]
pub struct GenerateCommand {
#[arg()]
#[builder(into)]
recipe: Option<PathBuf>,
#[arg(short, long)]
#[builder(into)]
output: Option<PathBuf>,
#[arg(long)]
#[builder(into)]
registry: Option<String>,
#[arg(long)]
#[builder(into)]
registry_namespace: Option<String>,
#[arg(short, long)]
#[builder(default)]
display_full_recipe: bool,
#[arg(short = 't', long)]
syntax_theme: Option<DefaultThemes>,
#[arg(long)]
platform: Option<Platform>,
#[arg(long, env = BB_SKIP_VALIDATION)]
#[builder(default)]
skip_validation: bool,
#[clap(flatten)]
#[builder(default)]
drivers: DriverArgs,
}
impl BlueBuildCommand for GenerateCommand {
fn try_run(&mut self) -> Result<()> {
Driver::init(self.drivers);
self.template_file()
}
}
impl GenerateCommand {
fn template_file(&self) -> Result<()> {
trace!("TemplateCommand::template_file()");
let recipe_path = self.recipe.clone().unwrap_or_else(|| {
let legacy_path = Path::new(CONFIG_PATH);
let recipe_path = Path::new(RECIPE_PATH);
if recipe_path.exists() && recipe_path.is_dir() {
recipe_path.join(RECIPE_FILE)
} else {
warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
legacy_path.join(RECIPE_FILE)
}
});
if self.skip_validation.not() {
ValidateCommand::builder()
.recipe(recipe_path.clone())
.build()
.try_run()?;
}
let registry = if let (Some(registry), Some(registry_namespace)) =
(&self.registry, &self.registry_namespace)
{
format!("{registry}/{registry_namespace}")
} else {
Driver::get_registry()?
};
debug!("Deserializing recipe");
let recipe = Recipe::parse(&recipe_path)?;
trace!("recipe_de: {recipe:#?}");
if self.display_full_recipe {
if let Some(output) = self.output.as_ref() {
std::fs::write(output, serde_yaml::to_string(&recipe).into_diagnostic()?)
.into_diagnostic()?;
} else {
syntax_highlighting::print_ser(&recipe, "yml", self.syntax_theme)?;
}
return Ok(());
}
info!("Templating for recipe at {}", recipe_path.display());
let base_image: Reference = format!("{}:{}", &recipe.base_image, &recipe.image_version)
.parse()
.into_diagnostic()
.wrap_err_with(|| {
format!(
"Failed to parse image with base {} and version {}",
recipe.base_image.bright_blue(),
recipe.image_version.to_string().bright_yellow()
)
})?;
let base_digest =
&Driver::get_metadata(GetMetadataOpts::builder().image(&base_image).build())?;
let base_digest = base_digest.digest();
let build_features = &[
#[cfg(feature = "bootc")]
"bootc".into(),
];
let build_scripts_dir = BuildScripts::extract_mount_dir()?;
let default_labels = generate_default_labels(&recipe)?;
let labels = recipe.generate_labels(&default_labels);
let template = ContainerFileTemplate::builder()
.os_version(
Driver::get_os_version()
.oci_ref(&recipe.base_image_ref()?)
.call()?,
)
.build_id(Driver::get_build_id())
.recipe(&recipe)
.recipe_path(recipe_path.as_path())
.registry(®istry)
.build_scripts_dir(&build_scripts_dir)
.base_digest(base_digest)
.maybe_nushell_version(recipe.nushell_version.as_ref())
.build_features(build_features)
.build_engine(Driver::get_build_driver().build_engine())
.labels(&labels)
.build();
let output_str = template.render().into_diagnostic().wrap_err_with(|| {
format!(
"Failed to render Containerfile for {}",
recipe_path.display().to_string().cyan()
)
})?;
if let Some(output) = self.output.as_ref() {
debug!("Templating to file {}", output.display());
trace!("Containerfile:\n{output_str}");
std::fs::write(output, output_str).into_diagnostic()?;
} else {
debug!("Templating to stdout");
syntax_highlighting::print(&output_str, "Dockerfile", self.syntax_theme)?;
}
Ok(())
}
}
pub fn generate_default_labels(recipe: &Recipe) -> Result<BTreeMap<String, String>> {
#[cached(
result = true,
key = "String",
convert = r"{ recipe.name.to_string() }"
)]
fn inner(recipe: &Recipe) -> Result<BTreeMap<String, String>> {
trace!("Generate LABELS for recipe: ({})", recipe.name);
let build_id = Driver::get_build_id().to_string();
let source = Driver::get_repo_url()?;
let image_metada = Driver::get_metadata(
GetMetadataOpts::builder()
.image(&recipe.base_image_ref()?)
.build(),
)?;
let base_digest = image_metada.digest().to_string();
let base_name = format!("{}:{}", recipe.base_image, recipe.image_version);
let current_timestamp = current_timestamp();
Ok(BTreeMap::from([
(
blue_build_utils::constants::BUILD_ID_LABEL.to_string(),
build_id,
),
(
"org.opencontainers.image.title".to_string(),
recipe.name.clone(),
),
(
"org.opencontainers.image.description".to_string(),
recipe.description.clone(),
),
("org.opencontainers.image.source".to_string(), source),
(
"org.opencontainers.image.base.digest".to_string(),
base_digest,
),
("org.opencontainers.image.base.name".to_string(), base_name),
(
"org.opencontainers.image.created".to_string(),
current_timestamp,
),
]))
}
inner(recipe)
}