mod error;
mod rules;
mod yaml_parser;
use std::convert::TryFrom;
use std::env;
use std::path::{Path, PathBuf};
pub use error::CircuitTemplateError;
use glob::glob;
pub use rules::RuleArgument;
use rules::Rules;
use yaml_parser::{v1, CircuitTemplate};
pub(self) use crate::admin::messages::{CreateCircuitBuilder, SplinterServiceBuilder};
pub const DEFAULT_TEMPLATE_DIR: &str = "/usr/share/splinter/circuit-templates";
pub const SPLINTER_CIRCUIT_TEMPLATE_PATH: &str = "SPLINTER_CIRCUIT_TEMPLATE_PATH";
pub struct CircuitTemplateManager {
paths: Vec<String>,
}
impl Default for CircuitTemplateManager {
fn default() -> Self {
CircuitTemplateManager {
paths: vec![DEFAULT_TEMPLATE_DIR.to_string()],
}
}
}
impl CircuitTemplateManager {
pub fn new(paths: &[String]) -> CircuitTemplateManager {
let paths: Vec<String> = paths
.iter()
.filter_map(|path| {
if Path::new(&path).is_dir() {
Some(path.to_string())
} else {
None
}
})
.collect();
CircuitTemplateManager { paths }
}
pub fn load(&self, name: &str) -> Result<CircuitCreateTemplate, CircuitTemplateError> {
let path = find_template(name, &self.paths)?;
CircuitCreateTemplate::from_yaml_file(&path)
}
pub fn load_raw_yaml(&self, name: &str) -> Result<String, CircuitTemplateError> {
let path = find_template(name, &self.paths)?;
let template = CircuitTemplate::load_from_file(&path)?;
debug!("Loading template file from {}", &path);
match template {
CircuitTemplate::V1(template) => serde_yaml::to_string(&template).map_err(|err| {
CircuitTemplateError::new_with_source(
"Failed to load template to yaml string",
Box::new(err),
)
}),
}
}
pub fn list_available_templates(&self) -> Result<Vec<(String, PathBuf)>, CircuitTemplateError> {
let mut available_templates: Vec<(String, PathBuf)> = Vec::new();
for entry in self
.paths
.iter()
.map(|path| {
let template_path = Path::new(path).join("*.yaml");
let pattern_string = template_path.to_str().ok_or_else(|| {
CircuitTemplateError::new(&String::from("Template path is not valid UTF-8"))
})?;
glob(pattern_string).map_err(|_| {
CircuitTemplateError::new(&format!(
"Cannot query path {:?} for pattern: {}",
path,
template_path.display()
))
})
})
.collect::<Result<Vec<_>, CircuitTemplateError>>()?
.into_iter()
.flatten()
{
match entry {
Ok(entry) => {
let full_path = std::fs::canonicalize(&entry).map_err(|err| {
CircuitTemplateError::new(&format!(
"Cannot get fully qualified path: {}",
err
))
})?;
let file_stem = entry
.file_stem()
.ok_or_else(|| {
CircuitTemplateError::new(&format!(
"Unable to get file's stem: {}",
entry.display(),
))
})?
.to_str()
.ok_or_else(|| {
CircuitTemplateError::new(&format!(
"File stem is not valid unicode: {}",
entry.display(),
))
})?;
available_templates.push((file_stem.to_string(), full_path));
}
Err(_) => {
error!("Unable to read file: {:?}", entry);
}
}
}
Ok(available_templates)
}
}
pub(in crate::circuit::template) fn find_template(
name: &str,
paths: &[String],
) -> Result<String, CircuitTemplateError> {
let path_name = Path::new(name);
if path_name.is_absolute() || path_name.starts_with("./") || path_name.starts_with("../") {
return Ok(name.to_string());
}
let name = format!("{}.yaml", name.trim_end_matches(".yaml"));
let paths = if paths.is_empty() {
let mut paths = vec![];
if let Ok(template_paths) = env::var(SPLINTER_CIRCUIT_TEMPLATE_PATH) {
paths.extend(
template_paths
.split(':')
.map(ToOwned::to_owned)
.collect::<Vec<String>>(),
);
}
paths.push(DEFAULT_TEMPLATE_DIR.to_string());
paths
} else {
paths.to_vec()
};
let valid_paths: Vec<String> = paths
.iter()
.filter_map(|path| {
let file_path = Path::new(path).join(&name);
if file_path.exists() {
file_path
.to_str()
.ok_or_else(|| {
CircuitTemplateError::new(&format!(
"Unable to find {} template file in paths: {:?}",
name, paths
))
})
.ok()
.map(ToOwned::to_owned)
} else {
None
}
})
.collect();
if valid_paths.is_empty() {
Err(CircuitTemplateError::new(&format!(
"Unable to find {} template file in paths: {:?}",
name, paths
)))
} else {
Ok(valid_paths
.first()
.ok_or_else(|| {
CircuitTemplateError::new(&format!(
"Unable to find {} template file in paths: {:?}",
name, paths
))
})?
.to_string())
}
}
pub struct CircuitCreateTemplate {
version: String,
arguments: Vec<RuleArgument>,
rules: Rules,
}
impl CircuitCreateTemplate {
pub fn from_yaml_file(path: &str) -> Result<Self, CircuitTemplateError> {
let circuit_template = CircuitTemplate::load_from_file(path)?;
match circuit_template {
CircuitTemplate::V1(template) => Ok(Self::try_from(template)?),
}
}
pub fn apply_to_builder(
&self,
circuit_builder: CreateCircuitBuilder,
) -> Result<CreateCircuitBuilder, CircuitTemplateError> {
let circuit_builder = self.rules.apply_rules(circuit_builder, &self.arguments)?;
Ok(circuit_builder)
}
pub fn set_argument_value(
&mut self,
key: &str,
value: &str,
) -> Result<(), CircuitTemplateError> {
let name = key.to_lowercase();
let (index, mut arg) = self
.arguments
.iter()
.enumerate()
.find_map(|(index, arg)| {
if arg.name() == name {
Some((index, arg.clone()))
} else {
None
}
})
.ok_or_else(|| {
CircuitTemplateError::new(&format!(
"Argument {} is not defined in the template",
key
))
})?;
arg.set_user_value(value);
self.arguments[index] = arg;
Ok(())
}
pub fn version(&self) -> &str {
&self.version
}
pub fn arguments(&self) -> &[RuleArgument] {
&self.arguments
}
pub fn rules(&self) -> &Rules {
&self.rules
}
}
impl TryFrom<v1::CircuitCreateTemplate> for CircuitCreateTemplate {
type Error = CircuitTemplateError;
fn try_from(create_circuit_template: v1::CircuitCreateTemplate) -> Result<Self, Self::Error> {
Ok(CircuitCreateTemplate {
version: create_circuit_template.version().to_string(),
arguments: create_circuit_template
.args()
.iter()
.cloned()
.map(RuleArgument::try_from)
.collect::<Result<_, CircuitTemplateError>>()?,
rules: Rules::from(create_circuit_template.rules().clone()),
})
}
}
#[cfg(test)]
mod test {
use super::*;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tempdir::TempDir;
use crate::admin::messages::SplinterService;
const EXAMPLE_TEMPLATE_YAML: &[u8] = br##"version: v1
args:
- name: ADMIN_KEYS
required: false
default: $(SIGNER_PUB_KEY)
- name: NODES
required: true
- name: SIGNER_PUB_KEY
required: false
- name: GAMEROOM_NAME
required: true
rules:
set-management-type:
management-type: "gameroom"
create-services:
service-type: 'scabbard'
service-args:
- key: 'admin-keys'
value: [$(ADMIN_KEYS)]
- key: 'peer_services'
value: '$(ALL_OTHER_SERVICES)'
first-service: 'a000'
set-metadata:
encoding: json
metadata:
- key: "scabbard_admin_keys"
value: ["$(ADMIN_KEYS)"]
- key: "alias"
value: "$(GAMEROOM_NAME)" "##;
#[test]
fn test_builds_template_v1() {
let temp_dir = TempDir::new("test_builds_template_v1").unwrap();
let temp_dir = temp_dir.path().to_path_buf();
let file_path = get_file_path(temp_dir);
write_yaml_file(&file_path, EXAMPLE_TEMPLATE_YAML);
let mut template =
CircuitCreateTemplate::from_yaml_file(&file_path).expect("failed to parse template");
template
.set_argument_value("nodes", "alpha-node-000,beta-node-000")
.expect("Error setting argument");
template
.set_argument_value("signer_pub_key", "signer_key")
.expect("Error setting argument");
template
.set_argument_value("gameroom_name", "my gameroom")
.expect("Error setting argument");
let circuit_create_builder = template
.apply_to_builder(CreateCircuitBuilder::new())
.expect("Error getting builders from templates");
assert_eq!(
circuit_create_builder.circuit_management_type(),
Some("gameroom".to_string())
);
let metadata = String::from_utf8(
circuit_create_builder
.application_metadata()
.expect("Application metadata is not set"),
)
.expect("Failed to parse metadata to string");
assert_eq!(
metadata,
"{\"scabbard_admin_keys\":[\"signer_key\"],\"alias\":\"my gameroom\"}"
);
let service_builders: Vec<SplinterService> = circuit_create_builder
.roster()
.ok_or(0)
.expect("Unable to get roster");
let service_alpha_node = service_builders
.iter()
.find(|service| service.allowed_nodes == vec!["alpha-node-000".to_string()])
.expect("service builder for alpha-node was not created correctly");
assert_eq!(service_alpha_node.service_id, "a000".to_string());
assert_eq!(service_alpha_node.service_type, "scabbard".to_string());
let alpha_service_args = &service_alpha_node.arguments;
assert!(alpha_service_args
.iter()
.any(|(key, value)| key == "admin-keys" && value == "[\"signer_key\"]"));
assert!(alpha_service_args
.iter()
.any(|(key, value)| key == "peer_services" && value == "[\"a001\"]"));
let service_beta_node = service_builders
.iter()
.find(|service| service.allowed_nodes == vec!["beta-node-000".to_string()])
.expect("service builder for beta-node was not created correctly");
assert_eq!(service_beta_node.service_id, "a001".to_string());
assert_eq!(service_beta_node.service_type, "scabbard".to_string());
let beta_service_args = &service_beta_node.arguments;
assert!(beta_service_args
.iter()
.any(|(key, value)| key == "admin-keys" && value == "[\"signer_key\"]"));
assert!(beta_service_args
.iter()
.any(|(key, value)| key == "peer_services" && value == "[\"a000\"]"));
}
#[test]
fn test_multiple_template_paths() {
let temp_dir1 = TempDir::new("test1").unwrap();
let temp_dir1 = temp_dir1.path().to_path_buf();
let temp_dir2 = TempDir::new("test2").unwrap();
let temp_dir2 = temp_dir2.path().to_path_buf();
let manager = CircuitTemplateManager::new(&[
temp_dir1
.to_str()
.expect("Unable to create str from temp_dir1 path")
.to_string(),
temp_dir2
.to_str()
.expect("Unable to create str from temp_dir2 path")
.to_string(),
]);
let file_path1 = get_file_path(temp_dir1);
write_yaml_file(&file_path1, EXAMPLE_TEMPLATE_YAML);
let template = manager.load("example_template.yaml");
assert!(template.is_ok());
let mut file_path2 = temp_dir2;
file_path2.push("example_template2.yaml");
let file_path2 = file_path2.to_str().unwrap().to_string();
write_yaml_file(&file_path2, EXAMPLE_TEMPLATE_YAML);
let template = manager.load("example_template2.yaml");
assert!(template.is_ok());
}
#[test]
fn test_list_available_templates() {
let temp_dir1 = TempDir::new("test1").unwrap();
let temp_dir1 = temp_dir1.path().to_path_buf();
let temp_dir2 = TempDir::new("test2").unwrap();
let temp_dir2 = temp_dir2.path().to_path_buf();
let manager = CircuitTemplateManager::new(&[
temp_dir1
.to_str()
.expect("Unable to create str from temp_dir1 path")
.to_string(),
temp_dir2
.to_str()
.expect("Unable to create str from temp_dir2 path")
.to_string(),
]);
let file_path1 = get_file_path(temp_dir1);
write_yaml_file(&file_path1, EXAMPLE_TEMPLATE_YAML);
let mut file_path2 = temp_dir2;
file_path2.push("example_template2.yaml");
let file_path2 = file_path2.to_str().unwrap().to_string();
write_yaml_file(&file_path2, EXAMPLE_TEMPLATE_YAML);
let templates = manager
.list_available_templates()
.expect("Error listing available circuit templates");
let expected_templates = vec![
(
"example_template".to_string(),
PathBuf::from(file_path1)
.canonicalize()
.expect("Unable to get full file path of temporary circuit file"),
),
(
"example_template2".to_string(),
PathBuf::from(file_path2)
.canonicalize()
.expect("Unable to get full file path of temporary circuit file"),
),
];
assert_eq!(templates, expected_templates);
}
#[test]
fn test_raw_yaml_string() {
let temp_dir1 = TempDir::new("test1").unwrap();
let temp_dir1 = temp_dir1.path().to_path_buf();
let manager = CircuitTemplateManager::new(&[temp_dir1
.to_str()
.expect("Unable to create str from temp_dir1 path")
.to_string()]);
let file_path1 = get_file_path(temp_dir1);
write_yaml_file(&file_path1, EXAMPLE_TEMPLATE_YAML);
let raw_yaml_result = manager.load_raw_yaml("example_template");
assert!(raw_yaml_result.is_ok());
let raw_yaml = raw_yaml_result.unwrap();
verify_example_yaml_string(raw_yaml);
}
fn get_file_path(mut temp_dir: PathBuf) -> String {
temp_dir.push("example_template.yaml");
let path = temp_dir.to_str().unwrap().to_string();
path
}
fn write_yaml_file(file_path: &str, data: &[u8]) {
let mut file = File::create(file_path).expect("Error creating test template yaml file.");
file.write_all(data)
.expect("Error writing example template yaml.");
}
fn verify_example_yaml_string(yaml: String) {
let _: serde_yaml::Value = serde_yaml::from_str(&yaml).expect("Invalid yaml was returned");
assert!(yaml.contains("version: v1"));
assert!(yaml.contains("args:"));
assert!(yaml.contains("rules:"));
assert!(yaml.contains("- name: ADMIN_KEYS"));
assert!(yaml.contains("- name: NODES"));
assert!(yaml.contains("- name: SIGNER_PUB_KEY"));
assert!(yaml.contains("- name: GAMEROOM_NAME"));
assert!(yaml.contains("set-management-type:"));
assert!(yaml.contains("management-type: gameroom"));
assert!(yaml.contains("create-services:"));
assert!(yaml.contains("service-type: scabbard"));
assert!(yaml.contains("service-args:"));
assert!(yaml.contains("- key: admin-keys"));
assert!(yaml.contains("- key: peer_services"));
assert!(yaml.contains("set-metadata:"));
assert!(yaml.contains("encoding: Json"));
assert!(yaml.contains("metadata:"));
assert!(yaml.contains("- key: scabbard_admin_keys"));
assert!(yaml.contains("- key: alias"));
}
}