From bb55527ffd99136b82331191e0e28a6b22bbb2cb Mon Sep 17 00:00:00 2001 From: Max Dymond Date: Wed, 3 Dec 2025 10:29:35 +0000 Subject: [PATCH] feat: allow `yaml` tera loader to work with GitLab !references Signed-off-by: Max Dymond --- src/config.rs | 68 +++++++++++++++++++++++++++- test_resources/gitlab_reference.yaml | 5 ++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 test_resources/gitlab_reference.yaml diff --git a/src/config.rs b/src/config.rs index 42d1cba2..7dcfafdc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use crate::errors::FlokiError; use crate::image; use serde::{Deserialize, Serialize}; +use serde_yaml::{Mapping as YamlMapping, Value as YamlValue}; use tera::from_value; use tera::Context; use tera::Tera; @@ -128,6 +129,30 @@ enum LoaderType { Toml, } +fn strip_yaml_tags(value: &YamlValue) -> YamlValue { + match value { + // For tagged values, we just return the inner value. + YamlValue::Tagged(tagged) => strip_yaml_tags(&tagged.value), + + // For sequences and mappings, we need to recursively strip tags. + YamlValue::Sequence(items) => { + let mut stripped = Vec::with_capacity(items.len()); + for item in items { + stripped.push(strip_yaml_tags(item)); + } + YamlValue::Sequence(stripped) + } + YamlValue::Mapping(map) => { + let mut stripped = YamlMapping::with_capacity(map.len()); + for (key, value) in map { + stripped.insert(strip_yaml_tags(key), strip_yaml_tags(value)); + } + YamlValue::Mapping(stripped) + } + _ => value.clone(), + } +} + fn makeloader(path: &Path, loader: LoaderType) -> impl tera::Function { // Get the dirname of the Path given (if a file), or just the directory. let directory = if path.is_file() { @@ -145,8 +170,15 @@ fn makeloader(path: &Path, loader: LoaderType) -> impl tera::Function { .and_then(|full_path| std::fs::read_to_string(full_path).map_err(Into::into)) // Parse the file using the relevant parser .and_then(|contents| match loader { - LoaderType::Yaml => serde_yaml::from_str(&contents) - .map_err(|err| format!("Failed to parse file as YAML: {err}").into()), + LoaderType::Yaml => { + let raw: YamlValue = serde_yaml::from_str(&contents).map_err(|err| { + tera::Error::msg(format!("Failed to parse file as YAML: {err}")) + })?; + let stripped = strip_yaml_tags(&raw); + serde_yaml::from_value::(stripped).map_err(|err| { + tera::Error::msg(format!("Failed to convert YAML value: {err}")) + }) + } LoaderType::Json => serde_json::from_str(&contents) .map_err(|err| format!("Failed to parse file as JSON: {err}").into()), LoaderType::Toml => toml::from_str(&contents) @@ -372,4 +404,36 @@ mod test { assert_eq!(config, "floki: floki"); Ok(()) } + + #[test] + fn test_strip_yaml_tags_drops_reference_tag() { + let yaml = "value: !reference [template, script]"; + let raw: YamlValue = serde_yaml::from_str(yaml).unwrap(); + let stripped = strip_yaml_tags(&raw); + + let map = match stripped { + YamlValue::Mapping(map) => map, + other => panic!("expected mapping, got {:?}", other), + }; + + let key = YamlValue::String("value".into()); + let field = map.get(&key).expect("missing value key"); + + match field { + YamlValue::Sequence(items) => { + assert_eq!(items.len(), 2); + assert_eq!(items[0], YamlValue::String("template".into())); + assert_eq!(items[1], YamlValue::String("script".into())); + } + other => panic!("expected sequence, got {:?}", other), + } + } + + #[test] + fn test_tera_yamlload_with_gitlab_reference() -> Result<(), Box> { + let template = r#"{% set values = yaml(file="test_resources/gitlab_reference.yaml") %}script0: {{ values.job.script[0] }} script1: {{ values.job.script[1] }}"#; + let rendered = render_template(template, Path::new("floki.yaml"))?; + assert_eq!(rendered, "script0: .shared_template script1: script"); + Ok(()) + } } diff --git a/test_resources/gitlab_reference.yaml b/test_resources/gitlab_reference.yaml new file mode 100644 index 00000000..f7a7a1c4 --- /dev/null +++ b/test_resources/gitlab_reference.yaml @@ -0,0 +1,5 @@ +.shared_template: + script: + - echo template +job: + script: !reference [.shared_template, script]