1use crate::common::error::MantaError as Error;
4use image::Image;
5use serde::{Deserialize, Serialize};
6use serde_yaml::{Mapping, Value};
7
8use self::sessiontemplate::SessionTemplate;
9
10#[derive(Deserialize, Serialize, Debug)]
12pub struct SatFile {
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub configurations: Option<Vec<configuration::Configuration>>,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub images: Option<Vec<image::Image>>,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub session_templates: Option<Vec<sessiontemplate::SessionTemplate>>,
22}
23
24impl SatFile {
25 pub fn filter(
27 &mut self,
28 image_only: bool,
29 session_template_only: bool,
30 ) -> Result<(), Error> {
31 if image_only {
35 let image_vec_opt: Option<&[Image]> = self.images.as_deref();
36
37 let configuration_name_image_vec: Vec<String> = match image_vec_opt {
38 Some(image_vec) => image_vec
39 .iter()
40 .filter_map(|sat_template_image| {
41 sat_template_image.configuration.clone()
42 })
43 .collect(),
44 None => {
45 return Err(Error::MissingField(
46 "'images' section missing in SAT file".to_string(),
47 ));
48 }
49 };
50
51 if let Some(configurations) = self.configurations.as_mut() {
53 configurations.retain(|configuration| {
54 configuration_name_image_vec.contains(&configuration.name)
55 });
56 }
57
58 self.session_templates = None;
60 }
61
62 if session_template_only {
66 let sessiontemplate_vec_opt: Option<&[SessionTemplate]> =
67 self.session_templates.as_deref();
68
69 let image_name_sessiontemplate_vec: Vec<String> = self
70 .session_templates
71 .as_deref()
72 .unwrap_or_default()
73 .iter()
74 .filter_map(|sessiontemplate| match &sessiontemplate.image {
75 sessiontemplate::Image::ImageRef { image_ref: name } => Some(name),
76 sessiontemplate::Image::Ims { ims } => match ims {
77 sessiontemplate::ImsDetails::Name { name } => Some(name),
78 sessiontemplate::ImsDetails::Id { .. } => None,
79 },
80 })
81 .cloned()
82 .collect();
83
84 if let Some(images) = self.images.as_mut() {
86 images
87 .retain(|image| image_name_sessiontemplate_vec.contains(&image.name));
88 }
89
90 if self.images.as_ref().is_some_and(|images| images.is_empty()) {
91 self.images = None;
92 }
93
94 let configuration_name_sessiontemplate_vec: Vec<String> =
96 match sessiontemplate_vec_opt {
97 Some(sessiontemplate_vec) => sessiontemplate_vec
98 .iter()
99 .map(|sat_sessiontemplate| {
100 sat_sessiontemplate.configuration.clone()
101 })
102 .collect(),
103 None => {
104 return Err(Error::MissingField(
105 "'session_templates' section not defined \
106 in SAT file"
107 .to_string(),
108 ));
109 }
110 };
111
112 let configuration_name_image_vec: Vec<String> = self
114 .images
115 .as_deref()
116 .unwrap_or_default()
117 .iter()
118 .filter_map(|image| image.configuration.as_ref().cloned())
119 .collect();
120
121 let configuration_to_keep_vec = [
123 configuration_name_image_vec,
124 configuration_name_sessiontemplate_vec,
125 ]
126 .concat();
127
128 if let Some(configurations) = self.configurations.as_mut() {
132 configurations.retain(|configuration| {
133 configuration_to_keep_vec.contains(&configuration.name)
134 });
135 }
136 }
137
138 Ok(())
139 }
140}
141
142pub mod sessiontemplate {
144 use std::collections::HashMap;
145 use strum_macros::Display;
146
147 use serde::{Deserialize, Serialize};
148
149 #[derive(Deserialize, Serialize, Debug)]
152 pub struct SessionTemplate {
153 pub name: String,
154 pub image: Image,
155 pub configuration: String,
156 pub bos_parameters: BosParamters,
157 }
158
159 #[derive(Deserialize, Serialize, Debug)]
161 #[serde(untagged)] pub enum ImsDetails {
163 Name { name: String },
165 Id { id: String },
167 }
168
169 #[derive(Deserialize, Serialize, Debug)]
172 #[serde(untagged)] pub enum Image {
174 Ims { ims: ImsDetails },
176 ImageRef { image_ref: String },
178 }
179
180 #[derive(Deserialize, Serialize, Debug)]
182 pub struct BosParamters {
183 pub boot_sets: HashMap<String, BootSet>,
184 }
185
186 #[derive(Deserialize, Serialize, Debug)]
189 pub struct BootSet {
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub arch: Option<Arch>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub kernel_parameters: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub network: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub node_list: Option<Vec<String>>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub node_roles_group: Option<Vec<String>>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub node_groups: Option<Vec<String>>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub rootfs_provider: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub rootfs_provider_passthrough: Option<String>,
206 }
207
208 #[derive(Deserialize, Serialize, Debug, Display)]
210 #[allow(clippy::upper_case_acronyms)]
211 pub enum Arch {
212 X86,
214 ARM,
216 Other,
218 Unknown,
220 }
221}
222
223pub mod image {
225 use serde::{Deserialize, Serialize};
226
227 #[derive(Deserialize, Serialize, Debug)]
229 pub enum Arch {
230 #[serde(rename(serialize = "aarch64", deserialize = "aarch64"))]
232 Aarch64,
233 #[serde(rename(serialize = "x86_64", deserialize = "x86_64"))]
235 X86_64,
236 }
237
238 #[derive(Deserialize, Serialize, Debug)]
240 #[serde(untagged)] pub enum ImageIms {
242 NameIsRecipe { name: String, is_recipe: bool },
244 IdIsRecipe { id: String, is_recipe: bool },
246 }
247
248 #[derive(Deserialize, Serialize, Debug)]
250 #[serde(untagged)] pub enum ImageBaseIms {
252 NameType { name: String, r#type: String },
254 IdType { id: String, r#type: String },
256 BackwardCompatible { is_recipe: Option<bool>, id: String },
258 }
259
260 #[derive(Deserialize, Serialize, Debug)]
262 #[serde(untagged)] pub enum Filter {
264 Prefix { prefix: String },
266 Wildcard { wildcard: String },
268 Arch { arch: Arch },
270 }
271
272 #[derive(Deserialize, Serialize, Debug)]
274 pub struct Product {
275 name: String,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 version: Option<String>,
278 r#type: String,
279 filter: Filter,
280 }
281
282 #[derive(Deserialize, Serialize, Debug)]
284 #[serde(untagged)] pub enum Base {
286 Ims { ims: ImageBaseIms },
288 Product { product: Product },
290 ImageRef { image_ref: String },
292 }
293
294 #[derive(Deserialize, Serialize, Debug)]
296 #[serde(untagged)] pub enum BaseOrIms {
298 Base { base: Base },
300 Ims { ims: ImageIms },
302 }
303
304 #[derive(Deserialize, Serialize, Debug)]
306 pub struct Image {
307 pub name: String,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub ref_name: Option<String>,
312 #[serde(flatten)]
314 pub base_or_ims: BaseOrIms,
315 #[serde(skip_serializing_if = "Option::is_none")]
317 pub configuration: Option<String>,
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub configuration_group_names: Option<Vec<String>>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub description: Option<String>,
323 }
324}
325
326pub mod configuration {
328 use serde::{Deserialize, Serialize};
329
330 #[derive(Deserialize, Serialize, Debug)]
333 #[serde(untagged)]
334 #[allow(clippy::enum_variant_names)]
336 pub enum Product {
337 ProductVersionBranch {
339 name: String,
340 version: Option<String>,
341 branch: String,
342 },
343 ProductVersionCommit {
345 name: String,
346 version: Option<String>,
347 commit: String,
348 },
349 ProductVersion { name: String, version: String },
351 }
352
353 #[derive(Deserialize, Serialize, Debug)]
356 #[serde(untagged)]
357 #[allow(clippy::enum_variant_names)]
359 pub enum Git {
360 GitCommit { url: String, commit: String },
362 GitBranch { url: String, branch: String },
364 GitTag { url: String, tag: String },
366 }
367
368 #[derive(Deserialize, Serialize, Debug)]
370 pub struct SpecialParameters {
371 pub ims_require_dkms: bool,
372 }
373
374 #[derive(Deserialize, Serialize, Debug)]
376 pub struct LayerGit {
377 #[serde(skip_serializing_if = "Option::is_none")]
378 pub name: Option<String>,
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub playbook: Option<String>, pub git: Git,
382 pub special_parameters: Option<SpecialParameters>,
383 }
384
385 #[derive(Deserialize, Serialize, Debug)]
387 pub struct LayerProduct {
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub name: Option<String>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub playbook: Option<String>, pub product: Product,
393 }
394
395 #[derive(Deserialize, Serialize, Debug)]
398 #[serde(untagged)] pub enum Layer {
400 LayerGit(LayerGit),
402 LayerProduct(LayerProduct),
404 }
405
406 #[derive(Deserialize, Serialize, Debug)]
409 #[serde(untagged)] pub enum Inventory {
411 InventoryCommit {
413 #[serde(skip_serializing_if = "Option::is_none")]
414 name: Option<String>,
415 url: String,
416 commit: String,
417 },
418 InventoryBranch {
420 #[serde(skip_serializing_if = "Option::is_none")]
421 name: Option<String>,
422 url: String,
423 branch: String,
424 },
425 }
426
427 #[derive(Deserialize, Serialize, Debug)]
430 pub struct Configuration {
431 pub name: String,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub description: Option<String>,
434 pub layers: Vec<Layer>,
435 #[serde(skip_serializing_if = "Option::is_none")]
436 pub additional_inventory: Option<Inventory>,
437 }
438}
439
440fn merge_yaml(base: Value, merge: Value) -> Option<Value> {
472 match (base, merge) {
473 (Value::Mapping(mut base_map), Value::Mapping(merge_map)) => {
474 for (key, value) in merge_map {
475 if let Some(base_value) = base_map.get_mut(&key) {
476 *base_value = merge_yaml(base_value.clone(), value)?;
477 } else {
478 base_map.insert(key, value);
479 }
480 }
481 Some(Value::Mapping(base_map))
482 }
483 (Value::Sequence(mut base_seq), Value::Sequence(merge_seq)) => {
484 base_seq.extend(merge_seq);
485 Some(Value::Sequence(base_seq))
486 }
487 (_, merge) => Some(merge),
488 }
489}
490
491fn dot_notation_to_yaml(
504 dot_notation: &str,
505) -> Result<serde_yaml::Value, Error> {
506 let parts: Vec<&str> = dot_notation.split('=').collect();
507 if parts.len() != 2 {
508 return Err(Error::InvalidPattern("Invalid format".to_string()));
509 }
510
511 let keys: Vec<&str> = parts[0].trim().split('.').collect();
512 let value_str = parts[1].trim().trim_matches('"'); let value: Value = Value::String(value_str.to_string());
514
515 let mut root = Value::Mapping(Mapping::new());
516 let mut current_level = &mut root;
517
518 for (i, &key) in keys.iter().enumerate() {
519 if i == keys.len() - 1 {
520 if let Value::Mapping(map) = current_level {
522 map.insert(Value::String(key.to_string()), value.clone());
523 }
524 } else {
525 let next_level = if let Value::Mapping(map) = current_level {
527 if map.contains_key(Value::String(key.to_string())) {
528 map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
530 Error::TemplateError(
531 "Failed to get mutable reference to \
532 existing YAML map entry"
533 .to_string(),
534 )
535 })?
536 } else {
537 map.insert(
539 Value::String(key.to_string()),
540 Value::Mapping(Mapping::new()),
541 );
542 map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
543 Error::TemplateError(
544 "Failed to get mutable reference to \
545 newly inserted YAML map entry"
546 .to_string(),
547 )
548 })?
549 }
550 } else {
551 return Err(Error::TemplateError(
552 "Unexpected structure encountered".to_string(),
553 ));
554 };
555 current_level = next_level;
556 }
557 }
558
559 Ok(root)
560}
561
562pub fn render_jinja2_sat_file_yaml(
566 sat_file_content: &str,
567 values_file_content_opt: Option<&str>,
568 value_cli_vec_opt: Option<&[String]>,
569) -> Result<Value, Error> {
570 let mut env = minijinja::Environment::new();
571 env.set_debug(true);
574 env.set_syntax(
576 minijinja::syntax::SyntaxConfig::builder()
577 .line_comment_prefix("#")
578 .build()
579 .map_err(|e| {
580 Error::TemplateError(format!(
581 "Failed to build jinja2 syntax config: {e}"
582 ))
583 })?,
584 );
585 env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
588
589 let mut values_file_yaml: Value = if let Some(values_file_content) =
591 values_file_content_opt
592 {
593 tracing::info!(
594 "'Session vars' file provided. Going to process SAT file as a jinja template."
595 );
596 tracing::info!("Expand variables in 'session vars' file");
597 let values_file_yaml: Value = serde_yaml::from_str(values_file_content)?;
599 let values_file_rendered = env
602 .render_str(values_file_content, values_file_yaml)
603 .map_err(|e| {
604 Error::TemplateError(format!("Error parsing values file to YAML: {e}"))
605 })?;
606 serde_yaml::from_str(&values_file_rendered)?
607 } else {
608 serde_yaml::from_str(sat_file_content)?
609 };
610
611 tracing::debug!(
613 "Convert variable values sent by cli argument from dot notation to yaml format"
614 );
615 if let Some(value_option_vec) = value_cli_vec_opt {
616 for value_option in value_option_vec {
617 let cli_var_context_yaml = dot_notation_to_yaml(value_option)?;
618
619 values_file_yaml =
620 merge_yaml(values_file_yaml.clone(), cli_var_context_yaml).ok_or_else(
621 || {
622 Error::TemplateError(
623 "Failed to merge CLI variable values into \
624 SAT file YAML"
625 .to_string(),
626 )
627 },
628 )?;
629 }
630 }
631
632 tracing::info!("Expand variables in 'SAT file'");
634 let sat_file_rendered = env
635 .render_str(sat_file_content, values_file_yaml)
636 .map_err(|e| {
637 Error::TemplateError(format!("Failed to render SAT file template: {e}"))
638 })?;
639
640 env.set_debug(false);
642
643 Ok(serde_yaml::from_str(&sat_file_rendered)?)
644}
645
646#[cfg(test)]
647mod tests;