From 7db737a4e1a7833dd05dbf4e1929a2b3fa1833ec Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 18 Dec 2025 13:53:55 +0100 Subject: [PATCH 1/2] feat(boil): Provide more build args --- boil.toml | 1 + rust/boil/src/build/bakefile.rs | 165 ++++++++++++++++++++++---------- rust/boil/src/build/cli.rs | 4 +- rust/boil/src/build/docker.rs | 4 + rust/boil/src/config.rs | 28 +++++- rust/boil/src/utils.rs | 27 ++++-- 6 files changed, 161 insertions(+), 68 deletions(-) diff --git a/boil.toml b/boil.toml index 2325681a5..e83a38236 100644 --- a/boil.toml +++ b/boil.toml @@ -8,6 +8,7 @@ DELETE_CACHES = "true" documentation = "https://docs.stackable.tech/home/stable/" source = "https://github.com/stackabletech/docker-images/" authors = "Stackable GmbH " +vendor-tag-prefix = "stackable" vendor = "Stackable GmbH" licenses = "Apache-2.0" diff --git a/rust/boil/src/build/bakefile.rs b/rust/boil/src/build/bakefile.rs index 547052649..7b221ba9e 100644 --- a/rust/boil/src/build/bakefile.rs +++ b/rust/boil/src/build/bakefile.rs @@ -24,8 +24,8 @@ use crate::{ image::{Image, ImageConfig, ImageConfigError, ImageOptions, VersionOptionsPair}, platform::TargetPlatform, }, - config::{self, Config}, - utils::{format_image_manifest_uri, format_image_repository_uri}, + config::{self, Config, Metadata}, + utils, }; pub const COMMON_TARGET_NAME: &str = "common--target"; @@ -272,23 +272,28 @@ impl Bakefile { } /// Creates the common target, containing shared data, which will be inherited by other targets. - fn common_target(args: &cli::BuildArguments, config: Config) -> Result { + fn common_target( + args: &cli::BuildArguments, + build_arguments: BuildArguments, + metadata: &Metadata, + ) -> Result { let revision = Self::git_head_revision().context(GetRevisionSnafu)?; let date_time = Self::now()?; // Load build arguments from a file if the user requested it - let mut build_arguments = args.build_arguments.clone(); + let mut user_build_arguments = args.build_arguments.clone(); if let Some(path) = &args.build_arguments_file { let build_arguments_from_file = BuildArguments::from_file(path).context(ParseBuildArgumentsSnafu)?; - build_arguments.extend(build_arguments_from_file); + user_build_arguments.extend(build_arguments_from_file); } let target = BakefileTarget::common( date_time, revision, - config, build_arguments, + metadata, + user_build_arguments, args.image_version.base_prerelease(), ); @@ -303,12 +308,46 @@ impl Bakefile { let mut bakefile_targets = BTreeMap::new(); let mut groups: BTreeMap = BTreeMap::new(); + // Destructure config so that we can move and borrow fields separately. + let Config { + build_arguments, + metadata, + } = config; + // Create a common target, which contains shared data, like annotations, arguments, labels, etc... - let common_target = Self::common_target(args, config)?; + let common_target = Self::common_target(args, build_arguments, &metadata)?; bakefile_targets.insert(COMMON_TARGET_NAME.to_owned(), common_target); + // The image registry, eg. `oci.stackable.tech` or `localhost` + let image_registry = if args.use_localhost_registry { + &HostPort::localhost() + } else { + &args.registry + }; + for (image_name, image_versions) in targets.into_iter() { for (image_version, (image_options, is_entry)) in image_versions { + let image_repository_uri = utils::format_image_repository_uri( + image_registry, + &args.registry_namespace, + &image_name, + ); + + let image_index_manifest_tag = utils::format_image_index_manifest_tag( + &image_version, + &metadata.vendor_tag_prefix, + &args.image_version, + ); + + let image_manifest_tag = utils::format_image_manifest_tag( + &image_index_manifest_tag, + args.target_platform.architecture(), + args.strip_architecture, + ); + + let image_manifest_uri = + utils::format_image_manifest_uri(&image_repository_uri, &image_manifest_tag); + // TODO (@Techassi): Clean this up // TODO (@Techassi): Move the arg formatting into functions let mut build_arguments = BuildArguments::new(); @@ -317,11 +356,8 @@ impl Bakefile { .local_images .iter() .map(|(image_name, image_version)| { - BuildArgument::new( - format!( - "{image_name}_VERSION", - image_name = image_name.to_uppercase().replace('-', "_") - ), + BuildArgument::local_image_version( + image_name.to_string(), image_version.to_string(), ) }) @@ -334,27 +370,22 @@ impl Bakefile { "PRODUCT_VERSION".to_owned(), image_version.to_string(), )); - - // The image registry, eg. `oci.stackable.tech` or `localhost` - let image_registry = if args.use_localhost_registry { - &HostPort::localhost() - } else { - &args.registry - }; - - let image_repository_uri = format_image_repository_uri( - image_registry, - &args.registry_namespace, - &image_name, - ); - - let image_manifest_uri = format_image_manifest_uri( - &image_repository_uri, - &image_version, - &args.image_version, - args.target_platform.architecture(), - args.strip_architecture, - ); + build_arguments.insert(BuildArgument::new( + "IMAGE_REPOSITORY_URI".to_owned(), + image_repository_uri, + )); + build_arguments.insert(BuildArgument::new( + "IMAGE_INDEX_MANIFEST_TAG".to_owned(), + image_index_manifest_tag, + )); + build_arguments.insert(BuildArgument::new( + "IMAGE_MANIFEST_TAG".to_owned(), + image_manifest_tag, + )); + build_arguments.insert(BuildArgument::new( + "IMAGE_MANIFEST_URI".to_owned(), + image_manifest_uri.clone(), + )); // By using a cap-std Dir, we can ensure that the paths provided must be relative to // the appropriate image folder and wont escape it by providing absolute or relative @@ -399,8 +430,11 @@ impl Bakefile { }) .collect(); - let annotations = - BakefileTarget::image_version_annotation(&image_version, &args.image_version); + let annotations = BakefileTarget::image_version_annotation( + &image_version, + &metadata.vendor_tag_prefix, + &args.image_version, + ); let target = BakefileTarget { tags: vec![image_manifest_uri], @@ -533,31 +567,50 @@ impl BakefileTarget { fn common( date_time: String, revision: String, - config: Config, - build_arguments: Vec, + build_arguments: BuildArguments, + metadata: &Metadata, + user_build_arguments: Vec, release_version: String, ) -> Self { let config::Metadata { - documentation, + documentation: docs, licenses, authors, source, vendor, - } = config.metadata; + .. + } = metadata; // Annotations describe OCI image components. - let annotations = vec![ + // Add annotations which are always present. + let mut annotations = vec![ format!("{ANNOTATION_CREATED}={date_time}"), - format!("{ANNOTATION_AUTHORS}={authors}"), - format!("{ANNOTATION_DOCUMENTATION}={documentation}"), - format!("{ANNOTATION_SOURCE}={source}"), format!("{ANNOTATION_REVISION}={revision}"), - format!("{ANNOTATION_VENDOR}={vendor}"), - format!("{ANNOTATION_LICENSES}={licenses}"), ]; - let mut arguments = config.build_arguments; - arguments.extend(build_arguments); + // Add optional annotations. + if let Some(authors) = authors { + annotations.push(format!("{ANNOTATION_AUTHORS}={authors}")); + } + + if let Some(docs) = docs { + annotations.push(format!("{ANNOTATION_DOCUMENTATION}={docs}")); + } + + if let Some(source) = source { + annotations.push(format!("{ANNOTATION_SOURCE}={source}")); + } + + if let Some(licenses) = licenses { + annotations.push(format!("{ANNOTATION_LICENSES}={licenses}")); + } + + if let Some(vendor) = vendor { + annotations.push(format!("{ANNOTATION_VENDOR}={vendor}")); + } + + let mut arguments = build_arguments; + arguments.extend(user_build_arguments); arguments.insert(BuildArgument::new( "RELEASE_VERSION".to_owned(), release_version, @@ -580,12 +633,18 @@ impl BakefileTarget { } } - fn image_version_annotation(image_version: &str, sdp_image_version: &Version) -> Vec { - vec![ - // TODO (@Techassi): Move this version formatting into a function - // TODO (@Techassi): Make this vendor agnostic, don't hard-code stackable here - format!("{ANNOTATION_VERSION}={image_version}-stackable{sdp_image_version}"), - ] + fn image_version_annotation( + image_version: &str, + vendor_tag_prefix: &str, + vendor_image_version: &Version, + ) -> Vec { + let image_index_manifest_tag = utils::format_image_index_manifest_tag( + image_version, + vendor_tag_prefix, + vendor_image_version, + ); + + vec![format!("{ANNOTATION_VERSION}={image_index_manifest_tag}")] } } diff --git a/rust/boil/src/build/cli.rs b/rust/boil/src/build/cli.rs index 4f33a841a..c3af30a09 100644 --- a/rust/boil/src/build/cli.rs +++ b/rust/boil/src/build/cli.rs @@ -22,9 +22,7 @@ pub struct BuildArguments { #[arg(help_heading = "Image Options", required = true)] pub images: Vec, - // The action currently does the wrong thing here. It includes the - // architecture even though it should come from the --target-platform arg. - // The release arg is NOT needed, because this version IS the release version. + // NOTE (@Techassi): Should this maybe be renamed to vendor_version? /// The image version being built. #[arg( short, long, diff --git a/rust/boil/src/build/docker.rs b/rust/boil/src/build/docker.rs index 23e524f1c..c338654d4 100644 --- a/rust/boil/src/build/docker.rs +++ b/rust/boil/src/build/docker.rs @@ -30,6 +30,10 @@ impl BuildArgument { Self((key, value)) } + pub fn local_image_version(image_name: String, image_version: String) -> Self { + Self::new(format!("{image_name}_VERSION"), image_version) + } + fn format_key(key: impl AsRef) -> String { key.as_ref().replace(['-', '/'], "_").to_uppercase() } diff --git a/rust/boil/src/config.rs b/rust/boil/src/config.rs index 19d4ff3c0..948be2901 100644 --- a/rust/boil/src/config.rs +++ b/rust/boil/src/config.rs @@ -30,10 +30,28 @@ impl Config { // NOTE (@Techassi): Think about if these metadata fields should be required or optional. If they // are optional, the appropriate annotations are only emitted if set. #[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] pub struct Metadata { - pub documentation: Url, - pub licenses: String, - pub authors: String, - pub vendor: String, - pub source: Url, + /// The URL to the documentation page. + pub documentation: Option, + + /// One ore more licenses used for images using the SPDX format. + pub licenses: Option, + + /// One or more authors of images. + /// + /// It is recommended to use the "NAME " format. + pub authors: Option, + + /// The vendor who builds the images. + pub vendor: Option, + + /// The vendor prefix used in the image (index) manifest tag. + /// + /// Defaults to an empty string. + #[serde(default)] + pub vendor_tag_prefix: String, + + /// The version control source of the images. + pub source: Option, } diff --git a/rust/boil/src/utils.rs b/rust/boil/src/utils.rs index d2e8270c1..81d6d1991 100644 --- a/rust/boil/src/utils.rs +++ b/rust/boil/src/utils.rs @@ -14,19 +14,32 @@ pub fn format_image_repository_uri( } /// Formats and returns the image manifest URI, eg. `oci.stackable.tech/sdp/opa:1.4.2-stackable25.7.0-amd64`. -pub fn format_image_manifest_uri( - image_repository_uri: &str, +pub fn format_image_manifest_uri(image_repository_uri: &str, image_manifest_tag: &str) -> String { + format!("{image_repository_uri}:{image_manifest_tag}") +} + +/// Formats and returns the image index manifest tag, eg. `1.4.2-stackable25.7.0`. +pub fn format_image_index_manifest_tag( image_version: &str, - sdp_image_version: &Version, + vendor_tag_prefix: &str, + vendor_image_version: &Version, +) -> String { + format!("{image_version}-{vendor_tag_prefix}{vendor_image_version}") +} + +/// Formats and returns the image manifest tag, eg. `1.4.2-stackable25.7.0-amd64`. +/// +/// The `strip_architecture` parameter controls if the architecture is included in the tag. +pub fn format_image_manifest_tag( + image_index_manifest_tag: &str, + // TODO (@Techassi): Maybe turn this into an Option to get rid of the bool architecture: &Architecture, strip_architecture: bool, ) -> String { if strip_architecture { - format!("{image_repository_uri}:{image_version}-stackable{sdp_image_version}") + image_index_manifest_tag.to_owned() } else { - format!( - "{image_repository_uri}:{image_version}-stackable{sdp_image_version}-{architecture}" - ) + format!("{image_index_manifest_tag}-{architecture}") } } From 0d48e75cd6fd71617fe3397eac29a00a9915f69b Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 18 Dec 2025 14:42:52 +0100 Subject: [PATCH 2/2] chore(boil): Clarify variable names --- rust/boil/src/build/bakefile.rs | 56 ++++++++++++++++----------------- rust/boil/src/build/mod.rs | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/rust/boil/src/build/bakefile.rs b/rust/boil/src/build/bakefile.rs index 7b221ba9e..06f3efe08 100644 --- a/rust/boil/src/build/bakefile.rs +++ b/rust/boil/src/build/bakefile.rs @@ -250,10 +250,10 @@ impl Bakefile { /// /// This will only create targets for selected entry images and their dependencies. There is no /// need to filter anything out afterwards. The filtering is done automatically internally. - pub fn from_args(args: &cli::BuildArguments, config: Config) -> Result { + pub fn from_cli_args(cli_args: &cli::BuildArguments, config: Config) -> Result { let targets = - Targets::set(&args.images, TargetsOptions::default()).context(CreateGraphSnafu)?; - Self::from_targets(targets, args, config) + Targets::set(&cli_args.images, TargetsOptions::default()).context(CreateGraphSnafu)?; + Self::from_targets(targets, cli_args, config) } /// Returns all image manifest URIs for entry images. @@ -273,28 +273,28 @@ impl Bakefile { /// Creates the common target, containing shared data, which will be inherited by other targets. fn common_target( - args: &cli::BuildArguments, - build_arguments: BuildArguments, + cli_args: &cli::BuildArguments, + container_build_args: BuildArguments, metadata: &Metadata, ) -> Result { let revision = Self::git_head_revision().context(GetRevisionSnafu)?; let date_time = Self::now()?; // Load build arguments from a file if the user requested it - let mut user_build_arguments = args.build_arguments.clone(); - if let Some(path) = &args.build_arguments_file { + let mut user_container_build_args = cli_args.build_arguments.clone(); + if let Some(path) = &cli_args.build_arguments_file { let build_arguments_from_file = BuildArguments::from_file(path).context(ParseBuildArgumentsSnafu)?; - user_build_arguments.extend(build_arguments_from_file); + user_container_build_args.extend(build_arguments_from_file); } let target = BakefileTarget::common( date_time, revision, - build_arguments, + cli_args.image_version.base_prerelease(), + container_build_args, + user_container_build_args, metadata, - user_build_arguments, - args.image_version.base_prerelease(), ); Ok(target) @@ -302,7 +302,7 @@ impl Bakefile { fn from_targets( targets: Targets, - args: &cli::BuildArguments, + cli_args: &cli::BuildArguments, config: Config, ) -> Result { let mut bakefile_targets = BTreeMap::new(); @@ -315,34 +315,34 @@ impl Bakefile { } = config; // Create a common target, which contains shared data, like annotations, arguments, labels, etc... - let common_target = Self::common_target(args, build_arguments, &metadata)?; + let common_target = Self::common_target(cli_args, build_arguments, &metadata)?; bakefile_targets.insert(COMMON_TARGET_NAME.to_owned(), common_target); // The image registry, eg. `oci.stackable.tech` or `localhost` - let image_registry = if args.use_localhost_registry { + let image_registry = if cli_args.use_localhost_registry { &HostPort::localhost() } else { - &args.registry + &cli_args.registry }; for (image_name, image_versions) in targets.into_iter() { for (image_version, (image_options, is_entry)) in image_versions { let image_repository_uri = utils::format_image_repository_uri( image_registry, - &args.registry_namespace, + &cli_args.registry_namespace, &image_name, ); let image_index_manifest_tag = utils::format_image_index_manifest_tag( &image_version, &metadata.vendor_tag_prefix, - &args.image_version, + &cli_args.image_version, ); let image_manifest_tag = utils::format_image_manifest_tag( &image_index_manifest_tag, - args.target_platform.architecture(), - args.strip_architecture, + cli_args.target_platform.architecture(), + cli_args.strip_architecture, ); let image_manifest_uri = @@ -404,13 +404,13 @@ impl Bakefile { PathBuf::new().join(&image_name).join(custom_path) } else { ensure!( - image_dir.exists(&args.target_containerfile), + image_dir.exists(&cli_args.target_containerfile), NoSuchContainerfileExistsSnafu { path: image_name } ); PathBuf::new() .join(&image_name) - .join(&args.target_containerfile) + .join(&cli_args.target_containerfile) }; let target_name = if is_entry { @@ -433,13 +433,13 @@ impl Bakefile { let annotations = BakefileTarget::image_version_annotation( &image_version, &metadata.vendor_tag_prefix, - &args.image_version, + &cli_args.image_version, ); let target = BakefileTarget { tags: vec![image_manifest_uri], arguments: build_arguments, - platforms: vec![args.target_platform.clone()], + platforms: vec![cli_args.target_platform.clone()], // NOTE (@Techassi): Should this instead be scoped to the folder of the image we build context: Some(PathBuf::from(".")), dockerfile: Some(dockerfile_path), @@ -567,10 +567,10 @@ impl BakefileTarget { fn common( date_time: String, revision: String, - build_arguments: BuildArguments, - metadata: &Metadata, - user_build_arguments: Vec, release_version: String, + container_build_args: BuildArguments, + user_container_build_args: Vec, + metadata: &Metadata, ) -> Self { let config::Metadata { documentation: docs, @@ -609,8 +609,8 @@ impl BakefileTarget { annotations.push(format!("{ANNOTATION_VENDOR}={vendor}")); } - let mut arguments = build_arguments; - arguments.extend(user_build_arguments); + let mut arguments = container_build_args; + arguments.extend(user_container_build_args); arguments.insert(BuildArgument::new( "RELEASE_VERSION".to_owned(), release_version, diff --git a/rust/boil/src/build/mod.rs b/rust/boil/src/build/mod.rs index ea070b6f0..17769a875 100644 --- a/rust/boil/src/build/mod.rs +++ b/rust/boil/src/build/mod.rs @@ -57,7 +57,7 @@ pub fn run_command(args: Box, config: Config) -> Result<(), Erro ); // Create bakefile - let bakefile = Bakefile::from_args(&args, config).context(CreateBakefileSnafu)?; + let bakefile = Bakefile::from_cli_args(&args, config).context(CreateBakefileSnafu)?; let image_manifest_uris = bakefile.image_manifest_uris(); let count = image_manifest_uris.len();