use std::collections::HashSet;
use serde_json::Value as JsonValue;
use serde_yaml::{Mapping, Value};
use crate::common::error::MantaError as Error;
pub fn apply_sat_file_filters(
sat_file: &mut JsonValue,
image_only: bool,
session_template_only: bool,
) -> Result<(), Error> {
if image_only {
let obj = sat_file.as_object_mut().ok_or_else(|| {
Error::TemplateError(
"SAT file root is not a YAML/JSON mapping".to_string(),
)
})?;
if !obj.contains_key("images") {
return Err(Error::MissingField(
"'images' section missing in SAT file".to_string(),
));
}
obj.remove("session_templates");
obj.remove("hardware");
let referenced: HashSet<String> = obj
.get("images")
.and_then(JsonValue::as_array)
.map(|imgs| {
imgs
.iter()
.filter_map(|img| {
img.get("configuration")?.as_str().map(str::to_string)
})
.collect()
})
.unwrap_or_default();
if let Some(configs) =
obj.get_mut("configurations").and_then(JsonValue::as_array_mut)
{
configs.retain(|cfg| {
cfg
.get("name")
.and_then(JsonValue::as_str)
.is_some_and(|n| referenced.contains(n))
});
}
}
if session_template_only {
let obj = sat_file.as_object_mut().ok_or_else(|| {
Error::TemplateError(
"SAT file root is not a YAML/JSON mapping".to_string(),
)
})?;
if !obj.contains_key("session_templates") {
return Err(Error::MissingField(
"'session_templates' section not defined in SAT file".to_string(),
));
}
obj.remove("hardware");
let image_keep: HashSet<String> = obj
.get("session_templates")
.and_then(JsonValue::as_array)
.map(|sts| {
sts
.iter()
.filter_map(image_name_referenced_by_session_template)
.collect()
})
.unwrap_or_default();
let images_empty = if let Some(imgs) =
obj.get_mut("images").and_then(JsonValue::as_array_mut)
{
imgs.retain(|img| {
img
.get("name")
.and_then(JsonValue::as_str)
.is_some_and(|n| image_keep.contains(n))
});
imgs.is_empty()
} else {
false
};
if images_empty {
obj.remove("images");
}
let mut config_keep: HashSet<String> = HashSet::new();
if let Some(imgs) = obj.get("images").and_then(JsonValue::as_array) {
for img in imgs {
if let Some(c) = img.get("configuration").and_then(JsonValue::as_str)
{
config_keep.insert(c.to_string());
}
}
}
if let Some(sts) =
obj.get("session_templates").and_then(JsonValue::as_array)
{
for st in sts {
if let Some(c) = st.get("configuration").and_then(JsonValue::as_str) {
config_keep.insert(c.to_string());
}
}
}
if let Some(configs) =
obj.get_mut("configurations").and_then(JsonValue::as_array_mut)
{
configs.retain(|cfg| {
cfg
.get("name")
.and_then(JsonValue::as_str)
.is_some_and(|n| config_keep.contains(n))
});
}
}
Ok(())
}
fn image_name_referenced_by_session_template(
st: &JsonValue,
) -> Option<String> {
let image = st.get("image")?;
if let Some(name) = image.get("image_ref").and_then(JsonValue::as_str) {
return Some(name.to_string());
}
image
.get("ims")
.and_then(|ims| ims.get("name"))
.and_then(JsonValue::as_str)
.map(str::to_string)
}
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<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<String, 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(sat_file_rendered)
}
#[cfg(test)]
mod tests;