use crate::common::error::MantaError as Error;
use image::Image;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use self::sessiontemplate::SessionTemplate;
#[derive(Deserialize, Serialize, Debug)]
pub struct SatFile {
#[serde(skip_serializing_if = "Option::is_none")]
pub configurations: Option<Vec<configuration::Configuration>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<image::Image>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_templates: Option<Vec<sessiontemplate::SessionTemplate>>,
}
impl SatFile {
pub fn filter(
&mut self,
image_only: bool,
session_template_only: bool,
) -> Result<(), Error> {
if image_only {
let image_vec_opt: Option<&[Image]> = self.images.as_deref();
let configuration_name_image_vec: Vec<String> = match image_vec_opt {
Some(image_vec) => image_vec
.iter()
.filter_map(|sat_template_image| {
sat_template_image.configuration.clone()
})
.collect(),
None => {
return Err(Error::MissingField(
"'images' section missing in SAT file".to_string(),
));
}
};
if let Some(configurations) = self.configurations.as_mut() {
configurations.retain(|configuration| {
configuration_name_image_vec.contains(&configuration.name)
});
}
self.session_templates = None;
}
if session_template_only {
let sessiontemplate_vec_opt: Option<&[SessionTemplate]> =
self.session_templates.as_deref();
let image_name_sessiontemplate_vec: Vec<String> = self
.session_templates
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(|sessiontemplate| match &sessiontemplate.image {
sessiontemplate::Image::ImageRef { image_ref: name } => Some(name),
sessiontemplate::Image::Ims { ims } => match ims {
sessiontemplate::ImsDetails::Name { name } => Some(name),
sessiontemplate::ImsDetails::Id { .. } => None,
},
})
.cloned()
.collect();
if let Some(images) = self.images.as_mut() {
images
.retain(|image| image_name_sessiontemplate_vec.contains(&image.name));
}
if self.images.as_ref().is_some_and(|images| images.is_empty()) {
self.images = None;
}
let configuration_name_sessiontemplate_vec: Vec<String> =
match sessiontemplate_vec_opt {
Some(sessiontemplate_vec) => sessiontemplate_vec
.iter()
.map(|sat_sessiontemplate| {
sat_sessiontemplate.configuration.clone()
})
.collect(),
None => {
return Err(Error::MissingField(
"'session_templates' section not defined \
in SAT file"
.to_string(),
));
}
};
let configuration_name_image_vec: Vec<String> = self
.images
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(|image| image.configuration.as_ref().cloned())
.collect();
let configuration_to_keep_vec = [
configuration_name_image_vec,
configuration_name_sessiontemplate_vec,
]
.concat();
if let Some(configurations) = self.configurations.as_mut() {
configurations.retain(|configuration| {
configuration_to_keep_vec.contains(&configuration.name)
});
}
}
Ok(())
}
}
pub mod sessiontemplate {
use std::collections::HashMap;
use strum_macros::Display;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
pub struct SessionTemplate {
pub name: String,
pub image: Image,
pub configuration: String,
pub bos_parameters: BosParamters,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum ImsDetails {
Name { name: String },
Id { id: String },
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum Image {
Ims { ims: ImsDetails },
ImageRef { image_ref: String },
}
#[derive(Deserialize, Serialize, Debug)]
pub struct BosParamters {
pub boot_sets: HashMap<String, BootSet>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct BootSet {
#[serde(skip_serializing_if = "Option::is_none")]
pub arch: Option<Arch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kernel_parameters: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_list: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_roles_group: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_groups: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rootfs_provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rootfs_provider_passthrough: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Display)]
#[allow(clippy::upper_case_acronyms)]
pub enum Arch {
X86,
ARM,
Other,
Unknown,
}
}
pub mod image {
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
pub enum Arch {
#[serde(rename(serialize = "aarch64", deserialize = "aarch64"))]
Aarch64,
#[serde(rename(serialize = "x86_64", deserialize = "x86_64"))]
X86_64,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum ImageIms {
NameIsRecipe { name: String, is_recipe: bool },
IdIsRecipe { id: String, is_recipe: bool },
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum ImageBaseIms {
NameType { name: String, r#type: String },
IdType { id: String, r#type: String },
BackwardCompatible { is_recipe: Option<bool>, id: String },
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum Filter {
Prefix { prefix: String },
Wildcard { wildcard: String },
Arch { arch: Arch },
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Product {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<String>,
r#type: String,
filter: Filter,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum Base {
Ims { ims: ImageBaseIms },
Product { product: Product },
ImageRef { image_ref: String },
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum BaseOrIms {
Base { base: Base },
Ims { ims: ImageIms },
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Image {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ref_name: Option<String>,
#[serde(flatten)]
pub base_or_ims: BaseOrIms,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration_group_names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
}
pub mod configuration {
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
#[allow(clippy::enum_variant_names)]
pub enum Product {
ProductVersionBranch {
name: String,
version: Option<String>,
branch: String,
},
ProductVersionCommit {
name: String,
version: Option<String>,
commit: String,
},
ProductVersion { name: String, version: String },
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
#[allow(clippy::enum_variant_names)]
pub enum Git {
GitCommit { url: String, commit: String },
GitBranch { url: String, branch: String },
GitTag { url: String, tag: String },
}
#[derive(Deserialize, Serialize, Debug)]
pub struct SpecialParameters {
pub ims_require_dkms: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct LayerGit {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub playbook: Option<String>, pub git: Git,
pub special_parameters: Option<SpecialParameters>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct LayerProduct {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub playbook: Option<String>, pub product: Product,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum Layer {
LayerGit(LayerGit),
LayerProduct(LayerProduct),
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] pub enum Inventory {
InventoryCommit {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
url: String,
commit: String,
},
InventoryBranch {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
url: String,
branch: String,
},
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Configuration {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub layers: Vec<Layer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_inventory: Option<Inventory>,
}
}
fn merge_yaml(base: Value, merge: Value) -> Option<Value> {
match (base, merge) {
(Value::Mapping(mut base_map), Value::Mapping(merge_map)) => {
for (key, value) in merge_map {
if let Some(base_value) = base_map.get_mut(&key) {
*base_value = merge_yaml(base_value.clone(), value)?;
} else {
base_map.insert(key, value);
}
}
Some(Value::Mapping(base_map))
}
(Value::Sequence(mut base_seq), Value::Sequence(merge_seq)) => {
base_seq.extend(merge_seq);
Some(Value::Sequence(base_seq))
}
(_, merge) => Some(merge),
}
}
fn dot_notation_to_yaml(
dot_notation: &str,
) -> Result<serde_yaml::Value, Error> {
let parts: Vec<&str> = dot_notation.split('=').collect();
if parts.len() != 2 {
return Err(Error::InvalidPattern("Invalid format".to_string()));
}
let keys: Vec<&str> = parts[0].trim().split('.').collect();
let value_str = parts[1].trim().trim_matches('"'); let value: Value = Value::String(value_str.to_string());
let mut root = Value::Mapping(Mapping::new());
let mut current_level = &mut root;
for (i, &key) in keys.iter().enumerate() {
if i == keys.len() - 1 {
if let Value::Mapping(map) = current_level {
map.insert(Value::String(key.to_string()), value.clone());
}
} else {
let next_level = if let Value::Mapping(map) = current_level {
if map.contains_key(Value::String(key.to_string())) {
map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
Error::TemplateError(
"Failed to get mutable reference to \
existing YAML map entry"
.to_string(),
)
})?
} else {
map.insert(
Value::String(key.to_string()),
Value::Mapping(Mapping::new()),
);
map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
Error::TemplateError(
"Failed to get mutable reference to \
newly inserted YAML map entry"
.to_string(),
)
})?
}
} else {
return Err(Error::TemplateError(
"Unexpected structure encountered".to_string(),
));
};
current_level = next_level;
}
}
Ok(root)
}
pub fn render_jinja2_sat_file_yaml(
sat_file_content: &str,
values_file_content_opt: Option<&str>,
value_cli_vec_opt: Option<&[String]>,
) -> Result<Value, Error> {
let mut env = minijinja::Environment::new();
env.set_debug(true);
env.set_syntax(
minijinja::syntax::SyntaxConfig::builder()
.line_comment_prefix("#")
.build()
.map_err(|e| {
Error::TemplateError(format!(
"Failed to build jinja2 syntax config: {e}"
))
})?,
);
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
let mut values_file_yaml: Value = if let Some(values_file_content) =
values_file_content_opt
{
tracing::info!(
"'Session vars' file provided. Going to process SAT file as a jinja template."
);
tracing::info!("Expand variables in 'session vars' file");
let values_file_yaml: Value = serde_yaml::from_str(values_file_content)?;
let values_file_rendered = env
.render_str(values_file_content, values_file_yaml)
.map_err(|e| {
Error::TemplateError(format!("Error parsing values file to YAML: {e}"))
})?;
serde_yaml::from_str(&values_file_rendered)?
} else {
serde_yaml::from_str(sat_file_content)?
};
tracing::debug!(
"Convert variable values sent by cli argument from dot notation to yaml format"
);
if let Some(value_option_vec) = value_cli_vec_opt {
for value_option in value_option_vec {
let cli_var_context_yaml = dot_notation_to_yaml(value_option)?;
values_file_yaml =
merge_yaml(values_file_yaml.clone(), cli_var_context_yaml).ok_or_else(
|| {
Error::TemplateError(
"Failed to merge CLI variable values into \
SAT file YAML"
.to_string(),
)
},
)?;
}
}
tracing::info!("Expand variables in 'SAT file'");
let sat_file_rendered = env
.render_str(sat_file_content, values_file_yaml)
.map_err(|e| {
Error::TemplateError(format!("Failed to render SAT file template: {e}"))
})?;
env.set_debug(false);
Ok(serde_yaml::from_str(&sat_file_rendered)?)
}
#[cfg(test)]
mod tests;