Skip to content

Commit

Permalink
use minijinja to parse configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
morenol committed Apr 20, 2023
1 parent b11cf70 commit 19de5d2
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 142 deletions.
20 changes: 18 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/fluvio-connector-package/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ default = ["toml"]
anyhow = { workspace = true }
bytesize = { workspace = true}
humantime-serde = "1.1.1"
# Move to crates.io release, once https://github.com/mitsuhiko/minijinja/commit/78b9b87eaa68ac4c1b1c814d420a8e48e054fefc is released
minijinja = { git = "https://github.com/mitsuhiko/minijinja", rev = "78b9b87eaa68ac4c1b1c814d420a8e48e054fefc", default-features = false, features = ["custom_syntax"] }
openapiv3 = { git = "https://github.com/galibey/openapiv3", rev = "bdd22f046d2bc19ede257504645d31f835545222", default-features = false }
once_cell = { workspace = true }
regex = { workspace = true }
Expand Down
11 changes: 11 additions & 0 deletions crates/fluvio-connector-package/src/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use std::collections::HashMap;

use serde::Serialize;

/// Context for the template engine
/// This is the data that is available to the template engine
/// when it is rendering the template.
#[derive(Serialize)]
pub(crate) struct Context {
pub(crate) secrets: HashMap<String, String>,
}
11 changes: 7 additions & 4 deletions crates/fluvio-connector-package/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use secret::{resolve_inline_secrets};
use renderer::ConfigRenderer;
use serde_yaml::Value;

pub mod metadata;
pub mod config;
pub mod secret;
mod renderer;
mod context;

pub fn resolve_config(input: Value) -> anyhow::Result<Value> {
let value = resolve_inline_secrets(&input)?;
// placeholder for resolving from other sourcer for the future
// let value = resolve_from_env(&value)?;
let renderer = ConfigRenderer::new_with_default_stores()?;

let value = renderer.render_value(&input)?;

Ok(value)
}

Expand Down
171 changes: 171 additions & 0 deletions crates/fluvio-connector-package/src/renderer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use std::collections::HashMap;

use minijinja::Environment;
use serde_yaml::{Value, Mapping, Sequence};

use crate::{
secret::{SecretStore, detect_secrets_from_str},
context::Context,
};

/// Config renderer. This is the main entry point for rendering a config.
/// It is responsible for resolving secrets and rendering the config.
pub(crate) struct ConfigRenderer {
secret_store: &'static dyn SecretStore,
env: Environment<'static>,
}

impl ConfigRenderer {
pub(crate) fn new(secret_store: &'static dyn SecretStore, env: Environment<'static>) -> Self {
Self { secret_store, env }
}

pub fn new_with_default_stores() -> anyhow::Result<Self> {
let secret_store = crate::secret::default_secret_store()?;
let mut env = Environment::new();
env.set_syntax(minijinja::Syntax {
block_start: "${%".into(),
block_end: "%}".into(),
variable_start: "${{".into(),
variable_end: "}}".into(),
comment_start: "${*".into(),
comment_end: "*}".into(),
})?;

Ok(Self::new(secret_store, env))
}

pub fn render_value(&self, input: &serde_yaml::Value) -> anyhow::Result<serde_yaml::Value> {
match input {
Value::String(s) => {
let resolved = self.render_str(s)?;
Ok(Value::String(resolved))
}
Value::Mapping(mapping) => {
let mut result = Mapping::new();
for (key, value) in mapping.iter() {
let resolved = self.render_value(value)?;
result.insert(key.clone(), resolved);
}
Ok(Value::Mapping(result))
}
Value::Sequence(seq) => {
let mut result = Sequence::new();
for value in seq.iter() {
let resolved = self.render_value(value)?;
result.push(resolved);
}
Ok(Value::Sequence(result))
}
other => {
// ignore numbers, booleans and null for now
Ok(other.clone())
}
}
}

fn render_str(&self, input: &str) -> anyhow::Result<String> {
let context = self.build_context(input)?;
let rendered = self.env.render_str(input, &context)?;
Ok(rendered)
}

fn build_context(&self, input: &str) -> anyhow::Result<Context> {
let secrets: HashMap<_, _> = detect_secrets_from_str(input)?
.into_iter()
.map(|s| {
self.secret_store
.read(&s)
.map(|secret_value| (s, secret_value))
})
.collect::<Result<HashMap<_, _>, _>>()?;
Ok(Context { secrets })
}
}

#[cfg(test)]
mod test {
use std::io::Write;

use crate::secret::{self, FileSecretStore};

use super::ConfigRenderer;

#[test]
fn test_build_context() {
let mut file = tempfile::NamedTempFile::new().expect("failed to create tmp file");
file.write_all(b"foo=bar\napi_key=my_api_key\ninterval=10s")
.expect("file to write");
secret::set_default_secret_store(::std::boxed::Box::new(FileSecretStore::from(
file.path(),
)))
.expect("failed to set default secret store");

let renderer = ConfigRenderer::new_with_default_stores().unwrap();
let context: crate::context::Context =
renderer.build_context("hello ${{ secrets.foo }}").unwrap();

assert_eq!(context.secrets["foo"], "bar");
assert_eq!(context.secrets.len(), 1);
}

#[test]
fn test_render_str() {
let mut file = tempfile::NamedTempFile::new().expect("failed to create tmp file");
file.write_all(b"foo=bar\napi_key=my_api_key\ninterval=10s")
.expect("file to write");
secret::set_default_secret_store(::std::boxed::Box::new(FileSecretStore::from(
file.path(),
)))
.expect("failed to set default secret store");

let renderer = ConfigRenderer::new_with_default_stores().unwrap();
let output = renderer.render_str("hello ${{ secrets.foo }}").unwrap();
assert_eq!(output, "hello bar");
}

#[test]
fn test_render_str_undefined_secret() {
let mut file = tempfile::NamedTempFile::new().expect("failed to create tmp file");
file.write_all(b"foo=bar\napi_key=my_api_key\ninterval=10s")
.expect("file to write");
secret::set_default_secret_store(::std::boxed::Box::new(FileSecretStore::from(
file.path(),
)))
.expect("failed to set default secret store");

let renderer = ConfigRenderer::new_with_default_stores().unwrap();
let output = renderer.render_str("hello ${{ secrets.undefined_var }}");
assert!(output.is_err());
}

#[test]
fn test_invalid_syntax() {
let mut file = tempfile::NamedTempFile::new().expect("failed to create tmp file");
file.write_all(b"foo=bar\napi_key=my_api_key\ninterval=10s")
.expect("file to write");
secret::set_default_secret_store(::std::boxed::Box::new(FileSecretStore::from(
file.path(),
)))
.expect("failed to set default secret store");

let renderer = ConfigRenderer::new_with_default_stores().unwrap();
let output = renderer.render_str("hello ${{");
assert!(output.is_err());
}

#[test]
fn test_undefined_context_variable() {
let mut file = tempfile::NamedTempFile::new().expect("failed to create tmp file");
file.write_all(b"foo=bar\napi_key=my_api_key\ninterval=10s")
.expect("file to write");
secret::set_default_secret_store(::std::boxed::Box::new(FileSecretStore::from(
file.path(),
)))
.expect("failed to set default secret store");

let renderer = ConfigRenderer::new_with_default_stores().unwrap();
let output = renderer.render_str("hello ${{ some_undefined.variable }}");
assert!(output.is_err());
}
}
Loading

0 comments on commit 19de5d2

Please sign in to comment.