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;