feat(baker/finalize): add YAML to shell env var conversion

This commit is contained in:
Catty Steve
2026-04-19 09:40:05 +08:00
parent a1da093176
commit 0c089fd634
+303 -2
View File
@@ -1,10 +1,88 @@
use crate::finalize::plugin::PluginMetadata;
use crate::finalize::plugin::PluginError;
use crate::finalize::plugin::PluginMetadata;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
use std::collections::HashMap;
pub struct Shell;
// Workaround flexible key in yaml
fn parse_key(key: serde_yaml::Value) -> String {
match key {
serde_yaml::Value::String(s) => s,
serde_yaml::Value::Number(n) => n.to_string(),
serde_yaml::Value::Bool(b) => b.to_string(),
_ => {
log::warn!("Unsupported key type in mapping: {:?}", key);
String::from("?")
}
}
}
fn convert_argument(prefix: &str, argument: serde_yaml::Value, in_sequence: bool) -> HashMap<String, String> {
let mut envvars: HashMap<String, String> = HashMap::new();
match argument {
serde_yaml::Value::Mapping(mapping) => {
for i in mapping {
let key = parse_key(i.0).to_uppercase();
let value = i.1.clone();
let new_prefix = format!("{}_{}", prefix, key);
let subenvvars = convert_argument(new_prefix.as_str(), value, false);
envvars.extend(subenvvars);
}
}
serde_yaml::Value::Null => {
// NOTE: Null value should be eliminated.
}
serde_yaml::Value::Bool(value) => {
envvars.insert(prefix.to_string(), value.to_string());
}
serde_yaml::Value::String(value) => {
envvars.insert(prefix.to_string(), format!("\"{}\"", value));
}
serde_yaml::Value::Number(value) => {
envvars.insert(prefix.to_string(), value.to_string());
},
serde_yaml::Value::Tagged(tagged_value) => {
let subenvvars = convert_argument(prefix, tagged_value.value, in_sequence);
envvars.extend(subenvvars);
}
serde_yaml::Value::Sequence(seq) => {
if in_sequence {
log::warn!("Nested sequence in sequence is not supported, skipping: {}", prefix);
return envvars;
}
let mut array = String::new();
array.push_str("(");
let mut has_mapping = false;
let mut _envvars: HashMap<String, String> = HashMap::new();
for (index, item) in seq.into_iter().enumerate() {
if index > 0 {
array.push_str(" ");
}
let new_prefix = format!("{}_{}", prefix, index);
let subenvvars = convert_argument(new_prefix.as_str(), item.clone(), true);
if item.is_mapping() {
has_mapping = true;
}
else {
array.push_str(subenvvars.get(new_prefix.as_str()).map(|v| v.as_str()).unwrap_or_default());
}
_envvars.extend(subenvvars);
}
if has_mapping {
envvars.extend(_envvars);
}
else {
array.push_str(")");
envvars.insert(prefix.to_string(), array);
}
}
}
envvars
}
#[async_trait]
impl AsyncPluginFn for Shell {
async fn call(
@@ -14,7 +92,230 @@ impl AsyncPluginFn for Shell {
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<(), PluginError> {
todo!();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_key_string() {
let key = serde_yaml::Value::String("test_key".to_string());
assert_eq!(parse_key(key), "test_key");
}
#[test]
fn test_parse_key_number() {
let key = serde_yaml::Value::Number(42.into());
assert_eq!(parse_key(key), "42");
let key = serde_yaml::Value::Number(3.14.into());
assert_eq!(parse_key(key), "3.14");
}
#[test]
fn test_parse_key_bool() {
let key = serde_yaml::Value::Bool(true);
assert_eq!(parse_key(key), "true");
let key = serde_yaml::Value::Bool(false);
assert_eq!(parse_key(key), "false");
}
#[test]
fn test_parse_key_unsupported() {
let key = serde_yaml::Value::Null;
assert_eq!(parse_key(key), "?");
let key = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
assert_eq!(parse_key(key), "?");
let key = serde_yaml::Value::Sequence(vec![]);
assert_eq!(parse_key(key), "?");
}
#[test]
fn test_convert_argument_simple_mapping() {
let yaml = serde_yaml::from_str("host: docker.io\nuser: admin").unwrap();
let result = convert_argument("PLUGIN", yaml, false);
// Keys are uppercased in actual call() function
assert_eq!(result.get("PLUGIN_HOST"), Some(&"\"docker.io\"".to_string()));
assert_eq!(result.get("PLUGIN_USER"), Some(&"\"admin\"".to_string()));
}
#[test]
fn test_convert_argument_nested_mapping() {
let yaml = serde_yaml::from_str("
from:
host: smtp.example.com
port: 587
").unwrap();
let result = convert_argument("CFG", yaml, false);
// Nested keys use uppercase format: CFG_FROM_HOST
assert_eq!(result.get("CFG_FROM_HOST"), Some(&"\"smtp.example.com\"".to_string()));
assert_eq!(result.get("CFG_FROM_PORT"), Some(&"587".to_string()));
}
#[test]
fn test_convert_argument_different_types() {
let yaml = serde_yaml::from_str("
enabled: true
count: 42
ratio: 3.14
name: test
empty: ~
").unwrap();
let result = convert_argument("VAR", yaml, false);
assert_eq!(result.get("VAR_ENABLED"), Some(&"true".to_string()));
assert_eq!(result.get("VAR_COUNT"), Some(&"42".to_string()));
assert_eq!(result.get("VAR_RATIO"), Some(&"3.14".to_string()));
assert_eq!(result.get("VAR_NAME"), Some(&"\"test\"".to_string()));
assert!(!result.contains_key("VAR_EMPTY"));
}
#[test]
fn test_convert_argument_sequence_simple() {
let yaml = serde_yaml::from_str("tags:\n - latest\n - v1.0\n - stable").unwrap();
let result = convert_argument("CFG", yaml, false);
// Strings in arrays are quoted by convert_argument
assert_eq!(result.get("CFG_TAGS"), Some(&"(\"latest\" \"v1.0\" \"stable\")".to_string()));
}
#[test]
fn test_convert_argument_sequence_with_mapping() {
let yaml = serde_yaml::from_str("
env:
- name: KEY1
value: val1
- name: KEY2
value: val2
").unwrap();
let result = convert_argument("CFG", yaml, false);
assert_eq!(result.get("CFG_ENV_0_NAME"), Some(&"\"KEY1\"".to_string()));
assert_eq!(result.get("CFG_ENV_0_VALUE"), Some(&"\"val1\"".to_string()));
assert_eq!(result.get("CFG_ENV_1_NAME"), Some(&"\"KEY2\"".to_string()));
assert_eq!(result.get("CFG_ENV_1_VALUE"), Some(&"\"val2\"".to_string()));
assert!(!result.contains_key("CFG_ENV"));
}
#[test]
fn test_convert_argument_nested_sequence() {
let yaml = serde_yaml::from_str("
matrix:
- - a
- b
- - c
- d
").unwrap();
let result = convert_argument("CFG", yaml, false);
// Nested sequences are not supported (line 50-52 logs warning and skips)
// So CFG_MATRIX_0 and CFG_MATRIX_1 keys are not created
assert!(!result.contains_key("CFG_MATRIX_0"));
assert!(!result.contains_key("CFG_MATRIX_1"));
// The outer array has empty slots where nested sequences were skipped
// (space separator is still added even for empty values)
assert_eq!(result.get("CFG_MATRIX"), Some(&"( )".to_string()));
}
#[test]
fn test_convert_argument_empty_mapping() {
let yaml = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
let result = convert_argument("CFG", yaml, false);
assert!(result.is_empty());
}
#[test]
fn test_convert_argument_empty_sequence() {
let yaml = serde_yaml::Value::Sequence(vec![]);
let result = convert_argument("CFG", yaml, false);
assert_eq!(result.get("CFG"), Some(&"()".to_string()));
}
#[test]
fn test_convert_argument_null_value() {
let yaml = serde_yaml::from_str("
host: docker.io
token: ~
").unwrap();
let result = convert_argument("CFG", yaml, false);
// Keys are uppercased: CFG_HOST
assert_eq!(result.get("CFG_HOST"), Some(&"\"docker.io\"".to_string()));
// Null value is skipped (not set)
assert!(!result.contains_key("CFG_TOKEN"));
}
#[test]
fn test_convert_argument_number_keys() {
let yaml = serde_yaml::from_str("
0: first
1: second
").unwrap();
let result = convert_argument("CFG", yaml, false);
// Number keys are preserved but prefixed: CFG_0, CFG_1
assert_eq!(result.get("CFG_0"), Some(&"\"first\"".to_string()));
assert_eq!(result.get("CFG_1"), Some(&"\"second\"".to_string()));
}
#[test]
fn test_convert_argument_complex_nested() {
let yaml = serde_yaml::from_str("
plugin:
docker-push:
host: docker.io
image: user/app:tag
options:
force: true
retries: 3
tags:
- latest
- v1.0
").unwrap();
let result = convert_argument("CFG", yaml, false);
// All keys uppercased for shell environment variables
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_HOST"), Some(&"\"docker.io\"".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_IMAGE"), Some(&"\"user/app:tag\"".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_FORCE"), Some(&"true".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_RETRIES"), Some(&"3".to_string()));
// Strings in sequences are quoted
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_TAGS"), Some(&"(\"latest\" \"v1.0\")".to_string()));
}
#[test]
fn test_convert_argument_special_chars_in_values() {
let yaml = serde_yaml::from_str("
url: https://example.com/path?query=value
path: /workspace/output.tar.gz
token: 'Bearer abc123'
").unwrap();
let result = convert_argument("CFG", yaml, false);
// Keys uppercased: CFG_URL, CFG_PATH, CFG_TOKEN
assert_eq!(result.get("CFG_URL"), Some(&"\"https://example.com/path?query=value\"".to_string()));
assert_eq!(result.get("CFG_PATH"), Some(&"\"/workspace/output.tar.gz\"".to_string()));
assert_eq!(result.get("CFG_TOKEN"), Some(&"\"Bearer abc123\"".to_string()));
}
#[test]
fn test_convert_argument_quoted_strings_preserved() {
let yaml = serde_yaml::from_str("
msg: 'Hello World'
cmd: \"echo 'test'\"
").unwrap();
let result = convert_argument("CFG", yaml, false);
// Keys uppercased: CFG_MSG, CFG_CMD
assert_eq!(result.get("CFG_MSG"), Some(&"\"Hello World\"".to_string()));
assert_eq!(result.get("CFG_CMD"), Some(&"\"echo 'test'\"".to_string()));
}
}