use crate::{conductor::base::DnaLoader, logger::LogRules};
use boolinator::*;
use holochain_core_types::{
agent::{AgentId, Base32},
dna::{
bridges::{BridgePresence, BridgeReference},
Dna,
},
error::{HcResult, HolochainError},
};
use holochain_json_api::json::JsonString;
use holochain_metrics::MetricPublisherConfig;
use holochain_net::sim2h_worker::Sim2hConfig;
use holochain_persistence_api::cas::content::AddressableContent;
use lib3h::engine::EngineConfig;
use petgraph::{algo::toposort, graph::DiGraph, prelude::NodeIndex};
use serde::Deserialize;
use std::{
collections::{HashMap, HashSet},
convert::TryFrom,
fs::File,
io::prelude::*,
net::Ipv4Addr,
path::PathBuf,
sync::Arc,
};
use toml;
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
pub struct Configuration {
pub agents: Vec<AgentConfiguration>,
#[serde(default)]
pub dnas: Vec<DnaConfiguration>,
#[serde(default)]
pub instances: Vec<InstanceConfiguration>,
#[serde(default)]
pub interfaces: Vec<InterfaceConfiguration>,
#[serde(default)]
pub bridges: Vec<Bridge>,
#[serde(default)]
pub ui_bundles: Vec<UiBundleConfiguration>,
#[serde(default)]
pub ui_interfaces: Vec<UiInterfaceConfiguration>,
#[serde(default)]
pub logger: LoggerConfiguration,
#[serde(default)]
pub tracing: Option<TracingConfiguration>,
#[serde(default)]
pub network: Option<NetworkConfig>,
#[serde(default = "default_persistence_dir")]
pub persistence_dir: PathBuf,
pub signing_service_uri: Option<String>,
pub encryption_service_uri: Option<String>,
pub decryption_service_uri: Option<String>,
pub dpki: Option<DpkiConfiguration>,
#[serde(default)]
pub signals: SignalConfig,
#[serde(default)]
pub passphrase_service: PassphraseServiceConfig,
#[serde(default)]
pub metric_publisher: Option<MetricPublisherConfig>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum PassphraseServiceConfig {
Cmd,
UnixSocket { path: String },
Mock { passphrase: String },
}
impl Default for PassphraseServiceConfig {
fn default() -> PassphraseServiceConfig {
PassphraseServiceConfig::Cmd
}
}
pub fn default_persistence_dir() -> PathBuf {
holochain_common::paths::config_root().join("conductor")
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct LoggerConfiguration {
#[serde(rename = "type")]
pub logger_level: String,
#[serde(default)]
pub rules: LogRules,
#[serde(default)]
pub state_dump: bool,
}
impl Default for LoggerConfiguration {
fn default() -> LoggerConfiguration {
LoggerConfiguration {
logger_level: "debug".into(),
rules: Default::default(),
state_dump: false,
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum TracingConfiguration {
None,
Jaeger(JaegerTracingConfiguration),
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct JaegerTracingConfiguration {
pub service_name: String,
pub socket_address: Option<String>,
}
impl Default for TracingConfiguration {
fn default() -> Self {
TracingConfiguration::None
}
}
fn detect_dupes<'a, I: Iterator<Item = &'a String>>(
name: &'static str,
items: I,
) -> Result<(), String> {
let mut set = HashSet::<&str>::new();
let mut dupes = Vec::<String>::new();
for item in items {
if !set.insert(item) {
dupes.push(item.to_string())
}
}
if !dupes.is_empty() {
Err(format!(
"Duplicate {} IDs detected: {}",
name,
dupes.join(", ")
))
} else {
Ok(())
}
}
#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_CONDUCTOR_LIB)]
#[allow(clippy::ptr_arg, clippy::toplevel_ref_arg)]
impl Configuration {
pub fn check_consistency(&self, mut dna_loader: &mut DnaLoader) -> Result<(), String> {
detect_dupes("agent", self.agents.iter().map(|c| &c.id))?;
detect_dupes("dna", self.dnas.iter().map(|c| &c.id))?;
detect_dupes("instance", self.instances.iter().map(|c| &c.id))?;
self.check_instances_storage()?;
detect_dupes("interface", self.interfaces.iter().map(|c| &c.id))?;
for ref instance in self.instances.iter() {
self.agent_by_id(&instance.agent).is_some().ok_or_else(|| {
format!(
"Agent configuration {} not found, mentioned in instance {}",
instance.agent, instance.id
)
})?;
let dna_config = self.dna_by_id(&instance.dna);
dna_config.is_some().ok_or_else(|| {
format!(
"DNA configuration \"{}\" not found, mentioned in instance \"{}\"",
instance.dna, instance.id
)
})?;
let dna_config = dna_config.unwrap();
let dna =
Arc::get_mut(&mut dna_loader).unwrap()(&PathBuf::from(dna_config.file.clone()))
.map_err(|_| format!("Could not load DNA file \"{}\"", dna_config.file))?;
for zome in dna.zomes.values() {
for bridge in zome.bridges.iter() {
if bridge.presence == BridgePresence::Required {
let handle = bridge.handle.clone();
let _ = self
.bridges
.iter()
.find(|b| b.handle == handle)
.ok_or_else(|| {
format!(
"Required bridge '{}' for instance '{}' missing",
handle, instance.id
)
})?;
}
}
}
}
for ref interface in self.interfaces.iter() {
for ref instance in interface.instances.iter() {
self.instance_by_id(&instance.id).is_some().ok_or_else(|| {
format!(
"Instance configuration \"{}\" not found, mentioned in interface",
instance.id
)
})?;
}
}
for bridge in self.bridges.iter() {
self.check_bridge_requirements(bridge, dna_loader)?;
}
for ref ui_interface in self.ui_interfaces.iter() {
self.ui_bundle_by_id(&ui_interface.bundle)
.is_some()
.ok_or_else(|| {
format!(
"UI bundle configuration {} not found, mentioned in UI interface {}",
ui_interface.bundle, ui_interface.id,
)
})?;
if let Some(ref dna_interface_id) = ui_interface.dna_interface {
self.interface_by_id(&dna_interface_id)
.is_some()
.ok_or_else(|| {
format!(
"DNA Interface configuration \"{}\" not found, mentioned in UI interface \"{}\"",
dna_interface_id, ui_interface.id,
)
})?;
}
}
if let Some(ref dpki_config) = self.dpki {
self.instance_by_id(&dpki_config.instance_id)
.is_some()
.ok_or_else(|| {
format!(
"Instance configuration \"{}\" not found, mentioned in dpki",
dpki_config.instance_id
)
})?;
}
let _ = self.instance_ids_sorted_by_bridge_dependencies()?;
#[cfg(not(unix))]
{
if let PassphraseServiceConfig::UnixSocket { path } = self.passphrase_service.clone() {
let _ = path;
return Err(String::from(
"Passphrase service type 'unixsocket' is not available on non-Unix systems",
));
}
}
Ok(())
}
fn check_bridge_requirements(
&self,
bridge_config: &Bridge,
mut dna_loader: &mut DnaLoader,
) -> Result<(), String> {
let caller_config = self
.instance_by_id(&bridge_config.caller_id)
.ok_or_else(|| {
format!(
"Instance configuration \"{}\" not found, mentioned in bridge",
bridge_config.caller_id
)
})?;
let caller_dna_config = self.dna_by_id(&caller_config.dna).ok_or_else(|| {
format!(
"DNA configuration \"{}\" not found, mentioned in instance \"{}\"",
caller_config.dna, caller_config.id
)
})?;
let caller_dna_file = caller_dna_config.file;
let caller_dna =
Arc::get_mut(&mut dna_loader).unwrap()(&PathBuf::from(caller_dna_file.clone()))
.map_err(|err| {
format!(
"Could not load DNA file \"{}\"; error was: {}",
caller_dna_file, err
)
})?;
let callee_config = self
.instance_by_id(&bridge_config.callee_id)
.ok_or_else(|| {
format!(
"Instance configuration \"{}\" not found, mentioned in bridge",
bridge_config.callee_id
)
})?;
let callee_dna_config = self.dna_by_id(&callee_config.dna).ok_or_else(|| {
format!(
"DNA configuration \"{}\" not found, mentioned in instance \"{}\"",
callee_config.dna, callee_config.id
)
})?;
let callee_dna_file = callee_dna_config.file;
let callee_dna =
Arc::get_mut(&mut dna_loader).unwrap()(&PathBuf::from(callee_dna_file.clone()))
.map_err(|err| {
format!(
"Could not load DNA file \"{}\"; error was: {}",
callee_dna_file, err
)
})?;
let mut maybe_bridge = None;
for zome in caller_dna.zomes.values() {
for bridge_def in zome.bridges.iter() {
if bridge_def.handle == bridge_config.handle {
maybe_bridge = Some(bridge_def.clone());
}
}
}
let bridge = maybe_bridge.ok_or_else(|| {
format!(
"No bridge definition with handle '{}' found in {}'s DNA",
bridge_config.handle, bridge_config.caller_id,
)
})?;
match bridge.reference {
BridgeReference::Address { ref dna_address } => {
if *dna_address != callee_dna.address() {
return Err(format!(
"Bridge '{}' of caller instance '{}' requires callee to be DNA with hash '{}', but the configured instance '{}' runs DNA with hash '{}'.",
bridge.handle,
bridge_config.caller_id,
dna_address,
callee_config.id,
callee_dna.address(),
));
}
}
BridgeReference::Trait { ref traits } => {
for (expected_trait_name, expected_trait) in traits {
let mut found = false;
for (_zome_name, zome) in callee_dna.zomes.iter() {
for (zome_trait_name, zome_trait_functions) in zome.traits.iter() {
if zome_trait_name == expected_trait_name {
let mut has_all_fns_exported = true;
for fn_def in expected_trait.functions.iter() {
if !zome_trait_functions.functions.contains(&fn_def.name) {
has_all_fns_exported = false;
}
}
let mut has_matching_signatures = true;
if has_all_fns_exported {
for fn_def in expected_trait.functions.iter() {
if !zome.fn_declarations.contains(&fn_def) {
has_matching_signatures = false;
}
}
}
if has_all_fns_exported && has_matching_signatures {
found = true;
}
}
}
}
if !found {
return Err(format!(
"Bridge '{}' of instance '{}' requires callee to to implement trait '{}' with functions: {:?}",
bridge.handle,
bridge_config.caller_id,
expected_trait_name,
expected_trait.functions,
));
}
}
}
};
Ok(())
}
pub fn agent_by_id(&self, id: &str) -> Option<AgentConfiguration> {
self.agents.iter().find(|ac| ac.id == id).cloned()
}
pub fn update_agent_address_by_id(&mut self, id: &str, agent_id: &AgentId) {
self.agents.iter_mut().for_each(|ac| {
if ac.id == id {
ac.public_address = agent_id.pub_sign_key.clone()
}
})
}
pub fn dna_by_id(&self, id: &str) -> Option<DnaConfiguration> {
self.dnas.iter().find(|dc| dc.id == id).cloned()
}
pub fn update_dna_hash_by_id(&mut self, id: &str, hash: String) -> bool {
self.dnas
.iter_mut()
.find(|dc| dc.id == id)
.map(|dna| dna.hash = hash)
.is_some()
}
pub fn instance_by_id(&self, id: &str) -> Option<InstanceConfiguration> {
self.instances.iter().find(|ic| ic.id == id).cloned()
}
pub fn interface_by_id(&self, id: &str) -> Option<InterfaceConfiguration> {
self.interfaces.iter().find(|ic| ic.id == id).cloned()
}
pub fn ui_bundle_by_id(&self, id: &str) -> Option<UiBundleConfiguration> {
self.ui_bundles.iter().find(|ic| ic.id == id).cloned()
}
pub fn instance_ids(&self) -> Vec<String> {
self.instances
.iter()
.map(|instance| instance.id.clone())
.collect()
}
pub fn instance_ids_sorted_by_bridge_dependencies(
&self,
) -> Result<Vec<String>, HolochainError> {
let mut graph = DiGraph::<&str, &str>::new();
let index_map: HashMap<_, _> = self
.instances
.iter()
.map(|instance| (instance.id.clone(), graph.add_node(&instance.id)))
.collect();
let reverse_map: HashMap<_, _> = self
.instances
.iter()
.map(|instance| (index_map.get(&instance.id).unwrap(), instance.id.clone()))
.collect();
let edges: Vec<(&NodeIndex<u32>, &NodeIndex<u32>)> = self.bridges
.iter()
.map(|bridge| -> Result<(&NodeIndex<u32>, &NodeIndex<u32>), HolochainError> {
let start = index_map.get(&bridge.caller_id);
let end = index_map.get(&bridge.callee_id);
if let (Some(start_inner), Some(end_inner)) = (start, end) {
Ok((start_inner, end_inner))
}
else {
Err(HolochainError::ConfigError(format!(
"Instance configuration not found, mentioned in bridge configuration: {} -> {}",
bridge.caller_id, bridge.callee_id,
)))
}
})
.collect::<Result<Vec<_>, _>>()?;
for &(node_a, node_b) in edges.iter() {
graph.add_edge(node_a.clone(), node_b.clone(), "");
}
let mut sorted_nodes = toposort(&graph, None).map_err(|_cycle_error| {
HolochainError::ConfigError("Cyclic dependency in bridge configuration".to_string())
})?;
sorted_nodes.reverse();
Ok(sorted_nodes
.iter()
.map(|node_index| reverse_map.get(node_index).unwrap())
.cloned()
.collect())
}
pub fn bridge_dependencies(&self, caller_instance_id: String) -> Vec<Bridge> {
self.bridges
.iter()
.filter(|bridge| bridge.caller_id == caller_instance_id)
.cloned()
.collect()
}
pub fn save_remove_instance(mut self, id: &String) -> Self {
self.instances = self
.instances
.into_iter()
.filter(|instance| instance.id != *id)
.collect();
self.interfaces = self
.interfaces
.into_iter()
.map(|mut interface| {
interface.instances = interface
.instances
.into_iter()
.filter(|instance| instance.id != *id)
.collect();
interface
})
.collect();
self
}
fn check_instances_storage(&self) -> Result<(), String> {
let storage_paths: Vec<&str> = self
.instances
.iter()
.filter_map(|stg_config| match stg_config.storage {
StorageConfiguration::File { ref path }
| StorageConfiguration::Lmdb { ref path, .. }
| StorageConfiguration::Pickle { ref path } => Some(path.as_str()),
_ => None,
})
.collect();
let mut path_set: HashSet<&str> = HashSet::new();
let has_uniq_values = storage_paths.iter().all(|&x| path_set.insert(x));
if !has_uniq_values {
Err(String::from(
"Forbidden duplicated file storage value encountered.",
))
} else {
Ok(())
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct AgentConfiguration {
pub id: String,
pub name: String,
pub public_address: Base32,
pub keystore_file: String,
pub holo_remote_key: Option<bool>,
pub test_agent: Option<bool>,
}
impl From<AgentConfiguration> for AgentId {
fn from(config: AgentConfiguration) -> Self {
AgentId::try_from(JsonString::from_json(&config.id)).expect("bad agent json")
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct DnaConfiguration {
pub id: String,
pub file: String,
pub hash: String,
#[serde(default)]
pub uuid: Option<String>,
}
impl TryFrom<DnaConfiguration> for Dna {
type Error = HolochainError;
fn try_from(dna_config: DnaConfiguration) -> Result<Self, Self::Error> {
let mut f = File::open(dna_config.file)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
Dna::try_from(JsonString::from_json(&contents)).map_err(|err| err.into())
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct InstanceConfiguration {
pub id: String,
pub dna: String,
pub agent: String,
pub storage: StorageConfiguration,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum StorageConfiguration {
Memory,
File {
path: String,
},
Pickle {
path: String,
},
Lmdb {
path: String,
initial_mmap_bytes: Option<usize>,
},
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct InterfaceConfiguration {
pub id: String,
pub driver: InterfaceDriver,
#[serde(default)]
pub admin: bool,
#[serde(default)]
pub instances: Vec<InstanceReferenceConfiguration>,
pub choose_free_port: Option<bool>,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum InterfaceDriver {
Websocket { port: u16 },
Http { port: u16 },
DomainSocket { file: String },
Custom(toml::value::Value),
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct InstanceReferenceConfiguration {
pub id: String,
pub alias: Option<String>,
}
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
pub struct Bridge {
pub caller_id: String,
pub callee_id: String,
pub handle: String,
}
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
pub struct UiBundleConfiguration {
pub id: String,
pub root_dir: String,
#[serde(default)]
pub hash: Option<String>,
}
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
pub struct UiInterfaceConfiguration {
pub id: String,
pub bundle: String,
pub port: u16,
#[serde(default)]
pub dna_interface: Option<String>,
#[serde(default = "default_reroute")]
pub reroute_to_root: bool,
#[serde(default = "default_address")]
pub bind_address: String,
}
fn default_reroute() -> bool {
true
}
fn default_address() -> String {
Ipv4Addr::LOCALHOST.to_string()
}
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "lowercase")]
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum NetworkConfig {
Lib3h(EngineConfig),
Memory(EngineConfig),
Sim2h(Sim2hConfig),
}
pub fn load_configuration<'a, T>(toml: &'a str) -> HcResult<T>
where
T: Deserialize<'a>,
{
toml::from_str::<T>(toml).map_err(|e| {
HolochainError::IoError(format!("Error loading configuration: {}", e.to_string()))
})
}
pub fn serialize_configuration(config: &Configuration) -> HcResult<String> {
let config_toml = toml::Value::try_from(config).map_err(|e| {
HolochainError::IoError(format!(
"Could not serialize configuration: {}",
e.to_string()
))
})?;
toml::to_string_pretty(&config_toml).map_err(|e| {
HolochainError::IoError(format!(
"Could not convert toml to string: {}",
e.to_string()
))
})
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct DpkiConfiguration {
pub instance_id: String,
pub init_params: String,
}
#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)]
pub struct SignalConfig {
pub trace: bool,
pub consistency: bool,
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::{
conductor::tests::test_dna_loader,
config::{load_configuration, Configuration, NetworkConfig},
};
use holochain_net::p2p_config::P2pConfig;
pub fn example_serialized_network_config() -> String {
String::from(JsonString::from(P2pConfig::new_with_unique_memory_backend()))
}
#[test]
fn test_agent_load() {
let toml = r#"
[[agents]]
id = "bob"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "file/to/serialize"
[[agents]]
id="alex"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "another/file"
[[dnas]]
id="dna"
file="file.dna.json"
hash="QmDontCare"
"#;
let agents = load_configuration::<Configuration>(toml).unwrap().agents;
assert_eq!(agents.get(0).expect("expected at least 2 agents").id, "bob");
assert_eq!(
agents
.get(0)
.expect("expected at least 2 agents")
.clone()
.keystore_file,
"file/to/serialize"
);
assert_eq!(
agents.get(1).expect("expected at least 2 agents").id,
"alex"
);
}
#[test]
fn test_dna_load() {
let toml = r#"
[[agents]]
id="agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "whatever"
[[dnas]]
id = "app spec rust"
file = "app_spec.dna.json"
hash = "Qm328wyq38924y"
"#;
let dnas = load_configuration::<Configuration>(toml).unwrap().dnas;
let dna_config = dnas.get(0).expect("expected at least 1 DNA");
assert_eq!(dna_config.id, "app spec rust");
assert_eq!(dna_config.file, "app_spec.dna.json");
assert_eq!(dna_config.hash, "Qm328wyq38924y".to_string());
}
#[test]
fn test_load_complete_config() {
let toml = r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "app spec rust"
file = "app_spec.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "app spec instance"
dna = "app spec rust"
agent = "test agent"
[instances.storage]
type = "file"
path = "app_spec_storage"
[[interfaces]]
id = "app spec websocket interface"
[interfaces.driver]
type = "websocket"
port = 8888
[[interfaces.instances]]
id = "app spec instance"
[[interfaces]]
id = "app spec http interface"
[interfaces.driver]
type = "http"
port = 4000
[[interfaces.instances]]
id = "app spec instance"
[[interfaces]]
id = "app spec domainsocket interface"
[interfaces.driver]
type = "domainsocket"
file = "/tmp/holochain.sock"
[[interfaces.instances]]
id = "app spec instance"
[network]
type = "sim2h"
sim2h_url = "test_sim2h_url"
[metric_publisher]
type = "cloudwatchlogs"
log_stream_name = "2019-11-22_20-53-31.sim2h_public"
log_group_name = "holochain"
"#;
let config = load_configuration::<Configuration>(toml).unwrap();
assert_eq!(config.check_consistency(&mut test_dna_loader()), Ok(()));
let dnas = config.dnas;
let dna_config = dnas.get(0).expect("expected at least 1 DNA");
assert_eq!(dna_config.id, "app spec rust");
assert_eq!(dna_config.file, "app_spec.dna.json");
assert_eq!(dna_config.hash, "Qm328wyq38924y".to_string());
let instances = config.instances;
let instance_config = instances.get(0).unwrap();
assert_eq!(instance_config.id, "app spec instance");
assert_eq!(instance_config.dna, "app spec rust");
assert_eq!(instance_config.agent, "test agent");
assert_eq!(config.logger.logger_level, "debug");
assert_eq!(format!("{:?}", config.metric_publisher), "Some(CloudWatchLogs(CloudWatchLogsConfig { region: None, log_group_name: Some(\"holochain\"), log_stream_name: Some(\"2019-11-22_20-53-31.sim2h_public\"), assume_role_arn: None }))");
assert_eq!(
config.network.unwrap(),
NetworkConfig::Sim2h(Sim2hConfig {
sim2h_url: "test_sim2h_url".to_string(),
})
);
}
#[test]
fn test_load_complete_config_default_network() {
let toml = r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "app spec rust"
file = "app_spec.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "app spec instance"
dna = "app spec rust"
agent = "test agent"
[instances.storage]
type = "file"
path = "app_spec_storage"
[[interfaces]]
id = "app spec websocket interface"
[interfaces.driver]
type = "websocket"
port = 8888
[[interfaces.instances]]
id = "app spec instance"
[[interfaces]]
id = "app spec http interface"
[interfaces.driver]
type = "http"
port = 4000
[[interfaces.instances]]
id = "app spec instance"
[[interfaces]]
id = "app spec domainsocket interface"
[interfaces.driver]
type = "domainsocket"
file = "/tmp/holochain.sock"
[[interfaces.instances]]
id = "app spec instance"
[logger]
type = "debug"
[[logger.rules.rules]]
pattern = ".*"
color = "red"
[[ui_bundles]]
id = "bundle1"
root_dir = "" # serves the current directory
hash = "Qm000"
[[ui_interfaces]]
id = "ui-interface-1"
bundle = "bundle1"
port = 3000
dna_interface = "app spec domainsocket interface"
"#;
let config = load_configuration::<Configuration>(toml).unwrap();
assert_eq!(config.check_consistency(&mut test_dna_loader()), Ok(()));
let dnas = config.dnas;
let dna_config = dnas.get(0).expect("expected at least 1 DNA");
assert_eq!(dna_config.id, "app spec rust");
assert_eq!(dna_config.file, "app_spec.dna.json");
assert_eq!(dna_config.hash, "Qm328wyq38924y".to_string());
let instances = config.instances;
let instance_config = instances.get(0).unwrap();
assert_eq!(instance_config.id, "app spec instance");
assert_eq!(instance_config.dna, "app spec rust");
assert_eq!(instance_config.agent, "test agent");
assert_eq!(config.logger.logger_level, "debug");
assert_eq!(config.logger.rules.rules.len(), 1);
assert_eq!(config.network, None);
}
#[test]
fn test_load_bad_network_config() {
let base_toml = r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "app spec rust"
file = "app_spec.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "app spec instance"
dna = "app spec rust"
agent = "test agent"
[instances.storage]
type = "file"
path = "app_spec_storage"
[[interfaces]]
id = "app spec websocket interface"
[interfaces.driver]
type = "websocket"
port = 8888
[[interfaces.instances]]
id = "app spec instance"
"#;
let toml = format!(
"{}{}",
base_toml,
r#"
[network]
type = "lib3h"
"#
);
if let Err(e) = load_configuration::<Configuration>(toml.as_str()) {
assert!(
true,
e.to_string().contains(
"Error loading configuration: missing field `socket_type` for key `network`"
)
)
} else {
panic!("Should have failed!")
}
}
#[test]
fn test_inconsistent_config() {
let toml = r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "app spec rust"
file = "app_spec.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "app spec instance"
dna = "WRONG DNA ID"
agent = "test agent"
[instances.storage]
type = "file"
path = "app_spec_storage"
"#;
let config: Configuration =
load_configuration(toml).expect("Failed to load config from toml string");
assert_eq!(config.check_consistency(&mut test_dna_loader()), Err("DNA configuration \"WRONG DNA ID\" not found, mentioned in instance \"app spec instance\"".to_string()));
}
#[test]
fn test_inconsistent_config_interface_1() {
let toml = r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "app spec rust"
file = "app_spec.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "app spec instance"
dna = "app spec rust"
agent = "test agent"
[instances.storage]
type = "file"
path = "app_spec_storage"
[[interfaces]]
id = "app spec interface"
[interfaces.driver]
type = "websocket"
port = 8888
[[interfaces.instances]]
id = "WRONG INSTANCE ID"
"#;
let config = load_configuration::<Configuration>(toml).unwrap();
assert_eq!(
config.check_consistency(&mut test_dna_loader()),
Err(
"Instance configuration \"WRONG INSTANCE ID\" not found, mentioned in interface"
.to_string()
)
);
}
#[test]
fn test_invalid_toml_1() {
let toml = &format!(
r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "app spec rust"
file = "app-spec-rust.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "app spec instance"
dna = "app spec rust"
agent = "test agent"
network = "{}"
[instances.storage]
type = "file"
path = "app_spec_storage"
[[interfaces]]
id = "app spec interface"
[interfaces.driver]
type = "invalid type"
port = 8888
[[interfaces.instances]]
id = "app spec instance"
"#,
example_serialized_network_config()
);
if let Err(e) = load_configuration::<Configuration>(toml) {
assert!(
true,
e.to_string().contains("unknown variant `invalid type`")
)
} else {
panic!("Should have failed!")
}
}
fn bridges_config(bridges: &str) -> String {
format!(
r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "bridge caller"
file = "bridge/caller_without_required.dna"
hash = "Qm328wyq38924y"
[[instances]]
id = "app1"
dna = "bridge caller"
agent = "test agent"
[instances.storage]
type = "file"
path = "app1_spec_storage"
[[instances]]
id = "app2"
dna = "bridge caller"
agent = "test agent"
[instances.storage]
type = "file"
path = "app2_spec_storage"
[[instances]]
id = "app3"
dna = "bridge caller"
agent = "test agent"
[instances.storage]
type = "file"
path = "app3_spec_storage"
{}
"#,
bridges
)
}
#[test]
fn test_bridge_config() {
let toml = bridges_config(
r#"
[[bridges]]
caller_id = "app1"
callee_id = "app2"
handle = "happ-store"
[[bridges]]
caller_id = "app2"
callee_id = "app3"
handle = "DPKI"
"#,
);
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
assert_eq!(config.check_consistency(&mut test_dna_loader()), Ok(()));
assert_eq!(
config.instance_ids_sorted_by_bridge_dependencies(),
Ok(vec![
String::from("app3"),
String::from("app2"),
String::from("app1")
])
);
}
#[test]
fn test_bridge_cycle() {
let toml = bridges_config(
r#"
[[bridges]]
caller_id = "app1"
callee_id = "app2"
handle = "happ-store"
[[bridges]]
caller_id = "app2"
callee_id = "app3"
handle = "DPKI"
[[bridges]]
caller_id = "app3"
callee_id = "app1"
handle = "test-callee"
"#,
);
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
assert_eq!(
config.check_consistency(&mut test_dna_loader()),
Err("Cyclic dependency in bridge configuration".to_string())
);
}
#[test]
fn test_bridge_non_existent() {
let toml = bridges_config(
r#"
[[bridges]]
caller_id = "app1"
callee_id = "app2"
handle = "happ-store"
[[bridges]]
caller_id = "app2"
callee_id = "app3"
handle = "DPKI"
[[bridges]]
caller_id = "app9000"
callee_id = "app1"
handle = "something"
"#,
);
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
assert_eq!(
config.check_consistency(&mut test_dna_loader()),
Err("Instance configuration \"app9000\" not found, mentioned in bridge".to_string())
);
}
#[test]
fn test_bridge_dependencies() {
let toml = bridges_config(
r#"
[[bridges]]
caller_id = "app1"
callee_id = "app2"
handle = "happ-store"
[[bridges]]
caller_id = "app1"
callee_id = "app3"
handle = "happ-store"
[[bridges]]
caller_id = "app2"
callee_id = "app1"
handle = "happ-store"
"#,
);
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
let bridged_ids: Vec<_> = config
.bridge_dependencies(String::from("app1"))
.iter()
.map(|bridge| bridge.callee_id.clone())
.collect();
assert_eq!(
bridged_ids,
vec![String::from("app2"), String::from("app3"),]
);
}
#[test]
fn test_inconsistent_ui_interface() {
let toml = r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "app spec rust"
file = "app_spec.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "app spec instance"
dna = "app spec rust"
agent = "test agent"
[instances.storage]
type = "file"
path = "app_spec_storage"
[[interfaces]]
id = "app spec websocket interface"
[interfaces.driver]
type = "websocket"
port = 8888
[[interfaces.instances]]
id = "app spec instance"
[[interfaces]]
id = "app spec http interface"
[interfaces.driver]
type = "http"
port = 4000
[[interfaces.instances]]
id = "app spec instance"
[[interfaces]]
id = "app spec domainsocket interface"
[interfaces.driver]
type = "domainsocket"
file = "/tmp/holochain.sock"
[[interfaces.instances]]
id = "app spec instance"
[logger]
type = "debug"
[[logger.rules.rules]]
pattern = ".*"
color = "red"
[[ui_bundles]]
id = "bundle1"
root_dir = "" # serves the current directory
hash = "Qm000"
[[ui_interfaces]]
id = "ui-interface-1"
bundle = "bundle1"
port = 3000
dna_interface = "<not existant>"
"#;
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
assert_eq!(
config.check_consistency(&mut test_dna_loader()),
Err("DNA Interface configuration \"<not existant>\" not found, mentioned in UI interface \"ui-interface-1\"".to_string())
);
}
#[test]
fn test_inconsistent_dpki() {
let toml = r#"
[[agents]]
id = "test agent"
name = "Holo Tester 1"
public_address = "HoloTester1-------------------------------------------------------------------------AHi1"
keystore_file = "holo_tester.key"
[[dnas]]
id = "deepkey"
file = "deepkey.dna.json"
hash = "Qm328wyq38924y"
[[instances]]
id = "deepkey"
dna = "deepkey"
agent = "test agent"
[instances.storage]
type = "file"
path = "deepkey_storage"
[dpki]
instance_id = "bogus instance"
init_params = "{}"
"#;
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
assert_eq!(
config.check_consistency(&mut test_dna_loader()),
Err(
"Instance configuration \"bogus instance\" not found, mentioned in dpki"
.to_string()
)
);
}
#[test]
fn test_check_instances_storage() -> Result<(), String> {
let toml = r#"
[[agents]]
id = "test agent 1"
keystore_file = "holo_tester.key"
name = "Holo Tester 1"
public_address = "HoloTester1-----------------------------------------------------------------------AAACZp4xHB"
[[agents]]
id = "test agent 2"
keystore_file = "holo_tester.key"
name = "Holo Tester 2"
public_address = "HoloTester2-----------------------------------------------------------------------AAAGy4WW9e"
[[instances]]
agent = "test agent 1"
dna = "app spec rust"
id = "app spec instance 1"
[instances.storage]
path = "example-config/tmp-storage-1"
type = "file"
[[instances]]
agent = "test agent 2"
dna = "app spec rust"
id = "app spec instance 2"
[instances.storage]
path = "example-config/tmp-storage-2"
type = "file"
"#;
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
assert_eq!(config.check_instances_storage(), Ok(()));
Ok(())
}
#[test]
fn test_check_instances_storage_err() -> Result<(), String> {
let toml = r#"
[[agents]]
id = "test agent 1"
keystore_file = "holo_tester.key"
name = "Holo Tester 1"
public_address = "HoloTester1-----------------------------------------------------------------------AAACZp4xHB"
[[instances]]
agent = "test agent 1"
dna = "app spec rust"
id = "app spec instance 1"
[instances.storage]
path = "forbidden-duplicated-storage-file-path"
type = "file"
[[instances]]
agent = "test agent 2"
dna = "app spec rust"
id = "app spec instance 2"
[instances.storage]
path = "forbidden-duplicated-storage-file-path"
type = "file"
"#;
let config = load_configuration::<Configuration>(&toml)
.expect("Config should be syntactically correct");
assert_eq!(
config.check_instances_storage(),
Err(String::from(
"Forbidden duplicated file storage value encountered."
))
);
Ok(())
}
}