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..06f3efe08 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"; @@ -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. @@ -272,24 +272,29 @@ 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( + 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 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)?; - build_arguments.extend(build_arguments_from_file); + user_container_build_args.extend(build_arguments_from_file); } let target = BakefileTarget::common( date_time, revision, - config, - build_arguments, - args.image_version.base_prerelease(), + cli_args.image_version.base_prerelease(), + container_build_args, + user_container_build_args, + metadata, ); Ok(target) @@ -297,18 +302,52 @@ impl Bakefile { fn from_targets( targets: Targets, - args: &cli::BuildArguments, + cli_args: &cli::BuildArguments, config: Config, ) -> Result { 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(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 cli_args.use_localhost_registry { + &HostPort::localhost() + } else { + &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, + &cli_args.registry_namespace, + &image_name, + ); + + let image_index_manifest_tag = utils::format_image_index_manifest_tag( + &image_version, + &metadata.vendor_tag_prefix, + &cli_args.image_version, + ); + + let image_manifest_tag = utils::format_image_manifest_tag( + &image_index_manifest_tag, + cli_args.target_platform.architecture(), + cli_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 @@ -373,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 { @@ -399,13 +430,16 @@ 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, + &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), @@ -533,31 +567,50 @@ impl BakefileTarget { fn common( date_time: String, revision: String, - config: Config, - build_arguments: Vec, release_version: String, + container_build_args: BuildArguments, + user_container_build_args: Vec, + metadata: &Metadata, ) -> 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 = container_build_args; + arguments.extend(user_container_build_args); 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/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(); 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}") } }