manta_shared/shared/
sat_file.rs1use std::collections::HashSet;
33
34use serde_json::Value as JsonValue;
35use serde_yaml::{Mapping, Value};
36
37use crate::common::error::MantaError as Error;
38
39pub fn apply_sat_file_filters(
54 sat_file: &mut JsonValue,
55 image_only: bool,
56 session_template_only: bool,
57) -> Result<(), Error> {
58 if image_only {
59 let obj = sat_file.as_object_mut().ok_or_else(|| {
60 Error::TemplateError(
61 "SAT file root is not a YAML/JSON mapping".to_string(),
62 )
63 })?;
64
65 if !obj.contains_key("images") {
66 return Err(Error::MissingField(
67 "'images' section missing in SAT file".to_string(),
68 ));
69 }
70
71 obj.remove("session_templates");
72 obj.remove("hardware");
73
74 let referenced: HashSet<String> = obj
75 .get("images")
76 .and_then(JsonValue::as_array)
77 .map(|imgs| {
78 imgs
79 .iter()
80 .filter_map(|img| {
81 img.get("configuration")?.as_str().map(str::to_string)
82 })
83 .collect()
84 })
85 .unwrap_or_default();
86
87 if let Some(configs) =
88 obj.get_mut("configurations").and_then(JsonValue::as_array_mut)
89 {
90 configs.retain(|cfg| {
91 cfg
92 .get("name")
93 .and_then(JsonValue::as_str)
94 .is_some_and(|n| referenced.contains(n))
95 });
96 }
97 }
98
99 if session_template_only {
100 let obj = sat_file.as_object_mut().ok_or_else(|| {
101 Error::TemplateError(
102 "SAT file root is not a YAML/JSON mapping".to_string(),
103 )
104 })?;
105
106 if !obj.contains_key("session_templates") {
107 return Err(Error::MissingField(
108 "'session_templates' section not defined in SAT file".to_string(),
109 ));
110 }
111
112 obj.remove("hardware");
113
114 let image_keep: HashSet<String> = obj
117 .get("session_templates")
118 .and_then(JsonValue::as_array)
119 .map(|sts| {
120 sts
121 .iter()
122 .filter_map(image_name_referenced_by_session_template)
123 .collect()
124 })
125 .unwrap_or_default();
126
127 let images_empty = if let Some(imgs) =
129 obj.get_mut("images").and_then(JsonValue::as_array_mut)
130 {
131 imgs.retain(|img| {
132 img
133 .get("name")
134 .and_then(JsonValue::as_str)
135 .is_some_and(|n| image_keep.contains(n))
136 });
137 imgs.is_empty()
138 } else {
139 false
140 };
141 if images_empty {
142 obj.remove("images");
143 }
144
145 let mut config_keep: HashSet<String> = HashSet::new();
148 if let Some(imgs) = obj.get("images").and_then(JsonValue::as_array) {
149 for img in imgs {
150 if let Some(c) = img.get("configuration").and_then(JsonValue::as_str)
151 {
152 config_keep.insert(c.to_string());
153 }
154 }
155 }
156 if let Some(sts) =
157 obj.get("session_templates").and_then(JsonValue::as_array)
158 {
159 for st in sts {
160 if let Some(c) = st.get("configuration").and_then(JsonValue::as_str) {
161 config_keep.insert(c.to_string());
162 }
163 }
164 }
165
166 if let Some(configs) =
167 obj.get_mut("configurations").and_then(JsonValue::as_array_mut)
168 {
169 configs.retain(|cfg| {
170 cfg
171 .get("name")
172 .and_then(JsonValue::as_str)
173 .is_some_and(|n| config_keep.contains(n))
174 });
175 }
176 }
177
178 Ok(())
179}
180
181fn image_name_referenced_by_session_template(
189 st: &JsonValue,
190) -> Option<String> {
191 let image = st.get("image")?;
192 if let Some(name) = image.get("image_ref").and_then(JsonValue::as_str) {
193 return Some(name.to_string());
194 }
195 image
196 .get("ims")
197 .and_then(|ims| ims.get("name"))
198 .and_then(JsonValue::as_str)
199 .map(str::to_string)
200}
201
202fn merge_yaml(base: Value, merge: Value) -> Option<Value> {
207 match (base, merge) {
208 (Value::Mapping(mut base_map), Value::Mapping(merge_map)) => {
209 for (key, value) in merge_map {
210 if let Some(base_value) = base_map.get_mut(&key) {
211 *base_value = merge_yaml(base_value.clone(), value)?;
212 } else {
213 base_map.insert(key, value);
214 }
215 }
216 Some(Value::Mapping(base_map))
217 }
218 (Value::Sequence(mut base_seq), Value::Sequence(merge_seq)) => {
219 base_seq.extend(merge_seq);
220 Some(Value::Sequence(base_seq))
221 }
222 (_, merge) => Some(merge),
223 }
224}
225
226fn dot_notation_to_yaml(dot_notation: &str) -> Result<Value, Error> {
239 let parts: Vec<&str> = dot_notation.split('=').collect();
240 if parts.len() != 2 {
241 return Err(Error::InvalidPattern("Invalid format".to_string()));
242 }
243
244 let keys: Vec<&str> = parts[0].trim().split('.').collect();
245 let value_str = parts[1].trim().trim_matches('"');
246 let value: Value = Value::String(value_str.to_string());
247
248 let mut root = Value::Mapping(Mapping::new());
249 let mut current_level = &mut root;
250
251 for (i, &key) in keys.iter().enumerate() {
252 if i == keys.len() - 1 {
253 if let Value::Mapping(map) = current_level {
254 map.insert(Value::String(key.to_string()), value.clone());
255 }
256 } else {
257 let next_level = if let Value::Mapping(map) = current_level {
258 if map.contains_key(Value::String(key.to_string())) {
259 map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
260 Error::TemplateError(
261 "Failed to get mutable reference to existing YAML map entry"
262 .to_string(),
263 )
264 })?
265 } else {
266 map.insert(
267 Value::String(key.to_string()),
268 Value::Mapping(Mapping::new()),
269 );
270 map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
271 Error::TemplateError(
272 "Failed to get mutable reference to newly inserted YAML map entry"
273 .to_string(),
274 )
275 })?
276 }
277 } else {
278 return Err(Error::TemplateError(
279 "Unexpected structure encountered".to_string(),
280 ));
281 };
282 current_level = next_level;
283 }
284 }
285
286 Ok(root)
287}
288
289pub fn render_jinja2_sat_file_yaml(
294 sat_file_content: &str,
295 values_file_content_opt: Option<&str>,
296 value_cli_vec_opt: Option<&[String]>,
297) -> Result<String, Error> {
298 let mut env = minijinja::Environment::new();
299 env.set_debug(true);
302 env.set_syntax(
304 minijinja::syntax::SyntaxConfig::builder()
305 .line_comment_prefix("#")
306 .build()
307 .map_err(|e| {
308 Error::TemplateError(format!(
309 "Failed to build jinja2 syntax config: {e}"
310 ))
311 })?,
312 );
313 env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
316
317 let mut values_file_yaml: Value = if let Some(values_file_content) =
319 values_file_content_opt
320 {
321 tracing::info!(
322 "'Session vars' file provided. Going to process SAT file as a jinja template."
323 );
324 tracing::info!("Expand variables in 'session vars' file");
325 let values_file_yaml: Value = serde_yaml::from_str(values_file_content)?;
326 let values_file_rendered = env
327 .render_str(values_file_content, values_file_yaml)
328 .map_err(|e| {
329 Error::TemplateError(format!("Error parsing values file to YAML: {e}"))
330 })?;
331 serde_yaml::from_str(&values_file_rendered)?
332 } else {
333 serde_yaml::from_str(sat_file_content)?
334 };
335
336 tracing::debug!(
338 "Convert variable values sent by cli argument from dot notation to yaml format"
339 );
340 if let Some(value_option_vec) = value_cli_vec_opt {
341 for value_option in value_option_vec {
342 let cli_var_context_yaml = dot_notation_to_yaml(value_option)?;
343
344 values_file_yaml =
345 merge_yaml(values_file_yaml.clone(), cli_var_context_yaml).ok_or_else(
346 || {
347 Error::TemplateError(
348 "Failed to merge CLI variable values into \
349 SAT file YAML"
350 .to_string(),
351 )
352 },
353 )?;
354 }
355 }
356
357 tracing::info!("Expand variables in 'SAT file'");
359 let sat_file_rendered = env
360 .render_str(sat_file_content, values_file_yaml)
361 .map_err(|e| {
362 Error::TemplateError(format!("Failed to render SAT file template: {e}"))
363 })?;
364
365 env.set_debug(false);
367
368 Ok(sat_file_rendered)
369}
370
371#[cfg(test)]
372mod tests;