Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ bon.workspace = true
# Top level features
default = []

v0_10_0 = [
v1_0_0 = [
"bootc",
"recipe-v2"
]
Expand Down
2 changes: 1 addition & 1 deletion process/drivers/opts/rechunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub struct RechunkOpts<'scope> {
pub platform: &'scope [Platform],
pub version: &'scope str,
pub name: &'scope str,
pub description: &'scope str,
pub description: Option<&'scope str>,
pub base_digest: &'scope str,
pub base_image: &'scope Reference,
pub repo: &'scope str,
Expand Down
29 changes: 22 additions & 7 deletions recipe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ trait RecipeSetters: RecipeGetters {

pub trait RecipeGetters {
fn get_name(&self) -> &str;
fn get_description(&self) -> &str;
fn get_description(&self) -> Option<&str>;
fn get_modules(&self) -> &[Module];
fn get_stages(&self) -> &[Stage];
fn get_labels(&self) -> HashMap<&String, &String>;
fn get_labels(&self) -> HashMap<&str, &str>;
fn get_alt_tags(&self) -> Option<&[Tag]>;
fn get_platforms(&self) -> &[Platform];
fn get_base_image(&self) -> Cow<'_, str>;
Expand All @@ -86,21 +86,21 @@ pub trait RecipeGetters {
&self,
default_labels: &BTreeMap<String, String>,
) -> BTreeMap<String, String> {
let mut labels = default_labels.iter().chain(self.get_labels()).fold(
let mut labels = default_labels.iter().map(|(key, value)| (&**key, &**value)).chain(self.get_labels()).fold(
BTreeMap::new(),
|mut acc, (k, v)| {
if let Some(existing_value) = acc.get(k) {
warn!("Found conflicting values for label: {k}, contains: {existing_value}, overwritten by: {v}");
}
acc.insert(k.clone(), v.clone());
acc.insert(k.to_owned(), v.to_owned());
acc
},
);

if !labels.contains_key("io.artifacthub.package.readme-url") {
// adding this if not included in the custom labeling to maintain backwards compatibility since this was hardcoded into the old template
labels.insert(
"io.artifacthub.package.readme-url".to_string(),
"io.artifacthub.package.readme-url".into(),
"https://raw.githubusercontent.com/blue-build/cli/main/README.md".into(),
);
}
Expand Down Expand Up @@ -209,6 +209,21 @@ impl Recipe {
inner(path.as_ref())
}

#[must_use]
#[cfg_attr(not(feature = "recipe-v2"), expect(clippy::missing_const_for_fn))]
pub fn upgrade(self) -> Self {
match self {
#[cfg(feature = "recipe-v2")]
Self::V1(recipe) => Self::V2(Box::new(RecipeV2::from(*recipe))),

#[cfg(feature = "recipe-v2")]
recipe @ Self::V2(_) => recipe,

#[cfg(not(feature = "recipe-v2"))]
recipe @ Self::V1(_) => recipe,
}
}

fn version_number(&self) -> Number {
match self {
Self::V1(_) => 1,
Expand Down Expand Up @@ -275,7 +290,7 @@ impl RecipeGetters for Recipe {
impl_recipe!(self, get_name())
}

fn get_description(&self) -> &str {
fn get_description(&self) -> Option<&str> {
impl_recipe!(self, get_description())
}

Expand All @@ -287,7 +302,7 @@ impl RecipeGetters for Recipe {
impl_recipe!(self, get_stages())
}

fn get_labels(&self) -> HashMap<&String, &String> {
fn get_labels(&self) -> HashMap<&str, &str> {
impl_recipe!(self, get_labels())
}

Expand Down
10 changes: 5 additions & 5 deletions recipe/src/recipe_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ impl RecipeGetters for RecipeV1 {
&self.name
}

fn get_description(&self) -> &str {
&self.description
fn get_description(&self) -> Option<&str> {
Some(&self.description)
}

fn get_base_image(&self) -> Cow<'_, str> {
Cow::Borrowed(&**self.base_image)
Cow::Borrowed(&self.base_image)
}

fn base_image_ref(&self) -> Result<Reference> {
Expand All @@ -118,11 +118,11 @@ impl RecipeGetters for RecipeV1 {
.with_context(|| format!("Unable to parse base image {base_image}"))
}

fn get_labels(&self) -> HashMap<&String, &String> {
fn get_labels(&self) -> HashMap<&str, &str> {
self.labels
.iter()
.flatten()
.map(|(key, value)| (key, &**value))
.map(|(key, value)| (&**key, &**value))
.collect()
}

Expand Down
141 changes: 115 additions & 26 deletions recipe/src/recipe_v2.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
use std::{borrow::Cow, collections::HashMap};
use std::{borrow::Cow, collections::HashMap, ops::Deref};

use blue_build_utils::{
constants::BLUE_BUILD_DEFAULT_IMAGE, container::Tag, env_str::EnvString, platform::Platform,
};
use bon::Builder;
use miette::{Context, IntoDiagnostic};
use oci_client::Reference;
use serde::{Deserialize, Serialize};
use structstruck::strike;

use crate::{Module, RecipeGetters, RecipeSetters, Stage};
use crate::{Module, RecipeGetters, RecipeSetters, RecipeV1, Stage};

use super::{MaybeVersion, ModuleExt, StagesExt};

#[derive(Debug, Clone, Serialize)]
pub struct RecipeV2BaseImageStr(EnvString);

impl From<Reference> for RecipeV2BaseImageStr {
fn from(value: Reference) -> Self {
Self(EnvString::from(value.to_string()))
}
}

impl<'de> Deserialize<'de> for RecipeV2BaseImageStr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let image = EnvString::deserialize(deserializer)?;

if let Err(e) = image.parse::<Reference>() {
return Err(serde::de::Error::custom(e));
}

Ok(Self(image))
}
}

impl Deref for RecipeV2BaseImageStr {
type Target = str;

fn deref(&self) -> &Self::Target {
&self.0
}
}

strike! {
/// The build recipe.
///
Expand All @@ -35,7 +68,7 @@ strike! {
#![serde(untagged)]

/// String representation of an image ref.
Str(Reference),
Str(RecipeV2BaseImageStr),

/// Object representation of an image ref.
Obj {
Expand Down Expand Up @@ -69,6 +102,7 @@ strike! {
/// This will validate the image before building with it.
///
/// URLs are supported. Paths are relative to the root of the project.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_key: Option<EnvString>,
},

Expand All @@ -78,7 +112,13 @@ strike! {
pub name: EnvString,

/// The image description. Published to GHCR in the image metadata.
pub description: EnvString,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<EnvString>,

/// Allows setting custom tags on the recipe’s final image.
/// Adding tags to this property will override the `latest` and timestamp tags.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<Tag>,

/// A collection of custom labels that will be applied to the image.
///
Expand All @@ -90,11 +130,6 @@ strike! {
/// Specifications for the image that modifies how it is built and published.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spec: Option<pub struct {
/// Allows setting custom tags on the recipe’s final image.
/// Adding tags to this property will override the `latest` and timestamp tags.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<Tag>,

/// Specify a list of the platforms to build for your image.
/// The resulting images will be added to a manifest list that
/// allows your host’s container runtime to pull the correct
Expand Down Expand Up @@ -159,13 +194,63 @@ strike! {
}
}

impl From<RecipeV1> for RecipeV2 {
fn from(value: RecipeV1) -> Self {
Self {
base: RecipeV2Base {
image: RecipeV2BaseImage::Str(RecipeV2BaseImageStr(EnvString::from(format!(
"{}:{}",
value.base_image.unexpanded(),
value.image_version.unexpanded(),
)))),
public_key: None,
},
metadata: RecipeV2Metadata {
name: value.name,
description: Some(value.description),
tags: value.alt_tags.unwrap_or_default(),
labels: value.labels,
},
spec: {
let has_versions = value.blue_build_tag.is_some()
|| value.cosign_version.is_some()
|| value.nushell_version.is_some();
let tool_versions = has_versions.then_some(RecipeV2SpecToolVersions {
Comment thread
gmpinder marked this conversation as resolved.
bluebuild: value.blue_build_tag,
nushell: match value.nushell_version {
None | Some(MaybeVersion::None) => None,
Some(MaybeVersion::VersionOrBranch(tag)) => Some(tag),
},
cosign: match value.cosign_version {
None | Some(MaybeVersion::None) => None,
Some(MaybeVersion::VersionOrBranch(tag)) => Some(tag),
},
});
match (value.platforms, has_versions) {
(None, false) => None,
(Some(platforms), false) => Some(RecipeV2Spec {
platforms,
tool_versions: None,
}),
(Some(platforms), true) => Some(RecipeV2Spec {
platforms,
tool_versions,
}),
(None, true) => Some(RecipeV2Spec {
platforms: Vec::default(),
Comment thread
gmpinder marked this conversation as resolved.
Outdated
tool_versions,
}),
}
},
stages_ext: value.stages_ext,
modules_ext: value.modules_ext,
}
}
}

impl Default for RecipeV2BaseImage {
fn default() -> Self {
Self::Str(
BLUE_BUILD_DEFAULT_IMAGE
.try_into()
.expect("Should be a valid image ref"),
)
Self::Str(RecipeV2BaseImageStr(BLUE_BUILD_DEFAULT_IMAGE.into()))
}
}

Expand All @@ -182,22 +267,21 @@ impl RecipeGetters for RecipeV2 {
&self.metadata.name
}

fn get_description(&self) -> &str {
&self.metadata.description
fn get_description(&self) -> Option<&str> {
self.metadata.description.as_deref()
}

fn get_labels(&self) -> HashMap<&String, &String> {
fn get_labels(&self) -> HashMap<&str, &str> {
self.metadata
.labels
.iter()
.flatten()
.map(|(key, value)| (key, &**value))
.map(|(key, value)| (&**key, &**value))
.collect()
}

fn get_alt_tags(&self) -> Option<&[Tag]> {
let spec = self.spec.as_ref()?;
match &spec.tags[..] {
match &self.metadata.tags[..] {
[] => None,
tags => Some(tags),
}
Expand All @@ -209,11 +293,13 @@ impl RecipeGetters for RecipeV2 {

fn get_base_image(&self) -> Cow<'_, str> {
match &self.base.image {
RecipeV2BaseImage::Str(image) => Cow::Owned(format!(
"{}/{}",
image.resolve_registry(),
image.repository()
)),
RecipeV2BaseImage::Str(image) => Cow::Borrowed(
image
.split_once(':') // Split at tag start
.or_else(|| image.split_once('@')) // or digest start
.unwrap_or((image, "")) // or the image without a tag
.0,
),
RecipeV2BaseImage::Obj {
registry,
repository,
Expand Down Expand Up @@ -256,7 +342,10 @@ impl RecipeGetters for RecipeV2 {

fn base_image_ref(&self) -> miette::Result<Reference> {
Ok(match &self.base.image {
RecipeV2BaseImage::Str(image) => image.clone(),
RecipeV2BaseImage::Str(RecipeV2BaseImageStr(image)) => image
.parse()
.into_diagnostic()
.wrap_err_with(|| format!("Failed to parse base image ref {image}"))?,
RecipeV2BaseImage::Obj {
registry,
repository,
Expand Down
3 changes: 3 additions & 0 deletions src/bin/bluebuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,8 @@ fn main() {
CommandArgs::Prune(mut command) => command.run(),
CommandArgs::BugReport(mut command) => command.run(),
CommandArgs::Completions(mut command) => command.run(),

#[cfg(feature = "recipe-v2")]
CommandArgs::Recipe(mut command) => command.run(),
});
}
6 changes: 6 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod generate_iso;
pub mod init;
pub mod login;
pub mod prune;
#[cfg(feature = "recipe-v2")]
pub mod recipe;
pub mod switch;
pub mod validate;

Expand Down Expand Up @@ -114,6 +116,10 @@ pub enum CommandArgs {

/// Generate shell completions for your shell to stdout
Completions(completions::CompletionsCommand),

/// Perform operations on recipe files
#[cfg(feature = "recipe-v2")]
Recipe(recipe::RecipeCommand),
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ impl BuildCommand {
.base_digest(base_digest)
.repo(&Driver::get_repo_url()?)
.name(recipe.get_name())
.description(recipe.get_description())
.maybe_description(recipe.get_description())
.base_image(&base_image)
.maybe_tempdir(self.tempdir.as_deref())
.clear_plan(self.rechunk_clear_plan)
Expand Down
Loading
Loading