Skip to content
This repository has been archived by the owner on Feb 3, 2023. It is now read-only.

Run hApp-bundles during develpment with hc run #1939

Merged
merged 24 commits into from
Dec 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ce6fb4a
WIP added HappBundle struct and conversion to conductor Configuration
lucksus Dec 3, 2019
fe128cd
rustfmt
lucksus Dec 3, 2019
91409e5
WIP loading bundle file
lucksus Dec 3, 2019
b6fbdfa
Merge branch 'develop' into hc-run-bundles
lucksus Dec 4, 2019
056bccf
Fix build
lucksus Dec 4, 2019
13fb9d4
Fix typos and property names to be able to read existing bundle files
lucksus Dec 4, 2019
f9c1714
From -> TryFrom and run checks
lucksus Dec 4, 2019
01f2afe
Make HappBundleUi::id optional
lucksus Dec 4, 2019
cd92743
Only check for "file:" without slashes since "file:./something" is al…
lucksus Dec 4, 2019
0929aac
Actually run conductor with config from bundle
lucksus Dec 4, 2019
306d2db
Start static web servers on hc run
lucksus Dec 4, 2019
9b41188
Don't rerout to root as it breaks the _dna_connections.js route
lucksus Dec 4, 2019
8337b27
Use provided port for bundles too in hc run
lucksus Dec 4, 2019
41bd0a6
Move HappBundle into conductor_lib
lucksus Dec 4, 2019
3380b1a
Extract port util funcrtions into own file
lucksus Dec 4, 2019
b931106
rustfmt
lucksus Dec 4, 2019
43657fb
Get free ports for interfaces and static UI servers in hc run bundle
lucksus Dec 4, 2019
b96ecb1
rustfmt
lucksus Dec 4, 2019
2ba2125
Wire up persist flag
lucksus Dec 4, 2019
b19b36d
Remove unused interface_type from bundle case
lucksus Dec 4, 2019
01d8467
rustfmt
lucksus Dec 4, 2019
3cbb859
Merge branch 'develop' into hc-run-bundles
lucksus Dec 4, 2019
636d3fe
clippy
lucksus Dec 4, 2019
e2d7984
changelog
lucksus Dec 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- Adds smarter ordering of pending validation. Builds a dependency graph and only will try to validate entries that do not have dependencies also awaiting validation as this will always fail. [#1924](https://github.com/holochain/holochain-rust/pull/1924)
- Adds support for [hApp-bundles](https://github.com/holochain/holoscape/tree/master/example-bundles) to `hc run`. This enables complex hApp setups with multiple DNAs and bridges to be easily run during development without having to write/maintain a conductor config file. [#1939](https://github.com/holochain/holochain-rust/pull/1939)

### Changed

Expand Down
4 changes: 2 additions & 2 deletions crates/cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod hash_dna;
mod init;
mod keygen;
pub mod package;
mod run;
pub mod run;
pub mod test;

pub use self::{
Expand All @@ -14,6 +14,6 @@ pub use self::{
init::init,
keygen::keygen,
package::package,
run::{get_interface_type_string, hc_run_configuration, run},
run::{get_interface_type_string, hc_run_bundle_configuration, hc_run_configuration, run},
test::{test, TEST_DIR_NAME},
};
22 changes: 22 additions & 0 deletions crates/cli/src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use holochain_common::env_vars::EnvVar;
use holochain_conductor_lib::{
conductor::{mount_conductor_from_config, Conductor, CONDUCTOR},
config::*,
happ_bundle::HappBundle,
key_loaders::{test_keystore, test_keystore_loader},
keystore::PRIMARY_KEYBUNDLE_ID,
logger::LogRules,
Expand Down Expand Up @@ -36,6 +37,9 @@ pub fn run(

conductor.start_all_interfaces();
conductor.start_all_instances()?;
conductor
.start_all_static_servers()
.map_err(|e| failure::err_msg(e))?;

println!(
"Holochain development conductor started. Running {} server on port {}",
Expand Down Expand Up @@ -88,6 +92,24 @@ pub fn hc_run_configuration(
})
}

pub fn hc_run_bundle_configuration(
bundle: &HappBundle,
port: u16,
persist: bool,
networked: bool,
logging: bool,
) -> DefaultResult<Configuration> {
bundle
.build_conductor_config(
port,
agent_configuration(),
storage_configuration(persist)?,
networking_configuration(networked),
logger_configuration(logging),
)
.map_err(|e| failure::err_msg(e))
}

// AGENT
const AGENT_NAME_DEFAULT: &str = "testAgent";
const AGENT_CONFIG_ID: &str = "hc-run-agent";
Expand Down
40 changes: 30 additions & 10 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ mod error;
mod util;

use crate::error::{HolochainError, HolochainResult};
use std::{path::PathBuf, str::FromStr};
use holochain_conductor_lib::happ_bundle::HappBundle;
use std::{fs::File, io::Read, path::PathBuf, str::FromStr};
use structopt::StructOpt;

#[derive(StructOpt)]
Expand Down Expand Up @@ -203,15 +204,34 @@ fn run() -> HolochainResult<()> {
let dna_path = dna_path
.unwrap_or(util::std_package_path(&project_path).map_err(HolochainError::Default)?);
let interface_type = cli::get_interface_type_string(interface);
let conductor_config = cli::hc_run_configuration(
&dna_path,
port,
persist,
networked,
&interface_type,
logging,
)
.map_err(HolochainError::Default)?;

let bundle_path = project_path.join("bundle.toml");
let conductor_config = if bundle_path.exists() {
let mut f = File::open(bundle_path)
.map_err(|e| HolochainError::Default(format_err!("{}", e)))?;
let mut contents = String::new();
f.read_to_string(&mut contents)
.map_err(|e| HolochainError::Default(format_err!("{}", e)))?;
let happ_bundle =
toml::from_str::<HappBundle>(&contents).expect("Error loading bundle.");

cli::hc_run_bundle_configuration(&happ_bundle, port, persist, networked, logging)
.map_err(HolochainError::Default)?
} else {
cli::hc_run_configuration(
&dna_path,
port,
persist,
networked,
&interface_type,
logging,
)
.map_err(HolochainError::Default)?
};
println!(
"Booting conductor with following configuration: {:?}",
conductor_config
);
cli::run(dna_path, package, port, interface_type, conductor_config)
.map_err(HolochainError::Default)?
}
Expand Down
25 changes: 1 addition & 24 deletions crates/conductor_lib/src/conductor/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
error::HolochainInstanceError,
key_loaders::test_keystore,
keystore::{Keystore, PRIMARY_KEYBUNDLE_ID},
port_utils::{try_with_port, INTERFACE_CONNECT_ATTEMPTS_MAX},
Holochain,
};
use crossbeam_channel::{unbounded, Receiver, Sender};
Expand Down Expand Up @@ -61,9 +62,6 @@ use holochain_net::{
p2p_network::P2pNetwork,
};

const INTERFACE_CONNECT_ATTEMPTS_MAX: usize = 30;
const INTERFACE_CONNECT_INTERVAL: Duration = Duration::from_secs(1);

lazy_static! {
/// This is a global and mutable Conductor singleton.
/// (Ok, not really. I've made Conductor::from_config public again so holochain_nodejs
Expand Down Expand Up @@ -1416,27 +1414,6 @@ impl Conductor {
}
}

fn try_with_port<T, F: FnOnce() -> T>(port: u16, f: F) -> T {
let mut attempts = 0;
while attempts <= INTERFACE_CONNECT_ATTEMPTS_MAX {
if port_is_available(port) {
return f();
}
warn!(
"Waiting for port {} to be available, sleeping (attempt #{})",
port, attempts
);
thread::sleep(INTERFACE_CONNECT_INTERVAL);
attempts += 1;
}
f()
}

fn port_is_available(port: u16) -> bool {
use std::net::TcpListener;
TcpListener::bind(format!("0.0.0.0:{}", port)).is_ok()
}

/// This can eventually be dependency injected for third party Interface definitions
fn _make_interface(interface_config: &InterfaceConfiguration) -> Box<dyn Interface> {
use crate::interface_impls::{http::HttpInterface, websocket::WebsocketInterface};
Expand Down
189 changes: 189 additions & 0 deletions crates/conductor_lib/src/happ_bundle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use crate::{config::*, port_utils::get_free_port};
use boolinator::Boolinator;
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
pub struct HappBundle {
pub instances: Vec<HappBundleInstance>,
pub bridges: Vec<Bridge>,
#[serde(rename = "UIs")]
pub uis: Vec<HappBundleUi>,
}

#[derive(Serialize, Deserialize)]
pub struct HappBundleInstance {
pub name: String,
pub id: String,
pub dna_hash: String,
pub uri: String,
pub dna_properties: Option<HashMap<String, String>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct HappBundleUi {
pub name: String,
pub id: Option<String>,
pub uri: String,
pub instance_references: Vec<HappBundleInstanceReference>,
}

impl HappBundleUi {
pub fn id(&self) -> String {
self.id.clone().unwrap_or_else(|| String::from(""))
}
}

#[derive(Serialize, Deserialize, Debug)]
pub struct HappBundleInstanceReference {
pub ui_handle: String,
pub instance_id: String,
}

impl HappBundle {
pub fn id_references_are_consistent(&self) -> Result<(), String> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. It would be really easy to wasm-bindgen this integrity check code for holoscape so it isn't duped in Rust and Javascript

for bridge in self.bridges.iter() {
for id in vec![bridge.callee_id.clone(), bridge.caller_id.clone()] {
self.instances.iter().find(|i| i.id == id).ok_or_else(|| {
format!(
"No instance with ID {} referenced in bridge {:?}",
id, bridge
)
})?;
}
}

for ui in self.uis.iter() {
for reference in ui.instance_references.iter() {
self.instances
.iter()
.find(|i| i.id == reference.instance_id)
.ok_or_else(|| {
format!(
"No instance with ID {} referenced in UI {:?}",
reference.instance_id, ui
)
})?;
}
}
Ok(())
}

pub fn only_file_uris(&self) -> Result<(), String> {
for instance in self.instances.iter() {
instance.uri.starts_with("file:").ok_or_else(|| {
format!(
"Instance {} uses non-file URI which is not supported in `hc run`",
instance.id
)
})?;
}

for ui in self.uis.iter() {
ui.uri.starts_with("dir:").ok_or_else(|| {
format!(
"UI {} uses non-dir URI which is not supported in `hc run`",
ui.id()
)
})?;
}

Ok(())
}

pub fn build_conductor_config(
&self,
ui_port: u16,
agent_config: AgentConfiguration,
storage: StorageConfiguration,
network: Option<NetworkConfig>,
logger: LoggerConfiguration,
) -> Result<Configuration, String> {
self.id_references_are_consistent()?;
self.only_file_uris()?;

let dnas = self
.instances
.iter()
.map(|happ_instance| {
// splitting off "file://"
let file = happ_instance.uri.clone().split_off(5);
DnaConfiguration {
id: happ_instance.id.clone(),
file,
hash: happ_instance.dna_hash.clone(),
uuid: None,
}
})
.collect::<Vec<_>>();

let instances = self
.instances
.iter()
.map(|happ_instance| InstanceConfiguration {
id: happ_instance.id.clone(),
dna: happ_instance.id.clone(),
agent: agent_config.id.clone(),
storage: storage.clone(),
})
.collect::<Vec<_>>();

let mut interfaces = Vec::new();
let mut ui_bundles = Vec::new();
let mut ui_interfaces = Vec::new();

const MIN_INTERFACE_PORT: u16 = 50000;
const MAX_INTERFACE_PORT: u16 = 60000;
let mut next_interface_port: u16 = MIN_INTERFACE_PORT;
let mut next_ui_port = ui_port;

for ui in self.uis.iter() {
let port = get_free_port(next_interface_port..MAX_INTERFACE_PORT)
.ok_or_else(|| String::from("Couldn't acquire free port"))?;
next_interface_port = port + 1;
interfaces.push(InterfaceConfiguration {
id: ui.id(),
driver: InterfaceDriver::Websocket { port },
admin: false,
instances: ui
.instance_references
.iter()
.map(|ui_ref| InstanceReferenceConfiguration {
id: ui_ref.instance_id.clone(),
alias: Some(ui_ref.ui_handle.clone()),
})
.collect(),
});

ui_bundles.push(UiBundleConfiguration {
id: ui.id(),
root_dir: ui.uri.clone().split_off(4), // splitting off "dir://"
hash: None,
});

let port = get_free_port(next_ui_port..MIN_INTERFACE_PORT - 1)
.ok_or_else(|| String::from("Couldn't acquire free port"))?;
next_ui_port = port + 1;
ui_interfaces.push(UiInterfaceConfiguration {
id: ui.id(),
bundle: ui.id(),
port,
dna_interface: Some(ui.id()),
reroute_to_root: false,
bind_address: String::from("127.0.0.1"),
});
}

Ok(Configuration {
agents: vec![agent_config],
dnas,
instances,
bridges: self.bridges.clone(),
interfaces,
ui_bundles,
ui_interfaces,
network,
logger,
..Default::default()
})
}
}
2 changes: 2 additions & 0 deletions crates/conductor_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ pub mod context_builder;
pub mod dna_location;
pub mod dpki_instance;
pub mod error;
pub mod happ_bundle;
pub mod holo_signing_service;
pub mod holochain;
pub mod interface;
pub mod interface_impls;
pub mod key_loaders;
pub mod keystore;
pub mod logger;
pub mod port_utils;
pub mod signal_wrapper;
pub mod static_file_server;
pub mod static_server_impls;
Expand Down
Loading