Skip to main content

manta_shared/shared/
sat_file.rs

1//! Deserialization types for HPE Cray SAT (System Admin Toolkit) YAML files.
2
3use 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/// Top-level representation of a SAT YAML file.
11#[derive(Deserialize, Serialize, Debug)]
12pub struct SatFile {
13  /// CFS configurations to create or update.
14  #[serde(skip_serializing_if = "Option::is_none")]
15  pub configurations: Option<Vec<configuration::Configuration>>,
16  /// IMS images to build.
17  #[serde(skip_serializing_if = "Option::is_none")]
18  pub images: Option<Vec<image::Image>>,
19  /// BOS session templates to apply.
20  #[serde(skip_serializing_if = "Option::is_none")]
21  pub session_templates: Option<Vec<sessiontemplate::SessionTemplate>>,
22}
23
24impl SatFile {
25  /// Filter either images or session_templates section according to user request
26  pub fn filter(
27    &mut self,
28    image_only: bool,
29    session_template_only: bool,
30  ) -> Result<(), Error> {
31    // Clean SAT template file if user only wan'ts to process the 'images' section. In this case,
32    // we will remove 'session_templates' section from SAT fiel and also the entries in
33    // 'configurations' section not used
34    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      // Remove configurations not used by any image
52      if let Some(configurations) = self.configurations.as_mut() {
53        configurations.retain(|configuration| {
54          configuration_name_image_vec.contains(&configuration.name)
55        });
56      }
57
58      // Remove section "session_templates"
59      self.session_templates = None;
60    }
61
62    // Clean SAT template file if user only wan'ts to process the 'session_template' section. In this case,
63    // we will remove 'images' section from SAT fiel and also the entries in
64    // 'configurations' section not used
65    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      // Remove images not used by any sessiontemplate
85      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(std::vec::Vec::is_empty) {
91        self.images = None;
92      }
93
94      // Get configuration names from session templates
95      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      // Get configuration names from images used by the session templates
113      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      // Merge configuration names from images and session templates
122      let configuration_to_keep_vec = [
123        configuration_name_image_vec,
124        configuration_name_sessiontemplate_vec,
125      ]
126      .concat();
127
128      // Remove configurations not used by any sessiontemplate or image used by the
129      // sessiontemplate
130
131      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
142/// struct to represent the `session_templates` section in SAT file
143pub mod sessiontemplate {
144  use std::collections::HashMap;
145  use strum_macros::Display;
146
147  use serde::{Deserialize, Serialize};
148
149  /// A BOS session template linking an image, configuration,
150  /// and boot parameters for a set of nodes.
151  #[derive(Deserialize, Serialize, Debug)]
152  pub struct SessionTemplate {
153    /// Unique name for the session template.
154    pub name: String,
155    /// IMS image (or cross-reference to a SAT `images` entry) to boot.
156    pub image: Image,
157    /// CFS configuration applied to the booted nodes.
158    pub configuration: String,
159    /// Per-boot-set kernel / rootfs / node-targeting parameters.
160    pub bos_parameters: BosParamters,
161  }
162
163  /// How the IMS image is referenced — by name or by UUID.
164  #[derive(Deserialize, Serialize, Debug)]
165  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
166  pub enum ImsDetails {
167    /// Reference by human-readable IMS image name.
168    Name {
169      /// IMS image name.
170      name: String,
171    },
172    /// Reference by IMS image UUID.
173    Id {
174      /// IMS image UUID.
175      id: String,
176    },
177  }
178
179  /// Image reference within a session template — either an
180  /// IMS image or a cross-reference to another SAT image.
181  #[derive(Deserialize, Serialize, Debug)]
182  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
183  pub enum Image {
184    /// Directly identifies an IMS image via name or UUID.
185    Ims {
186      /// IMS image identifier (name or UUID).
187      ims: ImsDetails,
188    },
189    /// Cross-references the `name` of another image in the SAT `images` section.
190    ImageRef {
191      /// `name` of a sibling image defined in the SAT file's
192      /// `images` section.
193      image_ref: String,
194    },
195  }
196
197  /// BOS boot parameters containing a map of named boot sets.
198  #[derive(Deserialize, Serialize, Debug)]
199  pub struct BosParamters {
200    /// Named boot sets keyed by their BOS identifier.
201    pub boot_sets: HashMap<String, BootSet>,
202  }
203
204  /// A single boot set defining the kernel, network, and node
205  /// targeting for a BOS session template.
206  #[derive(Deserialize, Serialize, Debug)]
207  pub struct BootSet {
208    /// Processor architecture; defaults to whatever the linked image
209    /// reports if omitted.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub arch: Option<Arch>,
212    /// Kernel command-line parameters passed to the booted nodes.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub kernel_parameters: Option<String>,
215    /// Network name to boot over (e.g. `nmn`).
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub network: Option<String>,
218    /// Explicit list of node xnames to target.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub node_list: Option<Vec<String>>,
221    /// HSM roles (e.g. `Compute`, `Application`) to target.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub node_roles_group: Option<Vec<String>>,
224    /// HSM group names to target.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub node_groups: Option<Vec<String>>,
227    /// Root-filesystem provider, e.g. `sbps` or `ais`.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub rootfs_provider: Option<String>,
230    /// Extra arguments forwarded to the rootfs provider.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub rootfs_provider_passthrough: Option<String>,
233  }
234
235  /// Processor architecture for a boot set.
236  #[derive(Deserialize, Serialize, Debug, Display)]
237  #[allow(clippy::upper_case_acronyms)]
238  pub enum Arch {
239    /// x86-64 nodes.
240    X86,
241    /// AArch64 / ARM nodes.
242    ARM,
243    /// Any other architecture.
244    Other,
245    /// Architecture could not be determined.
246    Unknown,
247  }
248}
249
250/// struct to represent the `images` section in SAT file
251pub mod image {
252  use serde::{Deserialize, Serialize};
253
254  /// Processor architecture for an IMS image build.
255  #[derive(Deserialize, Serialize, Debug)]
256  pub enum Arch {
257    /// 64-bit ARM (serialized as `"aarch64"`).
258    #[serde(rename(serialize = "aarch64", deserialize = "aarch64"))]
259    Aarch64,
260    /// x86-64 (serialized as `"x86_64"`).
261    #[serde(rename(serialize = "x86_64", deserialize = "x86_64"))]
262    X86_64,
263  }
264
265  /// Legacy IMS image reference with a recipe flag (older SAT format).
266  #[derive(Deserialize, Serialize, Debug)]
267  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
268  pub enum ImageIms {
269    /// Image identified by name; `is_recipe` indicates whether it is an IMS recipe.
270    NameIsRecipe {
271      /// IMS image name.
272      name: String,
273      /// `true` if `name` refers to an IMS recipe rather than a built image.
274      is_recipe: bool,
275    },
276    /// Image identified by UUID; `is_recipe` indicates whether it is an IMS recipe.
277    IdIsRecipe {
278      /// IMS image UUID.
279      id: String,
280      /// `true` if `id` refers to an IMS recipe rather than a built image.
281      is_recipe: bool,
282    },
283  }
284
285  /// Base IMS image reference used in newer SAT file format.
286  #[derive(Deserialize, Serialize, Debug)]
287  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
288  pub enum ImageBaseIms {
289    /// Image identified by name and type string.
290    NameType {
291      /// IMS image name.
292      name: String,
293      /// IMS object type, e.g. `"image"` or `"recipe"`.
294      r#type: String,
295    },
296    /// Image identified by UUID and type string.
297    IdType {
298      /// IMS image UUID.
299      id: String,
300      /// IMS object type, e.g. `"image"` or `"recipe"`.
301      r#type: String,
302    },
303    /// Older format with UUID and optional `is_recipe` flag.
304    BackwardCompatible {
305      /// `true` if `id` is a recipe; `None` defaults to image.
306      is_recipe: Option<bool>,
307      /// IMS image UUID.
308      id: String,
309    },
310  }
311
312  /// Criteria for filtering product catalog images.
313  #[derive(Deserialize, Serialize, Debug)]
314  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
315  pub enum Filter {
316    /// Match images whose name starts with `prefix`.
317    Prefix {
318      /// Required image-name prefix.
319      prefix: String,
320    },
321    /// Match images whose name matches the `wildcard` glob.
322    Wildcard {
323      /// Glob pattern applied to image names.
324      wildcard: String,
325    },
326    /// Match images built for the given architecture.
327    Arch {
328      /// Architecture filter.
329      arch: Arch,
330    },
331  }
332
333  /// A product catalog entry used as an image source.
334  #[derive(Deserialize, Serialize, Debug)]
335  pub struct Product {
336    /// Product name (e.g. `cos`, `slingshot-host-software`).
337    name: String,
338    /// Optional product version pin.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    version: Option<String>,
341    /// Product type, e.g. `"image"` or `"recipe"`.
342    r#type: String,
343    /// Filter applied to the product's image list.
344    filter: Filter,
345  }
346
347  /// Source for a base image — IMS, product catalog, or cross-reference.
348  #[derive(Deserialize, Serialize, Debug)]
349  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
350  pub enum Base {
351    /// Directly references an IMS image by name, UUID, or type.
352    Ims {
353      /// IMS image reference (name, UUID, or legacy `is_recipe`).
354      ims: ImageBaseIms,
355    },
356    /// Pulls the latest matching image from the product catalog.
357    Product {
358      /// Product entry to query the catalog for.
359      product: Product,
360    },
361    /// Cross-references the `name` of another image in the SAT `images` section.
362    ImageRef {
363      /// `name` of a sibling image defined in the SAT file's
364      /// `images` section.
365      image_ref: String,
366    },
367  }
368
369  /// Wrapper bridging the older `ims` key and the newer `base` key in SAT image entries.
370  #[derive(Deserialize, Serialize, Debug)]
371  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
372  pub enum BaseOrIms {
373    /// Newer format using the `base` key.
374    Base {
375      /// Newer-format base reference.
376      base: Base,
377    },
378    /// Legacy format using the `ims` key with a recipe flag.
379    Ims {
380      /// Legacy IMS reference carrying the `is_recipe` flag.
381      ims: ImageIms,
382    },
383  }
384
385  /// An image definition in the SAT file `images` section.
386  #[derive(Deserialize, Serialize, Debug)]
387  pub struct Image {
388    /// Unique name for this image; used as the cross-reference target for `image_ref`.
389    pub name: String,
390    /// Optional alias used to reference this image from session templates.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub ref_name: Option<String>,
393    /// Base image source (IMS or product catalog), in legacy or current format.
394    #[serde(flatten)]
395    pub base_or_ims: BaseOrIms,
396    /// CFS configuration to apply when building the image.
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub configuration: Option<String>,
399    /// HSM group names passed as Ansible group vars during the image build.
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub configuration_group_names: Option<Vec<String>>,
402    /// Free-form human description.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub description: Option<String>,
405  }
406}
407
408/// struct to represent the `configurations` section in SAT file
409pub mod configuration {
410  use serde::{Deserialize, Serialize};
411
412  /// A product reference within a CFS configuration layer.
413  /// Variants capture different ways to pin a version/branch.
414  #[derive(Deserialize, Serialize, Debug)]
415  #[serde(untagged)]
416  // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
417  #[allow(clippy::enum_variant_names)]
418  pub enum Product {
419    /// Product pinned to a specific branch (and optionally a version).
420    ProductVersionBranch {
421      /// Product name (e.g. `cos`).
422      name: String,
423      /// Optional product version pin.
424      version: Option<String>,
425      /// Git branch HEAD to track.
426      branch: String,
427    },
428    /// Product pinned to a specific commit (and optionally a version).
429    ProductVersionCommit {
430      /// Product name.
431      name: String,
432      /// Optional product version pin.
433      version: Option<String>,
434      /// Exact commit SHA to pin to.
435      commit: String,
436    },
437    /// Product pinned by exact version string.
438    ProductVersion {
439      /// Product name.
440      name: String,
441      /// Exact product version.
442      version: String,
443    },
444  }
445
446  /// A Git repository reference within a CFS configuration
447  /// layer, pinned by commit, branch, or tag.
448  #[derive(Deserialize, Serialize, Debug)]
449  #[serde(untagged)]
450  // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
451  #[allow(clippy::enum_variant_names)]
452  pub enum Git {
453    /// Layer pinned to an exact commit SHA.
454    GitCommit {
455      /// Git repository URL.
456      url: String,
457      /// Exact commit SHA.
458      commit: String,
459    },
460    /// Layer pinned to a branch HEAD.
461    GitBranch {
462      /// Git repository URL.
463      url: String,
464      /// Branch name whose HEAD is tracked.
465      branch: String,
466    },
467    /// Layer pinned to a tag.
468    GitTag {
469      /// Git repository URL.
470      url: String,
471      /// Tag name.
472      tag: String,
473    },
474  }
475
476  /// Extra CFS layer parameters (e.g., requiring DKMS).
477  #[derive(Deserialize, Serialize, Debug)]
478  pub struct SpecialParameters {
479    /// When `true`, the resulting image build must include DKMS.
480    pub ims_require_dkms: bool,
481  }
482
483  /// A CFS configuration layer sourced from a Git repo.
484  #[derive(Deserialize, Serialize, Debug)]
485  pub struct LayerGit {
486    /// Optional human-friendly name for this layer; defaults to a
487    /// CFS-generated identifier when absent.
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub name: Option<String>,
490    /// Optional Ansible playbook filename within the layer's repo;
491    /// CFS uses `site.yml` when absent.
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub playbook: Option<String>, // This field is optional but with default value. Therefore we won't
494    /// Git pin (commit / branch / tag).
495    pub git: Git,
496    /// Layer-specific knobs such as DKMS requirements.
497    pub special_parameters: Option<SpecialParameters>,
498  }
499
500  /// A CFS configuration layer sourced from a product catalog.
501  #[derive(Deserialize, Serialize, Debug)]
502  pub struct LayerProduct {
503    /// Optional human-friendly name for this layer; defaults to a
504    /// CFS-generated identifier when absent.
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub name: Option<String>,
507    /// Optional Ansible playbook filename within the product's repo.
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub playbook: Option<String>, // This field is optional but with default value. Therefore we won't
510    /// Product reference (name + version pin).
511    pub product: Product,
512  }
513
514  /// A CFS configuration layer — either Git-based or
515  /// product-catalog-based.
516  #[derive(Deserialize, Serialize, Debug)]
517  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
518  pub enum Layer {
519    /// CFS configuration layer sourced from a Git repository.
520    LayerGit(LayerGit),
521    /// CFS configuration layer sourced from a product catalog entry.
522    LayerProduct(LayerProduct),
523  }
524
525  /// An Ansible inventory source for a CFS configuration,
526  /// pinned by commit or branch.
527  #[derive(Deserialize, Serialize, Debug)]
528  #[serde(untagged)] // <-- this is important. More info https://serde.rs/enum-representations.html#untagged
529  pub enum Inventory {
530    /// Inventory repository pinned to a specific commit SHA.
531    InventoryCommit {
532      /// Optional human-friendly name for the inventory source.
533      #[serde(skip_serializing_if = "Option::is_none")]
534      name: Option<String>,
535      /// Git repository URL.
536      url: String,
537      /// Exact commit SHA.
538      commit: String,
539    },
540    /// Inventory repository pinned to a branch HEAD.
541    InventoryBranch {
542      /// Optional human-friendly name for the inventory source.
543      #[serde(skip_serializing_if = "Option::is_none")]
544      name: Option<String>,
545      /// Git repository URL.
546      url: String,
547      /// Branch name whose HEAD is tracked.
548      branch: String,
549    },
550  }
551
552  /// A CFS configuration definition in the SAT file
553  /// `configurations` section.
554  #[derive(Deserialize, Serialize, Debug)]
555  pub struct Configuration {
556    /// Configuration name; must be unique within the SAT file.
557    pub name: String,
558    /// Free-form human description.
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub description: Option<String>,
561    /// Ordered list of CFS layers applied to nodes using this
562    /// configuration.
563    pub layers: Vec<Layer>,
564    /// Optional Ansible inventory source for the configuration.
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub additional_inventory: Option<Inventory>,
567  }
568}
569
570// Removed unused module sat_file_image_old which contained Ims and Product structs
571
572/// Merge 2 yamls, 'b' values will overwrite 'a' values
573/// eg:
574/// having a:
575///
576/// ```text
577/// key_1
578///   key_1_1: value_1_1
579///   key_1_2: value_1_2
580/// key_2: value_2
581/// key_3: value_3
582/// ```
583/// and b:
584/// ```text
585/// key_1
586///   key_1_1: new_value_1_1
587///   key_1_2: value_1_2
588///   key_1_3: new_value_1_3
589/// key_2: new_value_2
590/// key_4: new_value_4
591/// ```
592/// would convert a into:
593/// ```text
594/// key_1
595///   key_1_1: new_value_1_1
596///   key_1_3: new_value_1_3
597/// key_2: new_value_2
598/// key_3: value_3
599/// key_4: new_value_4
600/// ```
601fn merge_yaml(base: Value, merge: Value) -> Option<Value> {
602  match (base, merge) {
603    (Value::Mapping(mut base_map), Value::Mapping(merge_map)) => {
604      for (key, value) in merge_map {
605        if let Some(base_value) = base_map.get_mut(&key) {
606          *base_value = merge_yaml(base_value.clone(), value)?;
607        } else {
608          base_map.insert(key, value);
609        }
610      }
611      Some(Value::Mapping(base_map))
612    }
613    (Value::Sequence(mut base_seq), Value::Sequence(merge_seq)) => {
614      base_seq.extend(merge_seq);
615      Some(Value::Sequence(base_seq))
616    }
617    (_, merge) => Some(merge),
618  }
619}
620
621/// Convert a String dot notation expression into a serde_yaml::Value.
622/// eg:
623/// dot notation input like:
624/// ```text
625/// key_1.key_2.key_3=1
626/// ```
627/// would result in a serde_yaml::Value equivalent to:
628/// ```text
629/// key_1
630///   key_2
631///     key_3: 1
632/// ```
633fn dot_notation_to_yaml(
634  dot_notation: &str,
635) -> Result<serde_yaml::Value, Error> {
636  let parts: Vec<&str> = dot_notation.split('=').collect();
637  if parts.len() != 2 {
638    return Err(Error::InvalidPattern("Invalid format".to_string()));
639  }
640
641  let keys: Vec<&str> = parts[0].trim().split('.').collect();
642  let value_str = parts[1].trim().trim_matches('"'); // Remove leading and trailing quotes
643  let value: Value = Value::String(value_str.to_string());
644
645  let mut root = Value::Mapping(Mapping::new());
646  let mut current_level = &mut root;
647
648  for (i, &key) in keys.iter().enumerate() {
649    if i == keys.len() - 1 {
650      // Last key, assign the value
651      if let Value::Mapping(map) = current_level {
652        map.insert(Value::String(key.to_string()), value.clone());
653      }
654    } else {
655      // Not the last key, create or use existing map
656      let next_level = if let Value::Mapping(map) = current_level {
657        if map.contains_key(Value::String(key.to_string())) {
658          // Use existing map
659          map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
660            Error::TemplateError(
661              "Failed to get mutable reference to \
662               existing YAML map entry"
663                .to_string(),
664            )
665          })?
666        } else {
667          // Create new map and insert
668          map.insert(
669            Value::String(key.to_string()),
670            Value::Mapping(Mapping::new()),
671          );
672          map.get_mut(Value::String(key.to_string())).ok_or_else(|| {
673            Error::TemplateError(
674              "Failed to get mutable reference to \
675               newly inserted YAML map entry"
676                .to_string(),
677            )
678          })?
679        }
680      } else {
681        return Err(Error::TemplateError(
682          "Unexpected structure encountered".to_string(),
683        ));
684      };
685      current_level = next_level;
686    }
687  }
688
689  Ok(root)
690}
691
692/// Render a SAT file as a Jinja2 template, optionally
693/// merging a values file and CLI-provided overrides in dot
694/// notation.
695pub fn render_jinja2_sat_file_yaml(
696  sat_file_content: &str,
697  values_file_content_opt: Option<&str>,
698  value_cli_vec_opt: Option<&[String]>,
699) -> Result<Value, Error> {
700  let mut env = minijinja::Environment::new();
701  // Set/enable debug in order to force minijinja to print debug error messages which are more
702  // descriptive. Eg https://github.com/mitsuhiko/minijinja/blob/main/examples/error/src/main.rs#L4-L5
703  env.set_debug(true);
704  // Set lines starting with `#` as comments
705  env.set_syntax(
706    minijinja::syntax::SyntaxConfig::builder()
707      .line_comment_prefix("#")
708      .build()
709      .map_err(|e| {
710        Error::TemplateError(format!(
711          "Failed to build jinja2 syntax config: {e}"
712        ))
713      })?,
714  );
715  // Set 'String' as undefined behaviour meaning, missing values won't pass the template
716  // rendering
717  env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
718
719  // Render session values file
720  let mut values_file_yaml: Value = if let Some(values_file_content) =
721    values_file_content_opt
722  {
723    tracing::info!(
724      "'Session vars' file provided. Going to process SAT file as a jinja template."
725    );
726    tracing::info!("Expand variables in 'session vars' file");
727    // Read sesson vars file and parse it to YAML
728    let values_file_yaml: Value = serde_yaml::from_str(values_file_content)?;
729    // Render session vars file with itself (copying ansible behaviour where the ansible vars
730    // file is also a jinja template and combine both vars and values in it)
731    let values_file_rendered = env
732      .render_str(values_file_content, values_file_yaml)
733      .map_err(|e| {
734        Error::TemplateError(format!("Error parsing values file to YAML: {e}"))
735      })?;
736    serde_yaml::from_str(&values_file_rendered)?
737  } else {
738    serde_yaml::from_str(sat_file_content)?
739  };
740
741  // Convert variable values sent by cli argument from dot notation to yaml format
742  tracing::debug!(
743    "Convert variable values sent by cli argument from dot notation to yaml format"
744  );
745  if let Some(value_option_vec) = value_cli_vec_opt {
746    for value_option in value_option_vec {
747      let cli_var_context_yaml = dot_notation_to_yaml(value_option)?;
748
749      values_file_yaml =
750        merge_yaml(values_file_yaml.clone(), cli_var_context_yaml).ok_or_else(
751          || {
752            Error::TemplateError(
753              "Failed to merge CLI variable values into \
754             SAT file YAML"
755                .to_string(),
756            )
757          },
758        )?;
759    }
760  }
761
762  // render sat template file
763  tracing::info!("Expand variables in 'SAT file'");
764  let sat_file_rendered = env
765    .render_str(sat_file_content, values_file_yaml)
766    .map_err(|e| {
767      Error::TemplateError(format!("Failed to render SAT file template: {e}"))
768    })?;
769
770  // Disable debug
771  env.set_debug(false);
772
773  Ok(serde_yaml::from_str(&sat_file_rendered)?)
774}
775
776#[cfg(test)]
777mod tests;