From 4ae0767211bea4368c712faefcd26dfeb70ae399 Mon Sep 17 00:00:00 2001 From: Gerald Pinder Date: Tue, 24 Dec 2024 18:01:40 -0500 Subject: [PATCH] feat: Support versioned modules --- recipe/src/module.rs | 25 ++++++--- recipe/src/module/type_ver.rs | 78 +++++++++++++++++++++++++++ recipe/src/module_ext.rs | 2 +- recipe/src/stage.rs | 2 +- template/templates/modules/modules.j2 | 20 +++---- utils/src/constants.rs | 1 + 6 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 recipe/src/module/type_ver.rs diff --git a/recipe/src/module.rs b/recipe/src/module.rs index e9701577..7093ea67 100644 --- a/recipe/src/module.rs +++ b/recipe/src/module.rs @@ -11,11 +11,15 @@ use serde_yaml::Value; use crate::{base_recipe_path, AkmodsInfo, ModuleExt}; -#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)] +mod type_ver; + +pub use type_ver::*; + +#[derive(Serialize, Deserialize, Debug, Clone, Builder)] pub struct ModuleRequiredFields<'a> { #[builder(into)] #[serde(rename = "type")] - pub module_type: Cow<'a, str>, + pub module_type: ModuleTypeVersion<'a>, #[builder(into)] #[serde(skip_serializing_if = "Option::is_none")] @@ -38,7 +42,7 @@ const fn is_false(b: &bool) -> bool { impl<'a> ModuleRequiredFields<'a> { #[must_use] pub fn get_module_type_list(&'a self, typ: &str, list_key: &str) -> Option> { - if self.module_type == typ { + if self.module_type.typ() == typ { Some( self.config .get(list_key)? @@ -217,7 +221,7 @@ impl Module<'_> { required_fields: None, from_file: Some(file_name), } => { - let file_name = PathBuf::from(file_name.as_ref()); + let file_name = PathBuf::from(&**file_name); if traversed_files.contains(&file_name) { bail!( "{} File {} has already been parsed:\n{traversed_files:?}", @@ -260,14 +264,21 @@ impl Module<'_> { } #[must_use] + #[allow(clippy::missing_panics_doc)] pub fn example() -> Self { Self::builder() .required_fields( ModuleRequiredFields::builder() - .module_type("module-name") + .module_type("script") .config(IndexMap::from_iter([ - ("module".to_string(), Value::String("config".to_string())), - ("goes".to_string(), Value::String("here".to_string())), + ( + "snippets".to_string(), + Value::Sequence(bon::vec!["echo 'Hello World!'"]), + ), + ( + "scripts".to_string(), + Value::Sequence(bon::vec!["install-program.sh"]), + ), ])) .build(), ) diff --git a/recipe/src/module/type_ver.rs b/recipe/src/module/type_ver.rs new file mode 100644 index 00000000..4de2d6e3 --- /dev/null +++ b/recipe/src/module/type_ver.rs @@ -0,0 +1,78 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Debug, Clone)] +pub struct ModuleTypeVersion<'scope> { + typ: Cow<'scope, str>, + version: Cow<'scope, str>, +} + +impl<'scope> ModuleTypeVersion<'scope> { + #[must_use] + pub fn typ(&self) -> &str { + &self.typ + } + + #[must_use] + pub fn version(&self) -> &str { + &self.version + } +} + +impl std::fmt::Display for ModuleTypeVersion<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", &self.typ, &self.version) + } +} + +impl<'scope> From<&'scope str> for ModuleTypeVersion<'scope> { + fn from(s: &'scope str) -> Self { + if let Some((typ, version)) = s.split_once('@') { + Self { + typ: Cow::Borrowed(typ), + version: Cow::Borrowed(version), + } + } else { + Self { + typ: Cow::Borrowed(s), + version: Cow::Owned("latest".into()), + } + } + } +} + +impl From for ModuleTypeVersion<'_> { + fn from(s: String) -> Self { + if let Some((typ, version)) = s.split_once('@') { + Self { + typ: Cow::Owned(typ.to_owned()), + version: Cow::Owned(version.to_owned()), + } + } else { + Self { + typ: Cow::Owned(s), + version: Cow::Owned("latest".into()), + } + } + } +} + +impl Serialize for ModuleTypeVersion<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for ModuleTypeVersion<'_> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: String = Deserialize::deserialize(deserializer)?; + Ok(value.into()) + } +} diff --git a/recipe/src/module_ext.rs b/recipe/src/module_ext.rs index 5e871345..193355b6 100644 --- a/recipe/src/module_ext.rs +++ b/recipe/src/module_ext.rs @@ -72,7 +72,7 @@ impl ModuleExt<'_> { module .required_fields .as_ref() - .is_some_and(|rf| rf.module_type == "akmods") + .is_some_and(|rf| rf.module_type.typ() == "akmods") }) .filter_map(|module| { Some( diff --git a/recipe/src/stage.rs b/recipe/src/stage.rs index 0b4bb6b4..4e5d77ef 100644 --- a/recipe/src/stage.rs +++ b/recipe/src/stage.rs @@ -108,7 +108,7 @@ impl Stage<'_> { required_fields: None, from_file: Some(file_name), } => { - let file_name = PathBuf::from(file_name.as_ref()); + let file_name = PathBuf::from(&**file_name); if traversed_files.contains(&file_name) { bail!( "{} File {} has already been parsed:\n{traversed_files:?}", diff --git a/template/templates/modules/modules.j2 b/template/templates/modules/modules.j2 index 08630320..443e5aa6 100644 --- a/template/templates/modules/modules.j2 +++ b/template/templates/modules/modules.j2 @@ -6,9 +6,9 @@ ARG CACHEBUST="{{ build_id }}" {%- endif %} - {%- if module.module_type == "containerfile" %} + {%- if module.module_type.typ() == "containerfile" %} {%- include "modules/containerfile/containerfile.j2" %} - {%- else if module.module_type == "copy" %} + {%- else if module.module_type.typ() == "copy" %} {%- include "modules/copy/copy.j2" %} {%- else %} RUN \ @@ -22,15 +22,15 @@ RUN \ {%- else if module.is_local_source() %} --mount=type=bind,from=stage-modules,src=/modules,dst=/tmp/modules,rw \ {%- else %} - --mount=type=bind,from=ghcr.io/blue-build/modules/{{ module.module_type }}:latest,src=/modules,dst=/tmp/modules,rw \ + --mount=type=bind,from={{ blue_build_utils::constants::BLUE_BUILD_MODULE_IMAGE_REF }}/{{ module.module_type.typ() }}:{{ module.module_type.version() }},src=/modules,dst=/tmp/modules,rw \ {%- endif %} - {%- if module.module_type == "akmods" %} + {%- if module.module_type.typ() == "akmods" %} --mount=type=bind,from=stage-akmods-{{ module.generate_akmods_info(os_version).stage_name }},src=/rpms,dst=/tmp/rpms,rw \ {%- endif %} --mount=type=bind,from={{ build_scripts_image }},src=/scripts/,dst=/tmp/scripts/ \ --mount=type=cache,dst=/var/cache/rpm-ostree,id=rpm-ostree-cache-{{ recipe.name }}-{{ recipe.image_version }},sharing=locked \ --mount=type=cache,dst=/var/cache/libdnf5,id=dnf-cache-{{ recipe.name }}-{{ recipe.image_version }},sharing=locked \ - /tmp/scripts/run_module.sh '{{ module.module_type }}' '{{ module|json|safe }}' + /tmp/scripts/run_module.sh '{{ module.module_type.typ() }}' '{{ module|json|safe }}' {%- endif %} {%- endif %} {%- endfor %} @@ -45,9 +45,9 @@ RUN \ ARG CACHEBUST="{{ build_id }}" {%- endif %} - {%- if module.module_type == "containerfile" %} + {%- if module.module_type.typ() == "containerfile" %} {%- include "modules/containerfile/containerfile.j2" %} - {%- else if module.module_type == "copy" %} + {%- else if module.module_type.typ() == "copy" %} {%- include "modules/copy/copy.j2" %} {%- else %} RUN \ @@ -58,11 +58,13 @@ RUN \ {%- endif %} {%- if let Some(source) = module.get_non_local_source() %} --mount=type=bind,from={{ source }},src=/modules,dst=/tmp/modules,rw \ - {%- else %} + {%- else if module.is_local_source() %} --mount=type=bind,from=stage-modules,src=/modules,dst=/tmp/modules,rw \ + {%- else %} + --mount=type=bind,from={{ blue_build_utils::constants::BLUE_BUILD_MODULE_IMAGE_REF }}/{{ module.module_type.typ() }}:{{ module.module_type.version() }},src=/modules,dst=/tmp/modules,rw \ {%- endif %} --mount=type=bind,from={{ build_scripts_image }},src=/scripts/,dst=/tmp/scripts/ \ - /tmp/scripts/run_module.sh '{{ module.module_type }}' '{{ module|json|safe }}' + /tmp/scripts/run_module.sh '{{ module.module_type.typ() }}' '{{ module|json|safe }}' {%- endif %} {%- endif %} {%- endfor %} diff --git a/utils/src/constants.rs b/utils/src/constants.rs index 80d5790d..23498554 100644 --- a/utils/src/constants.rs +++ b/utils/src/constants.rs @@ -75,6 +75,7 @@ pub const XDG_RUNTIME_DIR: &str = "XDG_RUNTIME_DIR"; // Misc pub const BUILD_SCRIPTS_IMAGE_REF: &str = "ghcr.io/blue-build/cli/build-scripts"; pub const BLUE_BULID_IMAGE_REF: &str = "ghcr.io/blue-build/cli"; +pub const BLUE_BUILD_MODULE_IMAGE_REF: &str = "ghcr.io/blue-build/modules"; pub const COSIGN_IMAGE: &str = "ghcr.io/sigstore/cosign/cosign:v2.4.1"; pub const NUSHELL_IMAGE: &str = "ghcr.io/blue-build/nushell-image:latest"; pub const OCI_ARCHIVE: &str = "oci-archive";