Skip to content

Commit

Permalink
feat: enable extensions to expose template helpers
Browse files Browse the repository at this point in the history
This allows extensions to provide arbitrary code to be run during
template rendering.
  • Loading branch information
cbgbt committed Oct 24, 2023
1 parent f94d62f commit 80cd263
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 10 deletions.
6 changes: 5 additions & 1 deletion bottlerocket-settings-sdk/src/cli/proto1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,17 @@ pub struct MigrateCommand {

#[derive(Args, Debug)]
pub struct TemplateHelperCommand {
/// The version of the setting which should be used
#[arg(long)]
pub setting_version: String,

/// The name of the helper to call
#[arg(long)]
pub helper_name: String,

/// The arguments for the given helper
#[arg(long, value_parser = parse_json)]
pub args: Vec<serde_json::Value>,
pub arg: Vec<serde_json::Value>,
}

/// Helper for `clap` to parse JSON values.
Expand Down
3 changes: 3 additions & 0 deletions bottlerocket-settings-sdk/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ pub mod error {
#[snafu(display("Set operation failed: {}", source))]
Set { source: BottlerocketSettingError },

#[snafu(display("Template helper execution failed: {}", source))]
TemplateHelper { source: BottlerocketSettingError },

#[snafu(display("Validate operation failed: {}", source))]
Validate { source: BottlerocketSettingError },

Expand Down
24 changes: 20 additions & 4 deletions bottlerocket-settings-sdk/src/extension/proto1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//! with function name collisions if needed.
use super::{error, SettingsExtensionError};
use crate::cli::proto1::{
GenerateCommand, MigrateCommand, Proto1Command, SetCommand, ValidateCommand,
GenerateCommand, MigrateCommand, Proto1Command, SetCommand, TemplateHelperCommand,
ValidateCommand,
};
use crate::migrate::Migrator;
use crate::model::erased::AsTypeErasedModel;
Expand Down Expand Up @@ -51,9 +52,7 @@ where
Proto1Command::Generate(g) => extension.generate(g).and_then(json_stringify),
Proto1Command::Migrate(m) => extension.migrate(m).and_then(json_stringify),
Proto1Command::Validate(v) => extension.validate(v).map(|_| String::new()),
Proto1Command::Helper(_h) => {
todo!("https://github.com/bottlerocket-os/bottlerocket-settings-sdk/issues/3")
}
Proto1Command::Helper(h) => extension.template_helper(h).and_then(json_stringify),
}
}

Expand All @@ -76,6 +75,10 @@ pub trait Proto1: Debug {
&self,
args: ValidateCommand,
) -> Result<(), SettingsExtensionError<Self::MigratorErrorKind>>;
fn template_helper(
&self,
args: TemplateHelperCommand,
) -> Result<serde_json::Value, SettingsExtensionError<Self::MigratorErrorKind>>;
}

impl<Mi, Mo> Proto1 for SettingsExtension<Mi, Mo>
Expand Down Expand Up @@ -160,4 +163,17 @@ where
.validate(args.value, args.required_settings)
.context(error::ValidateSnafu)
}

fn template_helper(
&self,
args: TemplateHelperCommand,
) -> Result<serde_json::Value, SettingsExtensionError<Self::MigratorErrorKind>> {
self.model(&args.setting_version)
.context(error::NoSuchModelSnafu {
setting_version: args.setting_version,
})?
.as_model()
.execute_template_helper(&args.helper_name, args.arg)
.context(error::TemplateHelperSnafu)
}
}
2 changes: 1 addition & 1 deletion bottlerocket-settings-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub mod model;

#[cfg(feature = "extension")]
pub use crate::extension::SettingsExtension;
pub use helper::{HelperDef, HelperError};
pub use helper::{template_helper, HelperDef, HelperError};
#[cfg(feature = "extension")]
pub use migrate::{
LinearMigrator, LinearMigratorExtensionBuilder, LinearMigratorModel, LinearlyMigrateable,
Expand Down
33 changes: 32 additions & 1 deletion bottlerocket-settings-sdk/src/model/erased.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! This module contains traits which erase the underlying [`SettingsModel`] types, allowing the
//! SDK to refer to the [`SettingsModel`]s as a collection of trait objects.
use super::{error, BottlerocketSetting, BottlerocketSettingError, GenerateResult, SettingsModel};
use snafu::ResultExt;
use snafu::{OptionExt, ResultExt};
use std::any::Any;
use std::fmt::Debug;
use tracing::{debug, instrument};
Expand Down Expand Up @@ -73,6 +73,13 @@ pub trait TypeErasedModel: Debug {
&self,
value: serde_json::Value,
) -> Result<Box<dyn Any>, BottlerocketSettingError>;

/// Executes a template helper associated with this model version.
fn execute_template_helper(
&self,
helper_name: &str,
args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, BottlerocketSettingError>;
}

/// A helper trait used to "upcast" supertraits over the [`TypeErasedModel`] trait.
Expand Down Expand Up @@ -191,6 +198,30 @@ impl<T: SettingsModel + 'static> TypeErasedModel for BottlerocketSetting<T> {
})
}

fn execute_template_helper(
&self,
helper_name: &str,
args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, BottlerocketSettingError> {
let all_helpers = T::template_helpers()
.map_err(Into::into)
.context(error::FetchTemplateHelpersSnafu)?;

let helper = all_helpers
.get(helper_name)
.context(error::FetchTemplateHelperSnafu {
helper_name: helper_name.to_string(),
helper_version: T::get_version(),
})?;

helper
.helper_fn(args)
.context(error::ExecuteTemplateHelperSnafu {
helper_name: helper_name.to_string(),
helper_version: T::get_version(),
})
}

#[instrument(skip(self), err)]
fn parse_erased(
&self,
Expand Down
41 changes: 40 additions & 1 deletion bottlerocket-settings-sdk/src/model/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Provides the [`SettingsModel`] trait interface required to model new settings in the
//! Bottlerocket API using the settings SDK.
use crate::HelperDef;
use serde::Deserialize;
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt::Debug, marker::PhantomData};
use std::collections::HashMap;
use std::fmt::Debug;
use std::marker::PhantomData;

#[doc(hidden)]
pub mod erased;
Expand Down Expand Up @@ -98,6 +101,11 @@ pub trait SettingsModel: Sized + Serialize + DeserializeOwned + Debug {
_value: Self,
_validated_settings: Option<serde_json::Value>,
) -> Result<(), Self::ErrorKind>;

/// Returns the set of template helpers associated with this settings model.
fn template_helpers() -> Result<HashMap<String, Box<dyn HelperDef>>, Self::ErrorKind> {
Ok(HashMap::new())
}
}

/// This struct wraps [`SettingsModel`]s in a referencable object which is passed to the
Expand Down Expand Up @@ -169,6 +177,7 @@ where

mod error {
#![allow(missing_docs)]
use crate::HelperError;
use snafu::Snafu;

/// The error type returned when interacting with a user-defined
Expand All @@ -190,6 +199,36 @@ mod error {
source: serde_json::Error,
},

#[snafu(display(
"Failed to execute helper '{}@{}': {}",
helper_name,
helper_version,
source
))]
ExecuteTemplateHelper {
helper_name: String,
helper_version: &'static str,
source: HelperError,
},

#[snafu(display(
"Failed to find template helper '{}@{}' from settings extension",
helper_name,
helper_version,
))]
FetchTemplateHelper {
helper_name: String,
helper_version: &'static str,
},

#[snafu(display(
"Failed to request template helpers from settings extension: {}",
source
))]
FetchTemplateHelpers {
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},

#[snafu(display(
"Failed to run 'generate' on setting version '{}': {}",
version,
Expand Down
File renamed without changes.
48 changes: 47 additions & 1 deletion bottlerocket-settings-sdk/tests/motd/v1.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use std::convert::Infallible;

use super::*;
use bottlerocket_settings_sdk::{GenerateResult, LinearlyMigrateable, NoMigration, SettingsModel};
use bottlerocket_settings_sdk::{
provide_template_helpers, GenerateResult, HelperDef, LinearlyMigrateable, NoMigration,
SettingsModel,
};
use bottlerocket_template_helper::template_helper;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct MotdV1(pub Option<String>);
Expand Down Expand Up @@ -43,6 +48,12 @@ impl SettingsModel for MotdV1 {
// No need to do any additional validation, any MotdV1 is acceptable
Ok(())
}

fn template_helpers() -> Result<HashMap<String, Box<dyn HelperDef>>> {
Ok(provide_template_helpers! {
"exclaim" => exclaim_helper,
})
}
}

impl LinearlyMigrateable for MotdV1 {
Expand All @@ -66,6 +77,11 @@ impl LinearlyMigrateable for MotdV1 {
}
}

#[template_helper(ident = exclaim_helper)]
fn exclaim(i: String) -> Result<String> {
Ok(i + "!")
}

#[test]
fn test_motdv1_set_success() {
// When set is called on motdv1 with a string input,
Expand Down Expand Up @@ -109,3 +125,33 @@ fn test_motdv1_validate() {
fn test_motdv1_failure() {
assert!(validate_cli(motd_settings_extension(), "v1", json!([1, 2, 3]), None).is_err(),);
}

#[test]
fn test_no_such_helper() {
assert!(template_helper_cli(motd_settings_extension(), "v1", "no_such_helper", vec![]).is_err())
}

#[test]
fn test_run_exclaim_helper() {
assert_eq!(
template_helper_cli(
motd_settings_extension(),
"v1",
"exclaim",
vec![json!("Hello")]
)
.unwrap(),
json!("Hello!")
);
}

#[test]
fn test_helper_too_many_args() {
assert!(template_helper_cli(
motd_settings_extension(),
"v1",
"exclaim",
vec![json!("too"), json!("many"), json!("arguments")]
)
.is_err());
}
53 changes: 52 additions & 1 deletion bottlerocket-settings-sdk/tests/motd/v2.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use std::collections::HashMap;

use super::*;
use anyhow::Result;
use bottlerocket_settings_sdk::{GenerateResult, LinearlyMigrateable, NoMigration, SettingsModel};
use bottlerocket_settings_sdk::{
provide_template_helpers, GenerateResult, HelperDef, LinearlyMigrateable, NoMigration,
SettingsModel,
};
use bottlerocket_template_helper::template_helper;
use serde::{Deserialize, Serialize};
use serde_json::json;

Expand Down Expand Up @@ -46,6 +52,13 @@ impl SettingsModel for MotdV2 {

Ok(())
}

fn template_helpers() -> Result<HashMap<String, Box<dyn HelperDef>>> {
Ok(provide_template_helpers! {
"exclaim" => exclaim_helper,
"question" => question_helper,
})
}
}

impl LinearlyMigrateable for MotdV2 {
Expand All @@ -70,6 +83,16 @@ impl LinearlyMigrateable for MotdV2 {
}
}

#[template_helper(ident = exclaim_helper)]
fn exclaim(i: String) -> Result<String> {
Ok(i + "!!")
}

#[template_helper(ident = question_helper)]
fn question(one: String, two: String) -> Result<String> {
Ok(format!("{}? {}??", one, two))
}

#[test]
fn test_motdv2_set_success() {
// When set is called on motdv2 with allowed input,
Expand Down Expand Up @@ -103,3 +126,31 @@ fn test_motdv2_generate() {
GenerateResult::<MotdV2, MotdV2>::Complete(MotdV2(vec![]))
);
}

#[test]
fn test_run_exclaim_helper() {
assert_eq!(
template_helper_cli(
motd_settings_extension(),
"v2",
"exclaim",
vec![json!("Hello")]
)
.unwrap(),
json!("Hello!!")
);
}

#[test]
fn test_run_question_helper() {
assert_eq!(
template_helper_cli(
motd_settings_extension(),
"v2",
"question",
vec![json!("two args"), json!("really")]
)
.unwrap(),
json!("two args? really??")
);
}
Loading

0 comments on commit 80cd263

Please sign in to comment.